packages feed

geek 1.0.0.3 → 1.0.0.4

raw patch · 10 files changed

+306/−271 lines, 10 filesdep +moePVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: moe

API changes (from Hackage documentation)

- Web.Geek.Application: register_monitor :: WatchManager -> FilePath -> (FilePath -> IO ()) -> IO ()
- Web.Geek.Application: spec :: Spec
- Web.Geek.Application: test_meta :: String
+ Web.Geek.RSS: rss :: Config -> [Post] -> Maybe Text -> ByteString
+ Web.Geek.Runtime: register_monitor :: WatchManager -> FilePath -> (FilePath -> IO ()) -> IO ()
+ Web.Geek.Runtime: spec :: Spec
+ Web.Geek.Type: __number_of_posts_per_feed :: :-> Config Integer
+ Web.Geek.Type: __rss_site_description :: :-> Config Text
+ Web.Geek.Type: __rss_site_link :: :-> Config Text
+ Web.Geek.Type: __rss_site_root_prefix :: :-> Config Text
+ Web.Geek.Type: __rss_site_title :: :-> Config Text
+ Web.Geek.Type: number_of_posts_per_feed :: Config -> Integer
+ Web.Geek.Type: rss_site_description :: Config -> Text
+ Web.Geek.Type: rss_site_link :: Config -> Text
+ Web.Geek.Type: rss_site_root_prefix :: Config -> Text
+ Web.Geek.Type: rss_site_title :: Config -> Text
- Web.Geek.Type: Config :: FilePath -> FilePath -> String -> [Text] -> Text -> Text -> Int -> Map Text [Text] -> Integer -> Text -> Bool -> Config
+ Web.Geek.Type: Config :: FilePath -> FilePath -> String -> [Text] -> Text -> Text -> Int -> Map Text [Text] -> Integer -> Integer -> Text -> Bool -> Text -> Text -> Text -> Text -> Config

Files

geek.cabal view
@@ -1,5 +1,5 @@ Name:                 geek-Version:              1.0.0.3+Version:              1.0.0.4 Build-type:           Simple Synopsis:             Geek blog engine Description:          A dynamic blog engine, customizable with standard web technology, i.e. HTML, CSS and Javascript (JSON)@@ -12,8 +12,8 @@ Cabal-version:        >= 1.6 category:             Web homepage:             http://github.com/nfjinjing/geek-data-files:           readme.md-                    , Nemesis+data-files:           +                      Nemesis                     , src/Web/Geek/ImportFromBamboo.hs                      @@ -48,6 +48,7 @@                     , text-icu                     , time                     , uuid+                    , moe                         hs-source-dirs:     src/@@ -57,7 +58,8 @@                       Web.Geek.DefaultConfig                       Web.Geek.DemoMiddlewareStack                       Web.Geek.Post+                      Web.Geek.RSS+                      Web.Geek.Runtime                       Web.Geek.Serve                       Web.Geek.Spec                       Web.Geek.Type-                      Web.Geek.Runtime
− readme.md
@@ -1,53 +0,0 @@-Geek-=====--A dynamic blog engine, customizable with standard web technology, i.e. HTML, CSS and Javascript (JSON)--Features-----------* Write posts in Markdown-* Full text search-* Layout is configurable through HTML, CSS and Javascript-* [Jekyll's front matter](http://jekyllrb.com/docs/frontmatter/) compatible--Installation---------------### Install Geek--    cabal install geek-server--### Get a Geek Blog Seed--    git clone https://github.com/nfjinjing/geek-blog-seed-jinjing-angular-mini.git geek-blog---Start----------    cd geek-blog-    geek geek-config.sh---Check---------<http://localhost:3001>---Configuration-----------------### Basic--* Geek Server: `geek-config.sh`-* Blog: `blog/angular/app/js/site-config.js`-* Layout: `blog/angular/index`, `blog/angular/partials`-* Style: `blog/angular/css/app.css`--Writing posts---------------* See `posts` directory for examples-* Posts are compatible with [Jekyll's front matter](http://jekyllrb.com/docs/frontmatter/) format
src/Web/Geek/Application.hs view
@@ -8,63 +8,46 @@ import Air.Data.Record.SimpleLabel hiding (get) import Air.Env import Air.Extra hiding (date)-import Air.Spec-import Air.TH (mkDefault, here)-import Control.Concurrent.Chan-import Control.Monad hiding (join, get)+import Air.TH (here) import Control.Monad.Reader (ask)+import Control.Monad.State (modify) import Data.Aeson.Encode.Pretty (encodePretty)-import Data.Aeson.Generic (encode, decode) import Data.Aeson.Generic (toJSON) import Data.Function (on)-import Data.IORef-import Data.List (find, sort)-import Data.List (sortBy)-import Data.Map (Map)+import Data.IORef (readIORef)+import Data.List (find, sortBy) import Data.Map (toAscList) import Data.Maybe (fromMaybe, catMaybes) import Data.Text (Text) import Data.Text.Encoding-import Data.Time (formatTime, fromGregorianValid, UTCTime(..))-import Data.UUID (toString)-import Filesystem.Path.CurrentOS (encodeString, decodeString)-import Hack2 (Application, Middleware)+import Data.Time (fromGregorianValid, UTCTime(..))+import Data.ByteString (ByteString)+import Hack2 (Application) import Hack2.Contrib.Middleware.File (serve) import Hack2.Contrib.Middleware.Static import Hack2.Contrib.Request (params)-import Hack2.Contrib.Response-import Hack2.Contrib.Utils (use, unescape_uri, query_string)+import Hack2.Contrib.Response (set_content_type)+import Hack2.Contrib.Utils (use, unescape_uri) import Network.Miku hiding (json) import Network.Miku.Type (AppMonadT) import Safe (readMay)-import System.Directory-import System.FSNotify-import System.FilePath-import System.FilePath.Glob-import System.Locale (defaultTimeLocale)-import System.Random-import Test.Hspec-import Text.Printf-import Web.Geek.Config-import Web.Geek.DefaultConfig-import Web.Geek.Post import Web.Geek.Runtime import Web.Geek.Type+import Web.Geek.RSS import qualified Control.Monad.State as State import qualified Data.ByteString.Char8 as B import qualified Data.Map as Map import qualified Data.Set as Set import qualified Data.Text as T import qualified Data.Text.ICU as ICU-import qualified Data.Text.IO as TextIO import qualified Network.Miku as Miku import qualified Prelude as P-import qualified Prelude as P+import Control.Arrow ((***))   initialize_geek :: Config -> [MarkupEngine] -> IO Application-initialize_geek _config markup_engines = do-  _runtime <- initialize_runtime _config markup_engines+initialize_geek _config _markup_engines = do+  _runtime <- initialize_runtime _config _markup_engines      let geek_app = geek _runtime @@ -79,9 +62,9 @@          _middleware = use static_serve_stack     -  let app = _middleware geek_app+  let _app = _middleware geek_app   -  return app+  return _app                 @@ -113,20 +96,20 @@        -- Miku stuff -    read_posts :: AppMonadT [Post]-    read_posts = io - readIORef - runtime.posts_ref+    read_posts :: IO [Post]+    read_posts = readIORef - runtime.posts_ref -    filter_posts_by_query :: Text -> AppMonadT [Post]-    filter_posts_by_query _query_text = do+    read_posts_by_query :: Text -> IO [Post]+    read_posts_by_query _query_text = do       _posts <- read_posts       if _query_text.T.null         then return _posts         else do-          _full_text_search_map <- io - readIORef - runtime.full_text_search_map_ref+          _full_text_search_map <- readIORef - runtime.full_text_search_map_ref            let _lowered_query_text = ICU.toLower ICU.Current _query_text -              _number_of_maximum_tokens = 7+              _number_of_maximum_tokens = 7 :: Int                _query_tokens = _lowered_query_text.T.words.take _number_of_maximum_tokens @@ -149,6 +132,37 @@                 .to_list            return - matched_post_ids.map (\_id -> _posts.at _id) .catMaybes.sortBy (compare `on` date).reverse+    +    text_params :: AppMonadT [(Text, Text)]+    text_params = do+      _params <- ask ^ params+      return - _params.map (decodeUtf8 *** decodeUtf8)+  +    text_captures :: AppMonadT [(Text, Text)]+    text_captures = do+      _captures <- captures+      return - _captures.map (decodeUtf8 *** (B.unpack > b2u > unescape_uri > T.pack))+  +    lookup_non_empty_and_stripped :: Text -> [(Text, Text)] -> Maybe Text+    lookup_non_empty_and_stripped x xs = +      case xs.lookup x of+        Nothing -> Nothing+        Just _found ->+          let _stripped = _found.T.strip+          in+          +          if _stripped.T.null+             then+               Nothing+              else+                Just _stripped+    ++    filter_posts_by_tag :: [Post] -> Maybe Text -> [Post]+    filter_posts_by_tag _posts _maybe_tag = +      case _maybe_tag of +        Nothing -> _posts+        Just _tag -> _posts.select (tags > has _tag)   in      miku - do@@ -156,49 +170,38 @@       _params <- ask ^ params              -      let tag_string = _params.(lookup "tag" > fromMaybe "" > B.unpack > b2u)-          tag_text = tag_string.T.pack.T.strip-          +      _maybe_tag <- text_params ^ lookup_non_empty_and_stripped "tag"              _query_text <- query_text              -- io - printf "query_text: %s" (T.unpack _query_text)       -      _posts_filtered_by_query <- filter_posts_by_query _query_text-      -      _posts_filtered_by_tag_and_query <--        if tag_text.T.null-          then do-            -- io - puts "no tag"-            return _posts_filtered_by_query-          else do-            -- io - puts - "tag: " + show tag_text-            return - _posts_filtered_by_query.select (tags > has tag_text)+      _posts_filtered_by_query <- io - read_posts_by_query _query_text       +      let _posts_filtered_by_tag_and_query = filter_posts_by_tag _posts_filtered_by_query _maybe_tag+          (_drop, _take) <- pager              let paginated_posts = _posts_filtered_by_tag_and_query.drop _drop.take _take              -- io - puts - paginated_posts.map (title > T.unpack).join "\n"       -          clean_raw_body post = post .set __raw_body ""+          clean_raw_body _post = _post .set __raw_body ""                  json- paginated_posts.map clean_raw_body                   get "/posts/:year/:month/:day/:title" - do-      _captures <- captures+      _captures <- text_captures -      posts <- read_posts-      -      +      posts <- io - read_posts              let          _post = fromMaybe def - do-          year <- _captures.lookup "year" >>= B.unpack > readMay-          month <- _captures.lookup "month" >>= B.unpack > readMay-          day <- _captures.lookup "day" >>= B.unpack > readMay-          _title <- _captures.lookup "title" >>= B.unpack > unescape_uri > T.pack > return+          year <- _captures.lookup "year" >>= T.unpack > readMay+          month <- _captures.lookup "month" >>= T.unpack > readMay+          day <- _captures.lookup "day" >>= T.unpack > readMay+          _title <- _captures.lookup "title"                    post_day <- fromGregorianValid year month day         @@ -213,11 +216,11 @@     get "/tags" - do       _query_text <- query_text       -      _posts_filtered_by_query <- filter_posts_by_query _query_text+      _posts_filtered_by_query <- io - read_posts_by_query _query_text       -      _posts <- read_posts+      _posts <- io - read_posts       -      let run_length_encoding base xs = (zip base (repeat (0 :: Integer)) + zip xs (repeat (1 :: Integer))) .Map.fromListWith (P.+)+      let run_length_encoding _base xs = (zip _base (repeat (0 :: Integer)) + zip xs (repeat (1 :: Integer))) .Map.fromListWith (P.+)           unique_tags = _posts.map tags.concat.unique                      all_tags = _posts_filtered_by_query.map tags.concat.run_length_encoding unique_tags@@ -236,85 +239,32 @@        State.put _response       -    {--    get "/post/:title" - do-      _captures <- captures-      posts <- read_posts--      let -        _post = fromMaybe def - do-          _title <- _captures.lookup "title" >>= B.unpack > unescape_uri > T.pack > return-          posts.find (title > is _title)---      json _post--    -}     -    get "*" - do-      text "Unknown route"-  -register_monitor :: WatchManager -> FilePath -> (FilePath -> IO ()) -> IO ()-register_monitor manager path callback = do-  -- printf "reigister_with_path: %s\n" path-  -- -  watchTree manager (decodeString path) (const True) (\event -> do-    -- puts - show event-    action event-    )-  where-    action (Added _path time) = callback - encodeString _path-    action (Modified _path time) = callback - encodeString _path-    action (Removed _path time) = callback - encodeString _path------ Spec--test_meta :: String-test_meta = [here|------layout: post-categories: 日常------|]--spec :: Spec-spec = do-  describe "Server" - do-    let _config = default_config+    -- Feeds     -    it "should list posts" - do-      posts <- get_posts _config []-      -      posts `shouldSatisfy` (null > not)     -      -    it "should callback on change" - do-      let tmp_path = "tmp"-      test_id <- randomIO ^ toString-      let test_path = tmp_path / test_id-    -      createDirectoryIfMissing True test_path-      -      watch_manager <- startManager-      path_chan <- newChan+    let +      rss_tag_controller :: AppMonadT ()+      rss_tag_controller = do+        _posts <- io - read_posts+        +        _maybe_tag <- text_captures ^ lookup_non_empty_and_stripped "tag"       -      register_monitor watch_manager test_path - \file_path -> do-        -- puts - printf "Changed: %s" file_path-        writeChan path_chan file_path +        let _posts_filtered_by_tag_and_query = filter_posts_by_tag _posts _maybe_tag       -      let test_case_id = "callback"-          test_case_path = test_path / test_case_id--      B.writeFile test_case_path "Hello"-      sleep 0.1-      event_paths <- getChanContents path_chan-      stopManager watch_manager+        text - rss _config _posts_filtered_by_tag_and_query _maybe_tag+        +        modify - set_content_type "text/xml"       -      test_case_full_path <- canonicalizePath test_case_path-      removeDirectoryRecursive test_path+    get "/rss.xml" - do+      rss_tag_controller       -      event_paths `shouldSatisfy` (has test_case_full_path)+    get "/:tag/rss.xml" - do+      rss_tag_controller+    +    get "/tag/:tag/rss.xml" - do+      rss_tag_controller+    +    get "*" - do+      text "Unknown route"+  
src/Web/Geek/Config.hs view
@@ -5,28 +5,23 @@  module Web.Geek.Config where -import Air.Data.Record.SimpleLabel import Air.Env hiding (mod) import Air.Extra import Air.Spec-import Air.TH (mkDefault, here, mkLabel)+import Air.TH (here) import Data.Aeson.Encode.Pretty (encodePretty)-import Data.Aeson.Generic (encode, decode)+import Data.Aeson.Generic (decode) import Data.Aeson.Generic (toJSON)-import Data.IORef-import Data.Text (Text) import Data.Time-import System.Directory (doesFileExist, makeRelativeToCurrentDirectory)+import System.Directory (doesFileExist) import System.Environment (getArgs) import System.Exit (exitWith, ExitCode(..))-import System.FSNotify-import System.Locale+import System.Locale (defaultTimeLocale) import Test.Hspec import Text.Printf import Web.Geek.DefaultConfig import Web.Geek.Type import qualified Data.ByteString.Char8 as B-import qualified Data.Text as T import qualified Data.Text.IO as TextIO import System.Process (readProcess) @@ -38,10 +33,10 @@ parse_config = s2l > decode  read_config_from_path :: FilePath -> IO (Maybe Config)-read_config_from_path path = do-  config <- readProcess ("." / path) [] ""+read_config_from_path _path = do+  _config <- readProcess ("." / _path) [] ""   -  return - config.u2b.B.pack.parse_config+  return - _config.u2b.B.pack.parse_config   @@ -55,7 +50,7 @@  get_config_from_args :: IO Config get_config_from_args = do-  let default_config_path = "geek-config"+  let default_config_path = "blog/geek-config.hs"      _args <- getArgs   @@ -91,7 +86,7 @@       puts - "Failed to parse the output of: " + _path       puts ""       puts - "Example:"-      puts - 20.times '-'+      puts - (20 :: Int).times '-'       puts ""       puts default_config_string       puts ""
src/Web/Geek/DefaultConfig.hs view
@@ -23,6 +23,11 @@   , maximum_number_of_posts_per_page = 20         , index_file_path = "blog/angular/app/index.html"   , full_text_search = True+  , number_of_posts_per_feed = 20+  , rss_site_title = "Shinny blog"+  , rss_site_description = ""+  , rss_site_link = "http://localhost:3000"+  , rss_site_root_prefix = "/#"   }   where     _static_serve = 
src/Web/Geek/Post.hs view
@@ -6,29 +6,24 @@ module Web.Geek.Post where  import Air.Env-import Air.Extra (u2b) import Air.Spec import Air.TH import Data.Aeson.Generic (encode, decode) import Data.Char-import Data.List.Split import Data.Maybe-import Data.Text (Text, pack, unpack, strip, stripStart)+import Data.Text (Text, pack, strip, stripStart) import Data.Time-import Data.Time.Clock (UTCTime(..)) import GHC.Exts( IsString(..) ) import System.FilePath import System.Locale import Test.Hspec import Web.Geek.Config import Web.Geek.Type-import qualified Data.ByteString.Char8 as B import qualified Data.Text as T-import qualified Data.Text as T  post_tags :: Text -> Post -> [Text]-post_tags meta_tag_keyword post = -  case post.meta.lookup meta_tag_keyword of+post_tags _meta_tag_keyword post = +  case post.meta.lookup _meta_tag_keyword of     Nothing -> []     Just s -> s.T.splitOn ",".map T.strip @@ -47,27 +42,27 @@           "post/" + post.title   in   -  text_link.T.unpack.fromString.URIEscapedText+  text_link.T.unpack.fromString  parse_title_and_date_from_path :: String -> FilePath -> (T.Text, Maybe UTCTime)-parse_title_and_date_from_path post_date_format path = +parse_title_and_date_from_path _post_date_format _path =    let-    base_name = path.takeBaseName-    _post_date_length = post_date_length - post_date_format+    base_name = _path.takeBaseName+    _post_date_length = post_date_length - _post_date_format     -    title = base_name.drop _post_date_length-    date = base_name.take _post_date_length.parseTime defaultTimeLocale post_date_format+    _title = base_name.drop _post_date_length+    _date = base_name.take _post_date_length.parseTime defaultTimeLocale _post_date_format   in-  (title.T.pack, date)+  (_title.T.pack, _date)      markup :: [MarkupEngine] -> FilePath -> Text -> HTMLText-markup markups path body = -  let ext = path.takeExtension.dropWhile (is '.').pack+markup markups _path _body = +  let ext = _path.takeExtension.dropWhile (is '.').pack   in      case markups.select (extensions > has ext) of-    [] -> transformer def body-    markup:_ -> transformer markup body+    [] -> transformer def _body+    _markup:_ -> transformer _markup _body  parse_meta :: Text -> (Meta, Text) parse_meta str = @@ -85,9 +80,9 @@                 _ -> Nothing           in           -          let meta = y.T.lines.reject (T.all isSpace).map parse_line.catMaybes+          let _meta = y.T.lines.reject (T.all isSpace).map parse_line.catMaybes           in-          (meta, zs.T.unlines.stripStart)+          (_meta, zs.T.unlines.stripStart)         else           no_meta     _ -> no_meta@@ -114,10 +109,10 @@       parse_meta test_meta === ([("layout", "post"), ("categories", "日常")], "some thing\n\n\n")          it "should parse title" - do-      let path = "blog/posts/2010-10-30-dummy title.md"+      let _path = "blog/posts/2010-10-30-dummy title.md"           title_and_date = ("dummy title", parseTime defaultTimeLocale "%Y-%m-%d" "2010-10-30")           -      parse_title_and_date_from_path "%Y-%m-%d-" path === title_and_date+      parse_title_and_date_from_path "%Y-%m-%d-" _path === title_and_date          {-     it "should markup" - do
+ src/Web/Geek/RSS.hs view
@@ -0,0 +1,111 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE NoImplicitPrelude #-}+++module Web.Geek.RSS (rss) where++import Air.Data.Record.SimpleLabel hiding (get)+import Air.Env hiding (head, div)+import Air.Extra hiding (date)+import Air.TH (here)++import qualified Web.Geek.Type as Geek+import Data.Text (Text)++import Text.HTML.Moe2 hiding ((/), text)+import Text.HTML.Moe2.Type (MoeUnit)+import Data.ByteString (ByteString)+import qualified Data.Text as T+import Data.Maybe+import Control.Monad (forM_)+import System.Locale (defaultTimeLocale)+import Data.Time (formatTime)++  {-+  +  <?xml version="1.0" encoding="UTF-8"?>+  <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">+    <channel>+      <title>{{ site.name }}</title>+      <description>{{ site.description }}</description>+      <link>{{ site.url }}</link>+      <atom:link href="{{ site.url }}/rss.xml" rel="self" type="application/rss+xml" />+      {% for post in site.posts limit:10 %}+        <item>+          <title>{{ post.title }}</title>+          <description>{{ post.content | xml_escape }}</description>+          <pubDate>{{ post.date | date: "%a, %d %b %Y %H:%M:%S %z" }}</pubDate>+          <link>{{ site.url }}{{ post.url }}</link>+          <guid isPermaLink="true">{{ site.url }}{{ post.url }}</guid>+        </item>+      {% endfor %}+    </channel>+  </rss>+  +  <link rel="alternate" type="application/rss+xml" title="{{ site.name }}" href="{{ site.url }}/rss.xml">+  +  -}++-- xml_header +xml_header :: MoeUnit+xml_header = no_escape_no_indent_str "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"++text :: T.Text -> MoeUnit+text = T.unpack > str++rss :: Geek.Config -> [Geek.Post] -> Maybe Text -> ByteString+rss _config _posts _mabye_tag = l2s - render_bytestring - do+  +  let drop_tail_slash xs = xs.reverse.dropWhile (is '/').reverse+  +      site_link = +        (+        [+          _config.Geek.rss_site_link.T.unpack.drop_tail_slash+        , _config.Geek.rss_site_root_prefix.T.unpack.drop_tail_slash.reverse.drop_tail_slash+        ]+        ).join "/"+        +      site_tag_link = +        case _mabye_tag of+          Nothing -> site_link+          Just _tag -> site_link + "/" + _tag.T.unpack+        +  xml_header+  +  element "rss" ! [attr "version" "2.0", attr "xmlns:atom" "http://www.w3.org/2005/Atom"] - do+    element "channel" - do+      title - do+        text - _config.Geek.rss_site_title+        +      element "description" - do+        text - _config.Geek.rss_site_description+        +      element "link" - do++              +        str - site_link+      +      element "atom:link" ! [href (site_tag_link + "/rss.xml"), attr "rel" "self", _type "application/rss+xml"] -+        return ()+      +      forM_ _posts - \_post ->+        element "item" - do+          title - do+            text - _post.Geek.title+          +          element "description" - do+            text - _post.Geek.body.Geek.unHTMLText+          +          element "pubDate" - do+            let pub_date_format_string = "%a, %d %b %Y %H:%M:%S %z"+            str - _post.Geek.date.fromMaybe def.formatTime defaultTimeLocale pub_date_format_string+            +            let post_link = site_link + "/" +  _post.Geek.link.Geek.unURIEscapedText.T.unpack+            element "link" - do+              str - post_link+            element "guid" ! [attr "isPermaLink" "true"] - do+              str - post_link+          +      
src/Web/Geek/Runtime.hs view
@@ -8,55 +8,36 @@ import Air.Data.Record.SimpleLabel hiding (get) import Air.Env import Air.Extra hiding (date)-import Air.Spec-import Air.TH (mkDefault, here) import Control.Concurrent.Chan-import Control.Monad hiding (join, get)-import Control.Monad.Reader (ask)-import Data.Aeson.Generic (encode, decode)+import Control.Monad hiding (join) import Data.Function (on) import Data.IORef-import Data.List (find, sort) import Data.List (sortBy)-import Data.Map (Map)-import Data.Map (toAscList)-import Data.Maybe (fromMaybe, catMaybes) import Data.Text (Text)-import Data.Text.Encoding-import Data.Time (formatTime, fromGregorianValid, UTCTime(..)) import Data.UUID (toString) import Filesystem.Path.CurrentOS (encodeString, decodeString)-import Network.Miku hiding (json)-import Network.Miku.Type (AppMonadT)-import Safe (readMay) import System.Directory import System.FSNotify-import System.FilePath import System.FilePath.Glob-import System.Locale (defaultTimeLocale) import System.Random import Test.Hspec import Text.Printf-import Web.Geek.Config+import Web.Geek.DefaultConfig import Web.Geek.Post import Web.Geek.Type import qualified Data.ByteString.Char8 as B import qualified Data.Map as Map-import qualified Data.Set as Set import qualified Data.Text as T import qualified Data.Text.ICU as ICU import qualified Data.Text.IO as TextIO-import qualified Network.Miku as Miku import qualified Prelude as P  - build_markup_engines :: [Text] -> [MarkupEngine] -> [MarkupEngine] build_markup_engines user_markup_engine_names _markup_engines =    _markup_engines.select (markup_engine_name > belongs_to user_markup_engine_names)  - get_post_from_path :: Config -> [MarkupEngine] -> FilePath -> IO Post get_post_from_path _config _markup_engines _path = do   let _meta_tag_keyword = _config.meta_tag_keyword@@ -91,7 +72,7 @@       .set __tags _tags  glob_dir :: String -> FilePath -> IO [String]-glob_dir pattern path = globDir1 (compile pattern) path+glob_dir pattern _path = globDir1 (compile pattern) _path  get_posts :: Config -> [MarkupEngine] -> IO [Post] get_posts _config _markup_engines = do@@ -104,13 +85,13 @@   return - posts.sortBy (compare `on` date).reverse  monitor_posts :: Config -> [MarkupEngine] -> Chan Event -> IORef [Post] -> IO ()-monitor_posts _config _markup_engines events posts_ref = do+monitor_posts _config _markup_engines events _posts_ref = do   last_event_time_stamp <- now >>= newIORef   last_update_time_stamp <- now >>= newIORef      let     record_event = do-      e <- readChan events+      _ <- readChan events       -- printf "Caught: %s\n" - show e       time_stamp <- now     @@ -120,13 +101,13 @@      let     lazy_update = do-      sleep 0.1+      sleep (0.1 :: Double)       event_time <- readIORef last_event_time_stamp       update_time <- readIORef last_update_time_stamp              if event_time P.> update_time         then do-          get_posts _config _markup_engines >>= writeIORef posts_ref+          get_posts _config _markup_engines >>= writeIORef _posts_ref           now >>= writeIORef last_update_time_stamp                    else@@ -163,17 +144,17 @@      -- print - _full_text_search_map.Map.toAscList.take 50   -  printf "Search database has %i unique keywords." (_full_text_search_map.Map.size)+  puts - printf "Search database has %i unique keywords." (_full_text_search_map.Map.size)      fork - withManager - \watch_manager -> do      events <- newChan :: (IO (Chan Event))-    fork - monitor_posts _config _markup_engines events _posts_ref+    fork - monitor_posts _config _markup_engines events _posts_ref       let post_watch_file_path = decodeString - _config.blog_directory / _config.post_directory     watchTreeChan watch_manager post_watch_file_path (const True) events     -    forever - sleep 1000+    forever - sleep (1000 :: Double)      return -      def @@ -184,4 +165,55 @@       , full_text_search_map_ref = _full_text_search_map_ref       } +register_monitor :: WatchManager -> FilePath -> (FilePath -> IO ()) -> IO ()+register_monitor manager _path callback = do+  -- printf "reigister_with_path: %s\n" path+  -- +  watchTree manager (decodeString _path) (const True) (\event -> do+    -- puts - show event+    action event+    )+  where+    action (Added _path _) = callback - encodeString _path+    action (Modified _path _) = callback - encodeString _path+    action (Removed _path _) = callback - encodeString _path +++spec :: Spec+spec = do+  describe "Server" - do+    let _config = default_config+    +    it "should list posts" - do+      posts <- get_posts _config []+      +      posts `shouldSatisfy` (null > not)+    +      +    it "should callback on change" - do+      let tmp_path = "tmp"+      test_id <- randomIO ^ toString+      let test_path = tmp_path / test_id+    +      createDirectoryIfMissing True test_path+      +      watch_manager <- startManager+      path_chan <- newChan+      +      register_monitor watch_manager test_path - \file_path -> do+        -- puts - printf "Changed: %s" file_path+        writeChan path_chan file_path +      +      let test_case_id = "callback"+          test_case_path = test_path / test_case_id++      B.writeFile test_case_path "Hello"+      sleep (0.1 :: Double)+      event_paths <- getChanContents path_chan+      stopManager watch_manager+      +      test_case_full_path <- canonicalizePath test_case_path+      removeDirectoryRecursive test_path+      +      event_paths `shouldSatisfy` (has test_case_full_path)
src/Web/Geek/Serve.hs view
@@ -6,9 +6,8 @@ module Web.Geek.Serve where  import Air.Env-import Air.Extra hiding (date) import Hack2-import Hack2.Contrib.Utils (use, unescape_uri)+import Hack2.Contrib.Utils (use) import Web.Geek.Application import Web.Geek.Config (get_config_from_args) import Web.Geek.Type@@ -16,9 +15,9 @@ type Port = Int  serve :: [MarkupEngine] -> [Middleware] -> (Application -> Port -> IO ()) -> IO ()-serve markup_engines middleware_stack server_backend = do+serve _markup_engines middleware_stack server_backend = do   _config <- get_config_from_args-  geek_app <- initialize_geek _config markup_engines+  geek_app <- initialize_geek _config _markup_engines      let _middleware = use middleware_stack       _app = _middleware geek_app
src/Web/Geek/Type.hs view
@@ -7,24 +7,18 @@ module Web.Geek.Type where  import Air.Data.Record.SimpleLabel-import Air.Env  import Air.Env hiding (mod) import Air.Heavy (escape_xml)-import Air.TH import Air.TH (mkDefault, here, mkLabel) import Data.Data import Data.IORef import Data.Map (Map) import Data.Text (Text, pack, unpack) import Data.Time-import Data.Typeable import GHC.Exts( IsString(..) ) import Hack2.Contrib.Utils (escape_uri) import Prelude ()-import System.FSNotify-import System.Locale import Text.Printf-import qualified Data.ByteString.Char8 as B import Data.Aeson.Encode.Pretty (encodePretty) import Data.Aeson.Generic (toJSON) import Data.Text.Encoding (decodeUtf8)@@ -99,8 +93,13 @@   , server_port :: Int   , static_serve :: Map Text [Text]   , maximum_number_of_posts_per_page :: Integer+  , number_of_posts_per_feed :: Integer   , index_file_path :: Text   , full_text_search :: Bool+  , rss_site_title :: Text+  , rss_site_description :: Text+  , rss_site_link :: Text+  , rss_site_root_prefix :: Text   }   deriving (Show, Data, Typeable)