panda 2008.10.24 → 2008.10.25
raw patch · 76 files changed
+1441/−1391 lines, 76 filesdep ~kibro
Dependency ranges changed: kibro
Files
- Panda.hs +0/−10
- Panda/Config/Global.hs +0/−95
- Panda/Controller/Application.hs +0/−172
- Panda/Extension/Analytics/T.hs +0/−19
- Panda/Helper/Env.hs +0/−38
- Panda/Helper/Helper.hs +0/−136
- Panda/Helper/StateHelper.hs +0/−43
- Panda/Model/Comment.hs +0/−96
- Panda/Model/Post.hs +0/−78
- Panda/Model/Static.hs +0/−28
- Panda/Model/Tag.hs +0/−39
- Panda/Type/Pager.hs +0/−13
- Panda/Type/Reader.hs +0/−37
- Panda/Type/Sidebar.hs +0/−20
- Panda/Type/State.hs +0/−64
- Panda/Type/Theme.hs +0/−44
- Panda/View/Atom/Comment.hs +0/−66
- Panda/View/Atom/Post.hs +0/−39
- Panda/View/Atom/Static.hs +0/−7
- Panda/View/Atom/Tag.hs +0/−12
- Panda/View/Control/Comment.hs +0/−25
- Panda/View/Control/Helper.hs +0/−26
- Panda/View/Control/Post.hs +0/−26
- Panda/View/Control/Search.hs +0/−15
- Panda/View/Control/Static.hs +0/−7
- Panda/View/Control/Tag.hs +0/−18
- Panda/View/Widget/Body.hs +0/−8
- Panda/View/Widget/Footer.hs +0/−17
- Panda/View/Widget/Head.hs +0/−17
- Panda/View/Widget/Header.hs +0/−18
- Panda/View/Widget/Helper.hs +0/−20
- Panda/View/Widget/Navigation.hs +0/−19
- Panda/View/Widget/RSS.hs +0/−37
- Panda/View/Widget/SearchBar.hs +0/−7
- Panda/View/Widget/Sidebar.hs +0/−37
- Panda/View/Widget/Template.hs +0/−33
- changelog.markdown +12/−0
- panda.cabal +5/−4
- readme.markdown +2/−1
- src/Main.hs +5/−0
- src/Panda.hs +10/−0
- src/Panda/Config/Global.hs +97/−0
- src/Panda/Controller/Application.hs +174/−0
- src/Panda/Extension/Analytics/T.hs +19/−0
- src/Panda/Helper/Env.hs +38/−0
- src/Panda/Helper/Helper.hs +138/−0
- src/Panda/Helper/StateHelper.hs +46/−0
- src/Panda/Model/Comment.hs +109/−0
- src/Panda/Model/Post.hs +78/−0
- src/Panda/Model/Static.hs +28/−0
- src/Panda/Model/Tag.hs +39/−0
- src/Panda/Type/Pager.hs +13/−0
- src/Panda/Type/Reader.hs +37/−0
- src/Panda/Type/Sidebar.hs +20/−0
- src/Panda/Type/State.hs +65/−0
- src/Panda/Type/Theme.hs +44/−0
- src/Panda/View/Atom/Comment.hs +66/−0
- src/Panda/View/Atom/Post.hs +39/−0
- src/Panda/View/Atom/Static.hs +7/−0
- src/Panda/View/Atom/Tag.hs +12/−0
- src/Panda/View/Control/Comment.hs +25/−0
- src/Panda/View/Control/Helper.hs +26/−0
- src/Panda/View/Control/Post.hs +30/−0
- src/Panda/View/Control/Search.hs +15/−0
- src/Panda/View/Control/Static.hs +7/−0
- src/Panda/View/Control/Tag.hs +18/−0
- src/Panda/View/Widget/Body.hs +8/−0
- src/Panda/View/Widget/Footer.hs +17/−0
- src/Panda/View/Widget/Head.hs +17/−0
- src/Panda/View/Widget/Header.hs +18/−0
- src/Panda/View/Widget/Helper.hs +20/−0
- src/Panda/View/Widget/Navigation.hs +19/−0
- src/Panda/View/Widget/RSS.hs +37/−0
- src/Panda/View/Widget/SearchBar.hs +9/−0
- src/Panda/View/Widget/Sidebar.hs +37/−0
- src/Panda/View/Widget/Template.hs +35/−0
− Panda.hs
@@ -1,10 +0,0 @@-module Panda- (- module Panda.Controller.Application,- panda- ) where--import Panda.Controller.Application-import Kibro--panda = startKibro pages
− Panda/Config/Global.hs
@@ -1,95 +0,0 @@-module Panda.Config.Global where--import Panda.Helper.Helper-import MPS-import Prelude hiding ((.), (/), (^), id, readFile)--import System.FilePath hiding ((<.>))-import Control.Arrow ((>>>), (&&&), (***))-import Data.Maybe-import System.Directory--import Panda.Type.Reader-import Panda.Type.Sidebar-import Panda.Type.Theme--db_id = "db"-flat_id = "."-post_id = "blog"-config_id = "config"-tag_id = "tag"-comment_id = "comment"-sidebar_id = "sidebar"-theme_id = "theme"--db_uri = db_id-flat_uri = db_uri / flat_id--config_uri = flat_uri / config_id / "site.txt"-sidebar_uri = flat_uri / config_id / sidebar_id-post_uri = flat_uri / post_id-tag_uri = flat_uri / tag_id-comment_uri = flat_uri / comment_id-theme_uri = flat_uri / config_id / theme_id----- unsafe, must restart server after changing config file, sorry about that ...--- but these configs are read only, and keep the view pure, so no monad headaches involved.--user_config = parse_config config_uri--config_for' s d = user_config.lookup s .fromMaybe d-config_for s = config_for' s s--blog_title = config_for "blog_title"-blog_subtitle = config_for "blog_subtitle"-host_name = config_for "host_name"-author_email = config_for "author_email" --per_page = config_for' "per_page" "7" .read :: Int-navigation = config_for' "navigation" "About" .parse_list.("Home" :)--panda_url = "http://github.com/nfjinjing/panda"-root - | user_root.belongs_to ["/", ""] = "/"- | otherwise = user_root.("/" /).remove_trailing_slash- where user_root = config_for' "root" "/"--default_reader = Markdown--sidebar = config_for "sidebar" .parse_list .map (sidebar_uri /).select (to_utf8 >>> doesFileExist >>> purify )- .map (read_sidebar_item default_reader >>> purify)- -favicon = config_for' "favicon" "/favicon.ico"----- extensions-analytics_account_id = config_for "analytics_account_id"---- theme-default_theme = blank_theme-user_theme_name = config_for "theme"-user_theme_uri = (theme_uri / user_theme_name) ++ ".txt"--theme = if user_theme_uri.doesFileExist.purify- then parse_config user_theme_uri .(("name", user_theme_name) : ) .to_theme- else default_theme- --- custom-as_l s = "[" ++ s ++ "]"-post_date_format = config_for' "post_date_format" "%y-%m-%d"-comment_date_format = config_for' "comment_date_format" "%y-%m-%d %T"-url_date_format = config_for' "url_date_format" "%y-%m-%d"-url_date_matcher = config_for' "url_date_matcher" "\\d{2}-\\d{2}-\\d{2}"-url_title_subs = config_for' "url_title_subs" "" .as_l .read :: [(String, String)]-url_date_title_seperator = config_for' "url_date_title_seperator" " "---- summary-cut = config_for' "cut" "✂-----" .to_utf8-parse_boolean = belongs_to ["true", "1", "y", "yes", "yeah"]-summary_for_root = config_for "summary_for_root" .parse_boolean-summary_for_tag = config_for "summary_for_tag" .parse_boolean-summary_for_rss = config_for "summary_for_rss" .parse_boolean---
− Panda/Controller/Application.hs
@@ -1,172 +0,0 @@--- the controller is the most complex module for a reason:--- models are made as simple and small as possible--- views are pure functions.---- a note on encoding:--- IOs are in utf-8 filter, internally use bytes--{-# OPTIONS -fno-monomorphism-restriction #-}--module Panda.Controller.Application where--import qualified Data.Map as Map---- env-import Panda.Helper.Env hiding (tag, uri)-import Panda.Helper.StateHelper-import Prelude hiding ((.), (/), (^), id)-import qualified Panda.Type.State as S-import qualified Panda.Config.Global as G-import qualified Panda.Type.Pager as Pager---- model-import qualified Panda.Model.Post as Post-import qualified Panda.Model.Static as Static-import qualified Panda.Model.Tag as Tag-import qualified Panda.Model.Comment as Comment---- view-import qualified Panda.View.Control.Post as PostV-import qualified Panda.View.Widget.Template as T-import qualified Panda.View.Control.Static as StaticV-import qualified Panda.View.Control.Tag as TagV-import qualified Panda.View.Control.Search as SearchV-import qualified Panda.View.Widget.RSS as RSSV---- helpers--- liftIO: move to an (u)pper level monad-u = liftIO-blog_regex = G.url_date_matcher--init_post tags x = do- x.Tag.fill_tag tags.Comment.fill_comment_size.u--tag_list = Tag.list.u----- main controller--pages = - [ ("$" ,index )- , ("(\\?.+)?$" ,index )- , ("rss.xml$" ,index_feed )- , (blog_regex ,blog )- , ("static/." ,static )- , ("tag/.*/rss.xml$" ,tag_feed )- , ("tag/." ,tag )- , ("search" ,search )- , ("comment/create" ,comment_create )- ] .map_fst ((G.root /) >>> ("^" ++))- ++- [ ("^" ++ G.root ++ "$" ,index )- , ("^" ++ G.root ++ "?" ,index )- ]- ---index = do- posts <- Post.list.u- p <- paginate posts- tags <- tag_list- let nav = if p.Pager.current == 1 then home_nav else no_navigation- let state = S.empty { S.uid = G.post_id, S.pager = p, S.tags = tags, S.nav_location = nav }- - posts.mapM (init_post tags) ^ PostV.list state >>= output_html--index_feed = do- posts <- Post.list.u- posts.RSSV.rss "" "" .output--blog = do- id <- uri ^ Post.uri_to_id- blog <- Post.get id .u- comments <- Comment.list_for id .u- tags <- tag_list- - test_data <- S.mk_human_test .u- let state = S.empty { S.uid = id, S.tags = tags, S.resource_title = blog.resource_title, S.human_test_data = test_data }- blog.(init_post tags) ^ PostV.view state comments >>= output_html---static = do- id <- uri- static_page <- Static.get id .u- tags <- tag_list- - let nav_id = static_page.Static.uid.id_to_resource- let nav = if nav_id.belongs_to G.navigation then nav_id else no_navigation- let state = S.empty { S.uid = id, S.tags = tags, S.nav_location = nav, S.resource_title = static_page.resource_title }- StaticV.view state static_page .output_html--tag = do- id <- uri- tags <- tag_list- let tag_name = Tag.get_name id- case Tag.tag_map' tags .Map.lookup tag_name of- Nothing -> not_found- Just post_set -> do- posts <- post_set.to_list.rsort.mapM (Post.get) .u >>= mapM (init_post tags)- p <- paginate posts- let state = S.empty { S.uid = id, S.pager = p, S.tags = tags, S.resource_title = tag_name.Tag.resource_title_from_name }- posts.TagV.view state.output_html--tag_feed = do- id <- uri ^ (split "/" >>> init >>> join "/")- tags <- tag_list- let tag_name = Tag.get_name id- case Tag.tag_map' tags .Map.lookup tag_name of- Nothing -> not_found- Just post_set -> do- posts <- post_set.to_list.rsort.mapM (Post.get) .u ^ map (Tag.fill_tag tags)- posts.RSSV.rss G.tag_id tag_name.output--search = do- s <- param_with_default "s" ""- posts <- Post.search s .u-- p <- paginate posts- tags <- tag_list- query <- uri- let state = S.empty { S.uid = query, S.pager = p, S.tags = tags, S.resource_title = query }-- posts.mapM (init_post tags) >>= \x -> x.SearchV.view state s.output_html--comment_create = do- post_id <- input_with_default (show_data Comment.PostId) (G.post_id / "nothing")- exists <- (G.flat_uri / post_id.to_utf8) .doesFileExist .u- let valid_path = equalFilePath G.post_id (takeDirectory post_id)- checked <- check_create- - if [checked, valid_path, exists].and- then- inputs >>= (Comment.create >>> u)- else- return ()- redirect $ (post_id.Post.id_to_uri.to_utf8).urlEncode--get_input_data = show_data >>> get_input--check_human = do- x <- [Comment.LeftNumber, Comment.RightNumber, Comment.Operator, Comment.HumanHack].mapM get_input_data ^ map fromJust- let [l, r, op, h] = x- return $ S.simple_eval (l.read) (r.read) (op.S.read_op) .is (h.read)---- create helper, should be refactored to some common aspect-check_create =- [ validate Comment.Author ( length >>> (> 0))- , validate Comment.AuthorLink ( const True)- , validate Comment.Body ( length >>> (> 0))- , validate Comment.EmptyField ( empty )- , validate Comment.LeftNumber ( belongs_to (S.nums.map show))- , validate Comment.RightNumber ( belongs_to (S.nums.map show))- , validate Comment.Operator ( belongs_to (S.ops.map S.display_op))- , validate Comment.HumanHack ( belongs_to (S.nums.map show))- , check_human- ] .sequence ^ and---validate s f = do- maybe_s <- get_input_data s- case maybe_s of- Nothing -> return False- Just v -> return $ f v
− Panda/Extension/Analytics/T.hs
@@ -1,19 +0,0 @@-module Panda.Extension.Analytics.T where--import Panda.Helper.Env-import Prelude hiding ((.), (/), (^), id, span)-import qualified Panda.Config.Global as G--analytics_snippet = - [ "<script type=\"text/javascript\"> "- , "var gaJsHost = ((\"https:\" == document.location.protocol) ? \"https://ssl.\" : \"http://www.\"); "- , "document.write(unescape(\"%3Cscript src='\" + gaJsHost + \"google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E\")); "- , "</script> "- , "<script type=\"text/javascript\"> "- , "var pageTracker = _gat._getTracker(\"UA-xxxxxx-x\"); "- , "pageTracker._trackPageview(); "- , "</script> "- ]- .map strip- .join "\n"- .gsub "UA-xxxxxx-x" G.analytics_account_id
− Panda/Helper/Env.hs
@@ -1,38 +0,0 @@--- this sets some scoping for every module, which prevents importing--- common modules in every file--module Panda.Helper.Env (- module MPS - , module Control.Monad - , module Control.Arrow - , module Data.List - , module Network.URI - , module Network.CGI - , module System.Locale - , module System.Time.Parse- , module Panda.Helper.Helper- , module Data.Maybe- , module System.FilePath- , module System.IO.UTF8- , module Text.XHtml.Strict- , module System.Time- , module System.Directory- , module Data.Foldable-) where- -import MPS hiding (base, col, sub, date)-import Control.Monad hiding (join)-import Control.Arrow ((>>>), (&&&), (***))-import Data.List hiding (find)-import Network.URI-import Network.CGI hiding (Html)-import System.Locale-import System.Time.Parse-import Panda.Helper.Helper-import Data.Maybe-import System.FilePath hiding ((<.>))-import System.IO.UTF8 (readFile, writeFile)-import Text.XHtml.Strict hiding (select)-import System.Time-import System.Directory-import Data.Foldable (find)
− Panda/Helper/Helper.hs
@@ -1,136 +0,0 @@--- helper module is a central place for reusable functions--- for this project, more general helpers are usually moved--- to MPS package, for multi-project usage--{-# OPTIONS -fno-monomorphism-restriction #-}--module Panda.Helper.Helper where--import System.IO.UTF8 (readFile, writeFile)-import Network.URI-import Network.CGI-import System.FilePath.Posix ((</>))-import Control.Arrow ((>>>), (&&&))-import Text.XHtml.Strict-import Control.Monad hiding (join)-import Data.Maybe-import Panda.Type.Pager as Pager hiding (empty)-import MPS hiding (date)-import Prelude hiding ((.), (/), (^), id, readFile, writeFile)-import Char-import Data.List-import qualified Data.List as L -import Panda.Type.Reader-import System.FilePath.Posix hiding ((<.>))-import System.Time---(/) = (</>)-infixl 5 /--(^) = (<.>)-infixl 9 ^---- global-parse_config_io s = readFile s ^ (\x -> x.filter_comment.lines.map strip .map (split "\\s*=\\s*") .map fill_snd_blank .map tuple2)- where fill_snd_blank [x] = [x,""]- fill_snd_blank xs = xs-parse_config = parse_config_io >>> purify--write_config_io s xs = xs.map(\(x, y) -> x ++ " = " ++ y) .join "\n" .writeFile s---- model-take_extension = takeExtension >>> split "\\." >>> last---- controller-raw_uri = requestURI ^ (uriPath >>> urlDecode >>> tail >>> remove_trailing_slash >>> from_utf8 )-remove_trailing_slash s = if s.last.is '/' then s.init else s- -params = do- query_string <- requestURI ^ uriQuery - case query_string of- '?':s -> s.formDecode .map_snd unescape_unicode_xml.return- otherwise -> return []--inputs = getInputs ^ map_snd (strip >>> unescape_unicode_xml)--param_with_default s d = params ^ (lookup s >>> fromMaybe d )-input_with_default s d = inputs ^ (lookup s >>> fromMaybe d )--get_param s = params ^ lookup s-get_input s = inputs ^ lookup s--just_param s = get_param s ^ fromJust-just_input s = get_input s ^ fromJust--full_paginate length total = liftM5 ( Pager length ) current has_next has_previous next previous where- current = param_with_default "page" "1" ^ read- has_next = current ^ ( (* length) >>> (< total.fromIntegral) )- has_previous = current ^ (> 1)- next = current ^ (+ 1)- previous = current ^ (+ (-1) )---for_current_page p xs = xs.drop ((p.Pager.current - 1) * p.Pager.length) .take (p.Pager.length)---- id: /type/resoruce-id_to_type id = id.split "/" .first-id_to_resource id = id.split "/" .tail.join "/"-no_navigation = ""-home_nav = "Home"----- view-id = identifier--css_link l = itag "link" ! [rel "stylesheet", thetype "text/css", href l]-js_link l = itag "script" ! [thetype "text/javascript", src l]-js_src s = tag "script" ! [thetype "text/javascript"] << s-rss_link l = itag "link" ! [rel "alternate", thetype "application/rss+xml", href l, title "RSS 2.0"]-favicon_link l = itag "link" ! [rel "icon", thetype "image/png", href l]-meta_tag = meta ! [httpequiv "Content-Type", content "text/html; charset=utf-8"]--div_id s = thediv ! [id s]-div_class s = thediv ! [theclass s]-div_class_id x y = thediv ! [theclass x, id y]--output_html = output ... renderHtml--spaced_url = gsub "/" " / "---- config-parse_list s = s.split "," .map strip .reject null-not_found = getVar "REQUEST_URI" >>= (fromMaybe "" >>> outputNotFound)----- generic--starts_with _ [] = False-starts_with (x:xs) (y:ys) | x == y = starts_with xs ys- | otherwise = False--ends_with x y = starts_with (x.reverse) (y.reverse)--capitalize (x:xs) = [x].upper ++ xs.lower-camel_case = split "_" >>> map capitalize >>> join'-snake_case = gsub "\\B[A-Z]" "_\\&" >>> lower-show_data = show >>> snake_case- ---- class-class DataRenderer a where- render_data :: a -> Html--class Resource a where- resource_title :: a -> String--class Markable a where- markup :: a -> Html- -class Datable a where- date :: a -> CalendarTime--class Addressable a where- uri :: a -> String--
− Panda/Helper/StateHelper.hs
@@ -1,43 +0,0 @@--- trying to merge this helper with the generic one results in cyclic imports--{-# OPTIONS -fno-monomorphism-restriction #-}--module Panda.Helper.StateHelper where--import Panda.Helper.Env-import qualified Panda.Config.Global as G-import Prelude hiding ((.), (/), (^), id)----- G.root = /blog--- raw_uri = blog/x--- full_uri = /blog/x--- uri = full_uri - (/blog/)-remove_root s- | G.root.is "/" = s- | otherwise = s.slice (G.root.length) (s.length)--uri = raw_uri ^ remove_root--- uri = raw_uri----- global-parse_date format s = case maybe_d of- Nothing -> Nothing- Just d -> Just $ if d.ctYear < 1910 then d {ctYear = d.ctYear + 100} else d - where- maybe_d = parseCalendarTime defaultTimeLocale format s- -format_date = formatCalendarTime defaultTimeLocale-default_date = parse_date "%Y-%m-%d %T" "2000-1-1 00:00:00".fromJust-parse_post_date = parse_date G.post_date_format-default_parse_date s = s.parse_post_date .fromMaybe default_date---- controller-paginate = length >>> full_paginate (G.per_page)--cut = G.cut-cut_re = "^\\s*" ++ cut-match_cut = to_utf8 >>> match cut_re-is_cut = match_cut >>> isJust-split_cut = to_utf8 >>> split cut_re
− Panda/Model/Comment.hs
@@ -1,96 +0,0 @@-module Panda.Model.Comment where---- env-import Panda.Helper.Env hiding (title, body, size)-import Prelude hiding ((.), (/), (^), id, readFile, writeFile)-import qualified Panda.Config.Global as G-import Panda.Type.Reader-import qualified Panda.Model.Post as Post-import System.Directory-import Panda.Helper.StateHelper-import Network.Gravatar--data Comment = Comment- { uid :: String -- comment/08-09-04 blog title- , author :: String- , body :: String- , author_email :: String- , author_link :: String- }- deriving (Show, Eq)--data CommentData = - Author- | AuthorEmail- | AuthorLink- | Body- | PostId- deriving (Show)--data SpanFilter = - HumanHack - | EmptyField- | LeftNumber- | RightNumber- | Operator- deriving (Show)--instance Resource Comment where- resource_title = uid >>> spaced_url- -instance Markable Comment where- markup x = render_to_html Markdown (x.body)--instance Datable Comment where- date x = x.uid.split "/".last.default_parse_date----- CRUD-list_for post_id = do- has_comments <- doesDirectoryExist d- if has_comments- then ls d ^ reject (match "\\.meta$" >>> isJust) ^ rsort ^ map (G.comment_id / r /) >>= mapM (from_utf8 >>> get)- else return []- where- d = (G.comment_uri / r)- r = post_id.id_to_resource.to_utf8---get id = do- body <- path.readFile- meta <- (path ++ ".meta").parse_config_io- let at s = meta.lookup (s.show_data) .fromJust- let author = at Author- let author_email = at AuthorEmail- let author_link = at AuthorLink-- return $ Comment id author body author_email author_link- where- path = G.flat_uri / id.to_utf8--gravatar_default_size = size 40-gravatar_link x = gravatarWith (x.author_email) Nothing gravatar_default_size Nothing--create h = do- let at s = h.lookup (s.show_data) .fromJust- let body = at Body- let post_id = at PostId-- timestamp <- ( getClockTime >>= toCalendarTime ) ^ format_date G.comment_date_format- let comment_path = post_id_to_uid post_id .to_utf8- createDirectoryIfMissing True comment_path- - let uid = comment_path / timestamp- writeFile uid body- let meta = [Author, AuthorLink, AuthorEmail] .labeling at .map_fst show_data- write_config_io (uid ++ ".meta") meta---- extra-from_post_id x = Comment (post_id_to_uid x) "" "" "" ""--post_id_to_uid x = G.flat_uri / G.comment_id / x.split "/" .last-uid_to_post_id x = G.post_id / x.split "/" .last--fill_comment_size x = do- size <- x.Post.uid.list_for ^ length - return $ x { Post.comment_size = size }
− Panda/Model/Post.hs
@@ -1,78 +0,0 @@--- what about the performance?--- Haskell takes care of that, since IOs are also lazy.--- Posts are not read unless specifically required, i.e. after pagination--module Panda.Model.Post where---- env-import Panda.Helper.Env hiding (match, title, body, date)-import qualified MPS as MPS-import Prelude hiding ((.), (/), (^), id, readFile)-import qualified Panda.Config.Global as G-import Panda.Type.Reader-import Panda.Helper.StateHelper hiding (uri)-import Panda.Helper.Helper (date)--data Post = Post - { uid :: String -- blog/08-09-04 blog title- , title :: String- , body :: String- , tags :: [String]- , comment_size :: Int- , reader :: Reader- }- deriving (Show, Eq)--instance Resource Post where- resource_title = title--instance Markable Post where- markup x = render_to_html (x.reader) (x.full)- -instance Datable Post where- date = uid >>> get_date--instance Addressable Post where- uri = uid >>> id_to_uri---- CRUD-list = ls G.post_uri ^ map from_utf8 ^ rsort ^ map (G.post_id /) >>= mapM get--get id = liftM4 (Post id (get_title id) ) (get_body id) (return []) (return 0) (return $ get_reader id)-get_extension = takeExtension-get_title id = id.words.tail.unwords.dropExtension-get_body id = (G.flat_uri / id.to_utf8) .readFile-get_reader id = id.take_extension.guess_reader.fromMaybe G.default_reader-get_date id = id.words.first.split "/".last.default_parse_date--match s x = [title, body] .map (send_to x >>> lower >>> isInfixOf (s.lower)) .or-search "" = return []-search s = list ^ filter (match s)--summary = body >>> split_cut >>> first >>> from_utf8-full x | x.body.match_cut.isNothing = x.body-full x = ( xs.takeWhile not_cut ++ xs.dropWhile not_cut .tail ).unlines where- not_cut = is_cut >>> not- xs = x.body.lines--has_continue = body >>> match_cut >>> isJust----- extra-id_to_uri id = G.root / ( pretty_date ++ G.url_date_title_seperator ++ formatted_title ++ ext ) where- formatted_title = G.url_title_subs.map (\(a,b) -> gsub a b).inject (id.get_title) apply- pretty_date = id.get_date.format_date G.url_date_format- ext = id.get_extension--uri_to_id s = G.post_id / (d ++ " " ++ t) where- (raw_d, (prefix, title_with_sep)) = s.MPS.match G.url_date_matcher.fromJust.fst- raw_t = title_with_sep.drop (G.url_date_title_seperator.length)- t = G.url_title_subs.map (\(a,b) -> gsub b a) .inject raw_t apply- d = raw_d.parse_date G.url_date_format .fromJust.format_date G.post_date_format----- summary-markup_summary x = post_summary x +++ next where- post_summary = (reader &&& summary) >>> splash render_to_html- next = if x.has_continue then toHtml $ hotlink (x.uri) << "Read the rest of the post »" else toHtml ""-
− Panda/Model/Static.hs
@@ -1,28 +0,0 @@-module Panda.Model.Static where---- env-import Panda.Helper.Env hiding (match, body)-import Prelude hiding ((.), (/), (^), id, readFile)-import qualified Panda.Config.Global as G-import Panda.Type.Reader--data Static = Static - { uid :: String- , body :: String- , reader :: Reader- }- deriving (Show, Eq)--instance Resource Static where- resource_title = uid >>> spaced_url--instance Markable Static where- markup x = render_to_html (x.reader) (x.body)- --- CRUD-get id = liftM2 (Static id) (get_body id) (return $ get_reader id)-get_title = id_to_resource >>> dropExtension-get_body id = (G.flat_uri / id.to_utf8) .readFile-get_reader id = id.take_extension.guess_reader.fromMaybe G.default_reader--title = uid >>> get_title
− Panda/Model/Tag.hs
@@ -1,39 +0,0 @@-module Panda.Model.Tag where--import qualified Panda.Model.Post as Post---- env-import Panda.Helper.Env hiding (name)-import Prelude hiding ((.), (/), (^), id, readFile)-import qualified Data.Set as S-import qualified Panda.Config.Global as G---data Tag = Tag - { uid :: String -- tag/name- , name :: String- , resources :: S.Set String- }- deriving (Show, Eq)--instance Resource Tag where- resource_title = uid >>> spaced_url---- CRUD-list = ls G.tag_uri ^ map from_utf8 ^ map (G.tag_id /) >>= mapM get--get id = get_resources id ^ Tag id (get_name id)-get_name id = id.split "/" .tail.join'-get_resources id = (G.flat_uri / id.to_utf8) .readFile- ^ filter_comment ^ lines ^ map (G.post_id / ) ^ to_set--resource_title_from_name = name_to_id >>> spaced_url-tag_map' xs = xs . map (name &&& resources) . to_h-tag_map = list ^ tag_map'--for_resource xs x = xs.select (resources >>> has x) .map name-fill_tag xs x = x { Post.tags = for_resource xs (x.Post.uid) }---- extra-sorted xs = xs.sortBy(compare_by (resources >>> S.size)).reverse-name_to_id x = G.tag_id / x
− Panda/Type/Pager.hs
@@ -1,13 +0,0 @@-module Panda.Type.Pager where- -data Pager = Pager {- length :: Int,- current :: Int,- has_next :: Bool,- has_previous :: Bool,- next :: Int,- previous :: Int- }- deriving (Eq, Show)- -empty = Pager 0 0 False False 0 0
− Panda/Type/Reader.hs
@@ -1,37 +0,0 @@-{-# OPTIONS -XDeriveDataTypeable #-}--module Panda.Type.Reader where--import MPS -import Prelude hiding ((.), (/), (^), id)-import qualified Data.Map as Map-import Text.Pandoc-import Control.Arrow ((>>>))-import Text.XHtml.Strict-import Data.Generics--data Reader = Markdown | RST | HTML | Latex deriving (Show, Eq, Typeable, Data)--readers = - [ (Markdown, ["markdown", "md"])- , (RST, ["rst"] )- , (HTML, ["html", "htm"])- , (Latex, ["tex", "latex"])- ]--gen_lookup (r, xs) = xs.labeling (const r)-reader_map = readers.map gen_lookup.join'.to_h-guess_reader ext = reader_map.Map.lookup ext---to_html r = r defaultParserState >>> writeHtml defaultWriterOptions---- this list can go on, as long as there is a library that does--- the convertion. pretty extensible, isn't it.-rr Markdown = to_html readMarkdown-rr RST = to_html readRST-rr HTML = primHtml-rr Latex = to_html readLaTeX--render_to_html = rr-
− Panda/Type/Sidebar.hs
@@ -1,20 +0,0 @@-module Panda.Type.Sidebar where--import Panda.Helper.Env hiding (body)-import Prelude hiding ((.), (/), (^), id, readFile)-import Panda.Type.Reader--data SidebarItem = SidebarItem- { name :: String- , body :: String- , reader :: Reader- }- deriving (Show, Eq)--instance Markable SidebarItem where- markup x = render_to_html (x.reader) (x.body)--read_sidebar_item default_reader s = liftM2 (SidebarItem name) body (return reader) where- body = s.to_utf8.readFile- reader = s.take_extension.guess_reader.fromMaybe default_reader- name = s.takeFileName.dropExtension
− Panda/Type/State.hs
@@ -1,64 +0,0 @@-module Panda.Type.State where--import qualified Panda.Type.Pager as Pager-import Panda.Model.Tag-import Random (randomRs, mkStdGen)-import System.Time-import Control.Arrow ((>>>), (&&&), (***))-import MPS-import Prelude hiding ((.), (/), (^), id, readFile, writeFile)--data State = State- -- model state- { uid :: String -- current view resource- , pager :: Pager.Pager -- pager-- -- theme state- , tags :: [Tag]- , nav_location :: String- , resource_title :: String- , human_test_data :: HumanTestData- }- deriving (Show)--show_left = human_test_data >>> left >>> show-show_right = human_test_data >>> right >>> show-show_op = human_test_data >>> op >>> display_op where--read_op "+" = Plus-read_op "-" = Minus-read_op x = error ("can not read operator: " ++ x)--display_op Plus = "+"-display_op Minus = "-"--data HumanTestData = HumanTestData - { left :: Int- , right :: Int- , op :: Op- } deriving (Show)--data Op = Plus | Minus deriving (Show)--empty = State "" Pager.empty [] "" "" empty_human_test-empty_human_test = HumanTestData 0 0 Plus--(^) = (<.>)-infixl 9 ^--ops = [Plus, Minus]-nums = [0, 5, 10, 15, 20]-simple_eval a b Plus = a + b-simple_eval a b Minus = a - b---mk_human_test = do- seed <- (getClockTime >>= toCalendarTime) ^ ctPicosec ^ fromIntegral- let (a,b,c) = randomRs (0,100) (mkStdGen seed) .in_group_of 3 .map make_sample .dropWhile (good_test >>> not) .first- return $ HumanTestData a b c-- where- make_sample [a,b,c] = ((get_num a), (get_num b), (get_op c))- good_test = splash3 simple_eval >>> belongs_to nums- get_num n = nums.at (n `mod` (nums.length))- get_op n = ops.at (n `mod` (ops.length))
− Panda/Type/Theme.hs
@@ -1,44 +0,0 @@-module Panda.Type.Theme where--import Prelude hiding ((.))-import Panda.Helper.Env hiding (name, header)--data Theme = Theme- { name :: String- , container :: String- , header :: String- , navigation :: String- , main :: String- , sidebar :: String- , footer :: String- , css :: [String]- , js :: [String]- } deriving (Show, Read)- -to_theme xs = Theme - { name = at "name"- , container = at "container"- , header = at "header"- , navigation = at "navigation"- , main = at "main"- , sidebar = at "sidebar"- , footer = at "footer"- , css = at "css" .css_list- , js = at "js" .js_list- }- where- at s = xs.lookup s .fromJust- css_list s = s.parse_list.map (\x -> "/theme/" ++ at "name" ++ "/css/" ++ x ++ ".css")- js_list s = s.parse_list.map (\x -> "/theme/" ++ at "name" ++ "/js/" ++ x ++ ".js")- -blank_theme = Theme- { name = ""- , container = ""- , header = ""- , navigation = ""- , main = ""- , sidebar = ""- , footer = ""- , css = [""]- , js = [""]- }
− Panda/View/Atom/Comment.hs
@@ -1,66 +0,0 @@-module Panda.View.Atom.Comment where- -import Panda.Helper.Env hiding (title)-import Prelude hiding ((.), (/), (^), id, span)-import qualified Panda.Config.Global as G-import Panda.Helper.StateHelper-import Panda.Type.State hiding (uid)---- view-import qualified Panda.View.Atom.Tag as Tag---- model-import Panda.Model.Comment hiding (create)---- render-instance DataRenderer Comment where- render_data = entry- -entry x = toHtml - [ gravatar- , cite << a- , toHtml " says:"- , br- , p ! [theclass "small"] << comment_date x- , x.markup- ]- where- a = if x.author_link.null then x.author.toHtml else toHtml $ hotlink (x.author_link) << x.author- gravatar = thediv ! [theclass "gravatar"] << image ! [src (gravatar_link x)]--comment_date x = toHtml $ x.date.format_date "%b %e, %Y at %I:%M %p"----- form-human_test_question state = ["What", "is", state.show_left, state.show_op, state.show_right, "?"] .join " "--create state x = toHtml- [ h2 ! [id "respond"] << "Leave a Response"- , gui (G.root / "comment/create") ! [id "commentform"] <<- [ field Author 22 1 "Name (required)"- , field AuthorEmail 22 2 "Email (hidden)"- , field AuthorLink 22 3 "Website"- , field HumanHack 22 4 (human_test_question state)- , empty_field- , hidden_field LeftNumber (state.show_left)- , hidden_field RightNumber (state.show_right)- , hidden_field Operator (state.show_op)- , p << hidden (show_data PostId) (x.uid.uid_to_post_id)- , p << textarea ! [name (show_data Body), id "comment", cols "10", rows "20", strAttr "tabindex" "5"] << ""- , p << submit "submit" "Submit Comment" ! [strAttr "tabindex" "6"]- ]- ]--field_with_value v x' s t m = p <<- [ label ! [thefor x] << small << m- , br - , textfield x ! [size (s.show), strAttr "tabindex" (t.show), value v]- ]- where x = x'.show_data--field x s t m = field_with_value "" x s t m--hidden_field x m = hidden_field_with_value m x m-hidden_field_with_value v x m = thespan ! [ thestyle "display: none;" ] << field_with_value v x 22 10 m--empty_field = hidden_field_with_value "" EmptyField "Leave this field empty:"
− Panda/View/Atom/Post.hs
@@ -1,39 +0,0 @@-module Panda.View.Atom.Post where- -import Panda.Helper.Env hiding (name, title)-import Prelude hiding ((.), (/), (^), id, span)-import qualified Panda.Config.Global as G-import Panda.Helper.StateHelper hiding (uri)---- view-import qualified Panda.View.Atom.Tag as Tag---- model-import Panda.Model.Post---- render-instance DataRenderer Post where- render_data = entry Full--data RenderStyle = Summary | Full---- instance helpers-entry style x = div_class "post" << [entry_title, entry_mark, entry_body] where- entry_title = x.title_link- entry_body = div_class "entry" << show_content style x- entry_mark = p ! [theclass "small"] << [ post_date, post_tags, post_comments ].map (send_to x)--show_content Summary x = x.markup_summary-show_content Full x = x.markup---- post could also be a summary-render_summary x = if x.is True then entry Summary else render_data--title_link x = h2 << hotlink (x.uri) << x.title-post_date x = toHtml $ x.date.format_date "%b %e, %Y"--post_tags x | x.tags.null = toHtml ""-post_tags x = " | " +++ "Published in " +++ x.tags.map Tag.tag_link .intersperse (", ".toHtml)--post_comments x | x.comment_size.is 0 = toHtml ""-post_comments x = " | " +++ hotlink ( x.uri / "#comments") << (x.comment_size.show ++ " Comments")
− Panda/View/Atom/Static.hs
@@ -1,7 +0,0 @@-module Panda.View.Atom.Static where- -import Panda.Helper.Env-import Panda.Model.Static--instance DataRenderer Static where- render_data = markup
− Panda/View/Atom/Tag.hs
@@ -1,12 +0,0 @@-module Panda.View.Atom.Tag where- -import Panda.Helper.Env hiding (name)-import Panda.Model.Tag-import qualified Panda.Config.Global as G-import Prelude hiding ((/))--instance DataRenderer Tag where- render_data = name >>> tag_link--tag_link s = toHtml $ hotlink (G.root / G.tag_id / s ) << s -
− Panda/View/Control/Comment.hs
@@ -1,25 +0,0 @@-module Panda.View.Control.Comment where- -import Panda.Helper.Env hiding (body, date)-import Prelude hiding ((.), (/), (^), id, span)-import qualified Panda.Type.State as State--import Panda.Model.Comment hiding (create)-import qualified Panda.Model.Post as Post---- view-import Panda.View.Control.Helper-import Panda.View.Widget.Template-import Panda.View.Atom.Comment as Comment----- api-styled_entry alt x = li ! [theclass alt] << x.render_data--list [] = []-list xs = - [ h2 ! [id "comments"] << "Responses"- , olist ! [theclass "commentlist" ] << xs.zip (cycle ["comments-alt", ""]).map (splash styled_entry)- ]- -create = Comment.create
− Panda/View/Control/Helper.hs
@@ -1,26 +0,0 @@-module Panda.View.Control.Helper where--import Panda.Helper.Env-import Prelude hiding ((.), (/), (^), id, span)- -import Panda.Type.Pager as Pager-import qualified Panda.Config.Global as G---nav p r = - [ div_class "alignleft" << nav_previous p r- , div_class "alignright" << nav_next p r- ]- -nav_previous p r = if p.Pager.has_previous - then toHtml $ hotlink ( r' ++ "page=" ++ p.Pager.previous.show ) << previous_sign- else spaceHtml- where r' = if isSuffixOf "&" r then r else r ++ "?"- -nav_next p r = if p.Pager.has_next- then toHtml $ hotlink ( r' ++ "page=" ++ p.Pager.next.show ) << next_sign- else spaceHtml- where r' = if isSuffixOf "&" r then r else r ++ "?"--next_sign = "Next Entries »"-previous_sign = "« Previous Entries"
− Panda/View/Control/Post.hs
@@ -1,26 +0,0 @@-module Panda.View.Control.Post where---- env-import Panda.Helper.Env-import Prelude hiding ((.), (/), (^), id, span)--import qualified Panda.Type.State as State-import qualified Panda.Config.Global as G---- model-import qualified Panda.Model.Comment as Comment-import Panda.View.Atom.Post-import Panda.Model.Post---- view-import Panda.View.Control.Helper-import qualified Panda.View.Control.Comment as CommentV-import Panda.View.Widget.Template---- entry view-view state xs x = (x.render_data +++ CommentV.list xs +++ CommentV.create state (x.uid.Comment.from_post_id) ).page state---- list view-list state = for_current_page p >>> map render >>> (+++ nav p G.root) >>> page state where - p = state.State.pager- render = render_summary G.summary_for_root
− Panda/View/Control/Search.hs
@@ -1,15 +0,0 @@-module Panda.View.Control.Search where- -import Panda.Helper.Env-import Prelude hiding ((.), (/), (^), id, span)-import qualified Panda.Type.State as State-import qualified Panda.Config.Global as G---- view-import Panda.View.Atom.Post-import Panda.View.Control.Helper-import Panda.View.Widget.Template--view state s = for_current_page p >>> map render_data >>> (+++ nav p ( (G.root / "search") ++ "?s=" ++ s ++ "&")) >>> page state- where p = state.State.pager-
− Panda/View/Control/Static.hs
@@ -1,7 +0,0 @@-module Panda.View.Control.Static where- -import Panda.Helper.Env-import Panda.View.Atom.Static-import Panda.View.Widget.Template--view state = render_data >>> page state
− Panda/View/Control/Tag.hs
@@ -1,18 +0,0 @@-module Panda.View.Control.Tag where- -import Panda.Helper.Env-import Prelude hiding ((.), (/), (^), id, span)-import qualified Panda.Type.State as State-import qualified Panda.Config.Global as G---- view-import Panda.View.Atom.Post-import Panda.View.Control.Helper-import Panda.View.Widget.Template---view state = for_current_page p >>> map render >>> (+++ nav p ( G.root / tag_id)) >>> page state where- p = state.State.pager- tag_id = state.State.uid- render = render_summary G.summary_for_tag-
− Panda/View/Widget/Body.hs
@@ -1,8 +0,0 @@-module Panda.View.Widget.Body where---- env-import Panda.Helper.Env--body_content x = div_id "maincontent" <<- div_class "content" << x-
@@ -1,17 +0,0 @@-module Panda.View.Widget.Footer where- -import Panda.Helper.Env-import Prelude hiding ((.), (/), (^), id, span)-import qualified Panda.Config.Global as G---footer c = div_class_id c "footer" <<- [ toHtml $ copyright- , toHtml $ "2008 "- , toHtml $ G.blog_title- , toHtml $ br- , toHtml $ "Powered by "- , toHtml $ hotlink G.panda_url << "Panda"- , toHtml $ " using "- , toHtml $ hotlink "http://www.haskell.org/" << "Haskell"- ]
− Panda/View/Widget/Head.hs
@@ -1,17 +0,0 @@-module Panda.View.Widget.Head where--import Panda.Helper.Env-import Prelude hiding ((.), (/), (^), id, span)-import qualified Panda.Config.Global as G-import Panda.Type.Theme hiding (header)-import qualified Panda.Type.State as S-import Panda.View.Widget.Helper--html_head state = header << ([meta_tag, title_tag state, favicon_tag, rss_tag state] ++ G.theme.css.map css_link ++ G.theme.js.map js_link )--title_tag state = thetitle << [G.blog_title ++ state.S.resource_title.format_title]-rss_tag state = rss_link $ rss_url_link_pair state .fst-favicon_tag = favicon_link G.favicon--format_title [] = ""-format_title s = " / " ++ s
− Panda/View/Widget/Header.hs
@@ -1,18 +0,0 @@-module Panda.View.Widget.Header where- -import Panda.Helper.Env hiding (header)-import Prelude hiding ((.), (/), (^), id, span)-import qualified Panda.Config.Global as G-import Panda.View.Widget.SearchBar--header c state =- div_class_id c "header" << - [ search_bar- , site_name- ]--site_name = toHtml $- [ toHtml $ hotlink G.root ! [theclass "logo"] << ""- , h1 << G.blog_title- , div_class "description" << G.blog_subtitle- ]
− Panda/View/Widget/Helper.hs
@@ -1,20 +0,0 @@-module Panda.View.Widget.Helper where--import Panda.Helper.Env-import Prelude hiding ((.), (/), (^), id, span)- -import qualified Panda.Config.Global as G-import qualified Panda.Type.State as State-import qualified Panda.Model.Tag as Tag--rss_url_link_pair state = - if tagged - then- let tag_name = Tag.get_name uid in- link ( G.tag_id / tag_name) tag_name- else link "" "Home"- where- uid = State.uid state- tagged = uid.match "^tag/.+" .isJust- url r = G.root / r / "rss.xml"- link r s = (url r, hotlink (url r) ! [ theclass "feedlink" ] << s)
@@ -1,19 +0,0 @@-module Panda.View.Widget.Navigation where- -import Panda.Helper.Env hiding (header)-import Prelude hiding ((.), (/), (^), id, span)-import qualified Panda.Config.Global as G-import qualified Panda.Type.State as State--navigation c state = - div_class_id c "nav" <<- div_class "content" <<- ulist << G.navigation.map (nav_item (state.State.nav_location))--nav_item nav s = li ! [theclass (home_tag ++ "page_item" ++ current)] << link s where- home_tag = if s == home_nav then "first " else ""- current = if s == nav then " current_page_item" else ""- link x = if x == home_nav then home_link else static_link x--home_link = hotlink G.root << home_nav-static_link s = hotlink (G.root / "static" / s) << s.dropExtension
− Panda/View/Widget/RSS.hs
@@ -1,37 +0,0 @@-module Panda.View.Widget.RSS where---- env-import Panda.Helper.Env -- hiding (Language)-import Prelude hiding ((.), (/), (^), id)-import qualified Panda.Config.Global as G-import qualified Panda.Type.Pager as Pager-import Text.RSS-import qualified Text.RSS as RSS---- model-import Panda.Model.Post as Post---- RSS-channel_rss_template = [ RSS.Language "en-us" ]--render_rss x | G.summary_for_rss.is True = x.markup_summary.show-render_rss x | otherwise = x.markup.show--item_rss_template x = - [ Title $ x.Post.title- , Description $ x.render_rss- , Author $ G.author_email- , Link $ x.item_uri- , PubDate $ x.date- ]--item_uri x = nullURI { uriScheme = "http://", uriPath = host_link (x.uri) }-rss_uri x = nullURI { uriScheme = "http://", uriPath = host_link x }--rss categary s xs = RSS title (rss_uri link) title channel_rss_template (xs.take 20 .map item_rss_template)- .rssToXML.showXML- where- link = categary / s- title = if s.empty then G.blog_title else G.blog_title ++ " / " ++ s--host_link s = G.host_name ++ (G.root / s)
− Panda/View/Widget/SearchBar.hs
@@ -1,7 +0,0 @@-module Panda.View.Widget.SearchBar where- -import Panda.Helper.Env hiding (header)-import qualified Panda.Config.Global as G-import Prelude hiding ((/))--search_bar = div_id "search" << form ! [action (G.root / "search"), method "get"] << thediv << textfield "s"
− Panda/View/Widget/Sidebar.hs
@@ -1,37 +0,0 @@-module Panda.View.Widget.Sidebar where--import Panda.Helper.Env hiding (rss_link)-import Prelude hiding ((.), (/), (^), id, span)-import qualified Panda.Type.State as State--import qualified Data.Set as Set-import qualified Panda.Config.Global as G-import qualified Panda.Type.Sidebar as Sidebar-import Panda.View.Widget.Helper---- model-import qualified Panda.Model.Tag as Tag---- view--sidebar c state = - div_class_id c "sidebar" << unordList formatted_list- where- stock_list = - [ feed state- , tag_list $ state.State.tags- ] - custom_list = G.sidebar.map (\x -> h2 << x.Sidebar.name +++ x.markup)- formatted_list = (stock_list ++ custom_list) .intersperse hr--feed state = toHtml- [ h2 << "Subscribe"- , p ! [theclass "feed"] << rss_link state- ]--rss_link = rss_url_link_pair >>> snd-tag_list tags = toHtml- [ h2 << "Tags"- , unordList $ tags.Tag.sorted.map tag_link]- -tag_link x = ( hotlink (G.root / x.Tag.uid) << x.Tag.name ) +++ ( " (" ++ x.Tag.resources.Set.size.show ++ ")" )
− Panda/View/Widget/Template.hs
@@ -1,33 +0,0 @@-{-# OPTIONS -fno-monomorphism-restriction #-}--module Panda.View.Widget.Template (page) where--import Panda.View.Widget.Head-import Panda.View.Widget.Body-import Panda.View.Widget.Header-import Panda.View.Widget.Navigation-import Panda.View.Widget.Sidebar-import Panda.View.Widget.Footer-import qualified Text.XHtml.Strict as Html-import qualified Panda.Config.Global as G-import qualified Panda.Type.Theme as T--import Panda.Helper.Env hiding (header, body)-import Prelude hiding ((.), (/), (^), id, span)---- extension-import Panda.Extension.Analytics.T--body t state x = Html.body << - [ div_class (t.T.container) << - [ header (t.T.header) state- , navigation (t.T.navigation) state- , div_id "page" << [ div_class (t.T.main) << body_content x, sidebar (t.T.sidebar) state ]- , footer (t.T.footer)- ]- , primHtml analytics_snippet- ]--template t state x = [html_head state, body t state x]--page = template G.theme
changelog.markdown view
@@ -1,3 +1,15 @@+2008.10.25+-----------++### Feature++* Modular system: search, comment, analytics are now extensions that can be disabled / enabled++### Fix++* backward compatible with old comment format, no migration required as in 2008.10.24+* works with kibro 4.1+ 2008.10.24 ----------
panda.cabal view
@@ -1,5 +1,5 @@ Name: panda-Version: 2008.10.24+Version: 2008.10.25 Build-type: Simple Synopsis: A simple static blog engine Description: A simple static blog engine@@ -10,11 +10,12 @@ Build-Depends: base Cabal-version: >= 1.2 category: Web-homepage: http://jinjing.blog.easymic.com/static/panda/readme+homepage: http://www.haskell.org/haskellwiki/Panda data-files: readme.markdown, changelog.markdown library- build-depends: base, cgi, network, haskell98, old-locale, old-time, directory, filepath, containers, mps >= 2008.10.15, parsedate >= 3000.0.0, rss >= 3000.0.1, xhtml, kibro == 0.3, utf8-string >= 0.3.1, pandoc >= 0.46, MissingH, parsec >= 2, gravatar >= 0.3+ build-depends: base, cgi, network, haskell98, old-locale, old-time, directory, filepath, containers, mps >= 2008.10.15, parsedate >= 3000.0.0, rss >= 3000.0.1, xhtml, kibro == 0.4.1, utf8-string >= 0.3.1, pandoc >= 0.46, MissingH, parsec >= 2, gravatar >= 0.3+ hs-source-dirs: src/ exposed-modules: Panda Panda.Config.Global @@ -60,5 +61,5 @@ Panda.View.Widget.Sidebar Panda.View.Widget.Template - + Main
readme.markdown view
@@ -1,3 +1,4 @@ ## Panda: a simple blog engine in Haskell -[Panda homepage](http://www.haskell.org/haskellwiki/Panda)+* [Panda homepage](http://www.haskell.org/haskellwiki/Panda)+* [doc](doc/readme)
+ src/Main.hs view
@@ -0,0 +1,5 @@+module Main where++import Panda++main = panda
+ src/Panda.hs view
@@ -0,0 +1,10 @@+module Panda+ (+ module Panda.Controller.Application,+ panda+ ) where++import Panda.Controller.Application+import Kibro++panda = startKibro pages
+ src/Panda/Config/Global.hs view
@@ -0,0 +1,97 @@+module Panda.Config.Global where++import Panda.Helper.Helper+import MPS+import Prelude hiding ((.), (/), (^), id, readFile)++import System.FilePath hiding ((<.>))+import Control.Arrow ((>>>), (&&&), (***))+import Data.Maybe+import System.Directory++import Panda.Type.Reader+import Panda.Type.Sidebar+import Panda.Type.Theme+import Panda.Type.Extension++db_id = "db"+flat_id = "."+post_id = "blog"+config_id = "config"+tag_id = "tag"+comment_id = "comment"+sidebar_id = "sidebar"+theme_id = "theme"++db_uri = db_id+flat_uri = db_uri / flat_id++config_uri = flat_uri / config_id / "site.txt"+sidebar_uri = flat_uri / config_id / sidebar_id+post_uri = flat_uri / post_id+tag_uri = flat_uri / tag_id+comment_uri = flat_uri / comment_id+theme_uri = flat_uri / config_id / theme_id+++-- unsafe, must restart server after changing config file, sorry about that ...+-- but these configs are read only, and keep the view pure, so no monad headaches involved.++user_config = parse_config config_uri++config_for' s d = user_config.lookup s .fromMaybe d+config_for s = config_for' s s++blog_title = config_for "blog_title"+blog_subtitle = config_for "blog_subtitle"+host_name = config_for "host_name"+author_email = config_for "author_email" ++per_page = config_for' "per_page" "7" .read :: Int+navigation = config_for' "navigation" "About" .parse_list.("Home" :)++panda_url = "http://github.com/nfjinjing/panda"+root + | user_root.belongs_to ["/", ""] = "/"+ | otherwise = user_root.("/" /).remove_trailing_slash+ where user_root = config_for' "root" "/"++default_reader = Markdown++sidebar = config_for "sidebar" .parse_list .map (sidebar_uri /).select (to_utf8 >>> doesFileExist >>> purify )+ .map (read_sidebar_item default_reader >>> purify)+ +favicon = config_for' "favicon" "/favicon.ico"+++-- extensions+analytics_account_id = config_for "analytics_account_id"+extensions = config_for' "extensions" "search, comment, analytics" .parse_list .to_extensions++-- theme+default_theme = blank_theme+user_theme_name = config_for "theme"+user_theme_uri = (theme_uri / user_theme_name) ++ ".txt"++theme = if user_theme_uri.doesFileExist.purify+ then parse_config user_theme_uri .(("name", user_theme_name) : ) .to_theme+ else default_theme+ +-- custom+as_l s = "[" ++ s ++ "]"+post_date_format = config_for' "post_date_format" "%y-%m-%d"+comment_date_format = config_for' "comment_date_format" "%y-%m-%d %T"+url_date_format = config_for' "url_date_format" "%y-%m-%d"+url_date_matcher = config_for' "url_date_matcher" "\\d{2}-\\d{2}-\\d{2}"+url_title_subs = config_for' "url_title_subs" "" .as_l .read :: [(String, String)]+url_date_title_seperator = config_for' "url_date_title_seperator" " "++-- summary+cut = config_for' "cut" "✂-----" .to_utf8+parse_boolean = belongs_to ["true", "1", "y", "yes", "yeah"]+summary_for_root = config_for "summary_for_root" .parse_boolean+summary_for_tag = config_for "summary_for_tag" .parse_boolean+summary_for_rss = config_for "summary_for_rss" .parse_boolean+++
+ src/Panda/Controller/Application.hs view
@@ -0,0 +1,174 @@+-- the controller is the most complex module for a reason:+-- models are made as simple and small as possible+-- views are pure functions.++-- a note on encoding:+-- IOs are in utf-8 filter, internally use bytes++{-# OPTIONS -fno-monomorphism-restriction #-}++module Panda.Controller.Application where++import qualified Data.Map as Map++-- env+import Panda.Helper.Env hiding (tag, uri)+import Panda.Helper.StateHelper+import Prelude hiding ((.), (/), (^), id)+import qualified Panda.Type.State as S+import qualified Panda.Config.Global as G+import qualified Panda.Type.Pager as Pager+import Panda.Type.Extension++-- model+import qualified Panda.Model.Post as Post+import qualified Panda.Model.Static as Static+import qualified Panda.Model.Tag as Tag+import qualified Panda.Model.Comment as Comment++-- view+import qualified Panda.View.Control.Post as PostV+import qualified Panda.View.Widget.Template as T+import qualified Panda.View.Control.Static as StaticV+import qualified Panda.View.Control.Tag as TagV+import qualified Panda.View.Control.Search as SearchV+import qualified Panda.View.Widget.RSS as RSSV++-- helpers+-- liftIO: move to an (u)pper level monad+u = liftIO+blog_regex = G.url_date_matcher++init_post tags x = do+ x.Tag.fill_tag tags.Comment.fill_comment_size.u++tag_list = Tag.list.u+++-- main controller++pages = + [ ("$" ,index )+ , ("(\\?.+)?$" ,index )+ , ("rss.xml$" ,index_feed )+ , (blog_regex ,blog )+ , ("static/." ,static )+ , ("tag/.*/rss.xml$" ,tag_feed )+ , ("tag/." ,tag )+ , ("search" ,route_for_extension Search $ search )+ , ("comment/create" ,route_for_extension Comment $ comment_create )+ ] .map_fst ((G.root /) >>> ("^" ++))+ +++ [ ("^" ++ G.root ++ "$" ,index )+ , ("^" ++ G.root ++ "?" ,index )+ ]+ ++route_for_extension ext x = if has_extension ext then x else not_found++index = do+ posts <- Post.list.u+ p <- paginate posts+ tags <- tag_list+ let nav = if p.Pager.current == 1 then home_nav else no_navigation+ let state = S.empty { S.uid = G.post_id, S.pager = p, S.tags = tags, S.nav_location = nav }+ + posts.mapM (init_post tags) ^ PostV.list state >>= output_html++index_feed = do+ posts <- Post.list.u+ posts.RSSV.rss "" "" .output++blog = do+ id <- uri ^ Post.uri_to_id+ blog <- Post.get id .u+ comments <- Comment.list_for id .u+ tags <- tag_list+ + test_data <- S.mk_human_test .u+ let state = S.empty { S.uid = id, S.tags = tags, S.resource_title = blog.resource_title, S.human_test_data = test_data }+ blog.(init_post tags) ^ PostV.view state comments >>= output_html+++static = do+ id <- uri+ static_page <- Static.get id .u+ tags <- tag_list+ + let nav_id = static_page.Static.uid.id_to_resource+ let nav = if nav_id.belongs_to G.navigation then nav_id else no_navigation+ let state = S.empty { S.uid = id, S.tags = tags, S.nav_location = nav, S.resource_title = static_page.resource_title }+ StaticV.view state static_page .output_html++tag = do+ id <- uri+ tags <- tag_list+ let tag_name = Tag.get_name id+ case Tag.tag_map' tags .Map.lookup tag_name of+ Nothing -> not_found+ Just post_set -> do+ posts <- post_set.to_list.rsort.mapM (Post.get) .u >>= mapM (init_post tags)+ p <- paginate posts+ let state = S.empty { S.uid = id, S.pager = p, S.tags = tags, S.resource_title = tag_name.Tag.resource_title_from_name }+ posts.TagV.view state.output_html++tag_feed = do+ id <- uri ^ (split "/" >>> init >>> join "/")+ tags <- tag_list+ let tag_name = Tag.get_name id+ case Tag.tag_map' tags .Map.lookup tag_name of+ Nothing -> not_found+ Just post_set -> do+ posts <- post_set.to_list.rsort.mapM (Post.get) .u ^ map (Tag.fill_tag tags)+ posts.RSSV.rss G.tag_id tag_name.output++search = do+ s <- param_with_default "s" ""+ posts <- Post.search s .u++ p <- paginate posts+ tags <- tag_list+ query <- uri+ let state = S.empty { S.uid = query, S.pager = p, S.tags = tags, S.resource_title = query }++ posts.mapM (init_post tags) >>= \x -> x.SearchV.view state s.output_html++comment_create = do+ post_id <- input_with_default (show_data Comment.PostId) (G.post_id / "nothing")+ exists <- (G.flat_uri / post_id.to_utf8) .doesFileExist .u+ let valid_path = equalFilePath G.post_id (takeDirectory post_id)+ checked <- check_create+ + if [checked, valid_path, exists].and+ then+ inputs >>= (Comment.create >>> u)+ else+ return ()+ redirect $ (post_id.Post.id_to_uri.to_utf8).urlEncode++get_input_data = show_data >>> get_input++check_human = do+ x <- [Comment.LeftNumber, Comment.RightNumber, Comment.Operator, Comment.HumanHack].mapM get_input_data ^ map fromJust+ let [l, r, op, h] = x+ return $ S.simple_eval (l.read) (r.read) (op.S.read_op) .is (h.read)++-- create helper, should be refactored to some common aspect+check_create =+ [ validate Comment.Author ( length >>> (> 0))+ , validate Comment.AuthorLink ( const True)+ , validate Comment.Body ( length >>> (> 0))+ , validate Comment.EmptyField ( empty )+ , validate Comment.LeftNumber ( belongs_to (S.nums.map show))+ , validate Comment.RightNumber ( belongs_to (S.nums.map show))+ , validate Comment.Operator ( belongs_to (S.ops.map S.display_op))+ , validate Comment.HumanHack ( belongs_to (S.nums.map show))+ , check_human+ ] .sequence ^ and+++validate s f = do+ maybe_s <- get_input_data s+ case maybe_s of+ Nothing -> return False+ Just v -> return $ f v
+ src/Panda/Extension/Analytics/T.hs view
@@ -0,0 +1,19 @@+module Panda.Extension.Analytics.T where++import Panda.Helper.Env+import Prelude hiding ((.), (/), (^), id, span)+import qualified Panda.Config.Global as G++analytics_snippet = + [ "<script type=\"text/javascript\"> "+ , "var gaJsHost = ((\"https:\" == document.location.protocol) ? \"https://ssl.\" : \"http://www.\"); "+ , "document.write(unescape(\"%3Cscript src='\" + gaJsHost + \"google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E\")); "+ , "</script> "+ , "<script type=\"text/javascript\"> "+ , "var pageTracker = _gat._getTracker(\"UA-xxxxxx-x\"); "+ , "pageTracker._trackPageview(); "+ , "</script> "+ ]+ .map strip+ .join "\n"+ .gsub "UA-xxxxxx-x" G.analytics_account_id
+ src/Panda/Helper/Env.hs view
@@ -0,0 +1,38 @@+-- this sets some scoping for every module, which prevents importing+-- common modules in every file++module Panda.Helper.Env (+ module MPS + , module Control.Monad + , module Control.Arrow + , module Data.List + , module Network.URI + , module Network.CGI + , module System.Locale + , module System.Time.Parse+ , module Panda.Helper.Helper+ , module Data.Maybe+ , module System.FilePath+ , module System.IO.UTF8+ , module Text.XHtml.Strict+ , module System.Time+ , module System.Directory+ , module Data.Foldable+) where+ +import MPS hiding (base, col, sub, date)+import Control.Monad hiding (join)+import Control.Arrow ((>>>), (&&&), (***))+import Data.List hiding (find)+import Network.URI+import Network.CGI hiding (Html)+import System.Locale+import System.Time.Parse+import Panda.Helper.Helper+import Data.Maybe+import System.FilePath hiding ((<.>))+import System.IO.UTF8 (readFile, writeFile)+import Text.XHtml.Strict hiding (select)+import System.Time+import System.Directory+import Data.Foldable (find)
+ src/Panda/Helper/Helper.hs view
@@ -0,0 +1,138 @@+-- helper module is a central place for reusable functions+-- for this project, more general helpers are usually moved+-- to MPS package, for multi-project usage++{-# OPTIONS -fno-monomorphism-restriction #-}++module Panda.Helper.Helper where++import System.IO.UTF8 (readFile, writeFile)+import Network.URI+import Network.CGI+import System.FilePath.Posix ((</>))+import Control.Arrow ((>>>), (&&&))+import Text.XHtml.Strict+import Control.Monad hiding (join)+import Data.Maybe+import Panda.Type.Pager as Pager hiding (empty)+import MPS hiding (date)+import Prelude hiding ((.), (/), (^), id, readFile, writeFile)+import Char+import Data.List+import qualified Data.List as L +import Panda.Type.Reader+import System.FilePath.Posix hiding ((<.>))+import System.Time+++(/) = (</>)+infixl 5 /++(^) = (<.>)+infixl 9 ^++-- global+parse_config_io s = readFile s ^ (\x -> x.filter_comment.lines.map strip .map (split "\\s*=\\s*") .map fill_snd_blank .map tuple2)+ where fill_snd_blank [x] = [x,""]+ fill_snd_blank xs = xs+parse_config = parse_config_io >>> purify++write_config_io s xs = xs.map(\(x, y) -> x ++ " = " ++ y) .join "\n" .writeFile s++-- model+take_extension = takeExtension >>> split "\\." >>> last++-- controller+raw_uri = requestURI ^ (uriPath >>> urlDecode >>> tail >>> remove_trailing_slash >>> from_utf8 )+remove_trailing_slash s = if s.last.is '/' then s.init else s+ +params = do+ query_string <- requestURI ^ uriQuery + case query_string of+ '?':s -> s.formDecode .map_snd unescape_unicode_xml.return+ otherwise -> return []++inputs = getInputs ^ map_snd (strip >>> unescape_unicode_xml)++param_with_default s d = params ^ (lookup s >>> fromMaybe d )+input_with_default s d = inputs ^ (lookup s >>> fromMaybe d )++get_param s = params ^ lookup s+get_input s = inputs ^ lookup s++just_param s = get_param s ^ fromJust+just_input s = get_input s ^ fromJust++full_paginate length total = liftM5 ( Pager length ) current has_next has_previous next previous where+ current = param_with_default "page" "1" ^ read+ has_next = current ^ ( (* length) >>> (< total.fromIntegral) )+ has_previous = current ^ (> 1)+ next = current ^ (+ 1)+ previous = current ^ (+ (-1) )+++for_current_page p xs = xs.drop ((p.Pager.current - 1) * p.Pager.length) .take (p.Pager.length)++-- id: /type/resoruce+id_to_type id = id.split "/" .first+id_to_resource id = id.split "/" .tail.join "/"+no_navigation = ""+home_nav = "Home"+++-- view+id = identifier++css_link l = itag "link" ! [rel "stylesheet", thetype "text/css", href l]+js_link l = itag "script" ! [thetype "text/javascript", src l]+js_src s = tag "script" ! [thetype "text/javascript"] << s+rss_link l = itag "link" ! [rel "alternate", thetype "application/rss+xml", href l, title "RSS 2.0"]+favicon_link l = itag "link" ! [rel "icon", thetype "image/png", href l]+meta_tag = meta ! [httpequiv "Content-Type", content "text/html; charset=utf-8"]++div_id s = thediv ! [id s]+div_class s = thediv ! [theclass s]+div_class_id x y = thediv ! [theclass x, id y]++output_html = output ... renderHtml++spaced_url = gsub "/" " / "++empty_html = toHtml ""++-- config+parse_list s = s.split "," .map strip .reject null+not_found = getVar "REQUEST_URI" >>= (fromMaybe "" >>> outputNotFound)+++-- generic++starts_with _ [] = False+starts_with (x:xs) (y:ys) | x == y = starts_with xs ys+ | otherwise = False++ends_with x y = starts_with (x.reverse) (y.reverse)++capitalize (x:xs) = [x].upper ++ xs.lower+camel_case = split "_" >>> map capitalize >>> join'+snake_case = gsub "\\B[A-Z]" "_\\&" >>> lower+show_data = show >>> snake_case+ ++-- class+class DataRenderer a where+ render_data :: a -> Html++class Resource a where+ resource_title :: a -> String++class Markable a where+ markup :: a -> Html+ +class Datable a where+ date :: a -> CalendarTime++class Addressable a where+ uri :: a -> String++
+ src/Panda/Helper/StateHelper.hs view
@@ -0,0 +1,46 @@+-- trying to merge this helper with the generic one results in cyclic imports++{-# OPTIONS -fno-monomorphism-restriction #-}++module Panda.Helper.StateHelper where++import Panda.Helper.Env+import qualified Panda.Config.Global as G+import Prelude hiding ((.), (/), (^), id)+++-- G.root = /blog+-- raw_uri = blog/x+-- full_uri = /blog/x+-- uri = full_uri - (/blog/)+remove_root s+ | G.root.is "/" = s+ | otherwise = s.slice (G.root.length) (s.length)++uri = raw_uri ^ remove_root+-- uri = raw_uri+++-- global+parse_date format s = case maybe_d of+ Nothing -> Nothing+ Just d -> Just $ if d.ctYear < 1910 then d {ctYear = d.ctYear + 100} else d + where+ maybe_d = parseCalendarTime defaultTimeLocale format s+ +format_date = formatCalendarTime defaultTimeLocale+default_date = parse_date "%Y-%m-%d %T" "2000-1-1 00:00:00".fromJust+parse_post_date = parse_date G.post_date_format+default_parse_date s = s.parse_post_date .fromMaybe default_date++has_extension x = G.extensions.has x+for_extension ext x = if has_extension ext then x else toHtml ""++-- controller+paginate = length >>> full_paginate (G.per_page)++cut = G.cut+cut_re = "^\\s*" ++ cut+match_cut = to_utf8 >>> match cut_re+is_cut = match_cut >>> isJust+split_cut = to_utf8 >>> split cut_re
+ src/Panda/Model/Comment.hs view
@@ -0,0 +1,109 @@+module Panda.Model.Comment where++-- env+import Panda.Helper.Env hiding (title, body, size, path, meta)+import Prelude hiding ((.), (/), (^), id, readFile, writeFile)+import qualified Panda.Config.Global as G+import Panda.Type.Reader+import qualified Panda.Model.Post as Post+import System.Directory+import Panda.Helper.StateHelper+import Network.Gravatar++data Comment = Comment+ { uid :: String -- comment/08-09-04 blog title+ , author :: String+ , body :: String+ , author_email :: String+ , author_link :: String+ }+ deriving (Show, Eq)++data CommentData = + Author+ | AuthorEmail+ | AuthorLink+ | Body+ | PostId+ deriving (Show)++data SpamFilter = + HumanHack + | EmptyField+ | LeftNumber+ | RightNumber+ | Operator+ deriving (Show)++instance Resource Comment where+ resource_title = uid >>> spaced_url+ +instance Markable Comment where+ markup x = render_to_html Markdown (x.body)++instance Datable Comment where+ date x = x.uid.split "/".last.default_parse_date+++-- CRUD+list_for post_id = do+ has_comments <- doesDirectoryExist d+ if has_comments+ then ls d ^ reject (match "\\.meta$" >>> isJust) ^ rsort ^ map (G.comment_id / r /) >>= mapM (from_utf8 >>> get)+ else return []+ where+ d = (G.comment_uri / r)+ r = post_id.id_to_resource.to_utf8++path id = G.flat_uri / id.to_utf8+meta = (++ ".meta")++get id = do+ meta_exists <- id.path.meta.doesFileExist+ if meta_exists+ then get_from_new_format id+ else get_from_old_format id++get_from_new_format id = do+ body <- id.path.readFile+ meta <- id.path.meta.parse_config_io+ let at s = meta.lookup (s.show_data) .fromJust+ let author = at Author+ let author_email = at AuthorEmail+ let author_link = at AuthorLink+ return $ Comment id author body author_email author_link++get_from_old_format id = do+ xs <- id.path.readFile ^ lines+ let author = xs.first+ let author_email = ""+ let author_link = xs.drop 2.first+ let body = xs.drop 4.unlines+ return $ Comment id author body author_email author_link++create h = do+ let at s = h.lookup (s.show_data) .fromJust+ let body = at Body+ let post_id = at PostId++ timestamp <- ( getClockTime >>= toCalendarTime ) ^ format_date G.comment_date_format+ let comment_path = post_id_to_uid post_id .to_utf8+ createDirectoryIfMissing True comment_path+ + let uid = comment_path / timestamp+ writeFile uid body+ let meta = [Author, AuthorLink, AuthorEmail] .labeling at .map_fst show_data+ write_config_io (uid ++ ".meta") meta++-- extra+from_post_id x = Comment (post_id_to_uid x) "" "" "" ""++post_id_to_uid x = G.flat_uri / G.comment_id / x.split "/" .last+uid_to_post_id x = G.post_id / x.split "/" .last++fill_comment_size x = do+ size <- x.Post.uid.list_for ^ length + return $ x { Post.comment_size = size }++gravatar_default_size = size 40+gravatar_link x = gravatarWith (x.author_email) Nothing gravatar_default_size Nothing
+ src/Panda/Model/Post.hs view
@@ -0,0 +1,78 @@+-- what about the performance?+-- Haskell takes care of that, since IOs are also lazy.+-- Posts are not read unless specifically required, i.e. after pagination++module Panda.Model.Post where++-- env+import Panda.Helper.Env hiding (match, title, body, date)+import qualified MPS as MPS+import Prelude hiding ((.), (/), (^), id, readFile)+import qualified Panda.Config.Global as G+import Panda.Type.Reader+import Panda.Helper.StateHelper hiding (uri)+import Panda.Helper.Helper (date)++data Post = Post + { uid :: String -- blog/08-09-04 blog title+ , title :: String+ , body :: String+ , tags :: [String]+ , comment_size :: Int+ , reader :: Reader+ }+ deriving (Show, Eq)++instance Resource Post where+ resource_title = title++instance Markable Post where+ markup x = render_to_html (x.reader) (x.full)+ +instance Datable Post where+ date = uid >>> get_date++instance Addressable Post where+ uri = uid >>> id_to_uri++-- CRUD+list = ls G.post_uri ^ map from_utf8 ^ rsort ^ map (G.post_id /) >>= mapM get++get id = liftM4 (Post id (get_title id) ) (get_body id) (return []) (return 0) (return $ get_reader id)+get_extension = takeExtension+get_title id = id.words.tail.unwords.dropExtension+get_body id = (G.flat_uri / id.to_utf8) .readFile+get_reader id = id.take_extension.guess_reader.fromMaybe G.default_reader+get_date id = id.words.first.split "/".last.default_parse_date++match s x = [title, body] .map (send_to x >>> lower >>> isInfixOf (s.lower)) .or+search "" = return []+search s = list ^ filter (match s)++summary = body >>> split_cut >>> first >>> from_utf8+full x | x.body.match_cut.isNothing = x.body+full x = ( xs.takeWhile not_cut ++ xs.dropWhile not_cut .tail ).unlines where+ not_cut = is_cut >>> not+ xs = x.body.lines++has_continue = body >>> match_cut >>> isJust+++-- extra+id_to_uri id = G.root / ( pretty_date ++ G.url_date_title_seperator ++ formatted_title ++ ext ) where+ formatted_title = G.url_title_subs.map (\(a,b) -> gsub a b).inject (id.get_title) apply+ pretty_date = id.get_date.format_date G.url_date_format+ ext = id.get_extension++uri_to_id s = G.post_id / (d ++ " " ++ t) where+ (raw_d, (prefix, title_with_sep)) = s.MPS.match G.url_date_matcher.fromJust.fst+ raw_t = title_with_sep.drop (G.url_date_title_seperator.length)+ t = G.url_title_subs.map (\(a,b) -> gsub b a) .inject raw_t apply+ d = raw_d.parse_date G.url_date_format .fromJust.format_date G.post_date_format+++-- summary+markup_summary x = post_summary x +++ next where+ post_summary = (reader &&& summary) >>> splash render_to_html+ next = if x.has_continue then toHtml $ hotlink (x.uri) << "Read the rest of the post »" else empty_html+
+ src/Panda/Model/Static.hs view
@@ -0,0 +1,28 @@+module Panda.Model.Static where++-- env+import Panda.Helper.Env hiding (match, body)+import Prelude hiding ((.), (/), (^), id, readFile)+import qualified Panda.Config.Global as G+import Panda.Type.Reader++data Static = Static + { uid :: String+ , body :: String+ , reader :: Reader+ }+ deriving (Show, Eq)++instance Resource Static where+ resource_title = uid >>> spaced_url++instance Markable Static where+ markup x = render_to_html (x.reader) (x.body)+ +-- CRUD+get id = liftM2 (Static id) (get_body id) (return $ get_reader id)+get_title = id_to_resource >>> dropExtension+get_body id = (G.flat_uri / id.to_utf8) .readFile+get_reader id = id.take_extension.guess_reader.fromMaybe G.default_reader++title = uid >>> get_title
+ src/Panda/Model/Tag.hs view
@@ -0,0 +1,39 @@+module Panda.Model.Tag where++import qualified Panda.Model.Post as Post++-- env+import Panda.Helper.Env hiding (name)+import Prelude hiding ((.), (/), (^), id, readFile)+import qualified Data.Set as S+import qualified Panda.Config.Global as G+++data Tag = Tag + { uid :: String -- tag/name+ , name :: String+ , resources :: S.Set String+ }+ deriving (Show, Eq)++instance Resource Tag where+ resource_title = uid >>> spaced_url++-- CRUD+list = ls G.tag_uri ^ map from_utf8 ^ map (G.tag_id /) >>= mapM get++get id = get_resources id ^ Tag id (get_name id)+get_name id = id.split "/" .tail.join'+get_resources id = (G.flat_uri / id.to_utf8) .readFile+ ^ filter_comment ^ lines ^ map (G.post_id / ) ^ to_set++resource_title_from_name = name_to_id >>> spaced_url+tag_map' xs = xs . map (name &&& resources) . to_h+tag_map = list ^ tag_map'++for_resource xs x = xs.select (resources >>> has x) .map name+fill_tag xs x = x { Post.tags = for_resource xs (x.Post.uid) }++-- extra+sorted xs = xs.sortBy(compare_by (resources >>> S.size)).reverse+name_to_id x = G.tag_id / x
+ src/Panda/Type/Pager.hs view
@@ -0,0 +1,13 @@+module Panda.Type.Pager where+ +data Pager = Pager {+ length :: Int,+ current :: Int,+ has_next :: Bool,+ has_previous :: Bool,+ next :: Int,+ previous :: Int+ }+ deriving (Eq, Show)+ +empty = Pager 0 0 False False 0 0
+ src/Panda/Type/Reader.hs view
@@ -0,0 +1,37 @@+{-# OPTIONS -XDeriveDataTypeable #-}++module Panda.Type.Reader where++import MPS +import Prelude hiding ((.), (/), (^), id)+import qualified Data.Map as Map+import Text.Pandoc+import Control.Arrow ((>>>))+import Text.XHtml.Strict+import Data.Generics++data Reader = Markdown | RST | HTML | Latex deriving (Show, Eq, Typeable, Data)++readers = + [ (Markdown, ["markdown", "md"])+ , (RST, ["rst"] )+ , (HTML, ["html", "htm"])+ , (Latex, ["tex", "latex"])+ ]++gen_lookup (r, xs) = xs.labeling (const r)+reader_map = readers.map gen_lookup.join'.to_h+guess_reader ext = reader_map.Map.lookup ext+++to_html r = r defaultParserState >>> writeHtml defaultWriterOptions++-- this list can go on, as long as there is a library that does+-- the convertion. pretty extensible, isn't it.+rr Markdown = to_html readMarkdown+rr RST = to_html readRST+rr HTML = primHtml+rr Latex = to_html readLaTeX++render_to_html = rr+
+ src/Panda/Type/Sidebar.hs view
@@ -0,0 +1,20 @@+module Panda.Type.Sidebar where++import Panda.Helper.Env hiding (body)+import Prelude hiding ((.), (/), (^), id, readFile)+import Panda.Type.Reader++data SidebarItem = SidebarItem+ { name :: String+ , body :: String+ , reader :: Reader+ }+ deriving (Show, Eq)++instance Markable SidebarItem where+ markup x = render_to_html (x.reader) (x.body)++read_sidebar_item default_reader s = liftM2 (SidebarItem name) body (return reader) where+ body = s.to_utf8.readFile+ reader = s.take_extension.guess_reader.fromMaybe default_reader+ name = s.takeFileName.dropExtension
+ src/Panda/Type/State.hs view
@@ -0,0 +1,65 @@+module Panda.Type.State where++import qualified Panda.Type.Pager as Pager+import Panda.Model.Tag+import Random (randomRs, mkStdGen)+import System.Time+import Control.Arrow ((>>>), (&&&), (***))+import MPS+import Prelude hiding ((.), (/), (^), id, readFile, writeFile)++data State = State+ -- model state+ { uid :: String -- current view resource+ , pager :: Pager.Pager -- pager++ -- theme state+ , tags :: [Tag]+ , nav_location :: String+ , resource_title :: String+ , human_test_data :: HumanTestData+ }+ deriving (Show)++show_left = human_test_data >>> left >>> show+show_right = human_test_data >>> right >>> show+show_op = human_test_data >>> op >>> display_op where++read_op "+" = Plus+read_op "-" = Minus+read_op x = error ("can not read operator: " ++ x)++display_op Plus = "+"+display_op Minus = "-"++data HumanTestData = HumanTestData + { left :: Int+ , right :: Int+ , op :: Op+ } deriving (Show)++data Op = Plus | Minus deriving (Show)++empty = State "" Pager.empty [] "" "" empty_human_test+empty_human_test = HumanTestData 0 0 Plus+++(^) = (<.>)+infixl 9 ^++ops = [Plus, Minus]+nums = [0, 5, 10, 15, 20]+simple_eval a b Plus = a + b+simple_eval a b Minus = a - b+++mk_human_test = do+ seed <- (getClockTime >>= toCalendarTime) ^ ctPicosec ^ fromIntegral+ let (a,b,c) = randomRs (0,100) (mkStdGen seed) .in_group_of 3 .map make_sample .dropWhile (good_test >>> not) .first+ return $ HumanTestData a b c++ where+ make_sample [a,b,c] = ((get_num a), (get_num b), (get_op c))+ good_test = splash3 simple_eval >>> belongs_to nums+ get_num n = nums.at (n `mod` (nums.length))+ get_op n = ops.at (n `mod` (ops.length))
+ src/Panda/Type/Theme.hs view
@@ -0,0 +1,44 @@+module Panda.Type.Theme where++import Prelude hiding ((.))+import Panda.Helper.Env hiding (name, header)++data Theme = Theme+ { name :: String+ , container :: String+ , header :: String+ , navigation :: String+ , main :: String+ , sidebar :: String+ , footer :: String+ , css :: [String]+ , js :: [String]+ } deriving (Show, Read)+ +to_theme xs = Theme + { name = at "name"+ , container = at "container"+ , header = at "header"+ , navigation = at "navigation"+ , main = at "main"+ , sidebar = at "sidebar"+ , footer = at "footer"+ , css = at "css" .css_list+ , js = at "js" .js_list+ }+ where+ at s = xs.lookup s .fromJust+ css_list s = s.parse_list.map (\x -> "/theme/" ++ at "name" ++ "/css/" ++ x ++ ".css")+ js_list s = s.parse_list.map (\x -> "/theme/" ++ at "name" ++ "/js/" ++ x ++ ".js")+ +blank_theme = Theme+ { name = ""+ , container = ""+ , header = ""+ , navigation = ""+ , main = ""+ , sidebar = ""+ , footer = ""+ , css = [""]+ , js = [""]+ }
+ src/Panda/View/Atom/Comment.hs view
@@ -0,0 +1,66 @@+module Panda.View.Atom.Comment where+ +import Panda.Helper.Env hiding (title)+import Prelude hiding ((.), (/), (^), id, span)+import qualified Panda.Config.Global as G+import Panda.Helper.StateHelper+import Panda.Type.State hiding (uid)++-- view+import qualified Panda.View.Atom.Tag as Tag++-- model+import Panda.Model.Comment hiding (create)++-- render+instance DataRenderer Comment where+ render_data = entry+ +entry x = toHtml + [ gravatar+ , cite << a+ , toHtml " says:"+ , br+ , p ! [theclass "small"] << comment_date x+ , x.markup+ ]+ where+ a = if x.author_link.null then x.author.toHtml else toHtml $ hotlink (x.author_link) << x.author+ gravatar = thediv ! [theclass "gravatar"] << image ! [src (gravatar_link x)]++comment_date x = toHtml $ x.date.format_date "%b %e, %Y at %I:%M %p"+++-- form+human_test_question state = ["What", "is", state.show_left, state.show_op, state.show_right, "?"] .join " "++create state x = toHtml+ [ h2 ! [id "respond"] << "Leave a Response"+ , gui (G.root / "comment/create") ! [id "commentform"] <<+ [ field Author 22 1 "Name (required)"+ , field AuthorEmail 22 2 "Email (hidden)"+ , field AuthorLink 22 3 "Website"+ , field HumanHack 22 4 (human_test_question state)+ , empty_field+ , hidden_field LeftNumber (state.show_left)+ , hidden_field RightNumber (state.show_right)+ , hidden_field Operator (state.show_op)+ , p << hidden (show_data PostId) (x.uid.uid_to_post_id)+ , p << textarea ! [name (show_data Body), id "comment", cols "10", rows "20", strAttr "tabindex" "5"] << ""+ , p << submit "submit" "Submit Comment" ! [strAttr "tabindex" "6"]+ ]+ ]++field_with_value v x' s t m = p <<+ [ label ! [thefor x] << small << m+ , br + , textfield x ! [size (s.show), strAttr "tabindex" (t.show), value v]+ ]+ where x = x'.show_data++field x s t m = field_with_value "" x s t m++hidden_field x m = hidden_field_with_value m x m+hidden_field_with_value v x m = thespan ! [ thestyle "display: none;" ] << field_with_value v x 22 10 m++empty_field = hidden_field_with_value "" EmptyField "Leave this field empty:"
+ src/Panda/View/Atom/Post.hs view
@@ -0,0 +1,39 @@+module Panda.View.Atom.Post where+ +import Panda.Helper.Env hiding (name, title)+import Prelude hiding ((.), (/), (^), id, span)+import qualified Panda.Config.Global as G+import Panda.Helper.StateHelper hiding (uri)++-- view+import qualified Panda.View.Atom.Tag as Tag++-- model+import Panda.Model.Post++-- render+instance DataRenderer Post where+ render_data = entry Full++data RenderStyle = Summary | Full++-- instance helpers+entry style x = div_class "post" << [entry_title, entry_mark, entry_body] where+ entry_title = x.title_link+ entry_body = div_class "entry" << show_content style x+ entry_mark = p ! [theclass "small"] << [ post_date, post_tags, post_comments ].map (send_to x)++show_content Summary x = x.markup_summary+show_content Full x = x.markup++-- post could also be a summary+render_summary x = if x.is True then entry Summary else render_data++title_link x = h2 << hotlink (x.uri) << x.title+post_date x = toHtml $ x.date.format_date "%b %e, %Y"++post_tags x | x.tags.null = empty_html+post_tags x = " | " +++ "Published in " +++ x.tags.map Tag.tag_link .intersperse (", ".toHtml)++post_comments x | x.comment_size.is 0 = empty_html+post_comments x = " | " +++ hotlink ( x.uri / "#comments") << (x.comment_size.show ++ " Comments")
+ src/Panda/View/Atom/Static.hs view
@@ -0,0 +1,7 @@+module Panda.View.Atom.Static where+ +import Panda.Helper.Env+import Panda.Model.Static++instance DataRenderer Static where+ render_data = markup
+ src/Panda/View/Atom/Tag.hs view
@@ -0,0 +1,12 @@+module Panda.View.Atom.Tag where+ +import Panda.Helper.Env hiding (name)+import Panda.Model.Tag+import qualified Panda.Config.Global as G+import Prelude hiding ((/))++instance DataRenderer Tag where+ render_data = name >>> tag_link++tag_link s = toHtml $ hotlink (G.root / G.tag_id / s ) << s +
+ src/Panda/View/Control/Comment.hs view
@@ -0,0 +1,25 @@+module Panda.View.Control.Comment where+ +import Panda.Helper.Env hiding (body, date)+import Prelude hiding ((.), (/), (^), id, span)+import qualified Panda.Type.State as State++import Panda.Model.Comment hiding (create)+import qualified Panda.Model.Post as Post++-- view+import Panda.View.Control.Helper+import Panda.View.Widget.Template+import Panda.View.Atom.Comment as Comment+++-- api+styled_entry alt x = li ! [theclass alt] << x.render_data++list [] = []+list xs = + [ h2 ! [id "comments"] << "Responses"+ , olist ! [theclass "commentlist" ] << xs.zip (cycle ["comments-alt", ""]).map (splash styled_entry)+ ]+ +create = Comment.create
+ src/Panda/View/Control/Helper.hs view
@@ -0,0 +1,26 @@+module Panda.View.Control.Helper where++import Panda.Helper.Env+import Prelude hiding ((.), (/), (^), id, span)+ +import Panda.Type.Pager as Pager+import qualified Panda.Config.Global as G+++nav p r = + [ div_class "alignleft" << nav_previous p r+ , div_class "alignright" << nav_next p r+ ]+ +nav_previous p r = if p.Pager.has_previous + then toHtml $ hotlink ( r' ++ "page=" ++ p.Pager.previous.show ) << previous_sign+ else spaceHtml+ where r' = if isSuffixOf "&" r then r else r ++ "?"+ +nav_next p r = if p.Pager.has_next+ then toHtml $ hotlink ( r' ++ "page=" ++ p.Pager.next.show ) << next_sign+ else spaceHtml+ where r' = if isSuffixOf "&" r then r else r ++ "?"++next_sign = "Next Entries »"+previous_sign = "« Previous Entries"
+ src/Panda/View/Control/Post.hs view
@@ -0,0 +1,30 @@+module Panda.View.Control.Post where++-- env+import Panda.Helper.Env+import Prelude hiding ((.), (/), (^), id, span)++import qualified Panda.Type.State as State+import qualified Panda.Config.Global as G++import Panda.Type.Extension+import Panda.Helper.StateHelper++-- model+import qualified Panda.Model.Comment as Comment+import Panda.View.Atom.Post+import Panda.Model.Post++-- view+import Panda.View.Control.Helper+import qualified Panda.View.Control.Comment as CommentV+import Panda.View.Widget.Template++-- entry view+view state xs x = (x.render_data +++ comment_view ).page state+ where comment_view = for_extension Comment $ CommentV.list xs +++ CommentV.create state (x.uid.Comment.from_post_id)++-- list view+list state = for_current_page p >>> map render >>> (+++ nav p G.root) >>> page state where + p = state.State.pager+ render = render_summary G.summary_for_root
+ src/Panda/View/Control/Search.hs view
@@ -0,0 +1,15 @@+module Panda.View.Control.Search where+ +import Panda.Helper.Env+import Prelude hiding ((.), (/), (^), id, span)+import qualified Panda.Type.State as State+import qualified Panda.Config.Global as G++-- view+import Panda.View.Atom.Post+import Panda.View.Control.Helper+import Panda.View.Widget.Template++view state s = for_current_page p >>> map render_data >>> (+++ nav p ( (G.root / "search") ++ "?s=" ++ s ++ "&")) >>> page state+ where p = state.State.pager+
+ src/Panda/View/Control/Static.hs view
@@ -0,0 +1,7 @@+module Panda.View.Control.Static where+ +import Panda.Helper.Env+import Panda.View.Atom.Static+import Panda.View.Widget.Template++view state = render_data >>> page state
+ src/Panda/View/Control/Tag.hs view
@@ -0,0 +1,18 @@+module Panda.View.Control.Tag where+ +import Panda.Helper.Env+import Prelude hiding ((.), (/), (^), id, span)+import qualified Panda.Type.State as State+import qualified Panda.Config.Global as G++-- view+import Panda.View.Atom.Post+import Panda.View.Control.Helper+import Panda.View.Widget.Template+++view state = for_current_page p >>> map render >>> (+++ nav p ( G.root / tag_id)) >>> page state where+ p = state.State.pager+ tag_id = state.State.uid+ render = render_summary G.summary_for_tag+
+ src/Panda/View/Widget/Body.hs view
@@ -0,0 +1,8 @@+module Panda.View.Widget.Body where++-- env+import Panda.Helper.Env++body_content x = div_id "maincontent" <<+ div_class "content" << x+
@@ -0,0 +1,17 @@+module Panda.View.Widget.Footer where+ +import Panda.Helper.Env+import Prelude hiding ((.), (/), (^), id, span)+import qualified Panda.Config.Global as G+++footer c = div_class_id c "footer" <<+ [ toHtml $ copyright+ , toHtml $ "2008 "+ , toHtml $ G.blog_title+ , toHtml $ br+ , toHtml $ "Powered by "+ , toHtml $ hotlink G.panda_url << "Panda"+ , toHtml $ " using "+ , toHtml $ hotlink "http://www.haskell.org/" << "Haskell"+ ]
+ src/Panda/View/Widget/Head.hs view
@@ -0,0 +1,17 @@+module Panda.View.Widget.Head where++import Panda.Helper.Env+import Prelude hiding ((.), (/), (^), id, span)+import qualified Panda.Config.Global as G+import Panda.Type.Theme hiding (header)+import qualified Panda.Type.State as S+import Panda.View.Widget.Helper++html_head state = header << ([meta_tag, title_tag state, favicon_tag, rss_tag state] ++ G.theme.css.map css_link ++ G.theme.js.map js_link )++title_tag state = thetitle << [G.blog_title ++ state.S.resource_title.format_title]+rss_tag state = rss_link $ rss_url_link_pair state .fst+favicon_tag = favicon_link G.favicon++format_title [] = ""+format_title s = " / " ++ s
+ src/Panda/View/Widget/Header.hs view
@@ -0,0 +1,18 @@+module Panda.View.Widget.Header where+ +import Panda.Helper.Env hiding (header)+import Prelude hiding ((.), (/), (^), id, span)+import qualified Panda.Config.Global as G+import Panda.View.Widget.SearchBar++header c state =+ div_class_id c "header" << + [ search_bar+ , site_name+ ]++site_name = toHtml $+ [ toHtml $ hotlink G.root ! [theclass "logo"] << ""+ , h1 << G.blog_title+ , div_class "description" << G.blog_subtitle+ ]
+ src/Panda/View/Widget/Helper.hs view
@@ -0,0 +1,20 @@+module Panda.View.Widget.Helper where++import Panda.Helper.Env+import Prelude hiding ((.), (/), (^), id, span)+ +import qualified Panda.Config.Global as G+import qualified Panda.Type.State as State+import qualified Panda.Model.Tag as Tag++rss_url_link_pair state = + if tagged + then+ let tag_name = Tag.get_name uid in+ link ( G.tag_id / tag_name) tag_name+ else link "" "Home"+ where+ uid = State.uid state+ tagged = uid.match "^tag/.+" .isJust+ url r = G.root / r / "rss.xml"+ link r s = (url r, hotlink (url r) ! [ theclass "feedlink" ] << s)
@@ -0,0 +1,19 @@+module Panda.View.Widget.Navigation where+ +import Panda.Helper.Env hiding (header)+import Prelude hiding ((.), (/), (^), id, span)+import qualified Panda.Config.Global as G+import qualified Panda.Type.State as State++navigation c state = + div_class_id c "nav" <<+ div_class "content" <<+ ulist << G.navigation.map (nav_item (state.State.nav_location))++nav_item nav s = li ! [theclass (home_tag ++ "page_item" ++ current)] << link s where+ home_tag = if s == home_nav then "first " else ""+ current = if s == nav then " current_page_item" else ""+ link x = if x == home_nav then home_link else static_link x++home_link = hotlink G.root << home_nav+static_link s = hotlink (G.root / "static" / s) << s.dropExtension
+ src/Panda/View/Widget/RSS.hs view
@@ -0,0 +1,37 @@+module Panda.View.Widget.RSS where++-- env+import Panda.Helper.Env -- hiding (Language)+import Prelude hiding ((.), (/), (^), id)+import qualified Panda.Config.Global as G+import qualified Panda.Type.Pager as Pager+import Text.RSS+import qualified Text.RSS as RSS++-- model+import Panda.Model.Post as Post++-- RSS+channel_rss_template = [ RSS.Language "en-us" ]++render_rss x | G.summary_for_rss.is True = x.markup_summary.show+render_rss x | otherwise = x.markup.show++item_rss_template x = + [ Title $ x.Post.title+ , Description $ x.render_rss+ , Author $ G.author_email+ , Link $ x.item_uri+ , PubDate $ x.date+ ]++item_uri x = nullURI { uriScheme = "http://", uriPath = host_link (x.uri) }+rss_uri x = nullURI { uriScheme = "http://", uriPath = host_link x }++rss categary s xs = RSS title (rss_uri link) title channel_rss_template (xs.take 20 .map item_rss_template)+ .rssToXML.showXML+ where+ link = categary / s+ title = if s.empty then G.blog_title else G.blog_title ++ " / " ++ s++host_link s = G.host_name ++ (G.root / s)
+ src/Panda/View/Widget/SearchBar.hs view
@@ -0,0 +1,9 @@+module Panda.View.Widget.SearchBar where+ +import Panda.Helper.Env hiding (header)+import qualified Panda.Config.Global as G+import Prelude hiding ((/))+import Panda.Type.Extension+import Panda.Helper.StateHelper++search_bar = for_extension Search $ div_id "search" << form ! [action (G.root / "search"), method "get"] << thediv << textfield "s"
+ src/Panda/View/Widget/Sidebar.hs view
@@ -0,0 +1,37 @@+module Panda.View.Widget.Sidebar where++import Panda.Helper.Env hiding (rss_link)+import Prelude hiding ((.), (/), (^), id, span)+import qualified Panda.Type.State as State++import qualified Data.Set as Set+import qualified Panda.Config.Global as G+import qualified Panda.Type.Sidebar as Sidebar+import Panda.View.Widget.Helper++-- model+import qualified Panda.Model.Tag as Tag++-- view++sidebar c state = + div_class_id c "sidebar" << unordList formatted_list+ where+ stock_list = + [ feed state+ , tag_list $ state.State.tags+ ] + custom_list = G.sidebar.map (\x -> h2 << x.Sidebar.name +++ x.markup)+ formatted_list = (stock_list ++ custom_list) .intersperse hr++feed state = toHtml+ [ h2 << "Subscribe"+ , p ! [theclass "feed"] << rss_link state+ ]++rss_link = rss_url_link_pair >>> snd+tag_list tags = toHtml+ [ h2 << "Tags"+ , unordList $ tags.Tag.sorted.map tag_link]+ +tag_link x = ( hotlink (G.root / x.Tag.uid) << x.Tag.name ) +++ ( " (" ++ x.Tag.resources.Set.size.show ++ ")" )
+ src/Panda/View/Widget/Template.hs view
@@ -0,0 +1,35 @@+{-# OPTIONS -fno-monomorphism-restriction #-}++module Panda.View.Widget.Template (page) where++import Panda.View.Widget.Head+import Panda.View.Widget.Body+import Panda.View.Widget.Header+import Panda.View.Widget.Navigation+import Panda.View.Widget.Sidebar+import Panda.View.Widget.Footer+import qualified Text.XHtml.Strict as Html+import qualified Panda.Config.Global as G+import qualified Panda.Type.Theme as T++import Panda.Helper.Env hiding (header, body)+import Prelude hiding ((.), (/), (^), id, span)++-- extension+import Panda.Extension.Analytics.T+import Panda.Type.Extension+import Panda.Helper.StateHelper++body t state x = Html.body << + [ div_class (t.T.container) << + [ header (t.T.header) state+ , navigation (t.T.navigation) state+ , div_id "page" << [ div_class (t.T.main) << body_content x, sidebar (t.T.sidebar) state ]+ , footer (t.T.footer)+ ]+ , for_extension Analytics $ primHtml analytics_snippet+ ]++template t state x = [html_head state, body t state x]++page = template G.theme