panda 2008.10.18 → 2008.10.19
raw patch · 17 files changed
+210/−221 lines, 17 files
Files
- Panda/Config/Global.hs +12/−2
- Panda/Controller/Application.hs +50/−49
- Panda/Helper/Env.hs +1/−1
- Panda/Helper/Helper.hs +12/−12
- Panda/Helper/StateHelper.hs +17/−0
- Panda/Model/Comment.hs +22/−18
- Panda/Model/Post.hs +27/−8
- Panda/Model/Static.hs +1/−0
- Panda/Model/Tag.hs +6/−4
- Panda/View/Atom/Comment.hs +9/−7
- Panda/View/Atom/Post.hs +9/−9
- Panda/View/Atom/Tag.hs +1/−1
- Panda/View/Widget/Helper.hs +1/−1
- Panda/View/Widget/RSS.hs +4/−7
- changelog.markdown +8/−0
- panda.cabal +1/−1
- readme.markdown +29/−101
Panda/Config/Global.hs view
@@ -16,7 +16,7 @@ db_id = "db" flat_id = "."-blog_id = "blog"+post_id = "blog" config_id = "config" tag_id = "tag" comment_id = "comment"@@ -28,7 +28,7 @@ config_uri = flat_uri / config_id / "site.txt" sidebar_uri = flat_uri / config_id / sidebar_id-blog_uri = flat_uri / blog_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@@ -76,3 +76,13 @@ theme = if user_theme_uri.doesFileExist.purify then user_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 %T"+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" " "+
Panda/Controller/Application.hs view
@@ -12,7 +12,7 @@ import qualified Data.Map as Map -- env-import Panda.Helper.Env hiding (tag)+import Panda.Helper.Env hiding (tag, uri) import Panda.Helper.StateHelper import Prelude hiding ((.), (/), (^), id) import qualified Panda.Type.State as S@@ -33,10 +33,19 @@ import qualified Panda.View.Control.Search as SearchV import qualified Panda.View.Widget.RSS as RSSV -per_page = G.per_page-root = G.root+-- helpers+-- liftIO: move to an (u)pper level monad+u = liftIO+blog_regex = G.url_date_matcher -blog_regex = "\\d{2}-\\d{2}-\\d{2}"+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 )@@ -47,88 +56,80 @@ , ("tag/." ,tag ) , ("search" ,search ) , ("comment/create" ,comment_create )- ] .map_fst ((root /) >>> ("^" ++))+ ] .map_fst ((G.root /) >>> ("^" ++)) ++- [ ("^" ++ root ++ "$" ,index )]-+ [ ("^" ++ G.root ++ "$" ,index )] index = do- blogs <- Post.list.liftIO- p <- paginate per_page $ blogs.length- tags <- Tag.list.liftIO+ 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.blog_id, S.pager = p, S.tags = tags, S.nav_location = nav }+ let state = S.empty { S.uid = G.post_id, S.pager = p, S.tags = tags, S.nav_location = nav } - blogs.map (Tag.fill_tag tags).mapM ( Comment.fill_comment_size >>> liftIO ) - ^ PostV.list state >>= output_html-+ posts.mapM (init_post tags) ^ PostV.list state >>= output_html index_feed = do- blogs <- Post.list.liftIO- blogs.RSSV.rss "" "" .output+ posts <- Post.list.u+ posts.RSSV.rss "" "" .output blog = do- id <- uri ^ ( "blog" / )- blog <- Post.get id .liftIO- comments <- Comment.list_for id .liftIO- tags <- Tag.list.liftIO+ id <- uri ^ Post.uri_to_id+ blog <- Post.get id .u+ comments <- Comment.list_for id .u+ tags <- tag_list let state = S.empty { S.uid = id, S.tags = tags, S.resource_title = blog.resource_title }- blog.Tag.fill_tag tags .Comment.fill_comment_size .liftIO- ^ PostV.view state comments >>= output_html+ blog.(init_post tags) ^ PostV.view state comments >>= output_html static = do id <- uri- static_page <- Static.get id .liftIO- tags <- Tag.list.liftIO+ 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.liftIO+ tags <- tag_list let tag_name = Tag.get_name id case Tag.tag_map' tags .Map.lookup tag_name of- Nothing -> "page not found" .output- Just blog_set -> do- blogs <- blog_set.to_list.rsort.mapM (Post.get) .liftIO ^ map (Tag.fill_tag tags)- p <- paginate per_page $ blogs.length+ 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 }- blogs.TagV.view state.output_html-+ posts.TagV.view state.output_html tag_feed = do id <- uri ^ (split "/" >>> init >>> join "/")- tags <- Tag.list.liftIO+ tags <- tag_list let tag_name = Tag.get_name id case Tag.tag_map' tags .Map.lookup tag_name of- Nothing -> "page not found" .output- Just blog_set -> do- blogs <- blog_set.to_list.rsort.mapM (Post.get) .liftIO ^ map (Tag.fill_tag tags)- blogs.RSSV.rss "tag" tag_name.output+ 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" ""- blogs <- Post.search s .liftIO+ posts <- Post.search s .u - p <- paginate per_page $ blogs.length- tags <- Tag.list.liftIO+ 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 } - blogs.map (Tag.fill_tag tags) .SearchV.view state s.output_html--+ posts.mapM (init_post tags) >>= \x -> x.SearchV.view state s.output_html comment_create = do- post_id <- input_with_default "post_id" (G.blog_id / "nothing")- exists <- (G.flat_uri / post_id.to_utf8) .doesFileExist .liftIO- let valid_path = equalFilePath G.blog_id (takeDirectory post_id)+ post_id <- input_with_default "post_id" (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@@ -136,15 +137,15 @@ inputs >>= (Comment.create >>> liftIO) else return ()- redirect $ (G.root / post_id.id_to_resource.to_utf8).urlEncode+ redirect $ (post_id.Post.id_to_uri.to_utf8).urlEncode -- create helper, should be refactored to some common aspect check_create = [ validate "author" ( length >>> (> 0))- , validate "url" (const True)+ , validate "author_link" (const True) , validate "comment" ( length >>> (> 0))- , validate "human-hack" (is "10")+ , validate "human_hack" (is "10") ] .sequence ^ and
Panda/Helper/Env.hs view
@@ -20,7 +20,7 @@ , module Data.Foldable ) where -import MPS hiding (base, col, sub)+import MPS hiding (base, col, sub, date) import Control.Monad hiding (join) import Control.Arrow ((>>>), (&&&), (***)) import Data.List hiding (find)
Panda/Helper/Helper.hs view
@@ -13,15 +13,15 @@ import Text.XHtml.Strict import Control.Monad hiding (join) import Data.Maybe- import Panda.Type.Pager as Pager hiding (empty)-import MPS+import MPS hiding (date) import Prelude hiding ((.), (/), (^), id) 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 /@@ -35,14 +35,14 @@ -- 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 ^ trace' ^ map_snd (strip >>> gsub "\r\n" "\n" >>> unescape_unicode_xml) ^ trace'+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 )@@ -53,7 +53,7 @@ just_param s = get_param s ^ fromJust just_input s = get_input s ^ fromJust -paginate length total = liftM5 ( Pager length ) current has_next has_previous next previous where+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)@@ -86,15 +86,11 @@ output_html = output ... renderHtml --ie s = - [ "<!-- [if IE]>"- , s.show- , "<![endif]-->"- ] .join' .primHtml+spaced_url = gsub "/" " / " -- config parse_config s = s.split "," .map strip .reject null+not_found = getVar "REQUEST_URI" >>= (fromMaybe "" >>> outputNotFound) -- class@@ -110,4 +106,8 @@ class Markable a where markup :: a -> Html -spaced_url = gsub "/" " / "+class Datable a where+ date :: a -> CalendarTime++class Addressable a where+ uri :: a -> String
Panda/Helper/StateHelper.hs view
@@ -8,6 +8,7 @@ import qualified Panda.Config.Global as G import Prelude hiding ((.), (/), (^), id) + -- G.root = /blog -- raw_uri = blog/x -- full_uri = /blog/x@@ -18,3 +19,19 @@ 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)
Panda/Model/Comment.hs view
@@ -6,14 +6,15 @@ import qualified Panda.Config.Global as G import Panda.Type.Reader import qualified Panda.Model.Post as Post-import System.Time import System.Directory+import Panda.Helper.StateHelper data Comment = Comment { uid :: String -- comment/08-09-04 blog title , author :: String- , url :: String , body :: String+ , author_email :: String+ , author_link :: String } deriving (Show, Eq) @@ -23,6 +24,10 @@ 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@@ -32,37 +37,36 @@ d = (G.comment_uri / r) r = post_id.id_to_resource.to_utf8 -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.blog_id / x.split "/" .last- get id = do xs <- (G.flat_uri / id.to_utf8) .readFile ^ lines let author = xs.first- let url = xs.drop 2.first+ let author_email = ""+ let author_link = xs.drop 2.first let body = xs.drop 4.unlines- return $ Comment id author url body+ return $ Comment id author body author_email author_link create h = do- let author = at "author"- let url = at "url"- let body = at "comment"- let post_id = at "post_id"+ let author = at "author"+ let author_link = at "author_link"+ -- let author_email = at "author_email"+ let body = at "comment"+ let post_id = at "post_id" - timestamp <- ( getClockTime >>= toCalendarTime ) ^ format_date+ 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- let content = [author, url, body].join "\n\n"+ let content = [author, author_link, body].join "\n\n" writeFile uid content where at s = h.lookup s .fromJust -format_date x = formatCalendarTime defaultTimeLocale "%y-%m-%d %T" x-parse_date = parseCalendarTime defaultTimeLocale "%Y-%m-%d %T"-date x = x.uid.split "/".last.("20"++).parse_date.fromMaybe (parse_date "2000-1-1 00:00:00".fromJust)+-- 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
Panda/Model/Post.hs view
@@ -5,10 +5,13 @@ module Panda.Model.Post where -- env-import Panda.Helper.Env hiding (match, title, body)+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+import Panda.Helper.Helper (date) data Post = Post { uid :: String -- blog/08-09-04 blog title@@ -25,21 +28,37 @@ instance Markable Post where markup x = render_to_html (x.reader) (x.body)- -list = ls G.blog_uri ^ map from_utf8 ^ rsort ^ map (G.blog_id /) >>= mapM get+ +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---parse_date = parseCalendarTime defaultTimeLocale "%Y-%m-%d"-date x = x.uid.words.first.split "/".last.("20"++).parse_date.fromMaybe (parse_date "2000-1-1".fromJust)-+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) +-- 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
Panda/Model/Static.hs view
@@ -19,6 +19,7 @@ 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
Panda/Model/Tag.hs view
@@ -19,15 +19,13 @@ instance Resource Tag where resource_title = uid >>> spaced_url -sorted xs = xs.sortBy(compare_by (resources >>> S.size)).reverse-name_to_id x = G.tag_id / x-+-- 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.blog_id / ) ^ to_set+ ^ 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@@ -35,3 +33,7 @@ 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/View/Atom/Comment.hs view
@@ -1,8 +1,9 @@ module Panda.View.Atom.Comment where -import Panda.Helper.Env hiding (title, date)+import Panda.Helper.Env hiding (title) import Prelude hiding ((.), (/), (^), id, span) import qualified Panda.Config.Global as G+import Panda.Helper.StateHelper -- view import qualified Panda.View.Atom.Tag as Tag@@ -21,9 +22,9 @@ , p ! [theclass "small"] << comment_date x , x.markup ]- where a = if x.url.null then x.author.toHtml else toHtml $ hotlink (x.url) << x.author+ where a = if x.author_link.null then x.author.toHtml else toHtml $ hotlink (x.author_link) << x.author -comment_date x = toHtml $ x.date.formatCalendarTime defaultTimeLocale "%b %e, %Y at %I:%M %p"+comment_date x = toHtml $ x.date.format_date "%b %e, %Y at %I:%M %p" -- form@@ -34,11 +35,12 @@ [ h2 ! [id "respond"] << "Leave a Response" , gui (G.root / "comment/create") ! [id "commentform"] << [ field "author" 22 1 "Name (required)"- , field "url" 22 2 "Website"- , field "human-hack" 22 3 "What is 5 + 5 ?"+ -- , field "author_email" 22 2 "Email"+ , field "author_link" 22 3 "Website"+ , field "human_hack" 22 4 "What is 5 + 5 ?" , p << hidden "post_id" (x.uid.uid_to_post_id)- , p << textarea ! [name "comment", id "comment", cols "10", rows "20", strAttr "tabindex" "4"] << ""- , p << submit "submit" "Submit Comment" ! [strAttr "tabindex" "5"]+ , p << textarea ! [name "comment", id "comment", cols "10", rows "20", strAttr "tabindex" "5"] << ""+ , p << submit "submit" "Submit Comment" ! [strAttr "tabindex" "6"] ] ]
Panda/View/Atom/Post.hs view
@@ -1,8 +1,9 @@ module Panda.View.Atom.Post where -import Panda.Helper.Env hiding (name, title, date)+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@@ -18,14 +19,13 @@ entry x = div_class "post" << [entry_title, entry_mark, entry_body] where entry_title = x.title_link entry_body = div_class "entry" << x.markup- entry_mark = p ! [theclass "small"] << [ blog_date, blog_tags, blog_comments ].map (send_to x)+ entry_mark = p ! [theclass "small"] << [ post_date, post_tags, post_comments ].map (send_to x) -title_link x = h2 << hotlink (entry_uri x) << x.title-entry_uri x = G.root / ( uid x .id_to_resource )-blog_date x = toHtml $ x.date.formatCalendarTime defaultTimeLocale "%b %e, %Y"+title_link x = h2 << hotlink (x.uri) << x.title+post_date x = toHtml $ x.date.format_date "%b %e, %Y" -blog_tags x | x.tags.null = toHtml ""-blog_tags x = " | " +++ "Published in " +++ x.tags.map Tag.tag_link .intersperse (", ".toHtml)+post_tags x | x.tags.null = toHtml ""+post_tags x = " | " +++ "Published in " +++ x.tags.map Tag.tag_link .intersperse (", ".toHtml) -blog_comments x | x.comment_size.is 0 = toHtml ""-blog_comments x = " | " +++ hotlink ( entry_uri x / "#comments") << (x.comment_size.show ++ " Comments")+post_comments x | x.comment_size.is 0 = toHtml ""+post_comments x = " | " +++ hotlink ( x.uri / "#comments") << (x.comment_size.show ++ " Comments")
Panda/View/Atom/Tag.hs view
@@ -8,5 +8,5 @@ instance DataRenderer Tag where render_data = name >>> tag_link -tag_link s = toHtml $ hotlink (G.root / "tag" / s ) << s +tag_link s = toHtml $ hotlink (G.root / G.tag_id / s ) << s
Panda/View/Widget/Helper.hs view
@@ -11,7 +11,7 @@ if tagged then let tag_name = Tag.get_name uid in- link ( "tag" / tag_name) tag_name+ link ( G.tag_id / tag_name) tag_name else link "" "Home" where uid = State.uid state
Panda/View/Widget/RSS.hs view
@@ -4,16 +4,13 @@ import Panda.Helper.Env -- hiding (Language) import Prelude hiding ((.), (/), (^), id) import qualified Panda.Config.Global as G-import Panda.Type.Pager as Pager+import qualified Panda.Type.Pager as Pager import Text.RSS import qualified Text.RSS as RSS -- model import Panda.Model.Post as Post --entry_uri x = Post.uid x.split "/".tail.join "/"- -- RSS channel_rss_template = [ RSS.Language "en-us" ] @@ -22,16 +19,16 @@ , Description $ x.markup.show , Author $ G.author_email , Link $ x.item_uri- , PubDate $ x.Post.date + , PubDate $ x.date ] -item_uri x = nullURI { uriScheme = "http://", uriPath = host_link (x.entry_uri) }+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 = G.blog_title ++ " / " ++ s+ title = if s.empty then G.blog_title else G.blog_title ++ " / " ++ s host_link s = G.host_name ++ (G.root / s)
changelog.markdown view
@@ -1,3 +1,11 @@+2008.10.19+-----------++### Feature++* Pretty URLs+* Configurable date format for plain text+ 2008.10.18 -----------
panda.cabal view
@@ -1,5 +1,5 @@ Name: panda-Version: 2008.10.18+Version: 2008.10.19 Build-type: Simple Synopsis: A simple static blog engine Description: A simple static blog engine
readme.markdown view
@@ -1,9 +1,9 @@-Panda - simple blogging engine-==============================+## Panda: a simple blog engine in Haskell -Sample file db structure--------------------------+### Features +#### simple file structure+ db |---- blog | |---- 08-09-01 first post@@ -15,78 +15,23 @@ | |---- static |---- About- -Why-------* Blogs are stored locally-* Write in Markdown, HTML, reStructuredText or even LaTeX format ( Math equations won't render yet )-* Remote server is a mirror of local repository-* No server side API for publishing / editing, just use Darcs, Git or any SCM-* No need to find external hosting of your videos, images or sound. Put them in public folder ( still managed by SCM if you wish )-* Easy to use, just create a file and the engine takes care of the rest-* Extensible through embedding any HTML snippets-* Use `custom.css` for theming--Quick Demo--------------It powers [my blog](http://jinjing.blog.easymic.com) ;)--Note: the demo might run on an experimental branch, so differences from the cabal version could occur.- -Install-----------### install [lighttpd (version 1.4.19)](http://www.lighttpd.net/ )--Note: 1.4.20 is bugged for reason still unknown yet.---### install panda-- cabal install panda--### bootstrap-- # panda is a kibro project- kibro new myblog- cd myblog- - # get a template to start- git clone git://github.com/nfjinjing/panda-template.git db- sh db/extra/scripts/bootstrap.sh- -### run-- kibro start--It should be running on `http://127.0.0.1:3000` now.--Since Panda is based on Kibro, it helps to read how [Kibro](http://chrisdone.com/blog/2008/08/06/kibro-haskell-lighttpd-and-fastcgi/) works.--Goodies-----------Install `Ruby` / `Rake`, then `rake -T` to see a list of helper commands.--Config----------Edit `db/config/site.txt`+#### simple config file - blog_title = My shiny blog- host_name = yourhost.com- author_email = your_mail@yourhost.com+ blog_title = C大调+ blog_subtitle = 野猫不吃薯片+ host_name = jinjing.easymic.com+ author_email = nfjinjing@gmail.com -Restart required. (hint: `rake r`)+ navigation = About+ per_page = 7+ sidebar = Blogroll.md+ favicon = panda_icon.png -Theme---------+#### clear separation of code and data -Example blueprint theme in `db/config/theme/blueprint.txt`+theme structure through, for example, `db/config/theme/blueprint.txt`: container = container header = column span-12 first@@ -94,44 +39,27 @@ main = column span-9 first sidebar = column span-3 last footer = footer-+ css = screen, blueprint-wp, custom- js = jquery-1.2.6.min, jquery.getUrlParam, jquery.highlight-2, custom-+ js = jquery-1.2.6.min, custom -These are custom class names for each element block.+theme styling inside, for example, `db/public/theme/blueprint`, where all theme specific resources, e.g. `css/js/images`, are hosted. -note: `css` and `js` are required to be placed under `db/public/theme/your_theme_name/css` and `db/public/theme/your_theme_name/js`, leave out the file extensions in `site.txt`.+#### standard web technology -You can create new themes by adding theme files in `db/config/theme`, and place+extend/customize with pure css and javascript ( note the custom part in css and js from the last section ) - theme = your_new_theme+#### hacker friendly -inside `site.txt`.+* posts are in plain text+* SCM for publishing / data managing ( not really a feature, but a design goal from the beginning )+* blindly follow the KISS principle from code to UI: be a simple blog, no more no less -Hacking---------+### Try -Another way to get Panda running with source is:+Follow the [install guide](/static/panda/install) - # clone the source- git clone git://github.com/nfjinjing/panda.git myblog- cd myblog- - # get a template to start- git clone git://github.com/nfjinjing/panda-template.git db- - # manually adjust path information for lighttpd- # edit first 6 lines of app/lighttpd/lighttpd.conf to match your path- - # manual staging- ln -s db/public public- - # run- kibro start- -I'm heavily using the [MPS](http://github.com/nfjinjing/mps/tree/master ) package which introduces things like+### Links - reject, join, belongs_to, match, gsub- -and so on.+* [Kibro](http://chrisdone.com/tag/Kibro)+* [Panda on Jinjing's blog](http://jinjing.blog.easymic.com/tag/Panda)