packages feed

rib 0.4.1.0 → 0.5.0.0

raw patch · 15 files changed

+782/−461 lines, 15 filesdep +base-nopreludedep +exceptionsdep +foldldep −basedep −bytestringdep −data-defaultsetup-changedPVP ok

version bump matches the API change (PVP)

Dependencies added: base-noprelude, exceptions, foldl, megaparsec, mmark, mmark-ext, modern-uri, named, path, path-io, relude

Dependencies removed: base, bytestring, data-default, http-types, lens, lens-aeson, safe, skylighting, time, wai-extra

API changes (from Hackage documentation)

- Rib.App: [force] :: App -> Bool
- Rib.Pandoc: getFirstImg :: Pandoc -> Maybe Text
- Rib.Pandoc: getH1 :: Pandoc -> Maybe (Html ())
- Rib.Pandoc: getMeta :: IsMetaValue a => String -> Pandoc -> Maybe a
- Rib.Pandoc: instance GHC.Read.Read a => Rib.Pandoc.IsMetaValue a
- Rib.Pandoc: instance Rib.Pandoc.IsMetaValue (Lucid.Base.Html ())
- Rib.Pandoc: instance Rib.Pandoc.IsMetaValue Data.Text.Internal.Text
- Rib.Pandoc: instance Rib.Pandoc.IsMetaValue [Text.Pandoc.Definition.Inline]
- Rib.Pandoc: instance Rib.Pandoc.IsMetaValue a => Rib.Pandoc.IsMetaValue [a]
- Rib.Pandoc: parse :: (ReaderOptions -> Text -> PandocIO Pandoc) -> Text -> IO Pandoc
- Rib.Pandoc: parseMeta :: ByteString -> IO Meta
- Rib.Pandoc: parsePure :: (ReaderOptions -> Text -> PandocPure Pandoc) -> Text -> Pandoc
- Rib.Pandoc: render :: Pandoc -> Html ()
- Rib.Pandoc: render' :: Pandoc -> Either PandocError Text
- Rib.Pandoc: renderInlines :: [Inline] -> Html ()
- Rib.Pandoc: renderInlines' :: [Inline] -> Either PandocError Text
- Rib.Pandoc: setMeta :: Show a => String -> a -> Pandoc -> Pandoc
- Rib.Server: getHTMLFileUrl :: FilePath -> Text
- Rib.Server: serve :: Int -> FilePath -> IO ()
- Rib.Shake: newtype Dirs
- Rib.Shake: readPandoc :: (ReaderOptions -> Text -> PandocIO Pandoc) -> FilePath -> Action Pandoc
- Rib.Shake: readPandocMulti :: (FilePattern, ReaderOptions -> Text -> PandocIO Pandoc) -> Action [(FilePath, Pandoc)]
- Rib.Simple: Page_Index :: [(FilePath, Pandoc)] -> Page
- Rib.Simple: Page_Post :: (FilePath, Pandoc) -> Page
- Rib.Simple: buildAction :: (Page -> Html ()) -> Action ()
- Rib.Simple: data Page
- Rib.Simple: instance Data.Aeson.Types.FromJSON.FromJSON Rib.Simple.Page
- Rib.Simple: instance Data.Aeson.Types.ToJSON.ToJSON Rib.Simple.Page
- Rib.Simple: instance GHC.Generics.Generic Rib.Simple.Page
- Rib.Simple: instance GHC.Show.Show Rib.Simple.Page
+ Rib: data Document repr meta
+ Rib: data MMark
+ Rib: data Pandoc
+ Rib: documentHtml :: Document repr meta -> Html ()
+ Rib: documentMeta :: Document repr meta -> meta
+ Rib: documentPath :: Document repr meta -> Path Rel File
+ Rib: documentUrl :: Document repr meta -> Text
+ Rib: documentVal :: Document repr meta -> repr
+ Rib: renderMarkdown :: Text -> Html ()
+ Rib: renderPandoc :: Path Rel File -> Text -> Html ()
+ Rib: serve :: Int -> FilePath -> IO ()
+ Rib.App: [full] :: App -> Bool
+ Rib.Document: data Document repr meta
+ Rib.Document: documentHtml :: Document repr meta -> Html ()
+ Rib.Document: documentMeta :: Document repr meta -> meta
+ Rib.Document: documentPath :: Document repr meta -> Path Rel File
+ Rib.Document: documentUrl :: Document repr meta -> Text
+ Rib.Document: documentVal :: Document repr meta -> repr
+ Rib.Document: instance (GHC.Show.Show repr, GHC.Show.Show meta) => GHC.Show.Show (Rib.Document.Document repr meta)
+ Rib.Document: instance GHC.Generics.Generic (Rib.Document.Document repr meta)
+ Rib.Document: instance GHC.Show.Show Rib.Document.DocumentError
+ Rib.Document: mkDocumentFrom :: forall m b repr meta. (MonadError DocumentError m, MonadIO m, Markup repr, FromJSON meta) => ("relpath" :! Path Rel File) -> ("path" :! Path b File) -> m (Document repr meta)
+ Rib.Markup.MMark: data MMark
+ Rib.Markup.MMark: getFirstImg :: MMark -> Maybe URI
+ Rib.Markup.MMark: instance Rib.Markup.Markup Text.MMark.Type.MMark
+ Rib.Markup.MMark: renderMarkdown :: Text -> Html ()
+ Rib.Markup.Pandoc: data Pandoc
+ Rib.Markup.Pandoc: getFirstImg :: Pandoc -> Maybe Text
+ Rib.Markup.Pandoc: getH1 :: Pandoc -> Maybe (Html ())
+ Rib.Markup.Pandoc: instance GHC.Classes.Eq Rib.Markup.Pandoc.UnknownExtension
+ Rib.Markup.Pandoc: instance GHC.Show.Show Rib.Markup.Pandoc.RibPandocError
+ Rib.Markup.Pandoc: instance GHC.Show.Show Rib.Markup.Pandoc.UnknownExtension
+ Rib.Markup.Pandoc: instance Rib.Markup.Markup Text.Pandoc.Definition.Pandoc
+ Rib.Markup.Pandoc: renderPandoc :: Path Rel File -> Text -> Html ()
+ Rib.Markup.Pandoc: renderPandocInlines :: [Inline] -> Html ()
+ Rib.Shake: data Dirs
+ Rib.Shake: readDocMulti :: forall repr meta. (Markup repr, FromJSON meta) => Path Rel File -> Action [Document repr meta]
- Rib.App: run :: FilePath -> FilePath -> Action () -> IO ()
+ Rib.App: run :: Path Rel Dir -> Path Rel Dir -> Action () -> IO ()
- Rib.App: runWith :: FilePath -> FilePath -> Action () -> App -> IO ()
+ Rib.App: runWith :: Path Rel Dir -> Path Rel Dir -> Action () -> App -> IO ()
- Rib.Shake: Dirs :: (FilePath, FilePath) -> Dirs
+ Rib.Shake: Dirs :: (Path Rel Dir, Path Rel Dir) -> Dirs
- Rib.Shake: buildHtml :: FilePath -> Html () -> Action ()
+ Rib.Shake: buildHtml :: Path Rel File -> Html () -> Action ()
- Rib.Shake: buildHtmlMulti :: (FilePattern, ReaderOptions -> Text -> PandocIO Pandoc) -> ((FilePath, Pandoc) -> Html ()) -> Action [(FilePath, Pandoc)]
+ Rib.Shake: buildHtmlMulti :: forall repr meta. (Markup repr, FromJSON meta) => Path Rel File -> (Document repr meta -> Html ()) -> Action [Document repr meta]
- Rib.Shake: buildStaticFiles :: [FilePattern] -> Action [FilePath]
+ Rib.Shake: buildStaticFiles :: [Path Rel File] -> Action ()

Files

CHANGELOG.md view
@@ -1,5 +1,25 @@ # Change Log for rib +## 0.5.0.0++This release comes with a major API refactor. Key changes:++- Added MMark support, as an alternative to Pandoc+- Allows using arbitrary records to load metadata+  - This replaces the previous complex metadata API+- Added `Document` type that uses the custom metadata record+- Add top-level `Rib` import namespace for ease of use+- Remove the following:+  - JSON cache+  - `Rib.Simple`+- Support for Table of Contents via MMark++Other changes:++- Use type-safe path types using the [path](http://hackage.haskell.org/package/path) library.+- Fix #40: Gracefully handle rendering/ parsing errors, without dying.+- Misc error reporting improvements+ ## 0.4.1.0  - `Rib.Pandoc`: @@ -16,4 +36,3 @@ ## 0.2.0.0  - Initial release.-
README.md view
@@ -4,6 +4,7 @@  [![BSD3](https://img.shields.io/badge/License-BSD-blue.svg)](https://en.wikipedia.org/wiki/BSD_License) [![Hackage](https://img.shields.io/hackage/v/rib.svg)](https://hackage.haskell.org/package/rib)+[![built with nix](https://builtwithnix.org/badge.svg)](https://builtwithnix.org)  Rib is a Haskell library for writing your own **static site generator**. @@ -14,25 +15,127 @@   ([Lucid](https://chrisdone.com/posts/lucid2/)) & CSS   ([Clay](http://fvisser.nl/clay/)) of your site    - Like Hakyll, Rib uses [Pandoc](https://pandoc.org/) for parsing the source-    documents-- Remain as simple as possible to use (see screenshot below)+    documents. It also supports [MMark](https://github.com/mmark-md/mmark) if you need a lightweight alternative.+- Remain as simple as possible to use (see example below) - Optional Nix based workflow for easily reproducible environment  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**++- [rib](#rib)+    - [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: -![Example](https://raw.githubusercontent.com/srid/rib/master/assets/rib-sample-main.png)+``` haskell+-- First we shall define two datatypes to represent our pages. One, the page+-- itself. Second, the metadata associated with each document. +-- | A generated page is either an index of documents, or an individual document.+--+-- The `Document` type takes two type variables:+-- 1. The first type variable specifies the parser to use: MMark or Pandoc+-- 2. The second type variable should be your metadata record+data Page+  = Page_Index [Document MMark DocMeta]+  | Page_Doc (Document MMark DocMeta)++-- | Type representing the metadata in our Markdown documents+--+-- Note that if a field is not optional (i.e., not Maybe) it must be present.+data DocMeta+  = DocMeta+      { title :: Text,+        description :: Maybe Text+      }+  deriving (Show, Eq, Generic, FromJSON)++-- | Main entry point to our generator.+--+-- `Rib.run` handles CLI arguments, and takes three parameters here.+--+-- 1. Directory `a`, from which static files will be read.+-- 2. Directory `b`, under which target files will be generated.+-- 3. Shake build action to run.+--+-- In the shake build action you would expect to use the utility functions+-- provided by Rib to do the actual generation of your static site.+main :: IO ()+main = Rib.run [reldir|a|] [reldir|b|] generateSite+  where+    -- Shake Action for generating the static site+    generateSite :: Action ()+    generateSite = do+      -- Copy over the static files+      Rib.buildStaticFiles [[relfile|static/**|]]+      -- Build individual markdown files, generating .html for each.+      docs <-+        Rib.buildHtmlMulti [relfile|*.md|] $+          renderPage . Page_Doc+      -- Build an index.html linking to the aforementioned files.+      Rib.buildHtml [relfile|index.html|]+        $ renderPage+        $ Page_Index docs+    -- Define your site HTML here+    renderPage :: Page -> Html ()+    renderPage page = with html_ [lang_ "en"] $ do+      head_ $ do+        meta_ [httpEquiv_ "Content-Type", content_ "text/html; charset=utf-8"]+        title_ $ case page of+          Page_Index _ -> "My website!"+          Page_Doc doc -> toHtml $ title $ Rib.documentMeta doc+        style_ [type_ "text/css"] $ Clay.render pageStyle+      body_+        $ with div_ [id_ "thesite"]+        $ do+          with a_ [href_ "/"] "Back to Home"+          hr_ []+          case page of+            Page_Index docs ->+              div_ $ forM_ docs $ \doc -> with li_ [class_ "links"] $ do+                let meta = Rib.documentMeta doc+                b_ $ with a_ [href_ (Rib.documentUrl doc)] $ toHtml $ title meta+                maybe mempty Rib.renderMarkdown $+                  description meta+            Page_Doc doc ->+              with article_ [class_ "post"] $ do+                h1_ $ toHtml $ title $ Rib.documentMeta doc+                Rib.documentHtml doc+    -- Define your site CSS here+    pageStyle :: Css+    pageStyle = "div#thesite" ? do+      margin (em 4) (pc 20) (em 1) (pc 20)+      "li.links" ? do+        listStyleType none+        marginTop $ em 1+        "b" ? fontSize (em 1.2)+        "p" ? sym margin (px 0)+```++(View full [`Main.hs`](https://github.com/srid/rib-sample/blob/master/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:@@ -86,10 +189,7 @@ 1. [`ghcid`](https://github.com/ndmitchell/ghcid) will compile your `Main.hs`    and run its `main` function. -1. `Main.hs:main` in turn calls the Shake build action (via `Rib.App.run`)-   defined in `Rib.Simple.buildAction` passing it your function `renderPage`.--There is quite a bit going on in that step 3! Let's break it down:+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@@ -97,11 +197,6 @@    default---without any explicit arguments---this will run the Shake build    action passed as argument on every file change and spin up a HTTP server.    -1. `Rib.Simple.buildAction`: The `run` function takes a Shake build action to-   run on file change. `Rib.Simple` provides a very simple build action for-   generating the most simple static site --- a list of posts with static assets-   --- which the sample repository uses.-    Run that command, and visit http://localhost:8080 to view your site.  ### Editing workflow@@ -115,24 +210,9 @@  Great, by now you should have your static site generator ready and running! What more can you do? Surely you may have specific needs; and this usually translates-to running custom Shake actions during the build.--Rib provides helper functions in `Rib.Shake` and `Rib.Pandoc` to make this-easier. Indeed the `Rib.Simple.buildAction` function which the sample project-readily uses makes use of these functions.--In order to customize your site's build actions,--1. Copy the source for `buildAction` from the-[`Rib.Simple`](https://github.com/srid/rib/blob/master/src/Rib/Simple.hs) module-to your `Main.hs`--1. Make any customizations you want in *your* `buildAction` function. Refer to-   [Hackage](http://hackage.haskell.org/package/rib) for API docs.--1. Use that as the argument to the `Rib.App.run` function in your `main`+to running custom Shake actions during the build. Rib provides helper functions in `Rib.Shake` to make this easier.  -Notice how Rib's builtin `buildAction` is +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. 
− Setup.hs
@@ -1,2 +0,0 @@-import Distribution.Simple-main = defaultMain
rib.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.2 name: rib-version: 0.4.1.0+version: 0.5.0.0 license: BSD-3-Clause copyright: 2019 Sridhar Ratnakumar maintainer: srid@srid.ca@@ -23,41 +23,49 @@  library     exposed-modules:+        Rib         Rib.App-        Rib.Pandoc-        Rib.Server+        Rib.Document+        Rib.Markup.MMark+        Rib.Markup.Pandoc         Rib.Shake-        Rib.Simple+    other-modules:+        Prelude+        Rib.Markup+        Rib.Server     hs-source-dirs: src     default-language: Haskell2010-    ghc-options: -Wall -Wincomplete-uni-patterns-                 -Wincomplete-record-updates+    ghc-options:+        -Wall+        -Wincomplete-record-updates+        -Wincomplete-uni-patterns     build-depends:         aeson >=1.4.2 && <1.5,-        bytestring >=0.10.8 && <0.11,-        containers >=0.6.0 && <0.7,-        binary >=0.8.6 && <0.9,-        text >=1.2.3 && <1.3,-        time >=1.8.0 && <1.9,         async,+        base-noprelude >=4.7 && <5,+        binary >=0.8.6 && <0.9,         clay >=0.13.1 && <0.14,-        mtl >=2.2.2 && <2.3,         cmdargs >=0.10.20 && <0.11,-        data-default >=0.7.1 && <0.8,+        containers >=0.6.0 && <0.7,+        directory >= 1.0 && <2.0,+        exceptions,+        foldl,         fsnotify >=0.3.0 && <0.4,-        http-types >=0.12.3 && <0.13,-        lens,-        lens-aeson >=1.0.2 && <1.1,         lucid >=2.9.11 && <2.10,-        pandoc-types >=1.17.5 && <1.18,-        safe >=0.3.17 && <0.4,-        skylighting,+        megaparsec,+        mmark,+        mmark-ext,+        modern-uri,+        mtl >=2.2.2 && <2.3,+        named,+        pandoc >=2.7 && <3,         pandoc-include-code >=1.4.0 && <1.5,+        pandoc-types >=1.17.5 && <1.18,+        path,+        path-io,+        relude >= 0.6 && < 0.7,         shake,+        text >=1.2.3 && <1.3,         wai >=3.2.2 && <3.3,         wai-app-static >=3.1.6 && <3.2,-        wai-extra,-        warp,-        base >=4.7 && <5,-        pandoc >=2.7 && <3,-        directory >= 1.0 && <2.0+        warp
+ src/Prelude.hs view
@@ -0,0 +1,6 @@+module Prelude+  ( module Relude,+  )+where++import Relude
+ src/Rib.hs view
@@ -0,0 +1,24 @@+-- |+module Rib+  ( module Rib.App,+    module Rib.Shake,+    module Rib.Server,+    Document,+    documentPath,+    documentVal,+    documentHtml,+    documentMeta,+    documentUrl,+    MMark,+    renderMarkdown,+    Pandoc,+    renderPandoc,+  )+where++import Rib.App+import Rib.Document+import Rib.Markup.MMark (MMark, renderMarkdown)+import Rib.Markup.Pandoc (Pandoc, renderPandoc)+import Rib.Server+import Rib.Shake
src/Rib/App.hs view
@@ -1,99 +1,102 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}  -- | CLI interface for Rib. -- -- Typically you would call `Rib.App.run` passing your Shake build action. module Rib.App-  ( App(..)-  , run-  , runWith-  ) where+  ( App (..),+    run,+    runWith,+  )+where  import Control.Concurrent (threadDelay) import Control.Concurrent.Async (concurrently_)-import Control.Monad-import Data.Bool (bool)-+import Control.Exception (catch) import Development.Shake import Development.Shake.Forward (shakeForward)-import System.Console.CmdArgs-import System.FSNotify (watchTree, withManager)-+import Path import qualified Rib.Server as Server import Rib.Shake (Dirs (..))+import System.Console.CmdArgs+import System.FSNotify (watchTree, withManager)  -- | Application modes -- -- The mode in which to run the Rib CLI data App-  = Generate-    { force :: Bool-      -- ^ Force generation of /all/ files-    }-  -- ^ Generate static files once.-  | WatchAndGenerate-  -- ^ Watch for changes in the input directory and run `Generate`-  | Serve-    { port :: Int-      -- ^ Port to bind the server-    , dontWatch :: Bool-      -- ^ Unless set run `WatchAndGenerate` automatically-    }-  -- ^ Run a HTTP server serving content from the output directory-  deriving (Data,Typeable,Show,Eq)-+  = -- | Generate static files 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`+    WatchAndGenerate+  | -- | 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 (Data, Typeable, Show, Eq)  -- | Run Rib using arguments passed in the command line.-run-  :: FilePath-  -- ^ Directory from which source content will be read.+run ::+  -- | Directory from which source content will be read.   --   -- NOTE: This should ideally *not* be `"."` as our use of watchTree (of   -- `runWith`) can interfere with Shake's file scaning.-  -> FilePath-  -- ^ The path where static files will be generated.  Rib's server uses this+  Path Rel Dir ->+  -- | The path where static files will be generated.  Rib's server uses this   -- directory when serving files.-  -> Action ()-  -- ^ Shake build rules for building the static site-  -> IO ()+  Path Rel Dir ->+  -- | Shake build rules for building the static site+  Action () ->+  IO () run src dst buildAction = runWith src dst buildAction =<< cmdArgs ribCli   where-    ribCli = modes-      [ Serve-          { port = 8080 &= help "Port to bind to"-          , dontWatch = False &= help "Do not watch in addition to serving generated files"-          } &= help "Serve the generated site"-            &= auto-      , WatchAndGenerate-          &= help "Watch for changes and generate"-      , Generate-          { force = False &= help "Force generation of all files"-          } &= help "Generate the site"-      ]+    ribCli =+      modes+        [ Serve+            { port = 8080 &= help "Port to bind to",+              dontWatch = False &= help "Do not watch in addition to serving generated files"+            }+            &= help "Serve the generated site"+            &= auto,+          WatchAndGenerate+            &= help "Watch for changes and generate",+          Generate+            { full = False &= help "Force a full generation of all files"+            }+            &= help "Generate the site"+        ]  -- | Like `run` but with an explicitly passed `App` mode-runWith :: FilePath -> FilePath -> Action () -> App -> IO ()+runWith :: Path Rel Dir -> Path Rel Dir -> Action () -> App -> IO () runWith src dst buildAction = \case   WatchAndGenerate -> withManager $ \mgr -> do     -- Begin with a *full* generation as the HTML layout may have been changed.     runWith src dst buildAction $ Generate True     -- And then every time a file changes under the current directory-    putStrLn $ "[Rib] Watching " <> src-    void $ watchTree mgr src (const True) $ const $-      runWith src dst buildAction $ Generate False+    putStrLn $ "[Rib] Watching " <> toFilePath src+    void $ watchTree mgr (toFilePath src) (const True) $ \_ -> do+      runWith src dst buildAction (Generate False)+        `catch` \(e :: SomeException) -> putStrLn $ show e     -- Wait forever, effectively.     forever $ threadDelay maxBound--  Serve p dw -> concurrently_-    (unless dw $ runWith src dst buildAction WatchAndGenerate)-    (Server.serve p dst)--  Generate forceGen ->-    let opts = shakeOptions-          { shakeVerbosity = Chatty-          , shakeRebuild = bool [] [(RebuildNow, "**")] forceGen-          , shakeExtra = addShakeExtra (Dirs (src, dst)) (shakeExtra shakeOptions)-          }-    in shakeForward opts buildAction+  Serve p dw ->+    concurrently_+      (unless dw $ runWith src dst buildAction WatchAndGenerate)+      (Server.serve p $ toFilePath dst)+  Generate fullGen ->+    let opts =+          shakeOptions+            { shakeVerbosity = Chatty,+              shakeRebuild = bool [] [(RebuildNow, "**")] fullGen,+              shakeExtra = addShakeExtra (Dirs (src, dst)) (shakeExtra shakeOptions)+            }+     in shakeForward opts buildAction
+ src/Rib/Document.hs view
@@ -0,0 +1,115 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedLabels #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE ViewPatterns #-}++module Rib.Document+  ( -- * Document type+    Document,+    mkDocumentFrom,++    -- * Document properties+    documentPath,+    documentVal,+    documentHtml,+    documentMeta,+    documentUrl,+  )+where++import Control.Monad.Except hiding (fail)+import Data.Aeson+import Development.Shake.FilePath ((-<.>))+import Lucid (Html)+import Named+import Path hiding ((-<.>))+import Rib.Markup+import qualified Text.Show++-- | A document written in a lightweight markup language (LML)+--+-- The type variable `repr` indicates the representation type of the Markup+-- parser to be used.+data Document repr meta+  = Document+      { -- | Path to the document; relative to the source directory.+        _document_path :: Path Rel File,+        -- | Parsed representation of the document.+        _document_val :: repr,+        -- | HTML rendering of the parsed representation.+        _document_html :: Html (),+        -- | The parsed metadata.+        _document_meta :: meta+      }+  deriving (Generic, Show)++documentPath :: Document repr meta -> Path Rel File+documentPath = _document_path++documentVal :: Document repr meta -> repr+documentVal = _document_val++documentHtml :: Document repr meta -> Html ()+documentHtml = _document_html++documentMeta :: Document repr meta -> meta+documentMeta = _document_meta++-- | Return the URL for the given @.html@ file under serve directory+--+-- File path must be relative to the serve directory.+--+-- You may also pass source paths as long as they map directly to destination+-- path except for file extension.+documentUrl :: Document repr meta -> Text+documentUrl doc = toText $ toFilePath ([absdir|/|] </> (documentPath doc)) -<.> ".html"++data DocumentError+  = DocumentError_MarkupError Text+  | DocumentError_MetadataMissing+  | DocumentError_MetadataMalformed Text++instance Show DocumentError where+  show = \case+    DocumentError_MarkupError e -> toString e+    DocumentError_MetadataMissing -> "Metadata missing"+    DocumentError_MetadataMalformed msg -> "Bad metadata JSON: " <> toString msg++-- | Parse, render to HTML and extract metadata from the given file.+--+-- Return the Document type containing converted values.+mkDocumentFrom ::+  forall m b repr meta.+  (MonadError DocumentError m, MonadIO m, Markup repr, FromJSON meta) =>+  -- | File path, used only to identify (not access) the document+  "relpath" :! Path Rel File ->+  -- | Actual file path, for access and reading+  "path" :! Path b File ->+  m (Document repr meta)+mkDocumentFrom k@(arg #relpath -> k') f = do+  v <-+    liftEither . first DocumentError_MarkupError+      =<< readDoc k f+  html <-+    liftEither . first DocumentError_MarkupError $+      renderDoc v+  metaValue <-+    liftEither . (first DocumentError_MetadataMalformed)+      =<< maybeToEither DocumentError_MetadataMissing (extractMeta v)+  meta <-+    liftEither . first (DocumentError_MetadataMalformed . toText) $+      resultToEither (fromJSON metaValue)+  pure $ Document k' v html meta+  where+    maybeToEither e = liftEither . maybeToRight e+    resultToEither = \case+      Error e -> Left e+      Success v -> Right v
+ src/Rib/Markup.hs view
@@ -0,0 +1,51 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}++module Rib.Markup+  ( -- * Type class+    Markup (..),+  )+where++import Data.Aeson (Value)+import Lucid (Html)+import Named+import Path++-- | Class for denoting Markup representations.+--+-- See `Rib.Markup.Pandoc` and `Rib.Markup.MMark` for two available instances.+class Markup repr where++  -- | Parse the given markup text+  parseDoc ::+    -- | File path, used to identify the document only.+    Path Rel File ->+    -- | Markup text to parse+    Text ->+    Either Text repr++  -- | Like `parseDoc` but take the actual filepath instead of text.+  readDoc ::+    forall m b.+    MonadIO m =>+    -- | File path, used to identify the document only.+    "relpath" :! Path Rel File ->+    -- | Actual path to the file to parse.+    "path" :! Path b File ->+    m (Either Text repr)++  extractMeta ::+    repr ->+    Maybe (Either Text Value)++  -- | Render the document as Lucid HTML+  renderDoc ::+    repr ->+    Either Text (Html ())
+ src/Rib/Markup/MMark.hs view
@@ -0,0 +1,87 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++-- Suppressing orphans warning for `Markup MMark` instance++module Rib.Markup.MMark+  ( -- * Manual rendering+    renderMarkdown,++    -- * Extracting information+    getFirstImg,++    -- * Re-exports+    MMark,+  )+where++import Control.Foldl (Fold (..))+import Control.Monad.Except+import Lucid (Html)+import Named+import Path+import Rib.Markup+import Text.MMark (MMark)+import qualified Text.MMark as MMark+import qualified Text.MMark.Extension as Ext+import qualified Text.MMark.Extension.Common as Ext+import qualified Text.Megaparsec as M+import Text.URI (URI)++instance Markup MMark where++  parseDoc f s = case MMark.parse (toFilePath f) s of+    Left e -> Left $ toText $ M.errorBundlePretty e+    Right doc -> Right $ MMark.useExtensions exts $ useTocExt doc++  readDoc (Arg k) (Arg f) = do+    content <- readFileText (toFilePath f)+    pure $ parseDoc k content++  extractMeta = fmap Right . MMark.projectYaml++  renderDoc = Right . MMark.render++-- | Parse and render the markup directly to HTML+renderMarkdown :: Text -> Html ()+renderMarkdown s = either error id $ runExcept $ do+  doc <- liftEither $ parseDoc @MMark [relfile|<memory>.md|] s+  liftEither $ renderDoc doc++-- | Get the first image in the document if one exists+getFirstImg :: MMark -> Maybe URI+getFirstImg = flip MMark.runScanner $ Fold f Nothing id+  where+    f acc blk = acc <|> listToMaybe (mapMaybe getImgUri (inlinesContainingImg blk))+    getImgUri = \case+      Ext.Image _ uri _ -> Just uri+      _ -> Nothing+    inlinesContainingImg :: Ext.Bni -> [Ext.Inline]+    inlinesContainingImg = \case+      Ext.Naked xs -> toList xs+      Ext.Paragraph xs -> toList xs+      _ -> []++exts :: [MMark.Extension]+exts =+  [ Ext.fontAwesome,+    Ext.footnotes,+    Ext.kbd,+    Ext.linkTarget,+    Ext.mathJax (Just '$'),+    Ext.obfuscateEmail "protected-email",+    Ext.punctuationPrettifier,+    Ext.ghcSyntaxHighlighter,+    Ext.skylighting+  ]++useTocExt :: MMark -> MMark+useTocExt doc = MMark.useExtension (Ext.toc "toc" toc) doc+  where+    toc = MMark.runScanner doc $ Ext.tocScanner (\x -> x > 1 && x < 5)
+ src/Rib/Markup/Pandoc.hs view
@@ -0,0 +1,185 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++-- Suppressing orphans warning for `Markup Pandoc` instance++-- | Helpers for working with Pandoc documents+module Rib.Markup.Pandoc+  ( -- * Manual rendering+    renderPandoc,+    renderPandocInlines,++    -- * Extracting information+    getH1,+    getFirstImg,++    -- * Re-exports+    Pandoc,+  )+where++import Control.Monad.Except+import Data.Aeson+import Lucid (Html, toHtmlRaw)+import Named+import Path+import Relude.Extra.Map ((!?))+import Rib.Markup+import Text.Pandoc+import Text.Pandoc.Filter.IncludeCode (includeCode)+import Text.Pandoc.Walk (query, walkM)+import qualified Text.Show++data RibPandocError+  = RibPandocError_PandocError PandocError+  | RibPandocError_UnknownFormat UnknownExtension++instance Show RibPandocError where+  show = \case+    RibPandocError_PandocError e ->+      show e+    RibPandocError_UnknownFormat s ->+      "Unsupported extension: " <> show s++instance Markup Pandoc where++  parseDoc k s = first show $ runExcept $ do+    r <-+      withExcept RibPandocError_UnknownFormat $+        detectReader k+    withExcept RibPandocError_PandocError+      $ runPure'+      $ r readerSettings s++  readDoc (Arg k) (Arg f) = fmap (first show) $ runExceptT $ do+    content <- readFileText (toFilePath f)+    r <-+      withExceptT RibPandocError_UnknownFormat $+        detectReader k+    withExceptT RibPandocError_PandocError $ do+      v' <- runIO' $ r readerSettings content+      liftIO $ walkM includeSources v'+    where+      includeSources = includeCode $ Just $ Format "html5"++  extractMeta (Pandoc meta _) = flattenMeta meta++  renderDoc doc = first show $ runExcept $ do+    withExcept RibPandocError_PandocError+      $ runPure'+      $ fmap toHtmlRaw+      $ writeHtml5String writerSettings doc++runPure' :: MonadError PandocError m => PandocPure a -> m a+runPure' = liftEither . runPure++runIO' :: (MonadError PandocError m, MonadIO m) => PandocIO a -> m a+runIO' = liftEither <=< liftIO . runIO++-- | Parse and render the markup directly to HTML+renderPandoc :: Path Rel File -> Text -> Html ()+renderPandoc f s = either (error . show) id $ runExcept $ do+  doc <- liftEither $ parseDoc @Pandoc f s+  liftEither $ renderDoc doc++-- | Render a list of Pandoc `Text.Pandoc.Inline` values as Lucid HTML+--+-- Useful when working with `Text.Pandoc.Meta` values from the document metadata.+renderPandocInlines :: [Inline] -> Html ()+renderPandocInlines =+  either (error . show) toHtmlRaw+    . renderDoc+    . Pandoc mempty+    . pure+    . Plain++-- | Get the top-level heading as Lucid HTML+getH1 :: Pandoc -> Maybe (Html ())+getH1 (Pandoc _ bs) = fmap renderPandocInlines $ flip query bs $ \case+  Header 1 _ xs -> Just xs+  _ -> Nothing++-- | Get the first image in the document if one exists+getFirstImg ::+  Pandoc ->+  -- | Relative URL path to the image+  Maybe Text+getFirstImg (Pandoc _ bs) = listToMaybe $ flip query bs $ \case+  Image _ _ (url, _) -> [toText url]+  _ -> []++exts :: Extensions+exts =+  mconcat+    [ extensionsFromList+        [ Ext_yaml_metadata_block,+          Ext_fenced_code_attributes,+          Ext_auto_identifiers,+          Ext_smart+        ],+      githubMarkdownExtensions+    ]++readerSettings :: ReaderOptions+readerSettings = def {readerExtensions = exts}++writerSettings :: WriterOptions+writerSettings = def {writerExtensions = exts}++-- Internal code++data UnknownExtension+  = UnknownExtension String+  deriving (Show, Eq)++-- | Detect the Pandoc reader to use based on file extension+detectReader ::+  forall m m1.+  (MonadError UnknownExtension m, PandocMonad m1) =>+  Path Rel File ->+  m (ReaderOptions -> Text -> m1 Pandoc)+detectReader f = do+  ext <-+    liftEither . first (UnknownExtension . show) $+      fileExtension f+  liftEither . maybeToRight (UnknownExtension ext) $+    formats !? ext+  where+    formats :: Map String (ReaderOptions -> Text -> m1 Pandoc)+    formats =+      fromList+        [ (".md", readMarkdown),+          (".rst", readRST),+          (".org", readOrg),+          (".tex", readLaTeX),+          (".ipynb", readIpynb)+        ]++-- | Flatten a Pandoc 'Meta' into a well-structured JSON object.+--+-- Renders Pandoc text objects into plain strings along the way.+flattenMeta :: Meta -> Maybe (Either Text Value)+flattenMeta (Meta meta) = fmap toJSON . traverse go <$> guarded null meta+  where+    go :: MetaValue -> Either Text Value+    go (MetaMap m) = toJSON <$> traverse go m+    go (MetaList m) = toJSONList <$> traverse go m+    go (MetaBool m) = pure $ toJSON m+    go (MetaString m) = pure $ toJSON m+    go (MetaInlines m) =+      bimap show toJSON+        $ runPure . plainWriter+        $ Pandoc mempty [Plain m]+    go (MetaBlocks m) =+      bimap show toJSON+        $ runPure . plainWriter+        $ Pandoc mempty m+    plainWriter = writePlain def
− src/Rib/Pandoc.hs
@@ -1,159 +0,0 @@-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE UndecidableInstances #-}---- | Helpers for working with Pandoc documents-module Rib.Pandoc-  ( module Text.Pandoc.Readers-  -- * Parsing-  , parse-  , parsePure-  -- * Converting to HTML-  , render-  , renderInlines-  , render'-  , renderInlines'-  -- * Metadata-  , getMeta-  , setMeta-  , parseMeta-  -- * Extracting information-  , getH1-  , getFirstImg-  )-where--import Control.Monad-import Data.ByteString.Lazy (ByteString)-import qualified Data.Map as Map-import Data.Text (Text)-import qualified Data.Text as T--import Lucid (Html, toHtmlRaw)-import Text.Pandoc-import Text.Pandoc.Filter.IncludeCode (includeCode)-import Text.Pandoc.Readers-import Text.Pandoc.Readers.Markdown (yamlToMeta)-import Text.Pandoc.Shared (stringify)-import Text.Pandoc.Walk (query, walkM)---class IsMetaValue a where-  parseMetaValue :: MetaValue -> a--instance IsMetaValue [Inline] where-  parseMetaValue = \case-    MetaInlines inlines -> inlines-    _ -> error "Not a MetaInline"--instance IsMetaValue (Html ()) where-  parseMetaValue = renderInlines . parseMetaValue @[Inline]--instance IsMetaValue Text where-  parseMetaValue = T.pack . stringify . parseMetaValue @[Inline]--instance {-# Overlappable #-} IsMetaValue a => IsMetaValue [a] where-  parseMetaValue = \case-    MetaList vals -> parseMetaValue <$> vals-    _ -> error "Not a MetaList"---- NOTE: This requires UndecidableInstances, but is there a better way?-instance {-# Overlappable #-} Read a => IsMetaValue a where-  parseMetaValue = read . T.unpack . parseMetaValue @Text---- | Get the metadata value for the given key in a Pandoc document.------ It is recommended to call this function with type application specifying the--- type of `a`.------ `MetaValue` is parsed in accordance with the `IsMetaValue` class constraint.--- Available instances:------ - `Html`: parse value as a Pandoc document and convert to Lucid Html--- - `Text`: parse a raw value (Inline with one Str value)--- - @[a]@: parse a list of values--- - @Read a => a@: parse a raw value and then read it.-getMeta :: IsMetaValue a => String -> Pandoc -> Maybe a-getMeta k (Pandoc meta _) = parseMetaValue <$> lookupMeta k meta---- | Add, or set, a metadata data key to the given Haskell value-setMeta :: Show a => String -> a -> Pandoc -> Pandoc-setMeta k v (Pandoc (Meta meta) bs) = Pandoc (Meta meta') bs-  where-    meta' = Map.insert k v' meta-    v' = MetaInlines [Str $ show v]---- | Pure version of `parse`-parsePure :: (ReaderOptions -> Text -> PandocPure Pandoc) -> Text -> Pandoc-parsePure r =-  either (error . show) id . runPure . r settings-  where-    settings = def { readerExtensions = exts }---- | Parse the source text as a Pandoc document------ Supports the [includeCode](https://github.com/owickstrom/pandoc-include-code) extension.-parse-  :: (ReaderOptions -> Text -> PandocIO Pandoc)-  -- ^ Document format. Example: `Text.Pandoc.Readers.readMarkdown`-  -> Text-  -- ^ Source text to parse-  -> IO Pandoc-parse r =-  either (error . show) (walkM includeSources) <=< runIO . r settings-  where-    settings = def { readerExtensions = exts }-    includeSources = includeCode $ Just $ Format "html5"---- | Parse the metadata source as a Pandoc Meta value-parseMeta :: ByteString -> IO Meta-parseMeta = either (error . show) pure <=< runIO . yamlToMeta settings-  where-    settings = def { readerExtensions = exts }---- | Like `render` but returns the raw HTML string, or the rendering error.-render' :: Pandoc -> Either PandocError Text-render' = runPure . writeHtml5String settings-  where-    settings = def { writerExtensions = exts }---- | Render a Pandoc document as Lucid HTML-render :: Pandoc -> Html ()-render = either (error . show) toHtmlRaw . render'---- | Like `renderInlines` but returns the raw HTML string, or the rendering error.-renderInlines' :: [Inline] -> Either PandocError Text-renderInlines' = render' . Pandoc mempty . pure . Plain---- | Render a list of Pandoc `Text.Pandoc.Inline` values as Lucid HTML------ Useful when working with `Text.Pandoc.Meta` values from the document metadata.-renderInlines :: [Inline] -> Html ()-renderInlines = either (error . show) toHtmlRaw . renderInlines'---- | Get the top-level heading as Lucid HTML-getH1 :: Pandoc -> Maybe (Html ())-getH1 (Pandoc _ bs) = fmap renderInlines $ flip query bs $ \case-  Header 1 _ xs -> Just xs-  _ -> Nothing---- | Get the first image in the document if one exists-getFirstImg-  :: Pandoc-  -> Maybe Text-  -- ^ Relative URL path to the image-getFirstImg (Pandoc _ bs) = flip query bs $ \case-  Image _ _ (url, _) -> Just $ T.pack url-  _ -> Nothing--exts :: Extensions-exts = mconcat-  [ extensionsFromList-    [ Ext_yaml_metadata_block-    , Ext_fenced_code_attributes-    , Ext_auto_identifiers-    , Ext_smart-    ]-  , githubMarkdownExtensions-  ]
src/Rib/Server.hs view
@@ -1,54 +1,35 @@-{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}  -- | Serve generated static files with HTTP module Rib.Server-  (-    serve-  , getHTMLFileUrl+  ( serve,   ) where -import Prelude hiding (init, last)--import Data.Text (Text)-import qualified Data.Text as T--import Development.Shake.FilePath ((-<.>)) import Network.Wai.Application.Static (defaultFileServerSettings, ssListing, ssLookupFile, staticApp) import qualified Network.Wai.Handler.Warp as Warp import WaiAppStatic.Types (StaticSettings)  -- | WAI Settings suited for serving statically generated websites. staticSiteServerSettings :: FilePath -> StaticSettings-staticSiteServerSettings root = settings-  { ssLookupFile = ssLookupFile settings-  , ssListing = Nothing  -- Disable directory listings-  }+staticSiteServerSettings root =+  settings+    { ssLookupFile = ssLookupFile settings,+      ssListing = Nothing -- Disable directory listings+    }   where     settings = defaultFileServerSettings root --- | Return the URL for the given @.html@ file under serve directory------ File path must be relative to the serve directory.------ You may also pass source paths as long as they map directly to destination--- path except for file extension.-getHTMLFileUrl-  :: FilePath-  -- ^ Relative path to a page (extension is ignored)-  -> Text-getHTMLFileUrl path = T.pack $ "/" ++  (path -<.> ".html")- -- | Run a HTTP server to serve a directory of static files -- -- Allow URLs of the form @//foo//bar@ to serve @${path}//foo//bar.html@-serve-  :: Int-  -- ^ Port number to bind to-  -> FilePath-  -- ^ Directory to serve.-  -> IO ()+serve ::+  -- | Port number to bind to+  Int ->+  -- | Directory to serve.+  FilePath ->+  IO () serve port path = do   putStrLn $ "[Rib] Serving at http://localhost:" <> show port   Warp.run port $ staticApp $ staticSiteServerSettings path
src/Rib/Shake.hs view
@@ -1,144 +1,117 @@ {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeApplications #-}  -- | Combinators for working with Shake. -- -- See the source of `Rib.Simple.buildAction` for example usage. module Rib.Shake-  (-  -- * Basic helpers-    buildHtmlMulti-  , buildHtml-  -- * Read helpers-  , readPandoc-  , readPandocMulti-  -- * Misc-  , buildStaticFiles-  , Dirs(..)+  ( -- * Basic helpers+    buildHtmlMulti,+    buildHtml,++    -- * Read helpers+    readDocMulti,++    -- * Misc+    buildStaticFiles,+    Dirs (..),   ) where -import Control.Monad-import Control.Monad.IO.Class-import Data.Aeson (FromJSON, ToJSON)-import qualified Data.Aeson as Aeson-import Data.Binary-import Data.Bool-import qualified Data.ByteString as BS-import qualified Data.ByteString.Lazy as BSL-import Data.Text (Text)-import qualified Data.Text.Encoding as T-import Data.Typeable-+import Data.Aeson import Development.Shake-import Development.Shake.FilePath-import Development.Shake.Forward (cacheAction) import Lucid (Html) import qualified Lucid-import System.Directory (createDirectoryIfMissing)-import Text.Pandoc (Pandoc (Pandoc), PandocIO, ReaderOptions)--import qualified Rib.Pandoc+import Named+import Path+import Path.IO+import Rib.Document+import Rib.Markup (Markup) -newtype Dirs = Dirs (FilePath, FilePath)+data Dirs = Dirs (Path Rel Dir, Path Rel Dir)+  deriving (Typeable) -getDirs :: Action (FilePath, FilePath)+getDirs :: Action (Path Rel Dir, Path Rel Dir) getDirs = getShakeExtra >>= \case   Just (Dirs d) -> return d   Nothing -> fail "Input output directories are not initialized" -ribInputDir :: Action FilePath+ribInputDir :: Action (Path Rel Dir) ribInputDir = fst <$> getDirs -ribOutputDir :: Action FilePath+ribOutputDir :: Action (Path Rel Dir) ribOutputDir = do   output <- snd <$> getDirs-  liftIO $ createDirectoryIfMissing True output+  liftIO $ createDirIfMissing True output   return output  -- | Shake action to copy static files as is-buildStaticFiles :: [FilePattern] -> Action [FilePath]+buildStaticFiles :: [Path Rel File] -> 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)-  pure files+    copyFileChanged' (input </> f) (output </> f)+  where+    copyFileChanged' old new =+      copyFileChanged (toFilePath old) (toFilePath new)  -- | Convert the given pattern of source files into their HTML.-buildHtmlMulti-  :: (FilePattern, ReaderOptions -> Text -> PandocIO Pandoc)-  -- ^ Source file patterns & their associated Pandoc readers-  -> ((FilePath, Pandoc) -> Html ())-  -- ^ How to render the given Pandoc document to HTML-  -> Action [(FilePath, Pandoc)]-  -- ^ List of relative path to generated HTML and the associated Pandoc document-buildHtmlMulti spec r = do-  xs <- readPandocMulti spec-  void $ forP xs $ \x ->-    buildHtml (fst x -<.> "html") (r x)+buildHtmlMulti ::+  forall repr meta.+  (Markup repr, FromJSON meta) =>+  -- | Source file patterns+  Path Rel File ->+  -- | How to render the given document to HTML+  (Document repr meta -> Html ()) ->+  -- | List of relative path to generated HTML and the associated document+  Action [Document repr meta]+buildHtmlMulti pat r = do+  xs <- readDocMulti pat+  void $ forP xs $ \x -> do+    outfile <- liftIO $ replaceExtension ".html" $ documentPath x+    buildHtml outfile (r x)   pure xs --- | Like `readPandoc` but operates on multiple files-readPandocMulti-  :: ( FilePattern-     , ReaderOptions -> Text -> PandocIO Pandoc-     )-     -- ^ Tuple of pattern of files to work on and document format.-  -> Action [(FilePath, Pandoc)]-readPandocMulti (pat, r) = do-  input <- ribInputDir-  fs <- getDirectoryFiles input [pat]-  forP fs $ \f ->-    jsonCacheAction f $ (f, ) <$> readPandoc r f---- | Read and parse a Pandoc source document------ If an associated metadata file exists (same filename, with @.yaml@ as--- extension), use it to specify the metadata of the document.-readPandoc-  :: (ReaderOptions -> Text -> PandocIO Pandoc)-  -- ^ Document format. Example: `Text.Pandoc.Readers.readMarkdown`-  -> FilePath-  -> Action Pandoc-readPandoc r f = do+-- | Like `readDoc'` but operates on multiple files+readDocMulti ::+  forall repr meta.+  (Markup repr, FromJSON meta) =>+  -- | Source file patterns+  Path Rel File ->+  Action [Document repr meta]+readDocMulti pat = do   input <- ribInputDir-  let inp = input </> f-  need [inp]-  content <- T.decodeUtf8 <$> liftIO (BS.readFile inp)-  doc <- liftIO $ Rib.Pandoc.parse r content-  -- FIXME: When _creating_ the yaml file for first time, Shake doesn't know to-  -- rebuild this.-  boolFileExists (inp -<.> "yaml") (pure doc) $-    fmap (overrideMeta doc) . readMeta-  where-    overrideMeta (Pandoc _ bs) meta = Pandoc meta bs-    readMeta mf = do-      need [mf]-      liftIO $ Rib.Pandoc.parseMeta =<< BSL.readFile mf-    -- | Like `bool` but works on file existence value-    ---    -- The second function takes the filepath as value.-    boolFileExists fp missingF existsF =-      doesFileExist fp >>= bool missingF (existsF fp)+  fs <- getDirectoryFiles' input [pat]+  forP fs $ \f -> do+    need $ toFilePath <$> [input </> f]+    result <-+      runExceptT $+        mkDocumentFrom+          ! #relpath f+          ! #path (input </> f)+    case result of+      Left e ->+        fail $ "Error converting " <> toFilePath f <> " to HTML: " <> show e+      Right v -> pure v  -- | Build a single HTML file with the given value-buildHtml :: FilePath -> Html () -> Action ()+buildHtml :: Path Rel File -> Html () -> Action () buildHtml f html = do   output <- ribOutputDir-  let out = output </> f-  writeHtml out html--writeHtml :: MonadIO m => FilePath -> Html () -> m ()-writeHtml f = liftIO . BSL.writeFile f . Lucid.renderBS+  writeHtml (output </> f) html+  where+    writeHtml :: MonadIO m => Path b File -> Html () -> m ()+    writeHtml p htmlVal =+      writeFileLText (toFilePath p) $! Lucid.renderText htmlVal --- | Like `Development.Shake.cacheAction` but uses JSON instance instead of Typeable / Binary on `b`.-jsonCacheAction :: (FromJSON b, Typeable k, Binary k, Show k, ToJSON a) => k -> Action a -> Action b-jsonCacheAction k =-    fmap (either error id . Aeson.eitherDecode)-  . cacheAction k-  . fmap Aeson.encode+-- | Like `getDirectoryFiles` but work with `Path`+getDirectoryFiles' :: Path b Dir -> [Path Rel File] -> Action [Path Rel File]+getDirectoryFiles' dir pat =+  traverse (liftIO . parseRelFile) =<< getDirectoryFiles (toFilePath dir) (toFilePath <$> pat)
− src/Rib/Simple.hs
@@ -1,50 +0,0 @@-{-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}---- | Sensible defaults for writing the most simple static site-module Rib.Simple where--import Control.Monad-import Data.Aeson (FromJSON, ToJSON)-import Data.Maybe (fromMaybe)-import GHC.Generics (Generic)--import Development.Shake (Action)-import Lucid (Html)-import Text.Pandoc (Pandoc, readLaTeX, readMarkdown, readOrg, readRST)--import Rib.Pandoc (getMeta)-import Rib.Shake---- | Type of page to be generated-data Page-  = Page_Index [(FilePath, Pandoc)]-  -- ^ Index page linking to a list of posts-  | Page_Post (FilePath, Pandoc)-  -- ^ Individual post page-  deriving (Generic, Show, FromJSON, ToJSON)---- | Shake build action for the most simple static site------ - Copies @static/@ as is.--- - Builds @*.md@, @*.rst@ and @*.org@ as HTML--- - Builds an @index.html@ of all pages unless `draft` metadata is set to `True`.-buildAction :: (Page -> Html ()) -> Action ()-buildAction renderPage = do-  void $ buildStaticFiles ["static/**"]-  posts <- concat <$> forM pats-    (flip buildHtmlMulti $ renderPage . Page_Post)-  let publicPosts = filter (not . isDraft . snd) posts-  buildHtml "index.html" $-    renderPage $ Page_Index publicPosts-  where-    isDraft = fromMaybe False . getMeta @Bool "draft"-    pats =-      [ ("*.md", readMarkdown)-      , ("*.rst", readRST)-      , ("*.org", readOrg)-      , ("*.tex", readLaTeX)-      ]