rib 0.8.0.0 → 0.10.0.0
raw patch · 15 files changed
+392/−459 lines, 15 filesdep +QuickCheckdep +hspecdep −pathdep −path-iodep ~reludePVP ok
version bump matches the API change (PVP)
Dependencies added: QuickCheck, hspec
Dependencies removed: path, path-io
Dependency ranges changed: relude
API changes (from Hackage documentation)
- Rib.App: Generate :: Bool -> Command
- Rib.App: OneOff :: Command
- Rib.App: Serve :: Int -> Bool -> Command
- Rib.App: Watch :: Command
- Rib.App: [dontWatch] :: Command -> Bool
- Rib.App: [full] :: Command -> Bool
- Rib.App: [port] :: Command -> Int
- Rib.App: commandParser :: Parser Command
- Rib.App: data Command
- Rib.App: instance GHC.Classes.Eq Rib.App.Command
- Rib.App: instance GHC.Generics.Generic Rib.App.Command
- Rib.App: instance GHC.Show.Show Rib.App.Command
- Rib.Settings: RibSettings :: Path Rel Dir -> Path Rel Dir -> Verbosity -> Bool -> RibSettings
- Rib.Settings: [_ribSettings_fullGen] :: RibSettings -> Bool
- Rib.Settings: [_ribSettings_inputDir] :: RibSettings -> Path Rel Dir
- Rib.Settings: [_ribSettings_outputDir] :: RibSettings -> Path Rel Dir
- Rib.Settings: [_ribSettings_verbosity] :: RibSettings -> Verbosity
- Rib.Settings: data RibSettings
- Rib.Shake: getDirectoryFiles' :: Typeable b => Path b Dir -> [Path Rel File] -> Action [Path Rel File]
+ Rib.Cli: CliConfig :: Bool -> Bool -> Maybe (Text, Int) -> Verbosity -> FilePath -> FilePath -> FilePath -> [FilePath] -> CliConfig
+ Rib.Cli: Diagnostic :: Verbosity
+ Rib.Cli: Error :: Verbosity
+ Rib.Cli: Info :: Verbosity
+ Rib.Cli: Silent :: Verbosity
+ Rib.Cli: Verbose :: Verbosity
+ Rib.Cli: Warn :: Verbosity
+ Rib.Cli: [inputDir] :: CliConfig -> FilePath
+ Rib.Cli: [outputDir] :: CliConfig -> FilePath
+ Rib.Cli: [rebuildAll] :: CliConfig -> Bool
+ Rib.Cli: [serve] :: CliConfig -> Maybe (Text, Int)
+ Rib.Cli: [shakeDbDir] :: CliConfig -> FilePath
+ Rib.Cli: [verbosity] :: CliConfig -> Verbosity
+ Rib.Cli: [watchIgnore] :: CliConfig -> [FilePath]
+ Rib.Cli: [watch] :: CliConfig -> Bool
+ Rib.Cli: cliParser :: FilePath -> FilePath -> Parser CliConfig
+ Rib.Cli: data CliConfig
+ Rib.Cli: data Verbosity
+ Rib.Cli: directoryReader :: ReadM FilePath
+ Rib.Cli: hostPortParser :: Parsec Void Text (Text, Int)
+ Rib.Cli: instance GHC.Classes.Eq Rib.Cli.CliConfig
+ Rib.Cli: instance GHC.Generics.Generic Rib.Cli.CliConfig
+ Rib.Cli: instance GHC.Show.Show Rib.Cli.CliConfig
+ Rib.Cli: serveOption :: Parser (Maybe (Text, Int))
+ Rib.Cli: watchOption :: Parser Bool
+ Rib.Log: logErr :: MonadIO m => String -> m ()
+ Rib.Log: logStrLn :: MonadIO m => CliConfig -> String -> m ()
+ Rib.Shake: getCliConfig :: Action CliConfig
- Rib.App: run :: Path Rel Dir -> Path Rel Dir -> Action () -> IO ()
+ Rib.App: run :: FilePath -> FilePath -> Action () -> IO ()
- Rib.App: runWith :: Path Rel Dir -> Path Rel Dir -> Action () -> Command -> IO ()
+ Rib.App: runWith :: Action () -> CliConfig -> IO ()
- Rib.Parser.Dhall: parse :: FromDhall a => [Path Rel File] -> Path Rel File -> Action a
+ Rib.Parser.Dhall: parse :: FromDhall a => [FilePath] -> FilePath -> Action a
- Rib.Parser.MMark: parse :: Path Rel File -> Action MMark
+ Rib.Parser.MMark: parse :: FilePath -> Action MMark
- Rib.Parser.MMark: parseWith :: [Extension] -> Path Rel File -> Action MMark
+ Rib.Parser.MMark: parseWith :: [Extension] -> FilePath -> Action MMark
- Rib.Parser.Pandoc: parse :: (ReaderOptions -> Text -> PandocIO Pandoc) -> Path Rel File -> Action Pandoc
+ Rib.Parser.Pandoc: parse :: (ReaderOptions -> Text -> PandocIO Pandoc) -> FilePath -> Action Pandoc
- Rib.Route: routeFile :: (IsRoute r, MonadThrow m) => r a -> m (Path Rel File)
+ Rib.Route: routeFile :: (IsRoute r, MonadThrow m) => r a -> m FilePath
- Rib.Shake: buildStaticFiles :: [Path Rel File] -> Action ()
+ Rib.Shake: buildStaticFiles :: [FilePath] -> Action ()
- Rib.Shake: forEvery :: [Path Rel File] -> (Path Rel File -> Action a) -> Action [a]
+ Rib.Shake: forEvery :: [FilePath] -> (FilePath -> Action a) -> Action [a]
- Rib.Shake: ribInputDir :: Action (Path Rel Dir)
+ Rib.Shake: ribInputDir :: Action FilePath
- Rib.Shake: ribOutputDir :: Action (Path Rel Dir)
+ Rib.Shake: ribOutputDir :: Action FilePath
- Rib.Shake: writeFileCached :: Path Rel File -> String -> Action ()
+ Rib.Shake: writeFileCached :: FilePath -> String -> Action ()
- Rib.Watch: onTreeChange :: Path b t -> ([Event] -> IO ()) -> IO ()
+ Rib.Watch: onTreeChange :: FilePath -> ([Event] -> IO ()) -> IO ()
Files
- CHANGELOG.md +14/−0
- README.md +12/−227
- rib.cabal +35/−21
- src/Rib/App.hs +75/−127
- src/Rib/Cli.hs +166/−0
- src/Rib/Log.hs +28/−0
- src/Rib/Parser/Dhall.hs +6/−6
- src/Rib/Parser/MMark.hs +5/−5
- src/Rib/Parser/Pandoc.hs +3/−3
- src/Rib/Route.hs +5/−5
- src/Rib/Server.hs +8/−4
- src/Rib/Settings.hs +0/−31
- src/Rib/Shake.hs +32/−27
- src/Rib/Watch.hs +2/−3
- test/Spec.hs +1/−0
CHANGELOG.md view
@@ -1,5 +1,19 @@ # Change Log for rib +## 0.10.0.0++- API+ - Dropped `path` and `path-io` in favour of good ol' `FilePath`+ - This also lifts the restriction with absolute paths+- Misc changes+ - #145: CLI arguments have been revamped+ - `serve` subcommand is replaced by the options `-wS`.+ - Added `--input-dir/--output-dir` to override these paths+ - Accept host string in addition to port number+ - Exposed `Rib.Shake.getCliConfig` to get full CLI configuration+ - Allow customizing fsnotify ignore list+ - #141: Allow quiet logging (useful when rib is used as a library)+ ## 0.8.0.0 - Dependency upgrades
README.md view
@@ -1,4 +1,4 @@-+<img width="64px" src="./assets/rib.svg"> # rib @@ -7,238 +7,23 @@ [](https://builtwithnix.org) [](https://funprog.zulipchat.com/#narrow/stream/218047-Rib) -Rib is a Haskell **static site generator** that aims to reuse existing libraries instead of reinventing the wheel.--How does it compare to the popular static site generator Hakyll?--- Uses the [Shake](https://shakebuild.com/) build system at its core.-- Write HTML ([Lucid](https://chrisdone.com/posts/lucid2/)) & CSS ([Clay](http://fvisser.nl/clay/)) in Haskell.-- Built-in support for [Pandoc](https://pandoc.org/) and [MMark](https://github.com/mmark-md/mmark).-- Remain as simple as possible to use (see example below)-- Nix-based environment for reproducibility-- `ghcid` and fsnotify for "hot reload"--Rib prioritizes the use of *existing* tools over reinventing them, and enables-the user to compose them as they wish instead of having to write code to fit a-custom framework.--**Table of Contents**--- [Quick Preview](#quick-preview)-- [Getting Started](#getting-started)-- [Concepts](#concepts)- - [Directory structure](#directory-structure)- - [Run the site](#run-the-site)- - [How Rib works](#how-rib-works)- - [Editing workflow](#editing-workflow)- - [What's next?](#whats-next)-- [Examples](#examples)--## Quick Preview--Here is how your code may look like if you were to generate your static site-using Rib:--```haskell--- | Route corresponding to each generated static page.------ The `a` parameter specifies the data (typically Markdown document) used to--- generate the final page text.-data Route a where- Route_Index :: Route [(Route Pandoc, Pandoc)]- Route_Article :: Path Rel File -> Route Pandoc---- | The `IsRoute` instance allows us to determine the target .html path for--- each route. This affects what `routeUrl` will return.-instance IsRoute Route where- routeFile = \case- Route_Index ->- pure [relfile|index.html|]- Route_Article srcPath ->- fmap ([reldir|article|] </>) $- replaceExtension ".html" srcPath---- | Main entry point to our generator.------ `Rib.run` handles CLI arguments, and takes three parameters here.------ 1. Directory `content`, from which static files will be read.--- 2. Directory `dest`, under which target files will be generated.--- 3. Shake action to run.------ In the shake action you would expect to use the utility functions--- provided by Rib to do the actual generation of your static site.-main :: IO ()-main = withUtf8 $ do- Rib.run [reldir|content|] [reldir|dest|] generateSite+Rib is a Haskell **static site generator** based on Shake, with a delightful workflow. --- | Shake action for generating the static site-generateSite :: Action ()-generateSite = do- -- Copy over the static files- Rib.buildStaticFiles [[relfile|static/**|]]- let writeHtmlRoute :: Route a -> a -> Action ()- writeHtmlRoute r = Rib.writeRoute r . Lucid.renderText . renderPage r- -- Build individual sources, generating .html for each.- articles <-- Rib.forEvery [[relfile|*.md|]] $ \srcPath -> do- let r = Route_Article srcPath- doc <- Pandoc.parse Pandoc.readMarkdown srcPath- writeHtmlRoute r doc- pure (r, doc)- writeHtmlRoute Route_Index articles+See <https://rib.srid.ca> for full documentation. --- | Define your site HTML here-renderPage :: Route a -> a -> Html ()-renderPage route val = html_ [lang_ "en"] $ do- head_ $ do- meta_ [httpEquiv_ "Content-Type", content_ "text/html; charset=utf-8"]- title_ routeTitle- style_ [type_ "text/css"] $ C.render pageStyle- body_ $ do- div_ [class_ "header"] $- a_ [href_ "/"] "Back to Home"- h1_ routeTitle- case route of- Route_Index ->- div_ $ forM_ val $ \(r, src) ->- li_ [class_ "pages"] $ do- let meta = getMeta src- b_ $ a_ [href_ (Rib.routeUrl r)] $ toHtml $ title meta- renderMarkdown `mapM_` description meta- Route_Article _ ->- article_ $- Pandoc.render val- where- routeTitle :: Html ()- routeTitle = case route of- Route_Index -> "Rib sample site"- Route_Article _ -> toHtml $ title $ getMeta val- renderMarkdown :: Text -> Html ()- renderMarkdown =- Pandoc.render . Pandoc.parsePure Pandoc.readMarkdown+## Developing rib --- | Define your site CSS here-pageStyle :: Css-pageStyle = C.body ? do- C.margin (em 4) (pc 20) (em 1) (pc 20)- ".header" ? do- C.marginBottom $ em 2- "li.pages" ? do- C.listStyleType C.none- C.marginTop $ em 1- "b" ? C.fontSize (em 1.2)- "p" ? sym C.margin (px 0)+Use ghcid for quicker compilation cycles: --- | Metadata in our markdown sources-data SrcMeta- = SrcMeta- { title :: Text,- -- | Description is optional, hence `Maybe`- description :: Maybe Text- }- deriving (Show, Eq, Generic, FromJSON) ```--(View full [`Main.hs`](https://github.com/srid/rib-sample/blob/master/src/Main.hs) at rib-sample)--## Getting Started--The easiest way to get started with [Rib](/) is to [use the-template](https://help.github.com/en/articles/creating-a-repository-from-a-template)-repository, [**rib-sample**](https://github.com/srid/rib-sample), from Github.--## Concepts--### Directory structure--Let's look at what's in the template repository:--```shell-$ git clone https://github.com/srid/rib-sample.git mysite-...-$ cd mysite-$ ls -F-content/ default.nix Main.hs README.md rib-sample.cabal+nix-shell --run ghcid ``` -The three key items here are:--1. `Main.hs`: Haskell source containing the DSL of the HTML/CSS of your site.-1. `content/`: The source content (eg: Markdown sources and static files)-1. `dest/`: The target directory, excluded from the git repository, will contain- _generated_ content (i.e., the HTML files, and copied over static content)- -The template repository comes with a few sample posts under `content/`, and a basic-HTML layout and CSS style defined in `Main.hs`. --### Run the site--Now let's run them all. --Clone the sample repository locally, install [Nix](https://nixos.org/nix/) (as-described in its README) and run your site as follows:+To test your changes, clone [rib-sample](https://github.com/srid/rib-sample) and run it using your local rib checkout: -```shell-nix-shell --run 'ghcid -T ":main serve"' ```--(Note that even though the author recommends it Nix is strictly not required; you may-simply run `ghcid -T ":main serve"` instead of the above command if you do not wish to-use Nix.)--Running this command gives you a local HTTP server at http://127.0.0.1:8080-(serving the generated files) that automatically reloads when either the content-(`content/`) or the HTML/CSS/build-actions (`Main.hs`) changes. Hot reload, in other-words.--### How Rib works--How does the aforementioned nix-shell command work?--1. `nix-shell` will run the given command in a shell environment with all of our-dependencies (notably the Haskell ones including the `rib` library itself)-installed. --1. [`ghcid`](https://github.com/ndmitchell/ghcid) will compile your `Main.hs`- and run its `main` function.--1. `Main.hs:main` in turn calls `Rib.App.run` which takes as argument your custom - Shake action that will build the static site.--1. `Rib.App.run`: this parses the CLI arguments and runs the rib CLI "app" which- can be run in one of a few modes --- generating static files, watching the- `content/` directory for changes, starting HTTP server for the `dest/` directory.- The "serve" subcommand will run the Shake build action passed as argument on - every file change and spin up a HTTP server.- -Run that command, and visit http://127.0.0.1:8080 to view your site.--### Editing workflow--Now try making some changes to the content, say `content/first-post.md`. You should-see it reflected when you refresh the page. Or change the HTML or CSS of your-site in `Main.hs`; this will trigger `ghcid` to rebuild the Haskell source and-restart the server.--### What's next?--Great, by now you should have your static site generator ready and running! --Rib recommends writing your Shake actions in the style of being -[forward-defined](http://hackage.haskell.org/package/shake-0.18.3/docs/Development-Shake-Forward.html)-which adds to the simplicity of the entire thing.--## Examples--* [rib-sample](https://github.com/srid/rib-sample): Use this to get started with- your own site.- -* [zulip-archive](https://github.com/srid/zulip-archive): Zulip chat archive viewer ([running here](https://funprog.srid.ca/)).--* [open-editions.org](https://github.com/open-editions/open-editions.org) ([running here](https://open-editions.org/)).--* Rib powers the Zettelkasten system [neuron](https://github.com/srid/neuron#neuron)- * Example: [www.srid.ca](https://www.srid.ca/)- * Example: [neuron.srid.ca](https://neuron.srid.ca/)- * Example: [haskell.srid.ca](https://haskell.srid.ca/)+cd ..+git clone https://github.com/srid/rib-sample.git+cd rib-sample+nix-shell --arg rib ../rib --run 'ghcid -T ":main -wS"'+```
rib.cabal view
@@ -1,6 +1,6 @@-cabal-version: 2.2+cabal-version: 2.4 name: rib-version: 0.8.0.0+version: 0.10.0.0 license: BSD-3-Clause copyright: 2019 Sridhar Ratnakumar maintainer: srid@srid.ca@@ -8,9 +8,9 @@ homepage: https://github.com/srid/rib#readme bug-reports: https://github.com/srid/rib/issues synopsis:- Static site generator using Shake+ Static site generator based on Shake description:- Haskell static site generator that aims to reuse existing libraries instead of reinventing the wheel+ Haskell static site generator based on Shake, with a delightful development experience. category: Web build-type: Simple extra-source-files:@@ -21,21 +21,7 @@ type: git location: https://github.com/srid/rib -library- exposed-modules:- Rib- Rib.App- Rib.Watch- Rib.Parser.Dhall- Rib.Parser.MMark- Rib.Parser.Pandoc- Rib.Route- Rib.Shake- Rib.Settings- Rib.Extra.CSS- Rib.Extra.OpenGraph- other-modules:- Rib.Server+common library-common hs-source-dirs: src default-language: Haskell2010 default-extensions: NoImplicitPrelude@@ -67,8 +53,6 @@ pandoc >=2.7 && <3, pandoc-include-code >=1.5 && <1.6, pandoc-types >=1.20,- path >= 0.7.0,- path-io >= 1.6.0, relude >= 0.6 && < 0.7, safe-exceptions, shake >= 0.18.5,@@ -77,3 +61,33 @@ wai >=3.2.2 && <3.3, wai-app-static >=3.1.6 && <3.2, warp++library+ import: library-common+ exposed-modules:+ Rib+ Rib.App+ Rib.Cli+ Rib.Watch+ Rib.Log+ Rib.Parser.Dhall+ Rib.Parser.MMark+ Rib.Parser.Pandoc+ Rib.Route+ Rib.Shake+ Rib.Extra.CSS+ Rib.Extra.OpenGraph+ other-modules:+ Rib.Server++test-suite rib-test + import: library-common+ type: exitcode-stdio-1.0+ hs-source-dirs: test + main-is: Spec.hs+ build-depends:+ base-noprelude >=4.7 && <5,+ relude,+ hspec,+ QuickCheck+
src/Rib/App.hs view
@@ -1,179 +1,127 @@+{-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} -- | CLI interface for Rib. -- -- Mostly you would only need `Rib.App.run`, passing it your Shake build action. module Rib.App- ( Command (..),- commandParser,- run,+ ( run, runWith, ) where -import Control.Concurrent (threadDelay) import Control.Concurrent.Async (race_) import Control.Exception.Safe (catch) import Development.Shake hiding (command) import Development.Shake.Forward (shakeForward) import Options.Applicative-import Path-import Path.IO import Relude+import Rib.Cli (CliConfig (CliConfig), cliParser)+import qualified Rib.Cli as Cli+import Rib.Log import qualified Rib.Server as Server-import Rib.Settings (RibSettings (..)) import Rib.Watch (onTreeChange)-import System.FSNotify (Event (..), eventIsDirectory, eventPath)+import System.Directory+import System.FSNotify (Event (..), eventPath)+import System.FilePath import System.IO (BufferMode (LineBuffering), hSetBuffering) --- | Rib CLI commands-data Command- = -- Run an one-off generation with silent logging- -- TODO: Eventually replace this with proper logging mechanism.- OneOff- | -- | Generate the site once.- Generate- { -- | Force a full generation of /all/ files even if they were not modified- full :: Bool- }- | -- | Watch for changes in the input directory and run `Generate`- Watch- | -- | Run a HTTP server serving content from the output directory- Serve- { -- | Port to bind the server- port :: Int,- -- | Unless set run `WatchAndGenerate` automatically- dontWatch :: Bool- }- deriving (Show, Eq, Generic)---- | Commandline parser `Parser` for the Rib CLI-commandParser :: Parser Command-commandParser =- hsubparser $- mconcat- [ command "generate" $ info generateCommand $ progDesc "Run one-off generation of static files",- command "watch" $ info watchCommand $ progDesc "Watch the source directory, and generate when it changes",- command "serve" $ info serveCommand $ progDesc "Like watch, but also starts a HTTP server"- ]- where- generateCommand =- Generate <$> switch (long "full" <> help "Do a full generation (toggles shakeRebuild)")- watchCommand =- pure Watch- serveCommand =- Serve- <$> option auto (long "port" <> short 'p' <> help "HTTP server port" <> showDefault <> value 8080 <> metavar "PORT")- <*> switch (long "no-watch" <> help "Serve only; don't watch and regenerate")- -- | Run Rib using arguments passed in the command line. run ::- -- | Directory from which source content will be read.- Path Rel Dir ->- -- | The path where static files will be generated. Rib's server uses this- -- directory when serving files.- Path Rel Dir ->+ -- | Default value for `Cli.inputDir`+ FilePath ->+ -- | Deault value for `Cli.outputDir`+ FilePath -> -- | Shake build rules for building the static site Action () -> IO ()-run src dst buildAction = runWith src dst buildAction =<< execParser opts+run src dst buildAction = runWith buildAction =<< execParser opts where opts = info- (commandParser <**> helper)+ (cliParser src dst <**> helper) ( fullDesc- <> progDesc "Rib static site generator CLI"+ <> progDesc "Generate a static site at OUTPUTDIR using input from INPUTDIR" ) --- | Like `run` but with an explicitly passed `Command`-runWith :: Path Rel Dir -> Path Rel Dir -> Action () -> Command -> IO ()-runWith src dst buildAction ribCmd = do- when (src == currentRelDir) $- -- Because otherwise our use of `watchTree` can interfere with Shake's file- -- scaning.- fail "cannot use '.' as source directory."+-- | Like `run` but with an explicitly passed `CliConfig`+runWith :: Action () -> CliConfig -> IO ()+runWith buildAction cfg@CliConfig {..} = do -- For saner output flip hSetBuffering LineBuffering `mapM_` [stdout, stderr]- let ribSettings =- case ribCmd of- OneOff ->- RibSettings src dst Silent False- Generate fullGen ->- RibSettings src dst Verbose fullGen- _ ->- RibSettings src dst Verbose False- case ribCmd of- OneOff ->- runShake ribSettings buildAction- Generate _ ->- -- FIXME: Shouldn't `catch` Shake exceptions when invoked without fsnotify.- runShakeBuild ribSettings- Watch ->- runShakeAndObserve ribSettings- Serve p dw -> do- race_ (Server.serve p $ toFilePath dst) $ do- if dw- then threadDelay maxBound- else runShakeAndObserve ribSettings+ case (watch, serve) of+ (True, Just (host, port)) -> do+ race_+ (Server.serve cfg host port $ outputDir)+ (runShakeAndObserve cfg buildAction)+ (True, Nothing) ->+ runShakeAndObserve cfg buildAction+ (False, Just (host, port)) ->+ Server.serve cfg host port $ outputDir+ (False, Nothing) ->+ runShakeBuild cfg buildAction++shakeOptionsFrom :: CliConfig -> ShakeOptions+shakeOptionsFrom cfg'@CliConfig {..} =+ shakeOptions+ { shakeVerbosity = verbosity,+ shakeFiles = shakeDbDir,+ shakeRebuild = bool [] [(RebuildNow, "**")] rebuildAll,+ shakeLintInside = [""],+ shakeExtra = addShakeExtra cfg' (shakeExtra shakeOptions)+ }++runShakeBuild :: CliConfig -> Action () -> IO ()+runShakeBuild cfg@CliConfig {..} buildAction = do+ runShake cfg $ do+ logStrLn cfg $ "[Rib] Generating " <> inputDir <> " (rebuildAll=" <> show rebuildAll <> ")"+ buildAction++runShake :: CliConfig -> Action () -> IO ()+runShake cfg shakeAction = do+ shakeForward (shakeOptionsFrom cfg) shakeAction+ `catch` handleShakeException where- currentRelDir = [reldir|.|]- -- Keep shake database directory under the src directory instead of the- -- (default) current working directory, which may not always be a project- -- root (as in the case of neuron).- shakeDatabaseDir :: Path Rel Dir = src </> [reldir|.shake|]- runShakeAndObserve ribSettings = do- -- Begin with a *full* generation as the HTML layout may have been changed.- -- TODO: This assumption is not true when running the program from compiled- -- binary (as opposed to say via ghcid) as the HTML layout has become fixed- -- by being part of the binary. In this scenario, we should not do full- -- generation (i.e., toggle the bool here to False). Perhaps provide a CLI- -- flag to disable this.- runShakeBuild $ ribSettings {_ribSettings_fullGen = True}- -- And then every time a file changes under the current directory- putStrLn $ "[Rib] Watching " <> toFilePath src <> " for changes"- onSrcChange $ runShakeBuild ribSettings- runShakeBuild ribSettings = do- runShake ribSettings $ do- putInfo $ "[Rib] Generating " <> toFilePath src <> " (full=" <> show (_ribSettings_fullGen ribSettings) <> ")"- buildAction- runShake ribSettings shakeAction = do- shakeForward (shakeOptionsFrom ribSettings) shakeAction- `catch` handleShakeException handleShakeException (e :: ShakeException) = -- Gracefully handle any exceptions when running Shake actions. We want -- Rib to keep running instead of crashing abruptly.- putStrLn $+ logErr $ "[Rib] Unhandled exception when building " <> shakeExceptionTarget e <> ": " <> show e- shakeOptionsFrom settings =- shakeOptions- { shakeVerbosity = _ribSettings_verbosity settings,- shakeFiles = toFilePath shakeDatabaseDir,- shakeRebuild = bool [] [(RebuildNow, "**")] (_ribSettings_fullGen settings),- shakeLintInside = [""],- shakeExtra = addShakeExtra settings (shakeExtra shakeOptions)- }++runShakeAndObserve :: CliConfig -> Action () -> IO ()+runShakeAndObserve cfg@CliConfig {..} buildAction = do+ -- Begin with a *full* generation as the HTML layout may have been changed.+ -- TODO: This assumption is not true when running the program from compiled+ -- binary (as opposed to say via ghcid) as the HTML layout has become fixed+ -- by being part of the binary. In this scenario, we should not do full+ -- generation (i.e., toggle the bool here to False). Perhaps provide a CLI+ -- flag to disable this.+ runShakeBuild (cfg {Cli.rebuildAll = True}) buildAction+ -- And then every time a file changes under the current directory+ logStrLn cfg $ "[Rib] Watching " <> inputDir <> " for changes"+ onSrcChange $ runShakeBuild cfg buildAction+ where+ onSrcChange :: IO () -> IO () onSrcChange f = do- workDir <- getCurrentDir+ -- Canonicalizing path is important as we are comparing path ancestor using isPrefixOf+ dir <- canonicalizePath inputDir -- Top-level directories to ignore from notifications- dirBlacklist <- traverse makeAbsolute [shakeDatabaseDir, src </> [reldir|.git|]] let isBlacklisted :: FilePath -> Bool- isBlacklisted p = or $ flip fmap dirBlacklist $ \b -> toFilePath b `isPrefixOf` p- onTreeChange src $ \allEvents -> do+ isBlacklisted p = or $ flip fmap watchIgnore $ \b -> (dir </> b) `isPrefixOf` p+ onTreeChange dir $ \allEvents -> do let events = filter (not . isBlacklisted . eventPath) allEvents unless (null events) $ do -- Log the changed events for diagnosis.- logEvent workDir `mapM_` events+ logEvent `mapM_` events f- logEvent workDir e = do- eventRelPath <-- if eventIsDirectory e- then fmap toFilePath . makeRelative workDir =<< parseAbsDir (eventPath e)- else fmap toFilePath . makeRelative workDir =<< parseAbsFile (eventPath e)- putStrLn $ eventLogPrefix e <> " " <> eventRelPath+ logEvent :: Event -> IO ()+ logEvent e = do+ logStrLn cfg $ eventLogPrefix e <> " " <> eventPath e eventLogPrefix = \case -- Single character log prefix to indicate file actions is a convention in Rib. Added _ _ _ -> "A"
+ src/Rib/Cli.hs view
@@ -0,0 +1,166 @@+{-# LANGUAGE ApplicativeDo #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++module Rib.Cli+ ( CliConfig (..),+ cliParser,+ Verbosity (..),++ -- * Parser helpers+ directoryReader,+ watchOption,+ serveOption,++ -- * Internal+ hostPortParser,+ )+where++import Development.Shake (Verbosity (..))+import Options.Applicative+import Relude+import Relude.Extra.Tuple+import System.FilePath+import qualified Text.Megaparsec as M+import qualified Text.Megaparsec.Char as M++-- Rib's CLI configuration+--+-- Can be retrieved using `Rib.Shake.getCliConfig` in the `Development.Shake.Action` monad.+data CliConfig+ = CliConfig+ { -- | Whether to rebuild all sources in Shake.+ rebuildAll :: Bool,+ -- | Whether to monitor `inputDir` for changes and re-generate+ watch :: Bool,+ -- | Whether to run a HTTP server on `outputDir`+ serve :: Maybe (Text, Int),+ -- | Shake's verbosity level.+ --+ -- Setting this to `Silent` will affect Rib's own logging as well.+ verbosity :: Verbosity,+ -- | Directory from which source content will be read.+ inputDir :: FilePath,+ -- | The path where static files will be generated. Rib's server uses this+ -- directory when serving files.+ outputDir :: FilePath,+ -- | Path to shake's database directory.+ shakeDbDir :: FilePath,+ -- | List of relative paths to ignore when watching the source directory+ watchIgnore :: [FilePath]+ }+ deriving (Show, Eq, Generic, Typeable)++cliParser :: FilePath -> FilePath -> Parser CliConfig+cliParser inputDirDefault outputDirDefault = do+ rebuildAll <-+ switch+ ( long "rebuild-all"+ <> help "Rebuild all sources"+ )+ watch <- watchOption+ serve <- serveOption+ verbosity <-+ fmap+ (bool Verbose Silent)+ ( switch+ ( long "quiet"+ <> help "Log nothing"+ )+ )+ ~(inputDir, shakeDbDir) <-+ fmap (mapToSnd shakeDbDirFrom) $+ option+ directoryReader+ ( long "input-dir"+ <> metavar "INPUTDIR"+ <> value inputDirDefault+ <> help ("Directory containing the source files (" <> "default: " <> inputDirDefault <> ")")+ )+ outputDir <-+ option+ directoryReader+ ( long "output-dir"+ <> metavar "OUTPUTDIR"+ <> value outputDirDefault+ <> help ("Directory where files will be generated (" <> "default: " <> outputDirDefault <> ")")+ )+ ~(watchIgnore) <- pure builtinWatchIgnores+ pure CliConfig {..}++watchOption :: Parser Bool+watchOption =+ switch+ ( long "watch"+ <> short 'w'+ <> help "Watch for changes and regenerate"+ )++serveOption :: Parser (Maybe (Text, Int))+serveOption =+ optional+ ( option+ (megaparsecReader hostPortParser)+ ( long "serve"+ <> short 's'+ <> metavar "[HOST]:PORT"+ <> help "Run a HTTP server on the generated directory"+ )+ )+ <|> ( fmap (bool Nothing $ Just (defaultHost, 8080)) $+ switch (short 'S' <> help ("Like `-s " <> toString defaultHost <> ":8080`"))+ )++builtinWatchIgnores :: [FilePath]+builtinWatchIgnores =+ [ ".shake",+ ".git"+ ]++shakeDbDirFrom :: FilePath -> FilePath+shakeDbDirFrom inputDir =+ -- Keep shake database directory under the src directory instead of the+ -- (default) current working directory, which may not always be a project+ -- root (as in the case of neuron).+ inputDir </> ".shake"++-- | Like `str` but adds a trailing slash if there isn't one.+directoryReader :: ReadM FilePath+directoryReader = fmap addTrailingPathSeparator str++megaparsecReader :: M.Parsec Void Text a -> ReadM a+megaparsecReader p =+ eitherReader (first M.errorBundlePretty . M.parse p "<optparse-input>" . toText)++hostPortParser :: M.Parsec Void Text (Text, Int)+hostPortParser = do+ host <-+ optional $+ M.string "localhost"+ <|> M.try parseIP+ void $ M.char ':'+ port <- parseNumRange 1 65535+ pure (fromMaybe defaultHost host, port)+ where+ readNum = maybe (fail "Not a number") pure . readMaybe+ parseIP :: M.Parsec Void Text Text+ parseIP = do+ a <- parseNumRange 0 255 <* M.char '.'+ b <- parseNumRange 0 255 <* M.char '.'+ c <- parseNumRange 0 255 <* M.char '.'+ d <- parseNumRange 0 255+ pure $ toText $ intercalate "." $ show <$> [a, b, c, d]+ parseNumRange :: Int -> Int -> M.Parsec Void Text Int+ parseNumRange a b = do+ n <- readNum =<< M.some M.digitChar+ if a <= n && n <= b+ then pure n+ else fail $ "Number not in range: " <> show a <> "-" <> show b++defaultHost :: Text+defaultHost = "127.0.0.1"
+ src/Rib/Log.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE ViewPatterns #-}++module Rib.Log+ ( logStrLn,+ logErr,+ )+where++import Development.Shake (Verbosity (..))+import Relude+import Rib.Cli (CliConfig (..))+import System.IO (hPutStrLn)++logStrLn :: MonadIO m => CliConfig -> String -> m ()+logStrLn CliConfig {..} s =+ unless (verbosity == Silent) $ do+ putStrLn s++logErr :: MonadIO m => String -> m ()+logErr s =+ liftIO $ hPutStrLn stderr s
src/Rib/Parser/Dhall.hs view
@@ -15,22 +15,22 @@ import Development.Shake import Dhall (FromDhall, auto, input)-import Path import Relude import Rib.Shake (ribInputDir) import System.Directory+import System.FilePath -- | Parse a Dhall file as Haskell type. parse :: FromDhall a => -- | Dependent .dhall files, which must trigger a rebuild- [Path Rel File] ->+ [FilePath] -> -- | The Dhall file to parse. Relative to `ribInputDir`.- Path Rel File ->+ FilePath -> Action a-parse (map toFilePath -> deps) f = do+parse deps f = do inputDir <- ribInputDir need deps- s <- toText <$> readFile' (toFilePath $ inputDir </> f)- liftIO $ withCurrentDirectory (toFilePath inputDir) $+ s <- toText <$> readFile' (inputDir </> f)+ liftIO $ withCurrentDirectory inputDir $ input auto s
src/Rib/Parser/MMark.hs view
@@ -34,9 +34,9 @@ import Control.Foldl (Fold (..)) import Development.Shake (Action, readFile') import Lucid.Base (HtmlT (..))-import Path import Relude import Rib.Shake (ribInputDir)+import System.FilePath import Text.MMark (MMark, projectYaml) import qualified Text.MMark as MMark import qualified Text.MMark.Extension as Ext@@ -73,16 +73,16 @@ parsePure = parsePureWith defaultExts -- | Parse Markdown using mmark-parse :: Path Rel File -> Action MMark+parse :: FilePath -> Action MMark parse = parseWith defaultExts -- | Like `parse` but takes a custom list of MMark extensions-parseWith :: [MMark.Extension] -> Path Rel File -> Action MMark+parseWith :: [MMark.Extension] -> FilePath -> Action MMark parseWith exts f = either (fail . toString) pure =<< do inputDir <- ribInputDir- s <- toText <$> readFile' (toFilePath $ inputDir </> f)- pure $ parsePureWith exts (toFilePath f) s+ s <- toText <$> readFile' (inputDir </> f)+ pure $ parsePureWith exts f s -- | Get the first image in the document if one exists getFirstImg :: MMark -> Maybe URI
src/Rib/Parser/Pandoc.hs view
@@ -32,9 +32,9 @@ import Data.Aeson import Development.Shake (Action, readFile') import Lucid (HtmlT, toHtmlRaw)-import Path import Relude import Rib.Shake (ribInputDir)+import System.FilePath import Text.Pandoc import Text.Pandoc.Filter.IncludeCode (includeCode) import qualified Text.Pandoc.Readers@@ -54,12 +54,12 @@ parse :: -- | The pandoc text reader function to use, eg: `readMarkdown` (ReaderOptions -> Text -> PandocIO Pandoc) ->- Path Rel File ->+ FilePath -> Action Pandoc parse textReader f = either fail pure =<< do inputDir <- ribInputDir- content <- toText <$> readFile' (toFilePath $ inputDir </> f)+ content <- toText <$> readFile' (inputDir </> f) fmap (first show) $ runExceptT $ do v' <- runIO' $ textReader readerSettings content liftIO $ walkM includeSources v'
src/Rib/Route.hs view
@@ -23,9 +23,9 @@ import Data.Kind import qualified Data.Text as T import Development.Shake (Action)-import Path import Relude import Rib.Shake (writeFileCached)+import System.FilePath -- | A route is a GADT which represents the individual routes in a static site. --@@ -33,16 +33,16 @@ class IsRoute (r :: Type -> Type) where -- | Return the filepath (relative to `Rib.Shake.ribInputDir`) where the -- generated content for this route should be written.- routeFile :: MonadThrow m => r a -> m (Path Rel File)+ routeFile :: MonadThrow m => r a -> m (FilePath) data UrlType = Relative | Absolute -path2Url :: Path Rel File -> UrlType -> Text-path2Url fp = toText . toFilePath . \case+path2Url :: FilePath -> UrlType -> Text+path2Url fp = toText . \case Relative -> fp Absolute ->- [absdir|/|] </> fp+ "/" </> fp -- | The absolute URL to this route (relative to site root) routeUrl :: IsRoute r => r a -> Text
src/Rib/Server.hs view
@@ -10,23 +10,27 @@ import Network.Wai.Application.Static (defaultFileServerSettings, staticApp) import qualified Network.Wai.Handler.Warp as Warp import Relude+import Rib.Cli (CliConfig)+import Rib.Log -- | Run a HTTP server to serve a directory of static files -- -- Binds the server to host 127.0.0.1. serve ::+ CliConfig ->+ -- | Host+ Text -> -- | Port number to bind to Int -> -- | Directory to serve. FilePath -> IO ()-serve port path = do- putStrLn $ "[Rib] Serving " <> path <> " at http://" <> host <> ":" <> show port+serve cfg host port path = do+ logStrLn cfg $ "[Rib] Serving " <> path <> " at http://" <> toString host <> ":" <> show port Warp.runSettings settings app where app = staticApp $ defaultFileServerSettings path- host = "127.0.0.1" settings =- Warp.setHost (fromString host)+ Warp.setHost (fromString $ toString host) $ Warp.setPort port $ Warp.defaultSettings
− src/Rib/Settings.hs
@@ -1,31 +0,0 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE Rank2Types #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE ViewPatterns #-}--module Rib.Settings- ( RibSettings (..),- )-where--import Development.Shake (Verbosity)-import Path-import Relude---- | The settings with which Rib is run------ RibSettings is initialized with the values passed to `Rib.App.run`-data RibSettings- = RibSettings- { _ribSettings_inputDir :: Path Rel Dir,- _ribSettings_outputDir :: Path Rel Dir,- -- | Shake verbosity level- _ribSettings_verbosity :: Verbosity,- -- | Whether we must try to generate all files even if they have not- -- been modified since last generation.- _ribSettings_fullGen :: Bool- }- deriving (Typeable)
src/Rib/Shake.hs view
@@ -16,68 +16,68 @@ writeFileCached, -- * Misc+ getCliConfig, ribInputDir, ribOutputDir,- getDirectoryFiles', ) where +import Control.Monad.Catch import Development.Shake-import Path-import Path.IO import Relude-import Rib.Settings+import Rib.Cli (CliConfig)+import qualified Rib.Cli as Cli+import System.Directory+import System.FilePath+import System.IO.Error (isDoesNotExistError) -- | Get rib settings from a shake Action monad.-ribSettings :: Action RibSettings-ribSettings = getShakeExtra >>= \case+getCliConfig :: Action CliConfig+getCliConfig = getShakeExtra >>= \case Just v -> pure v- Nothing -> fail "RibSettings not initialized"+ Nothing -> fail "CliConfig not initialized" -- | Input directory containing source files -- -- This is same as the first argument to `Rib.App.run`-ribInputDir :: Action (Path Rel Dir)-ribInputDir = _ribSettings_inputDir <$> ribSettings+ribInputDir :: Action FilePath+ribInputDir = Cli.inputDir <$> getCliConfig -- | Output directory where files are generated -- -- This is same as the second argument to `Rib.App.run`-ribOutputDir :: Action (Path Rel Dir)+ribOutputDir :: Action FilePath ribOutputDir = do- output <- _ribSettings_outputDir <$> ribSettings- liftIO $ createDirIfMissing True output+ output <- Cli.outputDir <$> getCliConfig+ liftIO $ createDirectoryIfMissing True output return output -- | Shake action to copy static files as is.-buildStaticFiles :: [Path Rel File] -> Action ()+buildStaticFiles :: [FilePath] -> Action () buildStaticFiles staticFilePatterns = do input <- ribInputDir output <- ribOutputDir- files <- getDirectoryFiles' input staticFilePatterns+ files <- getDirectoryFiles input staticFilePatterns void $ forP files $ \f ->- copyFileChanged' (input </> f) (output </> f)- where- copyFileChanged' (toFilePath -> old) (toFilePath -> new) =- copyFileChanged old new+ copyFileChanged (input </> f) (output </> f) -- | Run the given action when any file matching the patterns changes forEvery :: -- | Source file patterns (relative to `ribInputDir`)- [Path Rel File] ->- (Path Rel File -> Action a) ->+ [FilePath] ->+ (FilePath -> Action a) -> Action [a] forEvery pats f = do input <- ribInputDir- fs <- getDirectoryFiles' input pats+ fs <- getDirectoryFiles input pats forP fs f -- | Write the given file but only when it has been modified. -- -- Also, always writes under ribOutputDir-writeFileCached :: Path Rel File -> String -> Action ()+writeFileCached :: FilePath -> String -> Action () writeFileCached !k !s = do- f <- fmap (toFilePath . (</> k)) ribOutputDir+ f <- fmap (</> k) ribOutputDir currentS <- liftIO $ forgivingAbsence $ readFile f unless (Just s == currentS) $ do writeFile' f $! s@@ -85,7 +85,12 @@ -- logging modified files being read. putInfo $ "+ " <> f --- | Like `getDirectoryFiles` but works with `Path`-getDirectoryFiles' :: Typeable b => Path b Dir -> [Path Rel File] -> Action [Path Rel File]-getDirectoryFiles' (toFilePath -> dir) (fmap toFilePath -> pat) =- traverse (liftIO . parseRelFile) =<< getDirectoryFiles dir pat+-- | If argument of the function throws a+-- 'System.IO.Error.doesNotExistErrorType', 'Nothing' is returned (other+-- exceptions propagate). Otherwise the result is returned inside a 'Just'.+forgivingAbsence :: (MonadIO m, MonadCatch m) => m a -> m (Maybe a)+forgivingAbsence f =+ catchIf+ isDoesNotExistError+ (Just <$> f)+ (const $ return Nothing)
src/Rib/Watch.hs view
@@ -13,7 +13,6 @@ import Control.Concurrent (threadDelay) import Control.Concurrent.Async (race) import Control.Concurrent.Chan-import Path import Relude import System.FSNotify (Event (..), watchTreeChan, withManager) @@ -22,11 +21,11 @@ -- -- If multiple events fire rapidly, the IO action is invoked only once, taking -- those multiple events as its argument.-onTreeChange :: Path b t -> ([Event] -> IO ()) -> IO ()+onTreeChange :: FilePath -> ([Event] -> IO ()) -> IO () onTreeChange fp f = do withManager $ \mgr -> do eventCh <- newChan- void $ watchTreeChan mgr (toFilePath fp) (const True) eventCh+ void $ watchTreeChan mgr fp (const True) eventCh forever $ do firstEvent <- readChan eventCh events <- debounce 100 [firstEvent] $ readChan eventCh
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}