packages feed

pansite 0.1.0.0 → 0.2.0.0

raw patch · 23 files changed

+439/−231 lines, 23 filesdep +QuickChecksetup-changednew-component:exe:pansitePVP ok

version bump matches the API change (PVP)

Dependencies added: QuickCheck

API changes (from Hackage documentation)

- Pansite: ParserContext :: FilePathResolver -> ParserContext
- Pansite: ToolConfig :: (ToolConfigUpdater a) -> (ToolConfigRunner a) -> a -> ToolConfig
- Pansite: ToolContext :: FilePath -> [FilePath] -> [FilePath] -> ToolContext
- Pansite: ToolSpec :: String -> (ToolConfigUpdater a) -> (ToolConfigRunner a) -> ToolSpec
- Pansite: data ParserContext
- Pansite: data ToolConfig
- Pansite: data ToolContext
- Pansite: data ToolSpec
- Pansite: toolConfigRunner :: ToolContext -> ToolConfig -> IO ()
- Pansite: type ToolConfigRunner a = ToolContext -> a -> IO ()
- Pansite: type ToolConfigUpdater a = ParserContext -> a -> Value -> Parser a
+ Pansite: (%%>>) :: PathPattern -> (FilePath -> Action ()) -> Rules ()
+ Pansite: RunContext :: FilePath -> [FilePath] -> [FilePath] -> RunContext
+ Pansite: Tool :: String -> (UpdateContext -> Value -> Parser Tool) -> (RunContext -> IO ()) -> Tool
+ Pansite: UpdateContext :: FilePathResolver -> UpdateContext
+ Pansite: countOccurrences :: String -> String -> Int
+ Pansite: data PathPattern
+ Pansite: data RunContext
+ Pansite: data Tool
+ Pansite: data UpdateContext
+ Pansite: infix 1 %%>>
+ Pansite: pathPattern :: String -> Either String PathPattern
+ Pansite: pathPatternStem :: PathPattern -> FilePath -> String
+ Pansite: runTool :: RunContext -> Tool -> IO ()
+ Pansite: splitRoutePath :: String -> [String]
+ Pansite: stems :: (Eq a) => [a] -> [a] -> ([a], [a])
+ Pansite: substituteStem :: String -> PathPattern -> FilePath
- Pansite: Target :: FilePath -> ToolConfig -> [FilePath] -> [FilePath] -> Target
+ Pansite: Target :: PathPattern -> Tool -> [PathPattern] -> [PathPattern] -> Target
- Pansite: readApp :: ParserContext -> [ToolSpec] -> FilePath -> IO (Either String App)
+ Pansite: readApp :: UpdateContext -> [Tool] -> FilePath -> IO (Either String App)

Files

LICENSE view
@@ -1,4 +1,4 @@-Copyright (c) 2017 Richard Cook+Copyright (c) 2017-2018 Richard Cook  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the
README.md view
@@ -96,7 +96,7 @@  ```bash cd _app/-stack exec -- pansite-app --port 3000+stack exec -- pansite --port 3000 ```  In your web browser, navigate to a route defined in `.pansite.yaml`, e.g. `http://localhost:3000/page2`.@@ -104,10 +104,10 @@ ## Command-line options  ```terminal-$ stack exec -- pansite-app --help+$ stack exec -- pansite --help Pansite development server 0.1.0.0.772a2d5 (locally modified) -Usage: pansite-app ([-p|--port PORT] [-c|--config CONFIG]+Usage: pansite ([-p|--port PORT] [-c|--config CONFIG]                    [-o|--output-dir OUTPUTDIR] | [-v|--version])   Run Pansite development server @@ -124,7 +124,7 @@  Released under [MIT License][licence] -Copyright © 2017 Richard Cook+Copyright © 2017—2018 Richard Cook  [app-example]: _app/.pansite.yaml [cmd-hackage]: https://hackage.haskell.org/package/shake-0.15.11/docs/Development-Shake-Command.html
Setup.hs view
@@ -1,7 +1,7 @@ {-| Module      : Setup Description : Setup for Pansite-Copyright   : (C) Richard Cook, 2017+Copyright   : (C) Richard Cook, 2017-2018 Licence     : MIT Maintainer  : rcook@rcook.org Stability   : experimental
app/Main.hs view
@@ -1,7 +1,7 @@ {-| Module      : Main Description : Main entrypoint for Pansite app-Copyright   : (C) Richard Cook, 2017+Copyright   : (C) Richard Cook, 2017-2018 Licence     : MIT Maintainer  : rcook@rcook.org Stability   : experimental
app/PansiteApp/App.hs view
@@ -1,7 +1,7 @@ {-| Module      : PansiteApp.App Description : Main entrypoint for Pansite application-Copyright   : (C) Richard Cook, 2017+Copyright   : (C) Richard Cook, 2017-2018 Licence     : MIT Maintainer  : rcook@rcook.org Stability   : experimental
app/PansiteApp/Build.hs view
@@ -1,7 +1,7 @@ {-| Module      : PansiteApp.Build Description : Shake build function-Copyright   : (C) Richard Cook, 2017+Copyright   : (C) Richard Cook, 2017-2018 Licence     : MIT Maintainer  : rcook@rcook.org Stability   : experimental@@ -13,41 +13,32 @@ module PansiteApp.Build (build) where  import           Control.Monad-import           Data.String.Utils import           Development.Shake import           Pansite import           PansiteApp.ConfigInfo-import           PansiteApp.Util --- TODO: Patterns must only contain zero or one "%" characters--- This should be enforced!-shakePattern :: FilePath -> FilePath-shakePattern = replace "%" "*"---- TODO: Patterns must only contain zero or one "%" characters--- This should be enforced!-expandWildcardPattern :: String -> String -> FilePath-expandWildcardPattern = replace "%"- build :: ConfigInfo -> FilePath -> IO () build (ConfigInfo AppPaths{..} _ (App _ targets)) targetPath =     shake shakeOptions { shakeFiles = apShakeDir } $ do         liftIO $ putStrLn ("want: " ++ targetPath)         want [targetPath] -        forM_ targets $ \(Target path toolConfig inputPaths dependencyPaths) -> do-            liftIO $ putStrLn ("rule: " ++ path)-            shakePattern path %> \outputPath -> do-                let (stem, "%") = stems outputPath path -- TODO: Is this kind of irrefutable pattern OK? It's an assert of sorts.-                    replaceWithStem = expandWildcardPattern stem-                    inputPaths' = map replaceWithStem inputPaths-                    dependencyPaths' = map replaceWithStem dependencyPaths+        forM_ targets $ \(Target pathMakePattern tool inputPathPatterns dependencyPathPatterns) -> do+            liftIO $ do+                putStrLn $ "pathMakePattern: " ++ show pathMakePattern+                putStrLn $ "inputPathPatterns: " ++ show inputPathPatterns+                putStrLn $ "dependencyPathPatterns: " ++ show dependencyPathPatterns +            pathMakePattern %%>> \outputPath -> do+                let stem = pathPatternStem pathMakePattern outputPath+                    inputPaths = map (substituteStem stem) inputPathPatterns+                    dependencyPaths = map (substituteStem stem) dependencyPathPatterns+                 liftIO $ do-                    putStrLn ("need: " ++ show inputPaths')-                    putStrLn ("need: " ++ show dependencyPaths')-                need inputPaths'-                need dependencyPaths'+                    putStrLn $ "need: " ++ show inputPaths+                    putStrLn $ "need: " ++ show dependencyPaths+                need inputPaths+                need dependencyPaths -                let ctx = ToolContext outputPath inputPaths' dependencyPaths'-                liftIO $ toolConfigRunner ctx toolConfig+                let ctx = RunContext outputPath inputPaths dependencyPaths+                liftIO $ runTool ctx tool
app/PansiteApp/CommandLine.hs view
@@ -1,7 +1,7 @@ {-| Module      : PansiteApp.CommandLine Description : Command-line parsers for Pansite app-Copyright   : (C) Richard Cook, 2017+Copyright   : (C) Richard Cook, 2017-2018 Licence     : MIT Maintainer  : rcook@rcook.org Stability   : experimental@@ -14,6 +14,7 @@     , parseCommand     ) where +import           Data.Monoid ((<>)) import           Options.Applicative import           PansiteApp.VersionInfo 
app/PansiteApp/ConfigInfo.hs view
@@ -1,7 +1,7 @@ {-| Module      : PansiteApp.ConfigInfo Description : Configuration info for Pansite app-Copyright   : (C) Richard Cook, 2017+Copyright   : (C) Richard Cook, 2017-2018 Licence     : MIT Maintainer  : rcook@rcook.org Stability   : experimental@@ -51,7 +51,7 @@  readConfigInfo :: AppPaths -> IO ConfigInfo readConfigInfo appPaths@AppPaths{..} = do-    let ctx = ParserContext (resolveFilePath appPaths)+    let ctx = UpdateContext (resolveFilePath appPaths)      -- TODO: Use UTCTime field to determine if shakeVersion should be incremented     currentTime <- getCurrentTime@@ -61,7 +61,7 @@         then do             putStrLn $ "Getting timestamp for configuration file " ++ apAppYamlPath             t <- getModificationTime apAppYamlPath-            mbApp <- readApp ctx [copyToolSpec, pandocToolSpec] apAppYamlPath+            mbApp <- readApp ctx [copyTool, pandocTool] apAppYamlPath             case mbApp of                 Left message -> do                     putStrLn $ "Could not parse configuration file at " ++ apAppYamlPath ++ ": " ++ message
app/PansiteApp/CopyTool.hs view
@@ -1,7 +1,7 @@ {-| Module      : PansiteApp.CopyTool Description : Copy tool-Copyright   : (C) Richard Cook, 2017+Copyright   : (C) Richard Cook, 2017-2018 Licence     : MIT Maintainer  : rcook@rcook.org Stability   : experimental@@ -10,24 +10,16 @@  {-# LANGUAGE OverloadedStrings #-} -module PansiteApp.CopyTool (copyToolSpec) where+module PansiteApp.CopyTool (copyTool) where  import           Data.Aeson-import           Data.Aeson.Types-import           Data.Default import           Pansite  data CopySettings = CopySettings -instance Default CopySettings where-    def = CopySettings--updater :: ParserContext -> CopySettings -> Value -> Parser CopySettings-updater _ orig =-    withObject "copy" $ \_ -> pure orig--runner :: ToolContext -> CopySettings -> IO ()-runner _ _ = error "Not implemented"+copyTool :: Tool+copyTool = mkTool CopySettings -copyToolSpec :: ToolSpec-copyToolSpec = ToolSpec "copy" updater runner+mkTool :: CopySettings -> Tool+mkTool state = Tool "copy" updater (error "Not implemented")+    where updater _ value = mkTool <$> (withObject "copy" $ const (pure state)) value
app/PansiteApp/PandocTool.hs view
@@ -1,7 +1,7 @@ {-| Module      : PansiteApp.PandocTool Description : Pandoc tool-Copyright   : (C) Richard Cook, 2017+Copyright   : (C) Richard Cook, 2017-2018 Licence     : MIT Maintainer  : rcook@rcook.org Stability   : experimental@@ -11,27 +11,40 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} -module PansiteApp.PandocTool (pandocToolSpec) where+module PansiteApp.PandocTool (pandocTool) where  import           Data.Aeson import           Data.Aeson.Types import qualified Data.ByteString.Lazy as BL import           Data.Default import           Data.List+import qualified Data.Text as Text (pack)+import qualified Data.Text.IO as Text (writeFile) import           Pansite import           PansiteApp.Util import           System.FilePath import           Text.Blaze.Html.Renderer.String import           Text.Pandoc-import           Text.Pandoc.Walk-import           Text.Pandoc.XML+                    ( Extension(..)+                    , HTMLMathMethod(..)+                    , Inline(..)+                    , WriterOptions(..)+                    , enableExtension+                    , readMarkdown+                    , readerExtensions+                    , runPure+                    , writeDocx+                    , writeHtml5+                    )+import           Text.Pandoc.Walk (walk)+import           Text.Pandoc.XML (toEntities)  data PandocSettings = PandocSettings     { psNumberSections :: Bool     , psVars :: [(String, String)]     , psTemplatePath :: Maybe FilePath     , psTableOfContents :: Bool-    , psReferenceDocx :: Maybe FilePath+    , psReferenceDoc :: Maybe FilePath     , psMathJaxEnabled :: Bool     , psIncludeInHeaderPath :: Maybe FilePath     , psIncludeBeforeBodyPath :: Maybe FilePath@@ -41,8 +54,8 @@ instance Default PandocSettings where     def = PandocSettings False [] Nothing False Nothing False Nothing Nothing Nothing -updater :: ParserContext -> PandocSettings -> Value -> Parser PandocSettings-updater (ParserContext resolveFilePath) PandocSettings{..} =+updater :: PandocSettings -> UpdateContext -> Value -> Parser PandocSettings+updater PandocSettings{..} (UpdateContext resolveFilePath) =     withObject "pandoc" $ \o ->         let getFilePath key d = fmap (resolveFilePath <$>) (o .:? key .!= d)         in PandocSettings@@ -50,7 +63,7 @@             <*> o .:? "vars" .!= psVars             <*> getFilePath "template-path" psTemplatePath             <*> o .:? "table-of-contents" .!= psTableOfContents-            <*> getFilePath "reference-docx" psReferenceDocx+            <*> getFilePath "reference-doc" psReferenceDoc             <*> o .:? "mathjax" .!= psMathJaxEnabled             <*> getFilePath "include-in-header-path" psIncludeInHeaderPath             <*> getFilePath "include-before-body-path" psIncludeBeforeBodyPath@@ -87,30 +100,37 @@      return $ def         { writerNumberSections = psNumberSections-        , writerReferenceDocx = psReferenceDocx+        , writerReferenceDoc = psReferenceDoc         , writerTemplate = mbTemplate         , writerTableOfContents = psTableOfContents         , writerHTMLMathMethod = htmlMathMethod         , writerVariables = psVars3         } -runner :: ToolContext -> PandocSettings -> IO ()+runner :: PandocSettings -> RunContext -> IO () runner-    (ToolContext outputPath inputPaths _)-    ps@PandocSettings{..} = do+    ps@PandocSettings{..}+    (RunContext outputPath inputPaths _) = do      putStrLn "PandocTool"     putStrLn $ "  outputPath=" ++ outputPath     putStrLn $ "  inputPaths=" ++ show inputPaths     putStrLn $ "  psNumberSections=" ++ show psNumberSections-    putStrLn $ "  psReferenceDocx=" ++ show psReferenceDocx+    putStrLn $ "  psReferenceDoc=" ++ show psReferenceDoc     putStrLn $ "  psTableOfContents=" ++ show psTableOfContents     putStrLn $ "  psTemplatePath=" ++ show psTemplatePath     putStrLn $ "  psVars=" ++ show psVars      md <- (intercalate "\n\n") <$> sequence (map readFileUtf8 inputPaths) -    let Right doc' = readMarkdown def md -- TODO: Irrefutable pattern+    let mdText = Text.pack md+        readerOpts = def+        exts = foldl'+                (flip enableExtension)+                (readerExtensions readerOpts)+                [Ext_backtick_code_blocks, Ext_yaml_metadata_block]+        readerOpts' = readerOpts { readerExtensions = exts }+        Right doc' = runPure $ readMarkdown readerOpts' mdText -- TODO: Irrefutable pattern         doc = walk rewriteLinks doc'      writerOpts <- mkWriterOptions ps@@ -118,11 +138,12 @@     -- TODO: Ugh. Let's make this less hacky. It works for now though.     case (takeExtension outputPath) of         ".docx" -> do-                        docx <- writeDocx writerOpts doc+                        let Right docx = runPure $ writeDocx writerOpts doc -- TODO: Irrefutable pattern                         BL.writeFile outputPath docx         _ -> do-                        let html = toEntities (renderHtml (writeHtml writerOpts doc))-                        writeFileUtf8 outputPath html+                        let Right html = runPure $ writeHtml5 writerOpts doc -- TODO: Irrefutable pattern+                            t = toEntities (Text.pack $ renderHtml html)+                        Text.writeFile outputPath t  -- TODO: This is very application-specific -- TODO: Figure out how to allow this behaviour to be specified in app configuration@@ -139,5 +160,9 @@ rewriteLinks (Link attr is (url, title)) = Link attr is (transformUrl url, title) rewriteLinks i = i -pandocToolSpec :: ToolSpec-pandocToolSpec = ToolSpec "pandoc" updater runner+pandocTool :: Tool+pandocTool = mkTool $ PandocSettings False [] Nothing False Nothing False Nothing Nothing Nothing++mkTool :: PandocSettings -> Tool+mkTool state = Tool "pandoc" updater' (runner state)+    where updater' ctx value = mkTool <$> updater state ctx value
app/PansiteApp/Util.hs view
@@ -1,7 +1,7 @@ {-|-Module      : Pansite.Config.Types+Module      : PansiteApp.Util Description : Helper functions for Pansite application-Copyright   : (C) Richard Cook, 2017+Copyright   : (C) Richard Cook, 2017-2018 Licence     : MIT Maintainer  : rcook@rcook.org Stability   : experimental@@ -12,72 +12,11 @@     ( readFileUtf8     , readFileWithEncoding     , skipDirectory-    , stems-    , writeFileUtf8-    , writeFileWithEncoding     ) where  import           System.FilePath import           System.IO ---- | Compute common prefix of two lists------ Examples:------ >>> commonPrefix "abcdefghi" "abcdefghi"--- "abcdefghi"--- >>> commonPrefix "abcdefghi" "abcfoo"--- "abc"--- >>> commonPrefix "abc" "xyz"--- ""-commonPrefix :: (Eq a) => [a] -> [a] -> [a]-commonPrefix _ [] = []-commonPrefix [] _ = []-commonPrefix (x : xs) (y : ys)-    | x == y = x : commonPrefix xs ys-    | otherwise = []---- | Compute common suffix of two lists------ Examples:------ >>> commonSuffix "abcdefghi" "abcdefghi"--- "abcdefghi"--- >>> commonSuffix "abcdefghi" "fooghi"--- "ghi"--- >>> commonSuffix "abc" "xyz"--- ""-commonSuffix :: (Eq a) => [a] -> [a] -> [a]-commonSuffix xs ys = reverse (commonPrefix (reverse xs) (reverse ys))---- | Slice of list------ Examples:------ >>> slice 4 10 "helloworldgoodbye"--- "oworld"-slice :: Int -> Int -> [a] -> [a]-slice p0 p1 xs = take (p1 - p0) (drop p0 xs)---- | Stems of lists------ Examples:------ >>> stems "abcmiddledef" "abcfoodef"--- ("middle","foo")--- >>> stems "abc" "xyz"--- ("abc","xyz")-stems :: (Eq a) => [a] -> [a] -> ([a], [a])-stems xs ys =-    let prefix = commonPrefix xs ys-        prefixCount = length prefix-        suffix = commonSuffix xs ys-        suffixCount = length suffix-        xsCount = length xs-        ysCount = length ys-    in (slice prefixCount (xsCount - suffixCount) xs, slice prefixCount (ysCount - suffixCount) ys)- readFileWithEncoding :: TextEncoding -> FilePath -> IO String readFileWithEncoding encoding path = do     h <- openFile path ReadMode@@ -86,15 +25,6 @@  readFileUtf8 :: FilePath -> IO String readFileUtf8 = readFileWithEncoding utf8--writeFileWithEncoding :: TextEncoding -> FilePath -> String -> IO ()-writeFileWithEncoding encoding path content =-    withFile path WriteMode $ \h -> do-        hSetEncoding h encoding-        hPutStr h content--writeFileUtf8 :: FilePath -> String -> IO ()-writeFileUtf8 = writeFileWithEncoding utf8  skipDirectory :: FilePath -> FilePath skipDirectory p = let d = takeDirectory p in drop (length d + 1) p
app/PansiteApp/VersionInfo.hs view
@@ -1,7 +1,7 @@ {-| Module      : Pansite.VersionInfo Description : Version information for Pansite application-Copyright   : (C) Richard Cook, 2017+Copyright   : (C) Richard Cook, 2017-2018 Licence     : MIT Maintainer  : rcook@rcook.org Stability   : experimental
doctest/Main.hs view
@@ -1,23 +1,38 @@ {-| Module      : Main Description : Main entrypoint for Pansite doctests-Copyright   : (C) Richard Cook, 2017+Copyright   : (C) Richard Cook, 2017-2018 Licence     : MIT Maintainer  : rcook@rcook.org Stability   : experimental Portability : portable -} +{-# LANGUAGE CPP #-}+ module Main (main) where  import           System.FilePath.Glob import           Test.DocTest +compilerOpts :: [String]+compilerOpts =+#if defined(OS_LINUX)+    [ "-DOS_LINUX"+#elif defined(OS_MACOS)+    [ "-DOS_MACOS"+#elif defined(OS_WINDOWS)+    [ "-DOS_WINDOWS"+#else+#error Unsupported platform+#endif+    ]+ defaultIncludeDirs :: [FilePath] defaultIncludeDirs = []  doctestWithIncludeDirs :: [FilePath] -> IO ()-doctestWithIncludeDirs fs = doctest (map ("-I" ++) defaultIncludeDirs ++ fs)+doctestWithIncludeDirs fs = doctest (map ("-I" ++) defaultIncludeDirs ++ fs ++ compilerOpts)  sourceDirs :: [FilePath] sourceDirs = ["app", "src"]
pansite.cabal view
@@ -1,13 +1,13 @@ name:                             pansite-version:                          0.1.0.0+version:                          0.2.0.0 synopsis:                         Pansite: a simple web site management tool-description:                      Please see README.md+description:                      Pansite is a Pandoc-based web site management tool. Please see README.md homepage:                         https://github.com/rcook/pansite#readme license:                          MIT license-file:                     LICENSE author:                           Richard Cook maintainer:                       rcook@rcook.org-copyright:                        2017 Richard Cook+copyright:                        2017-2018 Richard Cook category:                         Web build-type:                       Simple extra-source-files:               README.md@@ -20,24 +20,41 @@ library   default-language:               Haskell2010   ghc-options:                    -W -Wall -fwarn-incomplete-patterns -fwarn-unused-imports+  if os(darwin)+    cpp-options:                  -DOS_MACOS+  if os(linux)+    cpp-options:                  -DOS_LINUX+  if os(windows)+    cpp-options:                  -DOS_WINDOWS   hs-source-dirs:                 src   exposed-modules:                Pansite   other-modules:                  Pansite.Config                                 , Pansite.Config.Funcs                                 , Pansite.Config.Types-  build-depends:                  aeson+                                , Pansite.Config.Util+                                , Pansite.PathPattern+                                , Pansite.Util+  build-depends:                  MissingH+                                , aeson                                 , base >= 4.7 && < 5                                 , bytestring                                 , data-default+                                , shake                                 , split                                 , text                                 , unordered-containers                                 , vector                                 , yaml -executable pansite-app+executable pansite   default-language:               Haskell2010   ghc-options:                    -threaded -rtsopts -with-rtsopts=-N -W -Wall -fwarn-incomplete-patterns -fwarn-unused-imports+  if os(darwin)+    cpp-options:                  -DOS_MACOS+  if os(linux)+    cpp-options:                  -DOS_LINUX+  if os(windows)+    cpp-options:                  -DOS_WINDOWS   hs-source-dirs:                 app   main-is:                        Main.hs   other-modules:                  PansiteApp.App@@ -48,6 +65,7 @@                                 , PansiteApp.PandocTool                                 , PansiteApp.Util                                 , PansiteApp.VersionInfo+                                , Paths_pansite   build-depends:                  MissingH                                 , aeson                                 , base >= 4.7 && < 5@@ -62,6 +80,7 @@                                 , pandoc-types                                 , pansite                                 , shake+                                , split                                 , template-haskell                                 , text                                 , time@@ -75,6 +94,12 @@   type:                           exitcode-stdio-1.0   default-language:               Haskell2010   ghc-options:                    -threaded -rtsopts -with-rtsopts=-N -W -Wall -fwarn-incomplete-patterns -fwarn-unused-imports+  if os(darwin)+    cpp-options:                  -DOS_MACOS+  if os(linux)+    cpp-options:                  -DOS_LINUX+  if os(windows)+    cpp-options:                  -DOS_WINDOWS   hs-source-dirs:                 doctest   main-is:                        Main.hs   build-depends:                  Glob@@ -85,8 +110,16 @@   type:                           exitcode-stdio-1.0   default-language:               Haskell2010   ghc-options:                    -threaded -rtsopts -with-rtsopts=-N -W -Wall -fwarn-incomplete-patterns -fwarn-unused-imports+  if os(darwin)+    cpp-options:                  -DOS_MACOS+  if os(linux)+    cpp-options:                  -DOS_LINUX+  if os(windows)+    cpp-options:                  -DOS_WINDOWS   hs-source-dirs:                 spec   main-is:                        Spec.hs-  build-depends:                  base >= 4.7 && < 5+  other-modules:                  PansiteSpec.Config.UtilSpec+  build-depends:                  QuickCheck+                                , base >= 4.7 && < 5                                 , hspec                                 , pansite
+ spec/PansiteSpec/Config/UtilSpec.hs view
@@ -0,0 +1,17 @@+module PansiteSpec.Config.UtilSpec (spec) where++import           Data.List+import           Pansite+import           Test.Hspec+import           Test.QuickCheck++genRoutePath :: Gen String+genRoutePath = intercalate "/" <$> listOf (listOf $ elements ['a'..'z'])++unsplitRoutePathProp :: Property+unsplitRoutePathProp = forAll genRoutePath $ \p -> intercalate "/" (splitRoutePath p) == p++spec :: Spec+spec = do+    describe "splitRoutePath" $+        it "satisfies unsplitRoutePathProp" $ property unsplitRoutePathProp
spec/Spec.hs view
@@ -1,7 +1,7 @@ {-| Module      : Main Description : Main entrypoint for Pansite spec tests-Copyright   : (C) Richard Cook, 2017+Copyright   : (C) Richard Cook, 2017-2018 Licence     : MIT Maintainer  : rcook@rcook.org Stability   : experimental
src/Pansite.hs view
@@ -1,7 +1,7 @@ {-| Module      : Pansite Description : Umbrella module for Pansite-Copyright   : (C) Richard Cook, 2017+Copyright   : (C) Richard Cook, 2017-2018 Licence     : MIT Maintainer  : rcook@rcook.org Stability   : experimental@@ -10,6 +10,10 @@  module Pansite     ( module Pansite.Config+    , module Pansite.PathPattern+    , module Pansite.Util     ) where  import           Pansite.Config+import           Pansite.PathPattern+import           Pansite.Util
src/Pansite/Config.hs view
@@ -1,7 +1,7 @@ {-| Module      : Pansite.Config Description : Umbrella module for Pansite app configuration-Copyright   : (C) Richard Cook, 2017+Copyright   : (C) Richard Cook, 2017-2018 Licence     : MIT Maintainer  : rcook@rcook.org Stability   : experimental@@ -11,7 +11,9 @@ module Pansite.Config     ( module Pansite.Config.Funcs     , module Pansite.Config.Types+    , module Pansite.Config.Util     ) where  import           Pansite.Config.Funcs import           Pansite.Config.Types+import           Pansite.Config.Util
src/Pansite/Config/Funcs.hs view
@@ -1,7 +1,7 @@ {-| Module      : Pansite.Config.Types Description : Functions for Pansite app configuration-Copyright   : (C) Richard Cook, 2017+Copyright   : (C) Richard Cook, 2017-2018 Licence     : MIT Maintainer  : rcook@rcook.org Stability   : experimental@@ -13,86 +13,83 @@  module Pansite.Config.Funcs     ( readApp-    , toolConfigRunner+    , runTool     ) where  import           Control.Monad import           Data.Aeson.Types import qualified Data.ByteString.Char8 as C8-import           Data.Default import           Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap-import           Data.List.Split import           Data.Text (Text) import qualified Data.Text as Text import           Data.Traversable import qualified Data.Vector as Vector import           Data.Yaml import           Pansite.Config.Types+import           Pansite.Config.Util+import           Pansite.PathPattern -type ToolConfigMap = HashMap String ToolConfig+type ToolMap = HashMap String Tool -defaultToolConfig :: ToolSpec -> ToolConfig-defaultToolConfig (ToolSpec _ u r) = ToolConfig u r def+updateTool :: UpdateContext -> Tool -> Value -> Parser Tool+updateTool ctx (Tool _ u _) value = u ctx value -toolConfigUpdater :: ParserContext -> ToolConfig -> Value -> Parser ToolConfig-toolConfigUpdater ctx (ToolConfig u r a) value = do-    result <- u ctx a value-    return $ ToolConfig u r result+updateTools :: UpdateContext -> ToolMap -> [(String, Value)] -> Parser ToolMap+updateTools ctx = foldM (\m (key, value) -> case HashMap.lookup key m of+                                    Nothing -> fail $ "Unsupported tool " ++ key+                                    Just toolConfigOrig -> do+                                        toolConfig <- updateTool ctx toolConfigOrig value+                                        return $ HashMap.insert key toolConfig m) -toolConfigRunner :: ToolContext -> ToolConfig -> IO ()-toolConfigRunner ctx (ToolConfig _ r a) = r ctx a+runTool :: RunContext -> Tool -> IO ()+runTool ctx (Tool _ _ r) = r ctx  arrayParser :: Object -> Text -> (Value -> Parser a) -> Parser [a]-arrayParser o key parser = helper (Text.unpack key) parser =<< (o .: key)+arrayParser o key parser = helper (Text.unpack key) parser =<< o .: key     where helper expected f = withArray expected $ \arr -> mapM f (Vector.toList arr) --- TODO: Create a unit test for this!-parseRoutePath :: String -> [String]-parseRoutePath path =-    let fragments = splitOn "/" path-        fragmentCount = length fragments-    in if (fragmentCount == 1 && fragments !! 0 == "")-          then []-          else fragments--appParser :: ParserContext -> [ToolSpec] -> Value -> Parser App-appParser ctx toolSpecs = withObject "App" $ \o -> do-    let toolConfigMapOrig = HashMap.fromList (map (\t@(ToolSpec k _ _) -> (k, defaultToolConfig t)) toolSpecs)-    toolConfigPairs <- toolConfigsParser =<< o .:? "tool-settings" .!= emptyObject-    toolConfigMap <- updateToolConfigs ctx toolConfigMapOrig toolConfigPairs+appParser :: UpdateContext -> [Tool] -> Value -> Parser App+appParser ctx tools = withObject "App" $ \o -> do+    let toolMapOrig = HashMap.fromList (map (\t@(Tool k _ _) -> (k, t)) tools)+    toolSettings <- toolSettingsParser =<< o .:? "tool-settings" .!= emptyObject+    toolMapNew <- updateTools ctx toolMapOrig toolSettings     routes <- arrayParser o "routes" (routeParser ctx)-    targets <- arrayParser o "targets" (targetParser ctx toolConfigMap)+    targets <- arrayParser o "targets" (targetParser ctx toolMapNew)     return $ App routes targets -toolConfigsParser :: Value -> Parser [(String, Value)]-toolConfigsParser = withObject "tool-settings" $ \o ->+toolSettingsParser :: Value -> Parser [(String, Value)]+toolSettingsParser = withObject "tool-settings" $ \o ->     for (HashMap.toList o) $ \(name, value) -> return (Text.unpack name, value) -updateToolConfigs :: ParserContext -> ToolConfigMap -> [(String, Value)] -> Parser ToolConfigMap-updateToolConfigs ctx = foldM (\m (key, value) -> case HashMap.lookup key m of-                                    Nothing -> fail $ "Unsupported tool " ++ key-                                    Just toolConfigOrig -> do-                                        toolConfig <- toolConfigUpdater ctx toolConfigOrig value-                                        return $ HashMap.insert key toolConfig m)--routeParser :: ParserContext -> Value -> Parser Route-routeParser (ParserContext resolveFilePath) =+routeParser :: UpdateContext -> Value -> Parser Route+routeParser (UpdateContext resolveFilePath) =     withObject "route" $ \o -> Route-        <$> parseRoutePath <$> o .: "path"+        <$> splitRoutePath <$> o .: "path"         <*> (resolveFilePath <$> o .: "target") -targetParser :: ParserContext -> ToolConfigMap -> Value -> Parser Target-targetParser ctx@(ParserContext resolveFilePath) toolConfigMap =+pathPatternParser :: FilePathResolver -> String -> Parser PathPattern+pathPatternParser resolveFilePath s = case pathPattern (resolveFilePath s) of+    Left message -> fail message+    Right p -> return p++targetParser :: UpdateContext -> ToolMap -> Value -> Parser Target+targetParser ctx@(UpdateContext resolveFilePath) toolMap =     withObject "target" $ \o -> do-        path <- resolveFilePath <$> o .: "path"+        let pathPatternParser' = pathPatternParser resolveFilePath++        path <- pathPatternParser' =<< o .: "path"+         key <- o .: "tool"-        toolConfigOrig <- case HashMap.lookup key toolConfigMap of+        toolConfigOrig <- case HashMap.lookup key toolMap of                         Nothing -> fail $ "Unsupported tool " ++ key                         Just p -> return p-        toolConfig <- toolConfigUpdater ctx toolConfigOrig =<< o .:? "tool-settings" .!= emptyObject-        inputPaths <- ((map resolveFilePath) <$> o .: "inputs")-        dependencyPaths <-  ((map resolveFilePath) <$> o .: "dependencies")+        toolConfig <- updateTool ctx toolConfigOrig =<< o .:? "tool-settings" .!= emptyObject++        inputPaths <- mapM pathPatternParser' =<< o .: "inputs"++        dependencyPaths <- mapM pathPatternParser' =<< o .: "dependencies"+         return $ Target path toolConfig inputPaths dependencyPaths  parseExceptionMessage :: FilePath -> ParseException -> String@@ -110,15 +107,15 @@     "Invalid configuration: " ++ problem ++ "\n" ++     "Location: " ++ appYamlPath -readApp :: ParserContext -> [ToolSpec] -> FilePath -> IO (Either String App)-readApp ctx toolSpecs appYamlPath = do+readApp :: UpdateContext -> [Tool] -> FilePath -> IO (Either String App)+readApp ctx tools appYamlPath = do     yaml <- C8.readFile appYamlPath     case decodeEither' yaml of         Left e -> do             putStrLn $ "WARNING: " ++ parseExceptionMessage appYamlPath e             return $ Left (show e)         Right value -> do-            case parse (appParser ctx toolSpecs) value of+            case parse (appParser ctx tools) value of                 Error problem -> do                     putStrLn $ "WARNING: " ++ resultErrorMessage appYamlPath problem                     return $ Left problem@@ -126,5 +123,5 @@                     forM_ routes $ \(Route path target) ->                         putStrLn $ "Route: " ++ show path ++ " -> " ++ target                     forM_ targets $ \(Target path _ inputPaths dependencyPaths) -> do-                        putStrLn $ "Target: " ++ path ++ ", " ++ show inputPaths ++ ", " ++ show dependencyPaths+                        putStrLn $ "Target: " ++ show path ++ ", " ++ show inputPaths ++ ", " ++ show dependencyPaths                     return $ Right app
src/Pansite/Config/Types.hs view
@@ -1,54 +1,43 @@ {-| Module      : Pansite.Config.Types Description : Types for Pansite app configuration-Copyright   : (C) Richard Cook, 2017+Copyright   : (C) Richard Cook, 2017-2018 Licence     : MIT Maintainer  : rcook@rcook.org Stability   : experimental Portability : portable -} -{-# LANGUAGE ExistentialQuantification #-}- module Pansite.Config.Types     ( App (..)     , FilePathResolver-    , ParserContext (..)     , Route (..)+    , RunContext (..)     , Target (..)-    , ToolConfig (..)-    , ToolConfigRunner-    , ToolConfigUpdater-    , ToolContext (..)-    , ToolSpec (..)+    , Tool (..)+    , UpdateContext (..)     ) where -import           Data.Default import           Data.Yaml+import           Pansite.PathPattern  type FilePathResolver = FilePath -> FilePath -type ToolConfigUpdater a = ParserContext -> a -> Value -> Parser a--type ToolConfigRunner a = ToolContext -> a -> IO ()--data ParserContext = ParserContext+data UpdateContext = UpdateContext     FilePathResolver        -- file path resolver -data ToolContext = ToolContext+data RunContext = RunContext     FilePath                -- output path     [FilePath]              -- input paths     [FilePath]              -- dependency paths -data ToolSpec = forall a. Default a => ToolSpec-    String                  -- key-    (ToolConfigUpdater a)   -- updater function-    (ToolConfigRunner a)    -- runner function+data Tool = Tool+    String                                  -- key+    (UpdateContext -> Value -> Parser Tool) -- update function+    (RunContext -> IO ())                   -- run function  data App = App [Route] [Target]  data Route = Route [String] FilePath -data Target = Target FilePath ToolConfig [FilePath] [FilePath]--data ToolConfig = forall a. ToolConfig (ToolConfigUpdater a) (ToolConfigRunner a) a+data Target = Target PathPattern Tool [PathPattern] [PathPattern]
+ src/Pansite/Config/Util.hs view
@@ -0,0 +1,30 @@+{-|+Module      : Pansite.Config.Util+Description : Helper functions for Pansite app configuration+Copyright   : (C) Richard Cook, 2017-2018+Licence     : MIT+Maintainer  : rcook@rcook.org+Stability   : experimental+Portability : portable+-}++module Pansite.Config.Util+    ( splitRoutePath+    ) where++import           Data.List.Split++-- | Split route path into fragments+--+-- Examples:+--+-- >>> splitRoutePath "aaa/bbb"+-- ["aaa","bbb"]+-- >>> splitRoutePath "aaa"+-- ["aaa"]+-- >>> splitRoutePath ""+-- []+splitRoutePath :: String -> [String]+splitRoutePath path = case splitOn "/" path of+    [""] -> []+    fragments -> fragments
+ src/Pansite/PathPattern.hs view
@@ -0,0 +1,90 @@+{-|+Module      : Pansite.PathPattern+Description : GNU Make-style path patterns+Copyright   : (C) Richard Cook, 2017-2018+Licence     : MIT+Maintainer  : rcook@rcook.org+Stability   : experimental+Portability : portable+-}++{-# LANGUAGE CPP #-}++module Pansite.PathPattern+    ( PathPattern+    , pathPattern+    , pathPatternStem+    , substituteStem+    , (%%>>)+    ) where++import           Data.String.Utils+import           Development.Shake+import           Development.Shake.FilePath+import           Pansite.Util++newtype PathPattern = PathPattern String deriving Show++makeToken :: String+makeToken = "%"++shakeToken :: String+shakeToken = "*"++-- | Convert string to path pattern+--+-- Examples:+--+-- >>> import Data.Either+-- >>> isRight $ pathPattern "%"+-- True+-- >>> isRight $ pathPattern "aaa"+-- True+-- >>> isLeft $ pathPattern "%%"+-- True+-- >>> isLeft $ pathPattern "%aaa%"+-- True+pathPattern :: String -> Either String PathPattern+pathPattern s+    | let n = countOccurrences s makeToken in n == 0 || n == 1 = Right $ PathPattern s+    | otherwise = Left $ "Invalid path pattern " ++ s++-- | Substitute stem in path pattern+--+-- Examples:+--+-- >>> Right p = pathPattern "/aaa/bbb/%/ddd/eee/"+-- >>> substituteStem "ccc" p+-- "/aaa/bbb/ccc/ddd/eee/"+substituteStem :: String -> PathPattern -> FilePath+substituteStem stem (PathPattern s) = replace makeToken stem s++(%%>>) :: PathPattern -> (FilePath -> Action ()) -> Rules ()+(%%>>) (PathPattern s) b = replace makeToken shakeToken s %> b+infix 1 %%>>++#if defined(OS_MACOS) || defined(OS_LINUX)+-- | Stem of pattern and path+--+-- Examples:+--+-- >>> Right p = pathPattern "C:/aaa/bbb/%.txt"+-- >>> pathPatternStem p "C:/aaa/bbb/stem.txt"+-- "stem"+#elif defined(OS_WINDOWS)+-- TODO: Enforce the wildcard token via the types!+-- | Find stem of path and rule+--+-- Examples:+--+-- >>> Right p0 = pathPattern "C:/aaa/bbb/%.txt"+-- >>> pathPatternStem p0 "C:\\aaa\\bbb\\stem.txt"+-- "stem"+-- >>> Right p1 = pathPattern "C:\\aaa\\bbb\\%.txt"+-- >>> pathPatternStem p1 "C:/aaa/bbb/stem.txt"+-- "stem"+#else+#error Unsupported platform+#endif+pathPatternStem :: PathPattern -> FilePath -> String+pathPatternStem (PathPattern s) path = let (stem, _) = stems (toNative path) (toNative s) in stem
+ src/Pansite/Util.hs view
@@ -0,0 +1,92 @@+{-|+Module      : Pansite.Util+Description : Helper functions for Pansite+Copyright   : (C) Richard Cook, 2017-2018+Licence     : MIT+Maintainer  : rcook@rcook.org+Stability   : experimental+Portability : portable+-}++module Pansite.Util+    ( countOccurrences+    , stems+    ) where++import           Data.List.Split++-- | Number of occurrences of a substring in a string+--+-- Examples:+--+-- >>> countOccurrences "" "abc"+-- 0+-- >>> countOccurrences "" ""+-- 0+-- >>> countOccurrences "abcdefghi" "xyz"+-- 0+-- >>> countOccurrences "abc" "abc"+-- 1+-- >>> countOccurrences "abcabc" "abc"+-- 2+-- >>> countOccurrences "abcdefabcghiabcjkl" "abc"+-- 3+countOccurrences :: String -> String -> Int+countOccurrences str substr = length (splitOn substr str) - 1++-- | Compute common prefix of two lists+--+-- Examples:+--+-- >>> commonPrefix "abcdefghi" "abcdefghi"+-- "abcdefghi"+-- >>> commonPrefix "abcdefghi" "abcfoo"+-- "abc"+-- >>> commonPrefix "abc" "xyz"+-- ""+commonPrefix :: (Eq a) => [a] -> [a] -> [a]+commonPrefix _ [] = []+commonPrefix [] _ = []+commonPrefix (x : xs) (y : ys)+    | x == y = x : commonPrefix xs ys+    | otherwise = []++-- | Compute common suffix of two lists+--+-- Examples:+--+-- >>> commonSuffix "abcdefghi" "abcdefghi"+-- "abcdefghi"+-- >>> commonSuffix "abcdefghi" "fooghi"+-- "ghi"+-- >>> commonSuffix "abc" "xyz"+-- ""+commonSuffix :: (Eq a) => [a] -> [a] -> [a]+commonSuffix xs ys = reverse (commonPrefix (reverse xs) (reverse ys))++-- | Stems of lists+--+-- Examples:+--+-- >>> stems "abcmiddledef" "abcfoodef"+-- ("middle","foo")+-- >>> stems "abc" "xyz"+-- ("abc","xyz")+stems :: (Eq a) => [a] -> [a] -> ([a], [a])+stems xs ys =+    let prefix = commonPrefix xs ys+        prefixCount = length prefix+        suffix = commonSuffix xs ys+        suffixCount = length suffix+        xsCount = length xs+        ysCount = length ys+    in (slice prefixCount (xsCount - suffixCount) xs, slice prefixCount (ysCount - suffixCount) ys)++-- | Slice of list+--+-- Examples:+--+-- >>> slice 4 10 "helloworldgoodbye"+-- "oworld"+slice :: Int -> Int -> [a] -> [a]+slice p0 p1 xs = take (p1 - p0) (drop p0 xs)