├── 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 | [![BSD3](https://img.shields.io/badge/License-BSD-blue.svg)](https://en.wikipedia.org/wiki/BSD_License) 6 | [![Hackage](https://img.shields.io/hackage/v/rib.svg)](https://hackage.haskell.org/package/rib) 7 | [![built with nix](https://img.shields.io/badge/builtwith-nix-purple.svg)](https://builtwithnix.org) 8 | [![Zulip chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](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 | 5 | 6 | 8 | 10 | 12 | 13 | 18 | 44 | 46 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 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 | --------------------------------------------------------------------------------