├── cabal.project
├── .github
├── FUNDING.yml
└── workflows
│ ├── ci.yaml
│ └── publish-site.yml
├── .gitignore
├── rib
├── test
│ ├── Spec.hs
│ └── Rib
│ │ └── CliSpec.hs
├── src
│ ├── Rib.hs
│ └── Rib
│ │ ├── Parser
│ │ ├── Dhall.hs
│ │ ├── MMark.hs
│ │ └── Pandoc.hs
│ │ └── Extra
│ │ ├── CSS.hs
│ │ └── OpenGraph.hs
└── rib.cabal
├── assets
├── rib.png
├── README.md
└── rib.svg
├── bin
└── test
├── guide
├── guide.md
├── neuron.dhall
├── examples.md
├── syntax-highlighting.md
├── index.md
├── tutorial.md
├── preview.md
└── typed-routes.md
├── CONTRIBUTING.md
├── rib-core
├── src
│ └── Rib
│ │ ├── Log.hs
│ │ ├── Server.hs
│ │ ├── Watch.hs
│ │ ├── Route.hs
│ │ ├── Shake.hs
│ │ ├── App.hs
│ │ └── Cli.hs
└── rib-core.cabal
├── README.md
├── LICENSE
├── default.nix
└── CHANGELOG.md
/cabal.project:
--------------------------------------------------------------------------------
1 | optional-packages:
2 | *
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: srid
2 | liberapay: srid
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist-newstyle
2 | dist
3 | result
4 | guide/.neuron
5 |
--------------------------------------------------------------------------------
/rib/test/Spec.hs:
--------------------------------------------------------------------------------
1 | {-# OPTIONS_GHC -F -pgmF hspec-discover #-}
2 |
--------------------------------------------------------------------------------
/assets/rib.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/srid/rib/HEAD/assets/rib.png
--------------------------------------------------------------------------------
/assets/README.md:
--------------------------------------------------------------------------------
1 | ## Credits
2 |
3 | * `rib.svg`: https://www.svgrepo.com/svg/24439/ribs
4 |
--------------------------------------------------------------------------------
/bin/test:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -xe
3 | nix-shell --run "ghcid -c 'cabal new-repl test:rib-test' -T \":main $*\""
4 |
--------------------------------------------------------------------------------
/guide/guide.md:
--------------------------------------------------------------------------------
1 | # Guide
2 |
3 | Assuming you have read the [[tutorial]], use this portal to
4 | understand how to do certain things in Rib.
5 |
6 | [[[z:zettels?tag=guide]]]
7 |
--------------------------------------------------------------------------------
/rib/src/Rib.hs:
--------------------------------------------------------------------------------
1 | -- |
2 | module Rib
3 | ( module Rib.App,
4 | module Rib.Shake,
5 | module Rib.Route,
6 | MMark,
7 | Pandoc,
8 | )
9 | where
10 |
11 | import Rib.App
12 | import Rib.Parser.MMark (MMark)
13 | import Rib.Parser.Pandoc (Pandoc)
14 | import Rib.Route
15 | import Rib.Shake
16 |
--------------------------------------------------------------------------------
/guide/neuron.dhall:
--------------------------------------------------------------------------------
1 | { siteTitle = "Rib"
2 | , siteBaseUrl = Some "https://rib.srid.ca"
3 | , theme = "green"
4 | , editUrl = Some "https://github.com/srid/rib/edit/master/guide/"
5 | , aliases =
6 | [ "2014302:examples"
7 | , "2015602:guide"
8 | , "2014301:tutorial"
9 | , "2015604:typed-routes"
10 | , "2015603:syntax-highlighting"
11 | , "2015601:preview"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Rib is designed to be used by not only its author, but also others. To that end, if you notice that the library can be changed or improved in anyway so as to make it easier to write your own static sites, please do not hesitate to write a short proposal in the Issue tracker.
2 |
3 | ## Coding style
4 |
5 | Source code should be auto-formatted using [ormolu](https://github.com/tweag/ormolu).
6 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: "CI"
2 | on:
3 | pull_request:
4 | push:
5 | jobs:
6 | build:
7 | runs-on: ${{ matrix.os }}
8 | strategy:
9 | matrix:
10 | os: [macos-latest, ubuntu-latest]
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: cachix/install-nix-action@v12
14 | # This also runs nix-build.
15 | - uses: cachix/cachix-action@v8
16 | with:
17 | name: srid
18 | signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}'
19 | # Only needed for private caches
20 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
21 |
--------------------------------------------------------------------------------
/guide/examples.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | * [rib-sample](https://github.com/srid/rib-sample): Use this to get started with
4 | your own site.
5 |
6 | * [zulip-archive](https://github.com/srid/zulip-archive): Zulip chat archive viewer ([running here](https://funprog.srid.ca/)).
7 |
8 | * [open-editions.org](https://github.com/open-editions/open-editions.org) ([running here](https://open-editions.org/)).
9 |
10 | * Rib powers the Zettelkasten system [neuron](https://github.com/srid/neuron#neuron)
11 | * Example: [rib.srid.ca](https://rib.srid.ca) - Rib's guide site
12 | * Example: [www.srid.ca](https://www.srid.ca/)
13 | * Example: [neuron.srid.ca](https://neuron.srid.ca/)
14 | * Example: [haskell.srid.ca](https://haskell.srid.ca/)
15 |
--------------------------------------------------------------------------------
/guide/syntax-highlighting.md:
--------------------------------------------------------------------------------
1 | ---
2 | tags:
3 | - guide
4 | ---
5 |
6 | # Enable Syntax Highlighting
7 |
8 | Use Pandoc to add syntax highlighting support to your rib site.
9 |
10 | 1. Import the desired style from Pandoc
11 |
12 | ```haskell
13 | import Text.Pandoc.Highlighting (styleToCss, tango)
14 | ```
15 |
16 | 2. Add it to the `
` section of your HTML:
17 |
18 | ```haskell
19 | style_ [type_ "text/css"] $ styleToCss tango
20 | ```
21 |
22 | 3. Make sure that your Markdown files specify the language in their fenced code
23 | blocks. See [Github
24 | documentation](https://help.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks#syntax-highlighting)
25 | for details.
26 |
--------------------------------------------------------------------------------
/rib-core/src/Rib/Log.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE BangPatterns #-}
2 | {-# LANGUAGE DeriveGeneric #-}
3 | {-# LANGUAGE LambdaCase #-}
4 | {-# LANGUAGE OverloadedStrings #-}
5 | {-# LANGUAGE QuasiQuotes #-}
6 | {-# LANGUAGE RecordWildCards #-}
7 | {-# LANGUAGE ScopedTypeVariables #-}
8 | {-# LANGUAGE ViewPatterns #-}
9 |
10 | module Rib.Log
11 | ( logStrLn,
12 | logErr,
13 | )
14 | where
15 |
16 | import Development.Shake (Verbosity (..))
17 | import Relude
18 | import Rib.Cli (CliConfig (..))
19 | import System.IO (hPutStrLn)
20 |
21 | logStrLn :: MonadIO m => CliConfig -> String -> m ()
22 | logStrLn CliConfig {..} s =
23 | unless (verbosity == Silent) $ do
24 | putStrLn s
25 |
26 | logErr :: MonadIO m => String -> m ()
27 | logErr s =
28 | liftIO $ hPutStrLn stderr s
29 |
--------------------------------------------------------------------------------
/rib/test/Rib/CliSpec.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | {-# LANGUAGE QuasiQuotes #-}
3 | {-# LANGUAGE ScopedTypeVariables #-}
4 | {-# LANGUAGE NoImplicitPrelude #-}
5 |
6 | module Rib.CliSpec
7 | ( spec,
8 | )
9 | where
10 |
11 | import Relude
12 | import Rib.Cli
13 | import Test.Hspec
14 | import Text.Megaparsec (parse)
15 |
16 | spec :: Spec
17 | spec = do
18 | describe "Host and Port parsing" $ do
19 | it "should parse port" $ do
20 | parseHostPort ":8080" `shouldBe` Right ("127.0.0.1", 8080)
21 | it "should parse localhost" $ do
22 | parseHostPort "localhost:8080" `shouldBe` Right ("localhost", 8080)
23 | it "should parse IP addr" $ do
24 | parseHostPort "132.45.0.254:8080" `shouldBe` Right ("132.45.0.254", 8080)
25 | where
26 | parseHostPort =
27 | parse hostPortParser ""
28 |
--------------------------------------------------------------------------------
/.github/workflows/publish-site.yml:
--------------------------------------------------------------------------------
1 | name: "Publish"
2 | on:
3 | # Run only when pushing to master branch
4 | push:
5 | branches:
6 | - master
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - uses: cachix/install-nix-action@v12
13 | - uses: cachix/cachix-action@v8
14 | with:
15 | name: srid
16 | - name: Build neuron site 🔧
17 | run: |
18 | cd ./guide
19 | mkdir -p .neuron/output && touch .neuron/output/.nojekyll
20 | docker run -v $PWD:/notes sridca/neuron neuron gen --pretty-urls
21 | - name: Deploy to GitHub Pages 🚀
22 | uses: JamesIves/github-pages-deploy-action@3.7.1
23 | with:
24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
25 | BRANCH: gh-pages
26 | FOLDER: guide/.neuron/output
27 |
--------------------------------------------------------------------------------
/rib/src/Rib/Parser/Dhall.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | {-# LANGUAGE Rank2Types #-}
3 | {-# LANGUAGE ScopedTypeVariables #-}
4 | {-# LANGUAGE ViewPatterns #-}
5 |
6 | -- | Parser for Dhall configuration files.
7 | --
8 | -- Use `Dhall.TH.makeHaskellTypes` to create the Haskell type first. And then
9 | -- call `parse` from your Shake action.
10 | module Rib.Parser.Dhall
11 | ( -- * Parsing
12 | parse,
13 | )
14 | where
15 |
16 | import Development.Shake
17 | import Dhall (FromDhall, auto, input)
18 | import Relude
19 | import Rib.Shake (ribInputDir)
20 | import System.Directory
21 | import System.FilePath
22 |
23 | -- | Parse a Dhall file as Haskell type.
24 | parse ::
25 | FromDhall a =>
26 | -- | Dependent .dhall files, which must trigger a rebuild
27 | [FilePath] ->
28 | -- | The Dhall file to parse. Relative to `ribInputDir`.
29 | FilePath ->
30 | Action a
31 | parse deps f = do
32 | inputDir <- ribInputDir
33 | need deps
34 | s <- toText <$> readFile' (inputDir > f)
35 | liftIO $ withCurrentDirectory inputDir $
36 | input auto s
37 |
--------------------------------------------------------------------------------
/rib-core/src/Rib/Server.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | {-# LANGUAGE ScopedTypeVariables #-}
3 |
4 | -- | Serve generated static files with HTTP
5 | module Rib.Server
6 | ( serve,
7 | )
8 | where
9 |
10 | import Network.Wai.Application.Static (defaultFileServerSettings, staticApp)
11 | import qualified Network.Wai.Handler.Warp as Warp
12 | import Relude
13 | import Rib.Cli (CliConfig)
14 | import Rib.Log
15 |
16 | -- | Run a HTTP server to serve a directory of static files
17 | --
18 | -- Binds the server to host 127.0.0.1.
19 | serve ::
20 | CliConfig ->
21 | -- | Host
22 | Text ->
23 | -- | Port number to bind to
24 | Int ->
25 | -- | Directory to serve.
26 | FilePath ->
27 | IO ()
28 | serve cfg host port path = do
29 | logStrLn cfg $ "[Rib] Serving " <> path <> " at http://" <> toString host <> ":" <> show port
30 | Warp.runSettings settings app
31 | where
32 | app = staticApp $ defaultFileServerSettings path
33 | settings =
34 | Warp.setHost (fromString $ toString host)
35 | $ Warp.setPort port
36 | $ Warp.defaultSettings
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # rib
4 |
5 | [](https://en.wikipedia.org/wiki/BSD_License)
6 | [](https://hackage.haskell.org/package/rib)
7 | [](https://builtwithnix.org)
8 | [](https://funprog.zulipchat.com/#narrow/stream/218047-Rib)
9 |
10 | Rib is a Haskell **static site generator** based on [Shake](https://shakebuild.com/), with a delightful workflow.
11 |
12 | See for full documentation.
13 |
14 | **UPDATE (Apr, 2021)**: Rib is superceded by [Ema](https://ema.srid.ca/)
15 |
16 | ## Developing rib
17 |
18 | Use ghcid for quicker compilation cycles:
19 |
20 | ```bash
21 | nix-shell --run "cd rib-core && ghcid"
22 | ```
23 |
24 | To test your changes, clone [rib-sample](https://github.com/srid/rib-sample) and run it using your local rib checkout:
25 |
26 | ```bash
27 | cd ..
28 | git clone https://github.com/srid/rib-sample.git
29 | cd rib-sample
30 | nix-shell --arg rib ../rib --run 'ghcid -T ":main -wS"'
31 | ```
32 |
--------------------------------------------------------------------------------
/guide/index.md:
--------------------------------------------------------------------------------
1 | # Rib
2 |
3 | [Rib](https://github.com/srid/rib) is a Haskell **static site generator** based
4 | on Shake, with a delightful development experience.
5 |
6 | **UPDATE (Apr, 2021)**: Rib is superceded by [Ema](https://ema.srid.ca/)
7 |
8 | ## Features
9 |
10 | Rib composes existing tools designed to do their task well, instead of
11 | reinventing their capabilities. Time spent using rib is time spent learning them.
12 |
13 | - Use [Shake](https://shakebuild.com/) as the core build engine.
14 | - Write HTML & CSS in Haskell with natural flow (using [Lucid](https://chrisdone.com/posts/lucid2/) &
15 | [Clay](http://fvisser.nl/clay/))
16 | - Optional route system for type-safe construction of your site routes.
17 | - Parse Markdown and other formats supported by
18 | [Pandoc](https://pandoc.org/) / [MMark](https://github.com/mmark-md/mmark).
19 | - Remain as simple as possible to use
20 | - Reproducible and a delightful workflow enabled by Nix, ghcid and fsnotify.
21 |
22 | See [[[preview]]] to get a feel for what your code may look like.
23 |
24 |
25 | ## Getting Started
26 |
27 | The easiest way to get started with [Rib](/) is to [use the
28 | template](https://help.github.com/en/articles/creating-a-repository-from-a-template)
29 | repository, [**rib-sample**](https://github.com/srid/rib-sample), from Github.
30 |
31 | ## Next
32 |
33 | * [[[tutorial]]]
34 | * [[[guide]]]
35 | * [[[examples]]]
36 |
37 |
--------------------------------------------------------------------------------
/rib/src/Rib/Extra/CSS.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | {-# LANGUAGE ScopedTypeVariables #-}
3 |
4 | -- | Some commonly useful CSS styles
5 | module Rib.Extra.CSS where
6 |
7 | import Clay
8 | import qualified Data.Text as T
9 | import Lucid
10 | import Relude
11 |
12 | -- | Stock CSS for the element
13 | --
14 | -- Based on the MDN demo at,
15 | -- https://developer.mozilla.org/en-US/docs/Web/HTML/Element/kbd
16 | mozillaKbdStyle :: Css
17 | mozillaKbdStyle = do
18 | backgroundColor $ rgb 238 238 238
19 | color $ rgb 51 51 51
20 | sym borderRadius (px 3)
21 | border solid (px 1) $ rgb 180 180 180
22 | padding (px 2) (px 4) (px 2) (px 4)
23 | boxShadow $
24 | (bsColor (rgba 0 0 0 0.2) $ shadowWithBlur (px 0) (px 1) (px 1))
25 | :| [(bsColor (rgba 255 255 255 0.7) $ bsInset $ shadowWithSpread (px 0) (px 2) (px 0) (px 0))]
26 | fontSize $ em 0.85
27 | fontWeight $ weight 700
28 | lineHeight $ px 1
29 | whiteSpace nowrap
30 |
31 | -- | Include the specified Google Fonts
32 | googleFonts :: Monad m => [Text] -> HtmlT m ()
33 | googleFonts fs =
34 | let fsEncoded = T.intercalate "|" $ T.replace " " "+" <$> fs
35 | fsUrl = "https://fonts.googleapis.com/css?family=" <> fsEncoded <> "&display=swap"
36 | in stylesheet fsUrl
37 |
38 | -- | Include the specified stylesheet URL
39 | stylesheet :: Monad m => Text -> HtmlT m ()
40 | stylesheet x = link_ [rel_ "stylesheet", href_ x]
41 |
--------------------------------------------------------------------------------
/rib-core/src/Rib/Watch.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE DeriveGeneric #-}
2 | {-# LANGUAGE LambdaCase #-}
3 | {-# LANGUAGE OverloadedStrings #-}
4 | {-# LANGUAGE QuasiQuotes #-}
5 | {-# LANGUAGE ScopedTypeVariables #-}
6 |
7 | -- | Filesystem watching using fsnotify
8 | module Rib.Watch
9 | ( onTreeChange,
10 | )
11 | where
12 |
13 | import Control.Concurrent (threadDelay)
14 | import Control.Concurrent.Async (race)
15 | import Control.Concurrent.Chan
16 | import Relude
17 | import System.FSNotify (Event (..), watchTreeChan, withManager)
18 |
19 | -- | Recursively monitor the contents of the given path and invoke the given IO
20 | -- action for every event triggered.
21 | --
22 | -- If multiple events fire rapidly, the IO action is invoked only once, taking
23 | -- those multiple events as its argument.
24 | onTreeChange :: FilePath -> ([Event] -> IO ()) -> IO ()
25 | onTreeChange fp f = do
26 | withManager $ \mgr -> do
27 | eventCh <- newChan
28 | void $ watchTreeChan mgr fp (const True) eventCh
29 | forever $ do
30 | firstEvent <- readChan eventCh
31 | events <- debounce 100 [firstEvent] $ readChan eventCh
32 | f events
33 |
34 | debounce :: Int -> [event] -> IO event -> IO [event]
35 | debounce millies events f = do
36 | -- Race the readEvent against the timelimit.
37 | race f (threadDelay (1000 * millies)) >>= \case
38 | Left event ->
39 | -- If the read event finishes first try again.
40 | debounce millies (events <> [event]) f
41 | Right () ->
42 | -- Otherwise continue
43 | return events
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2019, Sridhar Ratnakumar
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/rib-core/rib-core.cabal:
--------------------------------------------------------------------------------
1 | cabal-version: 2.4
2 | name: rib-core
3 | version: 1.0.0.0
4 | license: BSD-3-Clause
5 | copyright: 2019 Sridhar Ratnakumar
6 | maintainer: srid@srid.ca
7 | author: Sridhar Ratnakumar
8 | homepage: https://github.com/srid/rib#readme
9 | bug-reports: https://github.com/srid/rib/issues
10 | synopsis:
11 | Static site generator based on Shake
12 | description:
13 | Haskell static site generator based on Shake, with a delightful development experience.
14 | category: Web
15 | build-type: Simple
16 |
17 | source-repository head
18 | type: git
19 | location: https://github.com/srid/rib
20 |
21 | library
22 | hs-source-dirs: src
23 | default-language: Haskell2010
24 | default-extensions: NoImplicitPrelude
25 | ghc-options:
26 | -Wall
27 | -Wincomplete-record-updates
28 | -Wincomplete-uni-patterns
29 | build-depends:
30 | aeson,
31 | async,
32 | base-noprelude >=4.12 && <4.14,
33 | binary >=0.8.6 && <0.9,
34 | cmdargs >=0.10.20 && <0.11,
35 | containers >=0.6.0 && <0.7,
36 | directory >= 1.0 && <2.0,
37 | exceptions,
38 | foldl,
39 | fsnotify >=0.3.0 && <0.4,
40 | filepath,
41 | megaparsec >= 8.0,
42 | modern-uri,
43 | mtl >=2.2.2 && <2.3,
44 | optparse-applicative >= 0.15,
45 | relude >= 0.6 && < 0.8,
46 | safe-exceptions,
47 | shake >= 0.18.5,
48 | text >=1.2.3 && <1.3,
49 | time,
50 | iso8601-time,
51 | wai >=3.2.2 && <3.3,
52 | wai-app-static >=3.1.6 && <3.2,
53 | warp
54 | exposed-modules:
55 | Rib.App
56 | Rib.Cli
57 | Rib.Watch
58 | Rib.Log
59 | Rib.Route
60 | Rib.Shake
61 | other-modules:
62 | Rib.Server
63 |
--------------------------------------------------------------------------------
/default.nix:
--------------------------------------------------------------------------------
1 | let
2 | # Use https://status.nixos.org// to find the next hash to update nixpkgs to.
3 | # Look for the "Last updated" commit hash for the entry `nixpkgs-unstable`
4 | nixpkgsRev = "502845c3e31e";
5 | nixpkgsSrc = builtins.fetchTarball {
6 | url = "https://github.com/nixos/nixpkgs/archive/${nixpkgsRev}.tar.gz";
7 | sha256 = "0fcqpsy6y7dgn0y0wgpa56gsg0b0p8avlpjrd79fp4mp9bl18nda";
8 | };
9 |
10 | gitignoreSrc = builtins.fetchTarball {
11 | url = "https://github.com/hercules-ci/gitignore/archive/c4662e6.tar.gz";
12 | sha256 = "1npnx0h6bd0d7ql93ka7azhj40zgjp815fw2r6smg8ch9p7mzdlx";
13 | };
14 | inherit (import gitignoreSrc { }) gitignoreSource;
15 |
16 | ribRoot = gitignoreSource ./.;
17 | in {
18 | pkgs ? import nixpkgsSrc {}
19 | , compiler ? pkgs.haskellPackages
20 | , root ? (ribRoot + "/rib")
21 | , name ? "rib"
22 | , source-overrides ? {}
23 | , overrides ? self: super: {}
24 | , additional-packages ? _: []
25 | , ...
26 | }:
27 | let
28 | optionals = pkgs.lib.lists.optionals;
29 | in
30 | compiler.developPackage {
31 | inherit root name;
32 | source-overrides = {
33 | rib = ribRoot + "/rib";
34 | rib-core = ribRoot + "/rib-core";
35 | } // source-overrides;
36 | overrides = self: super: with pkgs.haskell.lib; {
37 | } // (overrides self super);
38 | modifier = with pkgs.haskell.lib;
39 | let
40 | addRibDeps = drv:
41 | addBuildTools drv (with pkgs.haskellPackages;
42 | [ cabal-install
43 | ghcid
44 | haskell-language-server
45 | ]
46 | # Additional packages would be available in `nix-build` as well, only
47 | # as long as the built executable references it. When using as a
48 | # Haskell library, however, you will have to override the package and
49 | # add it to propagateBuildInputs (see neuron for an example).
50 | ++ additional-packages pkgs
51 | # Shake recommends fsatrace, but it requires system configuration on
52 | # macOS.
53 | ++ optionals (builtins.currentSystem == "x86_64-linux") [pkgs.fsatrace]
54 | );
55 | in drv: addRibDeps (dontHaddock drv);
56 | }
57 |
--------------------------------------------------------------------------------
/rib/rib.cabal:
--------------------------------------------------------------------------------
1 | cabal-version: 2.4
2 | name: rib
3 | version: 1.0.0.0
4 | license: BSD-3-Clause
5 | copyright: 2019 Sridhar Ratnakumar
6 | maintainer: srid@srid.ca
7 | author: Sridhar Ratnakumar
8 | homepage: https://github.com/srid/rib#readme
9 | bug-reports: https://github.com/srid/rib/issues
10 | synopsis:
11 | Static site generator based on Shake
12 | description:
13 | Haskell static site generator based on Shake, with a delightful development experience.
14 | category: Web
15 | build-type: Simple
16 |
17 | source-repository head
18 | type: git
19 | location: https://github.com/srid/rib
20 |
21 | common library-common
22 | hs-source-dirs: src
23 | default-language: Haskell2010
24 | default-extensions: NoImplicitPrelude
25 | ghc-options:
26 | -Wall
27 | -Wincomplete-record-updates
28 | -Wincomplete-uni-patterns
29 | build-depends:
30 | aeson,
31 | async,
32 | base-noprelude >=4.12 && <4.14,
33 | binary >=0.8.6 && <0.9,
34 | clay >=0.13.3,
35 | cmdargs >=0.10.20 && <0.11,
36 | containers >=0.6.0 && <0.7,
37 | dhall >= 1.30 && <1.33,
38 | directory >= 1.0 && <2.0,
39 | exceptions,
40 | foldl,
41 | fsnotify >=0.3.0 && <0.4,
42 | filepath,
43 | lucid >=2.9.11 && <2.10,
44 | megaparsec >= 8.0,
45 | mmark >= 0.0.7.2,
46 | mmark-ext >= 0.2.1.0,
47 | modern-uri,
48 | mtl >=2.2.2 && <2.3,
49 | optparse-applicative >= 0.15,
50 | pandoc >=2.7 && <3,
51 | pandoc-types >=1.20,
52 | relude >= 0.6 && < 0.8,
53 | safe-exceptions,
54 | shake >= 0.18.5,
55 | text >=1.2.3 && <1.3,
56 | time,
57 | iso8601-time,
58 | wai >=3.2.2 && <3.3,
59 | wai-app-static >=3.1.6 && <3.2,
60 | warp,
61 | rib-core
62 |
63 | library
64 | import: library-common
65 | exposed-modules:
66 | Rib
67 | Rib.Parser.Dhall
68 | Rib.Parser.MMark
69 | Rib.Parser.Pandoc
70 | Rib.Extra.CSS
71 | Rib.Extra.OpenGraph
72 |
73 | test-suite rib-test
74 | import: library-common
75 | type: exitcode-stdio-1.0
76 | hs-source-dirs: test
77 | main-is: Spec.hs
78 | build-depends:
79 | base-noprelude >=4.12 && <4.14,
80 | relude,
81 | hspec,
82 | QuickCheck
83 |
84 |
--------------------------------------------------------------------------------
/rib-core/src/Rib/Route.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE KindSignatures #-}
2 | {-# LANGUAGE LambdaCase #-}
3 | {-# LANGUAGE MultiWayIf #-}
4 | {-# LANGUAGE OverloadedStrings #-}
5 | {-# LANGUAGE QuasiQuotes #-}
6 | {-# LANGUAGE ScopedTypeVariables #-}
7 |
8 | -- | Type-safe routes for static sites.
9 | module Rib.Route
10 | ( -- * Defining routes
11 | IsRoute (..),
12 |
13 | -- * Rendering routes
14 | routeUrl,
15 | routeUrlRel,
16 |
17 | -- * Writing routes
18 | writeRoute,
19 | )
20 | where
21 |
22 | import Control.Monad.Catch
23 | import Data.Kind
24 | import qualified Data.Text as T
25 | import Development.Shake (Action)
26 | import Relude
27 | import Rib.Shake (writeFileCached)
28 | import System.FilePath
29 |
30 | -- | A route is a GADT which represents the individual routes in a static site.
31 | --
32 | -- `r` represents the data used to render that particular route.
33 | class IsRoute (r :: Type -> Type) where
34 | -- | Return the filepath (relative to `Rib.Shake.ribInputDir`) where the
35 | -- generated content for this route should be written.
36 | routeFile :: MonadThrow m => r a -> m (FilePath)
37 |
38 | data UrlType = Relative | Absolute
39 |
40 | path2Url :: FilePath -> UrlType -> Text
41 | path2Url fp = toText . \case
42 | Relative ->
43 | fp
44 | Absolute ->
45 | "/" > fp
46 |
47 | -- | The absolute URL to this route (relative to site root)
48 | routeUrl :: IsRoute r => r a -> Text
49 | routeUrl = routeUrl' Absolute
50 |
51 | -- | The relative URL to this route
52 | routeUrlRel :: IsRoute r => r a -> Text
53 | routeUrlRel = routeUrl' Relative
54 |
55 | -- | Get the URL to a route
56 | routeUrl' :: IsRoute r => UrlType -> r a -> Text
57 | routeUrl' urlType = stripIndexHtml . flip path2Url urlType . either (error . toText . displayException) id . routeFile
58 | where
59 | stripIndexHtml s =
60 | -- Because path2Url can return relative URL, we must account for there
61 | -- not being a / at the beginning.
62 | if | "/index.html" `T.isSuffixOf` s ->
63 | T.dropEnd (T.length "index.html") s
64 | | s == "index.html" ->
65 | "."
66 | | otherwise ->
67 | s
68 |
69 | -- | Write the content `s` to the file corresponding to the given route.
70 | --
71 | -- This is similar to `Rib.Shake.writeFileCached`, but takes a route instead of
72 | -- a filepath as its argument.
73 | writeRoute :: (IsRoute r, ToString s) => r a -> s -> Action ()
74 | writeRoute r content = do
75 | fp <- liftIO $ routeFile r
76 | writeFileCached fp $ toString $ content
77 |
--------------------------------------------------------------------------------
/rib-core/src/Rib/Shake.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE BangPatterns #-}
2 | {-# LANGUAGE LambdaCase #-}
3 | {-# LANGUAGE OverloadedStrings #-}
4 | {-# LANGUAGE QuasiQuotes #-}
5 | {-# LANGUAGE Rank2Types #-}
6 | {-# LANGUAGE ScopedTypeVariables #-}
7 | {-# LANGUAGE ViewPatterns #-}
8 |
9 | -- | Combinators for working with Shake.
10 | module Rib.Shake
11 | ( -- * Basic helpers
12 | buildStaticFiles,
13 | forEvery,
14 |
15 | -- * Writing only
16 | writeFileCached,
17 |
18 | -- * Misc
19 | getCliConfig,
20 | ribInputDir,
21 | ribOutputDir,
22 | )
23 | where
24 |
25 | import Control.Monad.Catch
26 | import Development.Shake
27 | import Relude
28 | import Rib.Cli (CliConfig)
29 | import qualified Rib.Cli as Cli
30 | import System.Directory
31 | import System.FilePath
32 | import System.IO.Error (isDoesNotExistError)
33 |
34 | -- | Get rib settings from a shake Action monad.
35 | getCliConfig :: Action CliConfig
36 | getCliConfig = getShakeExtra >>= \case
37 | Just v -> pure v
38 | Nothing -> fail "CliConfig not initialized"
39 |
40 | -- | Input directory containing source files
41 | --
42 | -- This is same as the first argument to `Rib.App.run`
43 | ribInputDir :: Action FilePath
44 | ribInputDir = Cli.inputDir <$> getCliConfig
45 |
46 | -- | Output directory where files are generated
47 | --
48 | -- This is same as the second argument to `Rib.App.run`
49 | ribOutputDir :: Action FilePath
50 | ribOutputDir = do
51 | output <- Cli.outputDir <$> getCliConfig
52 | liftIO $ createDirectoryIfMissing True output
53 | return output
54 |
55 | -- | Shake action to copy static files as is.
56 | buildStaticFiles :: [FilePath] -> Action ()
57 | buildStaticFiles staticFilePatterns = do
58 | input <- ribInputDir
59 | output <- ribOutputDir
60 | files <- getDirectoryFiles input staticFilePatterns
61 | void $ forP files $ \f ->
62 | copyFileChanged (input > f) (output > f)
63 |
64 | -- | Run the given action when any file matching the patterns changes
65 | forEvery ::
66 | -- | Source file patterns (relative to `ribInputDir`)
67 | [FilePath] ->
68 | (FilePath -> Action a) ->
69 | Action [a]
70 | forEvery pats f = do
71 | input <- ribInputDir
72 | fs <- getDirectoryFiles input pats
73 | forP fs f
74 |
75 | -- | Write the given file but only when it has been modified.
76 | --
77 | -- Also, always writes under ribOutputDir
78 | writeFileCached :: FilePath -> String -> Action ()
79 | writeFileCached !k !s = do
80 | f <- fmap (> k) ribOutputDir
81 | currentS <- liftIO $ forgivingAbsence $ readFile f
82 | unless (Just s == currentS) $ do
83 | writeFile' f $! s
84 | -- Use a character (like +) that contrasts with what Shake uses (#) for
85 | -- logging modified files being read.
86 | putInfo $ "+ " <> f
87 |
88 | -- | If argument of the function throws a
89 | -- 'System.IO.Error.doesNotExistErrorType', 'Nothing' is returned (other
90 | -- exceptions propagate). Otherwise the result is returned inside a 'Just'.
91 | forgivingAbsence :: (MonadIO m, MonadCatch m) => m a -> m (Maybe a)
92 | forgivingAbsence f =
93 | catchIf
94 | isDoesNotExistError
95 | (Just <$> f)
96 | (const $ return Nothing)
97 |
--------------------------------------------------------------------------------
/rib/src/Rib/Extra/OpenGraph.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE GADTs #-}
2 | {-# LANGUAGE LambdaCase #-}
3 | {-# LANGUAGE OverloadedStrings #-}
4 | {-# LANGUAGE RecordWildCards #-}
5 |
6 | -- | Meta tags for The Open Graph protocol: https://ogp.me/
7 | module Rib.Extra.OpenGraph
8 | ( OpenGraph (..),
9 | OGType (..),
10 | Article (..),
11 | )
12 | where
13 |
14 | import Data.Time (UTCTime)
15 | import Data.Time.ISO8601 (formatISO8601)
16 | import Lucid
17 | import Lucid.Base (makeAttribute)
18 | import Relude
19 | import qualified Text.URI as URI
20 |
21 | -- The OpenGraph metadata
22 | --
23 | -- This type can be directly rendered to HTML using `toHTML`.
24 | data OpenGraph = OpenGraph
25 | { _openGraph_title :: Text,
26 | _openGraph_url :: Maybe URI.URI,
27 | _openGraph_author :: Maybe Text,
28 | _openGraph_description :: Maybe Text,
29 | _openGraph_siteName :: Text,
30 | _openGraph_type :: Maybe OGType,
31 | _openGraph_image :: Maybe URI.URI
32 | }
33 | deriving (Eq, Show)
34 |
35 | instance ToHtml OpenGraph where
36 | toHtmlRaw = toHtml
37 | toHtml OpenGraph {..} = do
38 | meta' "author" `mapM_` _openGraph_author
39 | meta' "description" `mapM_` _openGraph_description
40 | requireAbsolute "OGP URL" (\uri -> link_ [rel_ "canonical", href_ uri]) `mapM_` _openGraph_url
41 | metaOg "title" _openGraph_title
42 | metaOg "site_name" _openGraph_siteName
43 | toHtml `mapM_` _openGraph_type
44 | requireAbsolute "OGP image URL" (metaOg "image") `mapM_` _openGraph_image
45 | where
46 | meta' k v = meta_ [name_ k, content_ v]
47 | requireAbsolute description f uri =
48 | if isJust (URI.uriScheme uri)
49 | then f $ URI.render uri
50 | else error $ description <> " must be absolute. this URI is not: " <> URI.render uri
51 |
52 | -- TODO: Remaining ADT values & sub-fields
53 | data OGType
54 | = OGType_Article Article
55 | | OGType_Website
56 | deriving (Eq, Show)
57 |
58 | instance ToHtml OGType where
59 | toHtmlRaw = toHtml
60 | toHtml = \case
61 | OGType_Article article -> do
62 | metaOg "type" "article"
63 | toHtml article
64 | OGType_Website -> do
65 | metaOg "type" "website"
66 |
67 | -- TODO: _article_profile :: [Profile]
68 | data Article = Article
69 | { _article_section :: Maybe Text,
70 | _article_modifiedTime :: Maybe UTCTime,
71 | _article_publishedTime :: Maybe UTCTime,
72 | _article_expirationTime :: Maybe UTCTime,
73 | _article_tag :: [Text]
74 | }
75 | deriving (Eq, Show)
76 |
77 | instance ToHtml Article where
78 | toHtmlRaw = toHtml
79 | toHtml Article {..} = do
80 | metaOg "article:section" `mapM_` _article_section
81 | metaOgTime "article:modified_time" `mapM_` _article_modifiedTime
82 | metaOgTime "article:published_time" `mapM_` _article_publishedTime
83 | metaOgTime "article:expiration_time" `mapM_` _article_expirationTime
84 | metaOg "article:tag" `mapM_` _article_tag
85 | where
86 | metaOgTime k t =
87 | metaOg k $ toText $ formatISO8601 t
88 |
89 | -- Open graph meta element
90 | metaOg :: Applicative m => Text -> Text -> HtmlT m ()
91 | metaOg k v =
92 | meta_
93 | [ makeAttribute "property" $ "og:" <> k,
94 | content_ v
95 | ]
96 |
--------------------------------------------------------------------------------
/guide/tutorial.md:
--------------------------------------------------------------------------------
1 | # Tutorial
2 |
3 | Although Rib is at its core a Haskell library (and meant to be used as one, rather than as a framework), it provides toolset (based on nix, ghcid, fsnotify, etc.) to make working with static sites pleasant.
4 |
5 | ## Directory structure
6 |
7 | Let us clone the template repository, [**rib-sample**](https://github.com/srid/rib-sample), and inspect what's in it:
8 |
9 | ```bash
10 | $ git clone https://github.com/srid/rib-sample.git mysite
11 | ...
12 | $ cd mysite
13 | $ ls -F
14 | content/ default.nix Main.hs README.md rib-sample.cabal
15 | ```
16 |
17 | The three key items here are:
18 |
19 | 1. `Main.hs`: Haskell source containing the DSL of the HTML/CSS of your site.
20 | See [[preview]].
21 | 2. `content/`: The source content (eg: Markdown sources and static files)
22 | 3. `dest/`: The target directory, excluded from the git repository, will contain
23 | _generated_ content (i.e., the HTML files, and copied over static content)
24 |
25 | The template repository comes with a few sample posts under `content/`, and a basic
26 | HTML layout and CSS style defined in `Main.hs`.
27 |
28 | ## Run the site
29 |
30 | Now let's run them all.
31 |
32 | Clone the sample repository locally, install [Nix](https://nixos.org/nix/) (as
33 | described in its README) and run your site as follows:
34 |
35 | ```bash
36 | nix-shell --run 'ghcid -T ":main -wS"'
37 | ```
38 |
39 | Running this command gives you a local HTTP server at
40 | (serving the generated files) that automatically reloads when either the content
41 | (`content/`) or the HTML/CSS/build-actions (`Main.hs`) changes. Hot reload, in other
42 | words.
43 |
44 | ## How Rib works
45 |
46 | How does the aforementioned nix-shell command work?
47 |
48 | 1. `nix-shell` will run the given command in a shell environment with all of our
49 | dependencies (notably the Haskell ones including the `rib` library itself)
50 | installed.
51 |
52 | 2. [`ghcid`](https://github.com/ndmitchell/ghcid) will compile your `Main.hs`
53 | and run its `main` function.
54 |
55 | 3. `Main.hs:main` in turn calls `Rib.App.run` which takes as argument your custom
56 | Shake action that will build the static site.
57 |
58 | 4. `Rib.App.run`: this parses the CLI arguments and runs the rib CLI "app" which
59 | can be run in one of a few modes --- generating static files, watching the
60 | `content/` directory for changes, starting HTTP server for the `dest/` directory.
61 | The "-wS" options will run the Shake build action passed as argument on
62 | every file change and spin up a HTTP server.
63 |
64 | Run that command, and visit to view your site.
65 |
66 | ## Editing workflow
67 |
68 | Now try making some changes to the content, say `content/first-post.md`. You should
69 | see it reflected when you refresh the page. Or change the HTML or CSS of your
70 | site in `Main.hs`; this will trigger `ghcid` to rebuild the Haskell source and
71 | restart the server.
72 |
73 | ## What's next?
74 |
75 | Great, by now you should have your static site generator ready and running!
76 |
77 | Rib recommends writing your Shake actions in the style of being
78 | [forward-defined](http://hackage.haskell.org/package/shake-0.18.3/docs/Development-Shake-Forward.html)
79 | which adds to the simplicity of the entire thing.
80 |
81 |
--------------------------------------------------------------------------------
/guide/preview.md:
--------------------------------------------------------------------------------
1 | # Quick Preview
2 |
3 | Here is how your code may look like if you were to generate your static site
4 | using Rib:
5 |
6 | ```haskell
7 | -- | Route corresponding to each generated static page.
8 | --
9 | -- The `a` parameter specifies the data (typically Markdown document) used to
10 | -- generate the final page text.
11 | data Route a where
12 | Route_Index :: Route [(Route Pandoc, Pandoc)]
13 | Route_Article :: FilePath -> Route Pandoc
14 |
15 | -- | The `IsRoute` instance allows us to determine the target .html path for
16 | -- each route. This affects what `routeUrl` will return.
17 | instance IsRoute Route where
18 | routeFile = \case
19 | Route_Index ->
20 | pure "index.html"
21 | Route_Article srcPath ->
22 | pure $ "article" > srcPath -<.> ".html"
23 |
24 | -- | Main entry point to our generator.
25 | --
26 | -- `Rib.run` handles CLI arguments, and takes three parameters here.
27 | --
28 | -- 1. Directory `content`, from which static files will be read.
29 | -- 2. Directory `dest`, under which target files will be generated.
30 | -- 3. Shake action to run.
31 | --
32 | -- In the shake action you would expect to use the utility functions
33 | -- provided by Rib to do the actual generation of your static site.
34 | main :: IO ()
35 | main = withUtf8 $ do
36 | Rib.run "content" "dest" generateSite
37 |
38 | -- | Shake action for generating the static site
39 | generateSite :: Action ()
40 | generateSite = do
41 | -- Copy over the static files
42 | Rib.buildStaticFiles ["static/**"]
43 | let writeHtmlRoute :: Route a -> a -> Action ()
44 | writeHtmlRoute r = Rib.writeRoute r . Lucid.renderText . renderPage r
45 | -- Build individual sources, generating .html for each.
46 | articles <-
47 | Rib.forEvery ["*.md"] $ \srcPath -> do
48 | let r = Route_Article srcPath
49 | doc <- Pandoc.parse Pandoc.readMarkdown srcPath
50 | writeHtmlRoute r doc
51 | pure (r, doc)
52 | writeHtmlRoute Route_Index articles
53 |
54 | -- | Define your site HTML here
55 | renderPage :: Route a -> a -> Html ()
56 | renderPage route val = html_ [lang_ "en"] $ do
57 | head_ $ do
58 | meta_ [httpEquiv_ "Content-Type", content_ "text/html; charset=utf-8"]
59 | title_ routeTitle
60 | style_ [type_ "text/css"] $ C.render pageStyle
61 | body_ $ do
62 | div_ [class_ "header"] $
63 | a_ [href_ "/"] "Back to Home"
64 | h1_ routeTitle
65 | case route of
66 | Route_Index ->
67 | div_ $ forM_ val $ \(r, src) ->
68 | li_ [class_ "pages"] $ do
69 | let meta = getMeta src
70 | b_ $ a_ [href_ (Rib.routeUrl r)] $ toHtml $ title meta
71 | renderMarkdown `mapM_` description meta
72 | Route_Article _ ->
73 | article_ $
74 | Pandoc.render val
75 | where
76 | routeTitle :: Html ()
77 | routeTitle = case route of
78 | Route_Index -> "Rib sample site"
79 | Route_Article _ -> toHtml $ title $ getMeta val
80 | renderMarkdown :: Text -> Html ()
81 | renderMarkdown =
82 | Pandoc.render . Pandoc.parsePure Pandoc.readMarkdown
83 |
84 | -- | Define your site CSS here
85 | pageStyle :: Css
86 | pageStyle = C.body ? do
87 | C.margin (em 4) (pc 20) (em 1) (pc 20)
88 | ".header" ? do
89 | C.marginBottom $ em 2
90 | "li.pages" ? do
91 | C.listStyleType C.none
92 | C.marginTop $ em 1
93 | "b" ? C.fontSize (em 1.2)
94 | "p" ? sym C.margin (px 0)
95 |
96 | -- | Metadata in our markdown sources
97 | data SrcMeta
98 | = SrcMeta
99 | { title :: Text,
100 | -- | Description is optional, hence `Maybe`
101 | description :: Maybe Text
102 | }
103 | deriving (Show, Eq, Generic, FromJSON)
104 | ```
105 |
106 | (View full [`Main.hs`](https://github.com/srid/rib-sample/blob/master/src/Main.hs) at rib-sample)
107 |
108 |
109 |
--------------------------------------------------------------------------------
/guide/typed-routes.md:
--------------------------------------------------------------------------------
1 | ---
2 | tags:
3 | - guide
4 | ---
5 |
6 | # Typed Routes
7 |
8 | Rib includes an optional route system based on
9 | [GADT](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/gadt.html)s than
10 | can be used to define safe and structured routes for your site. The sample repo
11 | used in [[tutorial]] already uses routes, and you can see the entire code in
12 | [[preview]] to see all of this would fit together in a static site
13 | generator.
14 |
15 |
16 | ## Definining Routes
17 |
18 | To define a route, create a GADT type called `Route` with a type parameter `a`:
19 |
20 |
21 | ```haskell
22 | data Route a where
23 | Route_Home :: Route ()
24 | Route_Article :: FilePath -> Route Pandoc
25 | ```
26 |
27 | Why use GADTs instead of regular ADTs? There are two reasons:
28 |
29 | 1. GADTs provides us with a mechanism to specify an unique type (the type
30 | parameter `a`) for *each* of its constructor. This type, `a`, is generally
31 | the type of the value used to *generate* the static content (typically HTML)
32 | of that route.
33 |
34 | 2. The value used to generate a route is not necessary to create the route
35 |
36 | For example, this is how we create the article route (typically inside the
37 | [`forEvery`](http://hackage.haskell.org/package/rib-0.8.0.0/docs/Rib-Shake.html#v:forEvery) block):
38 |
39 | ```haskell
40 | let r = Route_Article "hello.md"
41 | ```
42 |
43 | Note that we are able to create a route without the value used to generate it,
44 | i.e., the Markdown content (point 2). This route is of type `Route Pandoc`,
45 | which means `a ~ Pandoc`. Therefore, wherever we pass this route - when we pass
46 | the value associated with it, it must be of the type `Pandoc` (point 1). The
47 | `renderPage` function which renders the HTML of a route is defined to take both
48 | the route and its value as an argument:
49 |
50 |
51 | ```haskell
52 | renderPage :: Route a -> a -> Html ()
53 | ...
54 | ```
55 |
56 | The `renderPage` function is thus polymorphic in the route it handles. It can
57 | render a common HTML layout, and `case` on the route value passed to render
58 | content to specific to routes. The author finds this way of rendering HTML to be
59 | much more ergonomic and pleasant compared to the various templating systems of
60 | other static site generators.
61 |
62 | ## Customizing route paths
63 |
64 | Once your route type is defined, you may specify the file paths for each of
65 | them. To do this, simply derive an instance for the `IsRoute` class:
66 |
67 | ```haskell
68 | instance IsRoute Route where
69 | routeFile = \case
70 | Route_Home ->
71 | pure "index.html"
72 | Route_Article srcPath ->
73 | pure $ "article" > srcPath -<.> ".html"
74 | ```
75 |
76 | Notice how in order to calculate the file path of a route, you only need the route
77 | (`Route a`), but not the data (`a`) used to generate it.
78 |
79 | The arguments your route constructors take are meant to be used in the
80 | calculation of this file path. For example, the `FilePath` argument of the
81 | `Route_Article` constructor specifies the source path of the Markdown document,
82 | from which we compute the target (generated) path by simply replacing the
83 | extension with ".html" (in addition to putting it in a sub directory "article").
84 |
85 | ## Generating a route
86 |
87 | `Rib.Route.writeRoute` takes a route, a string and writes that string to the
88 | file path associated with the route. You will use this in your Shake `Action`
89 | monad, typically inside a `forEvery` block.
90 |
91 | ## Route URLs
92 |
93 | Once routes are fully defined as above, it becomes very straightforward to use
94 | them when linking in your HTML. The `Rib.routeUrl` function takes a route
95 | (`Route a`) and returns the URL to that route.
96 |
97 | You will pass your routes to whichever function (`renderPage` is principle among
98 | them) that needs to know the URL to your generated files, as long as they remain
99 | polymorphic in the route type.
100 |
101 | ## See also
102 |
103 | - [obelisk-route](https://old.reddit.com/r/haskell/comments/fsgqd6/monthly_hask_anything_april_2020/fm6esky/?context=3):
104 | the library that inspired `Rib.Route`.
105 |
--------------------------------------------------------------------------------
/rib/src/Rib/Parser/MMark.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE FlexibleInstances #-}
2 | {-# LANGUAGE LambdaCase #-}
3 | {-# LANGUAGE MultiParamTypeClasses #-}
4 | {-# LANGUAGE OverloadedStrings #-}
5 | {-# LANGUAGE QuasiQuotes #-}
6 | {-# LANGUAGE Rank2Types #-}
7 | {-# LANGUAGE ScopedTypeVariables #-}
8 | {-# LANGUAGE TypeApplications #-}
9 | {-# LANGUAGE TypeFamilies #-}
10 | {-# LANGUAGE ViewPatterns #-}
11 |
12 | -- | Parsing Markdown using the mmark parser.
13 | module Rib.Parser.MMark
14 | ( -- * Parsing
15 | parse,
16 | parsePure,
17 | parseWith,
18 | parsePureWith,
19 | defaultExts,
20 |
21 | -- * Rendering
22 | render,
23 |
24 | -- * Extracting information
25 | getFirstImg,
26 | getFirstParagraphText,
27 | projectYaml,
28 |
29 | -- * Re-exports
30 | MMark,
31 | )
32 | where
33 |
34 | import Control.Foldl (Fold (..))
35 | import Development.Shake (Action, readFile')
36 | import Lucid.Base (HtmlT (..))
37 | import Relude
38 | import Rib.Shake (ribInputDir)
39 | import System.FilePath
40 | import Text.MMark (MMark, projectYaml)
41 | import qualified Text.MMark as MMark
42 | import qualified Text.MMark.Extension as Ext
43 | import qualified Text.MMark.Extension.Common as Ext
44 | import qualified Text.Megaparsec as M
45 | import Text.URI (URI)
46 |
47 | -- | Render a MMark document as HTML
48 | render :: Monad m => MMark -> HtmlT m ()
49 | render = liftHtml . MMark.render
50 | where
51 | liftHtml :: Monad m => HtmlT Identity () -> HtmlT m ()
52 | liftHtml = HtmlT . pure . runIdentity . runHtmlT
53 |
54 | -- | Like `parsePure` but takes a custom list of MMark extensions
55 | parsePureWith ::
56 | [MMark.Extension] ->
57 | -- | Filepath corresponding to the text to be parsed (used only in parse errors)
58 | FilePath ->
59 | -- | Text to be parsed
60 | Text ->
61 | Either Text MMark
62 | parsePureWith exts k s = case MMark.parse k s of
63 | Left e -> Left $ toText $ M.errorBundlePretty e
64 | Right doc -> Right $ MMark.useExtensions exts $ useTocExt doc
65 |
66 | -- | Pure version of `parse`
67 | parsePure ::
68 | -- | Filepath corresponding to the text to be parsed (used only in parse errors)
69 | FilePath ->
70 | -- | Text to be parsed
71 | Text ->
72 | Either Text MMark
73 | parsePure = parsePureWith defaultExts
74 |
75 | -- | Parse Markdown using mmark
76 | parse :: FilePath -> Action MMark
77 | parse = parseWith defaultExts
78 |
79 | -- | Like `parse` but takes a custom list of MMark extensions
80 | parseWith :: [MMark.Extension] -> FilePath -> Action MMark
81 | parseWith exts f =
82 | either (fail . toString) pure =<< do
83 | inputDir <- ribInputDir
84 | s <- toText <$> readFile' (inputDir > f)
85 | pure $ parsePureWith exts f s
86 |
87 | -- | Get the first image in the document if one exists
88 | getFirstImg :: MMark -> Maybe URI
89 | getFirstImg = flip MMark.runScanner $ Fold f Nothing id
90 | where
91 | f acc blk = acc <|> listToMaybe (mapMaybe getImgUri (inlinesContainingImg blk))
92 | getImgUri = \case
93 | Ext.Image _ uri _ -> Just uri
94 | _ -> Nothing
95 | inlinesContainingImg :: Ext.Bni -> [Ext.Inline]
96 | inlinesContainingImg = \case
97 | Ext.Naked xs -> toList xs
98 | Ext.Paragraph xs -> toList xs
99 | _ -> []
100 |
101 | -- | Get the first paragraph text of a MMark document.
102 | --
103 | -- Useful to determine "preview" of your notes.
104 | getFirstParagraphText :: MMark -> Maybe Text
105 | getFirstParagraphText =
106 | flip MMark.runScanner $ Fold f Nothing id
107 | where
108 | f acc blk = acc <|> (Ext.asPlainText <$> getPara blk)
109 | getPara = \case
110 | Ext.Paragraph xs -> Just xs
111 | _ -> Nothing
112 |
113 | defaultExts :: [MMark.Extension]
114 | defaultExts =
115 | [ Ext.fontAwesome,
116 | Ext.footnotes,
117 | Ext.kbd,
118 | Ext.linkTarget,
119 | Ext.mathJax (Just '$'),
120 | Ext.punctuationPrettifier,
121 | -- For list of parsers supported, see:
122 | -- https://github.com/jgm/skylighting/tree/master/skylighting-core/xml
123 | Ext.skylighting
124 | ]
125 |
126 | useTocExt :: MMark -> MMark
127 | useTocExt doc = MMark.useExtension (Ext.toc "toc" toc) doc
128 | where
129 | toc = MMark.runScanner doc $ Ext.tocScanner (\x -> x > 1 && x < 5)
130 |
--------------------------------------------------------------------------------
/rib/src/Rib/Parser/Pandoc.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE FlexibleContexts #-}
2 | {-# LANGUAGE LambdaCase #-}
3 | {-# LANGUAGE OverloadedStrings #-}
4 | {-# LANGUAGE Rank2Types #-}
5 | {-# LANGUAGE ScopedTypeVariables #-}
6 |
7 | -- | Helpers for working with Pandoc documents
8 | module Rib.Parser.Pandoc
9 | ( -- * Parsing
10 | parse,
11 | parsePure,
12 |
13 | -- * Rendering
14 | render,
15 | renderPandocInlines,
16 |
17 | -- * Extracting information
18 | extractMeta,
19 | getH1,
20 | getToC,
21 | getFirstImg,
22 |
23 | -- * Re-exports
24 | Pandoc,
25 | module Text.Pandoc.Readers,
26 | )
27 | where
28 |
29 | import Control.Monad.Except (MonadError, liftEither, runExcept)
30 | import Data.Aeson
31 | import Development.Shake (Action, readFile')
32 | import Lucid (HtmlT, toHtmlRaw)
33 | import Relude
34 | import Rib.Shake (ribInputDir)
35 | import System.FilePath
36 | import Text.Pandoc
37 | import qualified Text.Pandoc.Readers
38 | import Text.Pandoc.Walk (query)
39 | import Text.Pandoc.Writers.Shared (toTableOfContents)
40 |
41 | -- | Pure version of `parse`
42 | parsePure ::
43 | (ReaderOptions -> Text -> PandocPure Pandoc) ->
44 | Text ->
45 | Pandoc
46 | parsePure textReader s =
47 | either (error . show) id $ runExcept $ do
48 | runPure' $ textReader readerSettings s
49 |
50 | -- | Parse a lightweight markup language using Pandoc
51 | parse ::
52 | -- | The pandoc text reader function to use, eg: `readMarkdown`
53 | (ReaderOptions -> Text -> PandocIO Pandoc) ->
54 | FilePath ->
55 | Action Pandoc
56 | parse textReader f =
57 | either fail pure =<< do
58 | inputDir <- ribInputDir
59 | content <- toText <$> readFile' (inputDir > f)
60 | fmap (first show) $ runExceptT $ do
61 | runIO' $ textReader readerSettings content
62 |
63 | -- | Render a Pandoc document to HTML
64 | render :: Monad m => Pandoc -> HtmlT m ()
65 | render doc =
66 | either error id $ first show $ runExcept $ do
67 | runPure'
68 | $ fmap toHtmlRaw
69 | $ writeHtml5String writerSettings doc
70 |
71 | -- | Extract the Pandoc metadata as JSON value
72 | extractMeta :: Pandoc -> Maybe (Either Text Value)
73 | extractMeta (Pandoc meta _) = flattenMeta meta
74 |
75 | runPure' :: MonadError PandocError m => PandocPure a -> m a
76 | runPure' = liftEither . runPure
77 |
78 | runIO' :: (MonadError PandocError m, MonadIO m) => PandocIO a -> m a
79 | runIO' = liftEither <=< liftIO . runIO
80 |
81 | -- | Render a list of Pandoc `Text.Pandoc.Inline` values as Lucid HTML
82 | --
83 | -- Useful when working with `Text.Pandoc.Meta` values from the document metadata.
84 | renderPandocInlines :: Monad m => [Inline] -> HtmlT m ()
85 | renderPandocInlines =
86 | renderPandocBlocks . pure . Plain
87 |
88 | renderPandocBlocks :: Monad m => [Block] -> HtmlT m ()
89 | renderPandocBlocks =
90 | toHtmlRaw . render . Pandoc mempty
91 |
92 | -- | Get the top-level heading as Lucid HTML
93 | getH1 :: Monad m => Pandoc -> Maybe (HtmlT m ())
94 | getH1 (Pandoc _ bs) = fmap renderPandocInlines $ flip query bs $ \case
95 | Header 1 _ xs -> Just xs
96 | _ -> Nothing
97 |
98 | -- | Get the document table of contents
99 | getToC :: Monad m => Pandoc -> HtmlT m ()
100 | getToC (Pandoc _ bs) = renderPandocBlocks [toc]
101 | where
102 | toc = toTableOfContents writerSettings bs
103 |
104 | -- | Get the first image in the document if one exists
105 | getFirstImg ::
106 | Pandoc ->
107 | -- | Relative URL path to the image
108 | Maybe Text
109 | getFirstImg (Pandoc _ bs) = listToMaybe $ flip query bs $ \case
110 | Image _ _ (url, _) -> [toText url]
111 | _ -> []
112 |
113 | exts :: Extensions
114 | exts =
115 | mconcat
116 | [ extensionsFromList
117 | [ Ext_yaml_metadata_block,
118 | Ext_fenced_code_attributes,
119 | Ext_auto_identifiers,
120 | Ext_smart
121 | ],
122 | githubMarkdownExtensions
123 | ]
124 |
125 | readerSettings :: ReaderOptions
126 | readerSettings = def {readerExtensions = exts}
127 |
128 | writerSettings :: WriterOptions
129 | writerSettings = def {writerExtensions = exts}
130 |
131 | -- Internal code
132 |
133 | -- | Flatten a Pandoc 'Meta' into a well-structured JSON object.
134 | --
135 | -- Renders Pandoc text objects into plain strings along the way.
136 | flattenMeta :: Meta -> Maybe (Either Text Value)
137 | flattenMeta (Meta meta) = fmap toJSON . traverse go <$> guarded (not . null) meta
138 | where
139 | go :: MetaValue -> Either Text Value
140 | go (MetaMap m) = toJSON <$> traverse go m
141 | go (MetaList m) = toJSONList <$> traverse go m
142 | go (MetaBool m) = pure $ toJSON m
143 | go (MetaString m) = pure $ toJSON m
144 | go (MetaInlines m) =
145 | bimap show toJSON
146 | $ runPure . plainWriter
147 | $ Pandoc mempty [Plain m]
148 | go (MetaBlocks m) =
149 | bimap show toJSON
150 | $ runPure . plainWriter
151 | $ Pandoc mempty m
152 | plainWriter = writePlain def
153 |
--------------------------------------------------------------------------------
/assets/rib.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
79 |
--------------------------------------------------------------------------------
/rib-core/src/Rib/App.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE ApplicativeDo #-}
2 | {-# LANGUAGE DeriveGeneric #-}
3 | {-# LANGUAGE LambdaCase #-}
4 | {-# LANGUAGE OverloadedStrings #-}
5 | {-# LANGUAGE QuasiQuotes #-}
6 | {-# LANGUAGE RecordWildCards #-}
7 | {-# LANGUAGE ScopedTypeVariables #-}
8 |
9 | -- | CLI interface for Rib.
10 | --
11 | -- Mostly you would only need `Rib.App.run`, passing it your Shake build action.
12 | module Rib.App
13 | ( run,
14 | runWith,
15 | )
16 | where
17 |
18 | import Control.Concurrent.Async (race_)
19 | import Control.Exception.Safe (catch)
20 | import Development.Shake hiding (command)
21 | import Development.Shake.Forward (shakeForward)
22 | import Options.Applicative
23 | import Relude
24 | import Rib.Cli (CliConfig (CliConfig), cliParser)
25 | import qualified Rib.Cli as Cli
26 | import Rib.Log
27 | import qualified Rib.Server as Server
28 | import Rib.Watch (onTreeChange)
29 | import System.Directory
30 | import System.FSNotify (Event (..), eventPath)
31 | import System.FilePath
32 | import System.IO (BufferMode (LineBuffering), hSetBuffering)
33 |
34 | -- | Run Rib using arguments passed in the command line.
35 | run ::
36 | -- | Default value for `Cli.inputDir`
37 | FilePath ->
38 | -- | Deault value for `Cli.outputDir`
39 | FilePath ->
40 | -- | Shake build rules for building the static site
41 | Action () ->
42 | IO ()
43 | run src dst buildAction = runWith buildAction =<< execParser opts
44 | where
45 | opts =
46 | info
47 | (cliParser src dst <**> helper)
48 | ( fullDesc
49 | <> progDesc "Generate a static site at OUTPUTDIR using input from INPUTDIR"
50 | )
51 |
52 | -- | Like `run` but with an explicitly passed `CliConfig`
53 | runWith :: Action () -> CliConfig -> IO ()
54 | runWith buildAction cfg@CliConfig {..} = do
55 | -- For saner output
56 | flip hSetBuffering LineBuffering `mapM_` [stdout, stderr]
57 | case (watch, serve) of
58 | (True, Just (host, port)) -> do
59 | race_
60 | (Server.serve cfg host port $ outputDir)
61 | (runShakeAndObserve cfg buildAction)
62 | (True, Nothing) ->
63 | runShakeAndObserve cfg buildAction
64 | (False, Just (host, port)) ->
65 | Server.serve cfg host port $ outputDir
66 | (False, Nothing) ->
67 | runShakeBuild cfg buildAction
68 |
69 | shakeOptionsFrom :: CliConfig -> ShakeOptions
70 | shakeOptionsFrom cfg'@CliConfig {..} =
71 | shakeOptions
72 | { shakeVerbosity = verbosity,
73 | shakeFiles = shakeDbDir,
74 | shakeRebuild = bool [] [(RebuildNow, "**")] rebuildAll,
75 | shakeLintInside = [""],
76 | shakeExtra = addShakeExtra cfg' (shakeExtra shakeOptions)
77 | }
78 |
79 | runShakeBuild :: CliConfig -> Action () -> IO ()
80 | runShakeBuild cfg@CliConfig {..} buildAction = do
81 | runShake cfg $ do
82 | logStrLn cfg $ "[Rib] Generating " <> inputDir <> " (rebuildAll=" <> show rebuildAll <> ")"
83 | buildAction
84 |
85 | runShake :: CliConfig -> Action () -> IO ()
86 | runShake cfg shakeAction = do
87 | shakeForward (shakeOptionsFrom cfg) shakeAction
88 | `catch` handleShakeException
89 | where
90 | handleShakeException (e :: ShakeException) =
91 | -- Gracefully handle any exceptions when running Shake actions. We want
92 | -- Rib to keep running instead of crashing abruptly.
93 | logErr $
94 | "[Rib] Unhandled exception when building " <> shakeExceptionTarget e <> ": " <> show e
95 |
96 | runShakeAndObserve :: CliConfig -> Action () -> IO ()
97 | runShakeAndObserve cfg@CliConfig {..} buildAction = do
98 | -- Begin with a *full* generation as the HTML layout may have been changed.
99 | -- TODO: This assumption is not true when running the program from compiled
100 | -- binary (as opposed to say via ghcid) as the HTML layout has become fixed
101 | -- by being part of the binary. In this scenario, we should not do full
102 | -- generation (i.e., toggle the bool here to False). Perhaps provide a CLI
103 | -- flag to disable this.
104 | runShakeBuild (cfg {Cli.rebuildAll = True}) buildAction
105 | -- And then every time a file changes under the current directory
106 | logStrLn cfg $ "[Rib] Watching " <> inputDir <> " for changes"
107 | onSrcChange $ runShakeBuild cfg buildAction
108 | where
109 | onSrcChange :: IO () -> IO ()
110 | onSrcChange f = do
111 | -- Canonicalizing path is important as we are comparing path ancestor using isPrefixOf
112 | dir <- canonicalizePath inputDir
113 | -- Top-level directories to ignore from notifications
114 | let isBlacklisted :: FilePath -> Bool
115 | isBlacklisted p = or $ flip fmap watchIgnore $ \b -> (dir > b) `isPrefixOf` p
116 | onTreeChange dir $ \allEvents -> do
117 | let events = filter (not . isBlacklisted . eventPath) allEvents
118 | unless (null events) $ do
119 | -- Log the changed events for diagnosis.
120 | logEvent `mapM_` events
121 | f
122 | logEvent :: Event -> IO ()
123 | logEvent e = do
124 | logStrLn cfg $ eventLogPrefix e <> " " <> eventPath e
125 | eventLogPrefix = \case
126 | -- Single character log prefix to indicate file actions is a convention in Rib.
127 | Added _ _ _ -> "A"
128 | Modified _ _ _ -> "M"
129 | Removed _ _ _ -> "D"
130 | Unknown _ _ _ -> "?"
131 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log for rib
2 |
3 | ## 1.0
4 |
5 | - Split packages into `rib` and `rib-core`
6 |
7 | ## 0.12
8 |
9 | - Removed pandoc-include-code
10 | - Allow dhall 1.30
11 | - default.nix: Allow overriding compiler
12 |
13 | ## 0.10.0.0
14 |
15 | - API
16 | - Dropped `path` and `path-io` in favour of good ol' `FilePath`
17 | - This also lifts the restriction with absolute paths
18 | - Misc changes
19 | - #145: CLI arguments have been revamped
20 | - `serve` subcommand is replaced by the options `-wS`.
21 | - Added `--input-dir/--output-dir` to override these paths
22 | - Accept host string in addition to port number
23 | - Exposed `Rib.Shake.getCliConfig` to get full CLI configuration
24 | - Allow customizing fsnotify ignore list
25 | - #141: Allow quiet logging (useful when rib is used as a library)
26 |
27 | ## 0.8.0.0
28 |
29 | - Dependency upgrades
30 | - GHC 8.8
31 | - pandoc-include-code: 0.5.0.0
32 | - pandoc-types: 1.20
33 | - dhall: 1.30
34 | - clay: 0.13.3 (This is a downgrade, as 0.14 is not released yet)
35 | - New features:
36 | - API exposes the CLI parser (`optparse-applicative`) for user-level composition
37 | - Add `Rib.Parser.Pandoc.getToC` returning rendered Table of contents for a Pandoc document
38 | - Add `Rib.Parser.MMark.getFirstParagraphText`
39 | - Add `Rib.Extra.OpenGraph` for Open Graph protocol
40 | - Add to `Rib.Extra.CSS`, `googleFonts` and `stylesheet`
41 | - Bug fixes and misc changes:
42 | - `routeUrl`: Fix incorrect substitution of "foo-index.html" with "foo-"
43 | - Lucid rendering functions (like `MMark.render`) are now polymorphic in their monad.
44 | - #122: Fix Pandoc parser never returning metadata
45 | - #127: Rib's HTTP server now binds to `127.0.0.1`.
46 | - Allow directory listings in HTTP server
47 | - #130: Prevent unnecessary re-running of Shake action by debouncing fsnotify events
48 | - #136: Move `.shake` database directory under `ribInputDir`
49 | - default.nix: Takes `overrides` and `additional-packages` as extra arguments
50 |
51 | ## 0.7.0.0
52 |
53 | - Dependency upgrades
54 | - mmark: 0.0.7.2
55 | - megaparsec: 0.8
56 | - clay: 0.14
57 | - shake: 0.8.15
58 | - New features:
59 | - Added Dhall parser, `Rib.Parser.Dhall`
60 | - Add `Rib.Extra` containing useful but non-essential features
61 | - MMark, extensions removed:
62 | - `ghcSyntaxHighlighter`: we already have `skylighting` (which supports more parsers than Haskell)
63 | - `obfuscateEmail`: requires JS, which is not documented.
64 | - API changes:
65 | - Introduced `Route` functionality for simpler management of static routes.
66 | - Removed `buildHtmlMulti`, `buildHtml`, `readSource` functions and `Source` type.
67 | - Introduced `Rib.Shake.forEvery` to run a Shake action over a pattern of files when they change.
68 | - Exposed `Rib.Shake.writeFileCached`
69 | - `MMark.parse` and `Pandoc.parse` now automatically append path to `ribInputDir` and do not return Either.
70 | - Added `MMark.parseWith` (and `parsePureWith`), to specify a custom list of mmark extensions
71 | - Bug fixes
72 | - #95: Fix Shake error `resource busy (file is locked)`
73 | - #97: Fix Shake error `AsyncCancelled` when server thread crashes
74 | - #96 & #108: Drop problematic use of Shake `cacheActionWith`
75 |
76 | ## 0.6.0.0
77 |
78 | - Advance nixpkgs; require Shake >=0.18.4
79 | - Major API simplication: no more type class!
80 | - Allow user to specify their own source parser as a Haskell function
81 | - Removed types `Document` and `Markup` in favour of `Source`
82 | - Expose `ribInputDir` and `ribOutputDir` for use in custom Shake actions
83 | - Bug fixes:
84 | - #63: create intermediate directories when generating post HTML
85 | - #70: Don't crash on Shake errors
86 | - Fix unnecessary rebuild of all files when only one file changed
87 | - #66: Use caching (via Shake's `cacheActionWith`), to avoid writing HTML to disk until it has changed.
88 |
89 | ## 0.5.0.0
90 |
91 | This release comes with a major API refactor. Key changes:
92 |
93 | - Added MMark support, as an alternative to Pandoc
94 | - Allows using arbitrary records to load metadata
95 | - This replaces the previous complex metadata API
96 | - Added `Document` type that uses the custom metadata record
97 | - Add top-level `Rib` import namespace for ease of use
98 | - Remove the following:
99 | - JSON cache
100 | - `Rib.Simple`
101 | - Support for Table of Contents via MMark
102 |
103 | Other changes:
104 |
105 | - Use type-safe path types using the [path](http://hackage.haskell.org/package/path) library.
106 | - Fix #40: Gracefully handle rendering/ parsing errors, without dying.
107 | - Misc error reporting improvements
108 |
109 | ## 0.4.1.0
110 |
111 | - `Rib.Pandoc`:
112 | - Export `render'` and `renderInlines'` (the non-Lucid versions)
113 | - Re-export `Text.Pandoc.Readers` so the library user does not have to directly depend on `pandoc` only to render its documents.
114 | - `Rib.App`: The `run` funtion now takes two more arguments, specifying the input and output directory, which are no longer hardcoded.
115 | - `Rib.Simple`: add LaTeX to default list of readers
116 | - `Rib.Server`: Remove ".html" detection magic from URLs
117 |
118 | ## 0.3.0.0
119 |
120 | - Rename `Rib.App.Watch` to `Rib.App.WatchAndGenerate`
121 |
122 | ## 0.2.0.0
123 |
124 | - Initial release.
125 |
--------------------------------------------------------------------------------
/rib-core/src/Rib/Cli.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE ApplicativeDo #-}
2 | {-# LANGUAGE DeriveGeneric #-}
3 | {-# LANGUAGE OverloadedStrings #-}
4 | {-# LANGUAGE QuasiQuotes #-}
5 | {-# LANGUAGE RecordWildCards #-}
6 | {-# LANGUAGE ScopedTypeVariables #-}
7 | {-# LANGUAGE TypeApplications #-}
8 |
9 | module Rib.Cli
10 | ( CliConfig (..),
11 | cliParser,
12 | Verbosity (..),
13 |
14 | -- * Parser helpers
15 | directoryReader,
16 | watchOption,
17 | serveOption,
18 |
19 | -- * Internal
20 | hostPortParser,
21 | )
22 | where
23 |
24 | import Development.Shake (Verbosity (..))
25 | import Options.Applicative
26 | import Relude
27 | import Relude.Extra.Tuple
28 | import System.FilePath
29 | import qualified Text.Megaparsec as M
30 | import qualified Text.Megaparsec.Char as M
31 |
32 | -- Rib's CLI configuration
33 | --
34 | -- Can be retrieved using `Rib.Shake.getCliConfig` in the `Development.Shake.Action` monad.
35 | data CliConfig
36 | = CliConfig
37 | { -- | Whether to rebuild all sources in Shake.
38 | rebuildAll :: Bool,
39 | -- | Whether to monitor `inputDir` for changes and re-generate
40 | watch :: Bool,
41 | -- | Whether to run a HTTP server on `outputDir`
42 | serve :: Maybe (Text, Int),
43 | -- | Shake's verbosity level.
44 | --
45 | -- Setting this to `Silent` will affect Rib's own logging as well.
46 | verbosity :: Verbosity,
47 | -- | Directory from which source content will be read.
48 | inputDir :: FilePath,
49 | -- | The path where static files will be generated. Rib's server uses this
50 | -- directory when serving files.
51 | outputDir :: FilePath,
52 | -- | Path to shake's database directory.
53 | shakeDbDir :: FilePath,
54 | -- | List of relative paths to ignore when watching the source directory
55 | watchIgnore :: [FilePath]
56 | }
57 | deriving (Show, Eq, Generic, Typeable)
58 |
59 | cliParser :: FilePath -> FilePath -> Parser CliConfig
60 | cliParser inputDirDefault outputDirDefault = do
61 | rebuildAll <-
62 | switch
63 | ( long "rebuild-all"
64 | <> help "Rebuild all sources"
65 | )
66 | watch <- watchOption
67 | serve <- serveOption
68 | verbosity <-
69 | fmap
70 | (bool Verbose Silent)
71 | ( switch
72 | ( long "quiet"
73 | <> help "Log nothing"
74 | )
75 | )
76 | ~(inputDir, shakeDbDir) <-
77 | fmap (toSnd shakeDbDirFrom) $
78 | option
79 | directoryReader
80 | ( long "input-dir"
81 | <> metavar "INPUTDIR"
82 | <> value inputDirDefault
83 | <> help ("Directory containing the source files (" <> "default: " <> inputDirDefault <> ")")
84 | )
85 | outputDir <-
86 | option
87 | directoryReader
88 | ( long "output-dir"
89 | <> metavar "OUTPUTDIR"
90 | <> value outputDirDefault
91 | <> help ("Directory where files will be generated (" <> "default: " <> outputDirDefault <> ")")
92 | )
93 | ~(watchIgnore) <- pure builtinWatchIgnores
94 | pure CliConfig {..}
95 |
96 | watchOption :: Parser Bool
97 | watchOption =
98 | switch
99 | ( long "watch"
100 | <> short 'w'
101 | <> help "Watch for changes and regenerate"
102 | )
103 |
104 | serveOption :: Parser (Maybe (Text, Int))
105 | serveOption =
106 | optional
107 | ( option
108 | (megaparsecReader hostPortParser)
109 | ( long "serve"
110 | <> short 's'
111 | <> metavar "[HOST]:PORT"
112 | <> help "Run a HTTP server on the generated directory"
113 | )
114 | )
115 | <|> ( fmap (bool Nothing $ Just (defaultHost, 8080)) $
116 | switch (short 'S' <> help ("Like `-s " <> toString defaultHost <> ":8080`"))
117 | )
118 |
119 | builtinWatchIgnores :: [FilePath]
120 | builtinWatchIgnores =
121 | [ ".shake",
122 | ".git"
123 | ]
124 |
125 | shakeDbDirFrom :: FilePath -> FilePath
126 | shakeDbDirFrom inputDir =
127 | -- Keep shake database directory under the src directory instead of the
128 | -- (default) current working directory, which may not always be a project
129 | -- root (as in the case of neuron).
130 | inputDir > ".shake"
131 |
132 | -- | Like `str` but adds a trailing slash if there isn't one.
133 | directoryReader :: ReadM FilePath
134 | directoryReader = fmap addTrailingPathSeparator str
135 |
136 | megaparsecReader :: M.Parsec Void Text a -> ReadM a
137 | megaparsecReader p =
138 | eitherReader (first M.errorBundlePretty . M.parse p "" . toText)
139 |
140 | hostPortParser :: M.Parsec Void Text (Text, Int)
141 | hostPortParser = do
142 | host <-
143 | optional $
144 | M.string "localhost"
145 | <|> M.try parseIP
146 | void $ M.char ':'
147 | port <- parseNumRange 1 65535
148 | pure (fromMaybe defaultHost host, port)
149 | where
150 | readNum = maybe (fail "Not a number") pure . readMaybe
151 | parseIP :: M.Parsec Void Text Text
152 | parseIP = do
153 | a <- parseNumRange 0 255 <* M.char '.'
154 | b <- parseNumRange 0 255 <* M.char '.'
155 | c <- parseNumRange 0 255 <* M.char '.'
156 | d <- parseNumRange 0 255
157 | pure $ toText $ intercalate "." $ show <$> [a, b, c, d]
158 | parseNumRange :: Int -> Int -> M.Parsec Void Text Int
159 | parseNumRange a b = do
160 | n <- readNum =<< M.some M.digitChar
161 | if a <= n && n <= b
162 | then pure n
163 | else fail $ "Number not in range: " <> show a <> "-" <> show b
164 |
165 | defaultHost :: Text
166 | defaultHost = "127.0.0.1"
167 |
--------------------------------------------------------------------------------