mirror of
https://github.com/srid/ema.git
synced 2024-12-01 23:23:42 +03:00
Merge pull request #32 from srid/emanote-docs
Add back docs, and render using Emanote
This commit is contained in:
commit
d6fd722b0d
22
.github/workflows/publish.yaml
vendored
Normal file
22
.github/workflows/publish.yaml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
|
||||
name: "Publish"
|
||||
on:
|
||||
# Run only when pushing to master branch
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
neuron:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build emanote site 🔧
|
||||
run: |
|
||||
mkdir -p output
|
||||
docker run -v $PWD:/data sridca/emanote emanote -C /data/docs gen /data/output
|
||||
- name: Deploy to gh-pages 🚀
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./output/
|
||||
cname: ema.srid.ca
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -23,4 +23,4 @@ cabal.project.local~
|
||||
.ghc.environment.*
|
||||
result
|
||||
result-*
|
||||
docs-output/
|
||||
output
|
@ -2,6 +2,8 @@
|
||||
|
||||
## Unreleased (0.2.0.0)
|
||||
|
||||
- `routeUrl`: now returns relative URLs (ie. without the leading `/`)
|
||||
- Use the `<base>` tag to specify an explicit prefix for relative URLs in generated HTML. This way hosting on GitHub Pages without CNAME will continue to have functional links.
|
||||
- `Ema.Slug`
|
||||
- Add `Ord`, `Generic`, `Data` and Aeson instances to `Slug`
|
||||
- Unicode normalize slugs using NFC
|
||||
@ -21,7 +23,7 @@
|
||||
- add wikilink helpers
|
||||
- TODO(doc) Add `Ema.Helper.PathTree`
|
||||
- Examples
|
||||
- Remove Ex03_Documentation.hs (moved to separate repo, `ema-docs`)
|
||||
- ~~Remove Ex03_Documentation.hs (moved to separate repo, `ema-docs`)~~ Back to ./docs, but using Emanote.
|
||||
- Add Ex03_Basic.hs example
|
||||
|
||||
## 0.1.0.0 -- 2021-04-26
|
||||
|
@ -1,6 +1,6 @@
|
||||
# ema
|
||||
|
||||
<img width="10%" src="https://ema.srid.ca/ema.svg">
|
||||
<img width="10%" src="https://ema.srid.ca/favicon.svg">
|
||||
|
||||
[![Hackage](https://img.shields.io/hackage/v/ema.svg?logo=haskell)](https://hackage.haskell.org/package/ema)
|
||||
[![FAIR](https://img.shields.io/badge/FAIR-pledge-blue)](https://www.fairforall.org/about/)
|
||||
|
11
docs/concepts.md
Normal file
11
docs/concepts.md
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
order: 9
|
||||
---
|
||||
|
||||
# Concepts
|
||||
|
||||
* [Hot Reload](concepts/hot-reload.md)
|
||||
* [LVar](concepts/lvar.md)
|
||||
* [Slug](concepts/slug.md)
|
||||
* [CLI](concepts/cli.md)
|
||||
* [Logging](concepts/logging.md)
|
11
docs/concepts/cli.md
Normal file
11
docs/concepts/cli.md
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
order: 4
|
||||
---
|
||||
# CLI
|
||||
|
||||
Ema apps have a basic CLI argument structure that takes two kinds of input:
|
||||
|
||||
1. `-C <dir>`: specifies the "input directory" (current working directory by default)
|
||||
2. `gen` subcommand: generates the static site, instead of starting up the dev server
|
||||
|
||||
Ema (`runEma`) will change the [current working directory](https://hackage.haskell.org/package/directory-1.3.6.1/docs/System-Directory.html#v:getCurrentDirectory) to the "input directory" before running your application code. It, along with the "gen" subcommand (if used), is passed as the `Ema.CLI.Action` type to your `render` function. You can also use `runEmaWith` if you are manually handling the CLI arguments yourself.
|
30
docs/concepts/hot-reload.md
Normal file
30
docs/concepts/hot-reload.md
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
order: 1
|
||||
---
|
||||
# Hot Reload
|
||||
|
||||
**Hot Reloading** is a feature of Ema's dev server wherein any changes to your Haskell source or data files (such as Markdown files or HTML templates) _propagate instantly_ to the web browser without requiring any manual intervention like a full browser refresh. In practice, this is a such a delightful feature to work with. Imagine changing CSS style of an element, and see it reflect on your site in a split second.
|
||||
|
||||
## How Ema implements hot reload
|
||||
|
||||
### Websocket
|
||||
|
||||
The Ema dev server uses websockets to keep a bi-directional connection open between the web browser and the backend server. When you click on a link or when something changes in the backend, they are communicated via this connection. In a statically generated site, however, no such activity happens - and a link click behaves like a normal link, in that the browser makes a full HTTP request to the linked page.
|
||||
|
||||
### DOM patching
|
||||
|
||||
When switching to a new route or when receiving the new HTML, Ema uses [morphdom](https://github.com/patrick-steele-idem/morphdom) to _patch_ the existing DOM tree rather than replace it in its entirety. This, in addition to use of websockets, makes it possible to support **instant** hot reload with nary a delay.
|
||||
|
||||
### Haskell reload
|
||||
|
||||
Finally, hot reload on _code_ changes are supported via [ghcid](https://github.com/ndmitchell/ghcid). The [template repo](https://github.com/srid/ema-template)'s `bin/run` script uses ghcid underneath. Any HTML DSL (like blaze-html -- as used by the [Tailwind helper](guide/helpers/tailwind.md)) or CSS DSL automatically gets supported for hot-reload. If you choose to use a file-based HTML template language, you can enable hot-reload on template change using the [FileSystem helper](guide/helpers/filesystem.md).
|
||||
|
||||
Note that if your application makes use of threads, it is important to setup cleanup handlers so that `ghcid` doesn't leave [ghost](https://stackoverflow.com/q/24999636/55246) processes behind. Helpers like [`race_`](https://hackage.haskell.org/package/async-2.2.3/docs/Control-Concurrent-Async.html#v:race_) will do this automatically (incidentally it is used by `runEma` for running the user IO action).
|
||||
|
||||
### Data reload
|
||||
|
||||
For anything outside of the Haskell code, your code becomes responsible for monitoring and updating the model [LVar](concepts/lvar.md). The [filesystem helper](guide/helpers/filesystem.md) already provides utilities to facilitate this for monitoring changes to files and directories.
|
||||
|
||||
## Handling errors
|
||||
|
||||
If your code throws a Haskell exception, they will be gracefully handled and displayed in the browser, allowing you to recover without breaking hot-reload flow.
|
10
docs/concepts/logging.md
Normal file
10
docs/concepts/logging.md
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
order: 5
|
||||
---
|
||||
# Logging
|
||||
|
||||
`runEma`'s action monad supports the `MonadLoggerIO` constraint, as defined by [monad-logger](https://hackage.haskell.org/package/monad-logger). This means that you can use any of the logging functions from `monad-logger` to add logging to your application. [monad-logger-extras](https://hackage.haskell.org/package/monad-logger-extras) is used to colorize the logs.
|
||||
|
||||
```haskell
|
||||
logInfoNS "myapp" "This is an info message"
|
||||
logDebugNS "myapp" "This is a debug message info"
|
8
docs/concepts/lvar.md
Normal file
8
docs/concepts/lvar.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
order: 2
|
||||
---
|
||||
# LVar
|
||||
|
||||
If you are familiar with Haskell's `stm` package, a `LVar` is essentially a [`TMVar`](https://hackage.haskell.org/package/stm-2.5.0.0/docs/Control-Concurrent-STM-TMVar.html) but with an extra ability for other threads to observe changes. Ema uses it for [hot reload](concepts/hot-reload.md), and your application code is expected to set and update its [model](guide/model.md) through the LVar.
|
||||
|
||||
Documentation on `LVar` is available [on Hackage](https://hackage.haskell.org/package/lvar).
|
16
docs/concepts/slug.md
Normal file
16
docs/concepts/slug.md
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
order: 3
|
||||
---
|
||||
# Slug
|
||||
|
||||
A slug is a component of a URL or file path. In an _URL_ like `/foo/bar`, there are two _slugs_: "foo" and "bar". URLs (as well as filepaths) can therefore be represented as lists of slugs (`[Slug]`).
|
||||
|
||||
```haskell
|
||||
import Ema (Slug)
|
||||
|
||||
type URL = [Slug]
|
||||
```
|
||||
|
||||
Slugs are integral to Ema's routing system. When defining [route](guide/routes.md) encoders and decoders (via [Ema class instance](guide/class.md)) you are effectively writing functions that convert back and forth between your route type and `[Slug]`. These functions are ultimately used to determine the *filename* of the statically generated HTML (i.e., `./foo/bar.html`) as well as the linking URL in the rendered HTML (i.e., `/foo/bar`).
|
||||
|
||||
Slugs are also automatically [unicode normalized](https://www.unicode.org/faq/normalization.html) to NFC to ensure that route links work reliably regardless of the underlying representation of any non-ascii link text.
|
114
docs/favicon.svg
Normal file
114
docs/favicon.svg
Normal file
@ -0,0 +1,114 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 464 464" style="enable-background:new 0 0 464 464;" xml:space="preserve">
|
||||
<g>
|
||||
<path style="fill:#FFD7A3;" d="M448,72c0-41.333-23.5-72-64-72c-20.712,0-36.963,8.368-47.914,16.546
|
||||
c-2.988-2.239-7.235-2.14-10.124,0.445c-2.013,1.801-2.894,4.392-2.607,6.886C303.415,7.644,282.16,0,272,0c-26,0-40,8-40,8
|
||||
s-14-8-40-8c-10.16,0-31.415,7.644-51.355,23.877c0.288-2.494-0.593-5.084-2.607-6.886c-2.889-2.585-7.136-2.684-10.124-0.445
|
||||
C116.963,8.368,100.712,0,80,0C39.5,0,16,30.667,16,72c0,41.967,46.667,154.667,24,224c0,0,52.387-18.053,75.575-88.414
|
||||
C117.01,207.854,118.488,208,120,208v8c0,19.377,11.651,36.854,29.538,44.308L184,274.667v25.801
|
||||
c0,6.887-4.407,13.001-10.94,15.179l-32.343,11.9l-0.553-0.885c-2.312-3.699-7.16-4.864-10.9-2.62l-26.278,15.767
|
||||
c-3.555,2.133-4.842,6.581-3.128,10.241c-3.698,5.207-6.172,11.307-7.047,17.87l-9.188,68.908
|
||||
C81.705,451.216,92.898,464,107.413,464h249.175c14.515,0,25.708-12.784,23.789-27.172l-9.188-68.908
|
||||
c-0.875-6.563-3.348-12.664-7.047-17.87c1.714-3.661,0.426-8.108-3.128-10.241l-26.278-15.767c-3.74-2.244-8.588-1.079-10.9,2.62
|
||||
l-0.553,0.885l-32.343-11.9c-6.534-2.178-10.94-8.292-10.94-15.179v-25.802l34.461-14.359C332.349,252.855,344,235.378,344,216v-8
|
||||
c1.512,0,2.99-0.146,4.425-0.414C371.613,277.947,424,296,424,296C401.334,226.667,448,113.967,448,72z"/>
|
||||
<path style="fill:#FDC88E;" d="M280,274h-96v26.468l0,0v0.001c0,0.391-0.017,0.777-0.046,1.161c-0.014,0.2-0.04,0.396-0.062,0.595
|
||||
c-0.018,0.16-0.034,0.32-0.057,0.478c-0.043,0.307-0.098,0.609-0.158,0.91c-0.001,0.006-0.003,0.013-0.004,0.02
|
||||
c-0.888,4.4-3.587,8.228-7.425,10.537C243.173,325.73,280,274,280,274z"/>
|
||||
<g>
|
||||
<path style="fill:#D48F60;" d="M320,32c0,0,23.5-32,64-32s64,30.667,64,72c0,41.967-46.667,154.667-24,224c0,0-60-20.667-80-104
|
||||
c-18.34-76.418,8-128,8-128L320,32z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#C57E52;" d="M348,40c0,0,25.5-19,44-8c40.721,24.213,7.454,90.732,0,128c-10,50-6,98,32,136
|
||||
c0,0-60-20.667-80-104c-18.34-76.418,4-128,4-128V40z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#43A0C5;" d="M356.092,50.666L356.092,50.666c3.293-2.946,3.574-8.004,0.628-11.296l-19.461-21.75
|
||||
c-2.946-3.293-8.004-3.574-11.296-0.628l0,0c-3.293,2.946-3.574,8.004-0.628,11.296l19.461,21.75
|
||||
C347.741,53.331,352.799,53.612,356.092,50.666z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#D48F60;" d="M144,32c0,0-23.5-32-64-32S16,30.667,16,72c0,41.967,46.667,154.667,24,224c0,0,60-20.667,80-104
|
||||
c18.34-76.418-8-128-8-128L144,32z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#C57E52;" d="M117,40c0,0-26.5-19-45-8c-40.721,24.213-7.454,90.732,0,128c10,50,6,98-32,136
|
||||
c0,0,60-20.667,80-104c18.34-76.418-3-128-3-128V40z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#43A0C5;" d="M107.909,50.666L107.909,50.666c-3.293-2.946-3.574-8.004-0.628-11.296l19.461-21.75
|
||||
c2.946-3.293,8.004-3.574,11.296-0.628l0,0c3.293,2.946,3.574,8.004,0.628,11.296l-19.461,21.75
|
||||
C116.259,53.331,111.202,53.612,107.909,50.666z"/>
|
||||
</g>
|
||||
<path style="fill:#FFE1B2;" d="M344,160v-16c0-22.091-17.909-56-40-56H160c-22.091,0-40,33.909-40,56v16c-13.255,0-24,10.746-24,24
|
||||
c0,13.255,10.745,24,24,24v8c0,19.377,11.651,36.854,29.538,44.308l51.691,21.538c9.75,4.063,20.208,6.154,30.77,6.154l0,0
|
||||
c10.562,0,21.019-2.092,30.769-6.154l51.694-21.539C332.349,252.855,344,235.378,344,216v-8c13.255,0,24-10.745,24-24
|
||||
S357.255,160,344,160z"/>
|
||||
<g>
|
||||
<path style="fill:#623F33;" d="M176,192L176,192c-4.4,0-8-3.6-8-8v-8c0-4.4,3.6-8,8-8l0,0c4.4,0,8,3.6,8,8v8
|
||||
C184,188.4,180.4,192,176,192z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#623F33;" d="M288,192L288,192c-4.4,0-8-3.6-8-8v-8c0-4.4,3.6-8,8-8l0,0c4.4,0,8,3.6,8,8v8
|
||||
C296,188.4,292.4,192,288,192z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#E4B07B;" d="M232,248.219c-14.223,0-27.527-3.5-36.5-9.605c-3.652-2.484-4.602-7.461-2.113-11.113
|
||||
c2.48-3.648,7.461-4.598,11.113-2.113c6.289,4.277,16.57,6.832,27.5,6.832s21.211-2.555,27.5-6.832
|
||||
c3.66-2.492,8.629-1.539,11.113,2.113c2.488,3.652,1.539,8.629-2.113,11.113C259.528,244.719,246.223,248.219,232,248.219z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path style="fill:#FFD7A3;" d="M120,160c-13.255,0-24,10.745-24,24s10.745,24,24,24v8c0,19.378,11.651,36.855,29.538,44.308
|
||||
l51.69,21.538c2.53,1.054,5.112,1.962,7.727,2.749c-22.844-16.711-38.05-31.32-46.96-40.872c-6.482-6.95-9.995-16.121-9.995-25.625
|
||||
v-44.711c22.9-7.993,87.178-34.248,88-77.387c0.192-10.066-0.464-20.642-1.667-26C226.367,134.113,128.076,160,120,160z"/>
|
||||
<g>
|
||||
<path style="fill:#57B9DD;" d="M323.837,326.662L295.05,372.72c-4.386,7.017-12.077,11.28-20.352,11.28h-85.396
|
||||
c-8.275,0-15.966-4.263-20.352-11.28l-28.786-46.058c-2.312-3.699-7.16-4.864-10.9-2.62l-26.278,15.767
|
||||
c-3.837,2.302-5.04,7.305-2.668,11.1L136,408h192l35.682-57.092c2.372-3.795,1.169-8.797-2.668-11.1l-26.278-15.767
|
||||
C330.997,321.798,326.148,322.964,323.837,326.662z"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="136" y="408" style="fill:#43A0C5;" width="192" height="56"/>
|
||||
</g>
|
||||
<path style="fill:#D48F60;" d="M272,0c-26,0-40,8-40,8s-14-8-40-8S67.333,50.055,104,166.055c0,0,128-22.722,128-78.055
|
||||
c0,55.333,128,78.055,128,78.055C396.667,50.055,298,0,272,0z"/>
|
||||
<g>
|
||||
<path style="fill:#CA8357;" d="M232,8.004V8c0,0-14-8-40-8S67.334,50.055,104,166.055c0,0,15.495-2.757,35.69-8.67
|
||||
C117.089,53.368,207.192,8.084,232,8.004z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 5.6 KiB |
13
docs/guide.md
Normal file
13
docs/guide.md
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
order: 2
|
||||
---
|
||||
|
||||
# Guide
|
||||
|
||||
After having familiarized yourself with Ema by following the [earlier section](start/tutorial.md), you are now ready to dive deep into learning how to achieve specific things.
|
||||
|
||||
* [Defining your model](guide/model.md) -- [Define your site model such that it supports hot reload]{.item-intro}
|
||||
* [Working with routes](guide/routes.md) -- [Unless you site has a single page (`index.html`), you will need to manage a set of routes]{.item-intro}
|
||||
* [Defining Ema instance](guide/class.md) -- [Constrain your `model` and `route` to work with static sites]{.item-intro}
|
||||
* [Rendering HTML](guide/render.md) -- [You could use plain strings to build HTML, or use templates, or use one of the delightful Haskell DSLs]{.item-intro}
|
||||
* [Helpers](guide/helpers.md) -- [Bring Your Own Libraries, or choose from existing helpers]{.item-intro}
|
52
docs/guide/class.md
Normal file
52
docs/guide/class.md
Normal file
@ -0,0 +1,52 @@
|
||||
---
|
||||
order: 3
|
||||
---
|
||||
# Defining Ema instance
|
||||
|
||||
Once you have [model](guide/model.md) and [route](guide/routes.md) types in place, we must tell the Haskell compiler that they are suitable for generating static sites. We do this by creating an instance of the `Ema` typeclass.
|
||||
|
||||
Using some `MyModel` and the route `Route` shown in the [previous](guide/routes.md) section, we can create an instance as follows:
|
||||
|
||||
```haskell
|
||||
class Ema MyModel Route where
|
||||
-- Convert our route to browser URL, represented as a list of slugs
|
||||
encodeRoute = \case
|
||||
Index -> [] -- An empty slug represents the index route: index.html
|
||||
About -> ["about"]
|
||||
|
||||
-- Convert back the browser URL, represented as a list of slugs, to our route
|
||||
decodeRoute = \case
|
||||
[] -> Just Index
|
||||
["about"] -> Just About
|
||||
_ -> Nothing
|
||||
|
||||
-- The third method is optional; and used during static site generation.
|
||||
-- This tells Ema which routes to generate .html files for.
|
||||
-- By default, Enum & Bounded will be used to determine this list.
|
||||
staticRoutes model =
|
||||
[Index, About]
|
||||
|
||||
-- The fourth method is also optional; if you have static assets to serve, specify
|
||||
-- them here. Paths are relative to current working directory.
|
||||
staticAssets Proxy =
|
||||
["css", "images", "favicon.ico", "resume.pdf"]
|
||||
```
|
||||
|
||||
The `Ema` typeclass has four methods, with the last two of them being optional with default implementations:
|
||||
|
||||
1. Define `encodeRoute` that converts our route type to a browser URL [slug](concepts/slug.md) path representing relative URLs like `/foo/bar`
|
||||
2. Define `decodeRoute` that does the *reverse* converstion (the conversion must be isomorphic)
|
||||
3. _Optionally_, define `staticRoutes` indicating the routes to statically generate
|
||||
4. _Optionally_, define the list of static assets to copy over as-is during static generation
|
||||
|
||||
## `runEma`
|
||||
|
||||
The `Ema` constraint is used by the `runEma` function that acts as the main entry point to your static site generator. It takes two arguments:
|
||||
|
||||
1. `render` function that renders your HTML (we'll go over this in [the next](guide/render.md) section)
|
||||
2. an IO action that takes [`LVar model`](guide/model.md) as an argument.
|
||||
|
||||
This IO action is expected to be a long-running one, wherein you have full control over setting the value of the model over time.
|
||||
|
||||
{.last}
|
||||
[Next]{.next}, with our model and routes in place constrained by `Ema` type class, [we will define the HTML for our site](guide/render.md) using Ema.
|
10
docs/guide/helpers.md
Normal file
10
docs/guide/helpers.md
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
order: 5
|
||||
---
|
||||
# Helpers
|
||||
|
||||
Beyond the model and route types, Ema leaves it up to you as to how to develop your site. The following are not *required* when using Ema; nevertheless they are useful inasmuch as they capture common patterns in writing a good static site:
|
||||
|
||||
* [Tailwind + Blaze](guide/helpers/tailwind.md) -- [We recommend--but not mandate--Tailwind for CSS and blaze-html as HTML DSL]{.item-intro}
|
||||
* [Working with files](guide/helpers/filesystem.md) -- [Ema provides a helper to support hot-reload on files]{.item-intro}
|
||||
* [Converting Markdown](guide/helpers/markdown.md) -- [Pointers on how to work with Markdown files]{.item-intro}
|
56
docs/guide/helpers/filesystem.md
Normal file
56
docs/guide/helpers/filesystem.md
Normal file
@ -0,0 +1,56 @@
|
||||
---
|
||||
order: 2
|
||||
---
|
||||
# Working with files
|
||||
|
||||
If your static site is generated depending on local files on disk, the general flow of things is as follows:
|
||||
|
||||
```haskell
|
||||
runEma render $ \model -> do
|
||||
-- Load everything on launch
|
||||
initialModel <- loadFilesAndBuildModel
|
||||
LVar.set model initialModel
|
||||
-- Continue to monitor and update the model
|
||||
observeFileSystem $ \action ->
|
||||
LVar.modify model $ applyAction action
|
||||
```
|
||||
|
||||
For monitoring local files on disk you would typically use something like [fsnotify](https://hackage.haskell.org/package/fsnotify) in place of `observeFileSystem`. What is the point of doing this? To support [hot reload](concepts/hot-reload.md) on _data_ change. Imagine that your static site is generated based on Markdown files as well as HTML templates on disk. If either the Markdown file, or a HTML template file is modified, we want the web browser to hot reload the updated HTML *instantly*. This is enabled by storing both these kinds of files in the application [model](guide/model.md) and using [LVar](concepts/lvar.md) to update it *over time*.
|
||||
|
||||
For filesystem changes, Ema provides a helper based on `fsnotify` in the `Ema.Helper.FileSystem` module. You can use it as follows
|
||||
|
||||
```haskell
|
||||
import qualified Ema.Helper.FileSystem as FileSystem
|
||||
|
||||
type Model = Map FilePath Text
|
||||
|
||||
Ema.runEma render $ \model -> do
|
||||
LVar.set model =<< do
|
||||
mdFiles <- FileSystem.filesMatching "." ["**/*.md"]
|
||||
forM mdFiles readFileText
|
||||
<&> Map.fromList
|
||||
FileSystem.onChange "." $ \fp -> \case
|
||||
FileSystem.Update ->
|
||||
when (takeExtension fp == ".md") $ do
|
||||
log $ "Update: " <> fp
|
||||
s <- readFileText fp
|
||||
LVar.modify model $ Map.insert fp s
|
||||
FileSystem.Delete ->
|
||||
whenJust (takeExtension fp == ".md") $ do
|
||||
log $ "Delete: " <> fp
|
||||
LVar.modify model $ Map.delete fp
|
||||
```
|
||||
|
||||
In most cases, however, you probably want to use the higher level function `mountOnLVar`. It "mounts" the files you specify onto the [model LVar](concepts/lvar.md) such that any changes to them are *automatically* reflected in your [model](guide/model.md) value.
|
||||
|
||||
```haskell
|
||||
Ema.runEma render $ \model -> do
|
||||
FileSystem.mountOnLVar "." ["**/*.md"] model $ \fp -> \case
|
||||
FileSystem.Update -> do
|
||||
s <- readFileText fp
|
||||
pure $ Map.insert fp s
|
||||
FileSystem.Delete ->
|
||||
pure $ Map.delete fp
|
||||
```
|
||||
|
||||
[Full example here](https://github.com/srid/ema-template/blob/master/src/Main.hs).
|
33
docs/guide/helpers/markdown.md
Normal file
33
docs/guide/helpers/markdown.md
Normal file
@ -0,0 +1,33 @@
|
||||
---
|
||||
order: 3
|
||||
---
|
||||
|
||||
# Using Markdown
|
||||
|
||||
There are quite a few packages to convert Markdown to HTML,
|
||||
|
||||
- [Pandoc](https://hackage.haskell.org/package/pandoc) -- [Supports formats other than Markdown]{.item-intro}
|
||||
- [commonmark-hs](https://github.com/jgm/commonmark-hs) -- [Lightweight parser by the same author of Pandoc]{.item-intro}
|
||||
- [mmark](https://github.com/mmark-md/mmark) -- [*Strict* Markdown parser]{.item-intro}
|
||||
|
||||
## Helper
|
||||
|
||||
Ema provides a helper to parse Markdown files with YAML frontmatter, using commonmark-hs. If you are parsing front matter, you can use any type that has a [`FromYAML`](https://hackage.haskell.org/package/HsYAML-0.2.1.0/docs/Data-YAML.html#t:FromYAML) instance.
|
||||
|
||||
```haskell
|
||||
import qualified Ema.Helper.Markdown as Markdown
|
||||
|
||||
-- Front matter metadata can be any type with a `FromYAML` instance
|
||||
--
|
||||
-- Using a `Map` is a lazy way to capture metadata, but in real code we
|
||||
-- generally define a sum type and manually derive `FromYAML` for it.
|
||||
type Metadata = Map Text Text
|
||||
|
||||
-- Returns `Either Text (Metadata, Pandoc)`
|
||||
Markdown.parseMarkdownWithFrontMatter @Metadata
|
||||
"test.md" "Hello *world*"
|
||||
```
|
||||
|
||||
The template repo, as well as [Emanote](https://github.com/srid/emanote) (used to generate this site), uses this helper to parse Markdown files into Pandoc AST. Consult [the template repo's source code](https://github.com/srid/ema-template/blob/master/src/Main.hs) for details.
|
||||
|
||||
Note that with Ema you can get [hot reload](concepts/hot-reload.md) support for your Markdown files using [filesystem notifications](guide/helpers/filesystem.md).
|
19
docs/guide/helpers/tailwind.md
Normal file
19
docs/guide/helpers/tailwind.md
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
order: 1
|
||||
---
|
||||
# Using Tailwind CSS
|
||||
|
||||
The `Ema.Helper.Tailwind` module provides a `layout` function that uses [twind](https://twind.dev/) shim that is used in the statically generated site, and otherwise uses Tailwind CSS from CDN in the dev server mode. This helper is for those that **use [Tailwind CSS](https://tailwindcss.com/) in conjunction with [blaze-html](https://hackage.haskell.org/package/blaze-html) DSL**.
|
||||
|
||||
To use the layout helper in your [render](guide/render.md) function:
|
||||
|
||||
```haskell
|
||||
render :: Ema.CLI.Action -> MyModel -> MyRoute -> LByteString
|
||||
render emaAction model route = do
|
||||
Tailwind.layout emaAction (H.title "My site") $ do
|
||||
H.p "Hello world"
|
||||
```
|
||||
|
||||
The very site you are viewing (ema.srid.ca) is a live demonstration of this helper.
|
||||
|
||||
**Note** that because the [twind JS shim](https://twind.dev/handbook/the-shim.html) is used to support Tailwind styles your site will not render properly on web browsers with JavaScript disabled if you use this helper; it might also have trouble interoperating with other JS initializers on the site. See [this issue](https://github.com/srid/ema/issues/20) for upcoming alternatives.
|
33
docs/guide/model.md
Normal file
33
docs/guide/model.md
Normal file
@ -0,0 +1,33 @@
|
||||
---
|
||||
order: 1
|
||||
---
|
||||
# Defining your model
|
||||
|
||||
A "*model*" in Ema represents the state to use to generate your site. It could be as simple as a variable, or it could be a list of parsed Markdown files (as in the case of a weblog). Ema's model is also conceptually similar to [Elm](https://guide.elm-lang.org/architecture/)'s model, in that - changing the model [automatically](concepts/hot-reload.md) changes the [view](guide/render.md).
|
||||
|
||||
Here's an example model:
|
||||
|
||||
```haskell
|
||||
newtype BlogPosts = BlogPosts (Map Slug Text}
|
||||
```
|
||||
|
||||
Here `BlogPosts` is the model type. If we are generating a weblog site, then all the "data" we need is loaded into memory as a value of `BlogPosts`.
|
||||
|
||||
## Modifying the model
|
||||
|
||||
Ema's dev server supports [hot reload](concepts/hot-reload.md); it will observe changes to your model, in addition to code. To facilitate this you will manage your model as a [LVar](concepts/lvar.md). The `runEma` function ([described here](guide/class.md)) takes an IO action that gets `LVar model` as an argument.
|
||||
|
||||
For example,
|
||||
|
||||
```haskell
|
||||
runEma render $ \model ->
|
||||
forever $ do
|
||||
LVar.set model =<< liftIO getCurrentTime
|
||||
liftIO $ threadDelay $ 1 * 1000000
|
||||
```
|
||||
|
||||
In this contrived example ([full code here](https://github.com/srid/ema/blob/master/src/Ema/Example/Ex02_Clock.hs)), we are using `UTCTime` as the model. We set the initial value using `LVar.set`, and then continually update the current time every second. Every time the model gets updated, the web browser will [hot reload](concepts/hot-reload.md) to display the up to date value. For the `BlogPosts` model, you would typically use [fsnotify](https://hackage.haskell.org/package/fsnotify) to monitor changes to the underlying Markdown files, but note that Ema provides [a helper](guide/helpers/filesystem.md) for that.
|
||||
|
||||
|
||||
{.last}
|
||||
[Next]{.next}, we will [talk about routes](guide/routes.md).
|
44
docs/guide/render.md
Normal file
44
docs/guide/render.md
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
order: 4
|
||||
---
|
||||
# Rendering HTML
|
||||
|
||||
Once you have [model](guide/model.md) and [routes](guide/routes.md) in place and [constrained](guide/class.md), the last piece of the puzzle is to write a function that takes both as arguments and returns the HTML string (lazy bytestring, to be exact). This function can be as simple as the following:
|
||||
|
||||
```haskell
|
||||
render :: MyModel -> Route -> ByteString
|
||||
render model route =
|
||||
"<b>Hello</b>, world!"
|
||||
```
|
||||
|
||||
Of course we want it to be real, by using our model value, as well as generate the HTML based on the route. We will also use the [blaze-html](https://hackage.haskell.org/package/blaze-html) library to make writing HTML in Haskell palatable (see also [the layout helper](guide/helpers/tailwind.md)). A more realistic starting point (if not the finishing product) would be:
|
||||
|
||||
```haskell
|
||||
render :: MyModel -> Route -> ByteString
|
||||
render model route = Blaze.renderHtml $
|
||||
H.html $ do
|
||||
H.head $ do
|
||||
H.title "My site"
|
||||
H.body $ do
|
||||
H.h1 "My site"
|
||||
case route of
|
||||
Index ->
|
||||
H.h1 "Welcome to my website!"
|
||||
H.p $ do
|
||||
"Checkout the"
|
||||
H.a ! A.href (H.toValue $ Ema.routeUrl About) $ "About"
|
||||
" page."
|
||||
About ->
|
||||
H.div $ H.p "This website is managed by yours truly"
|
||||
H.footer $ do
|
||||
A.a ! A.href "https://github.com/user/repo" $ "Source on GitHub"
|
||||
```
|
||||
|
||||
Note that Ema provides a `routeUrl` helper function that serializes your route to the final URL (here, `/about`) for linking to.
|
||||
|
||||
Spend a few moments trying to appreciate how this is *much simpler* to write than dealing with HTML template files spread across the disk as is the case with traditional static site generators. If you [choose](https://vrom911.github.io/blog/html-libraries) to go the DSL route, Haskell's type-safety now applies to your HTML as well. On top of it, Ema's [hot reload](concepts/hot-reload.md) will instantly update the dev server's browser view whenever you change your HTML (or any of the Haskell source code).
|
||||
|
||||
Of course when using Ema nothing prevents you from choosing to use traditional HTML templates, and you can get [hot reload](concepts/hot-reload.md) on them too with [a little bit of plumbing](guide/helpers/filesystem.md).
|
||||
|
||||
{.last}
|
||||
[Next]{.next}, you might want to peruse [the helper topics](guide/helpers.md) if you need some extra functionality provided.
|
40
docs/guide/routes.md
Normal file
40
docs/guide/routes.md
Normal file
@ -0,0 +1,40 @@
|
||||
---
|
||||
order: 2
|
||||
---
|
||||
# Working with routes
|
||||
|
||||
Ema gives you the freedom to use any Haskell type for representing your site routes. You don't need complicated rewrite rules. Routes are best represented using what are known as *sum types* (or ADT, short for *Abstract Data Type*). Here's an example of a route type:
|
||||
|
||||
```haskell
|
||||
data Route
|
||||
= Index
|
||||
| About
|
||||
```
|
||||
|
||||
This type represents two routes pointing to -- the index page (`/`) and the about page (`/about`). Designing the route type is only half the job; you will also need to tell Ema how to convert it to / from the browser URL. We will explain this in the next section.
|
||||
|
||||
## Advanced example
|
||||
|
||||
Here's one possible way to design the route type for a weblog site,
|
||||
|
||||
```haskell
|
||||
data Route
|
||||
= Home
|
||||
| Blog BlogRoute
|
||||
| Tag TagRoute
|
||||
|
||||
data BlogRoute
|
||||
= BlogIndex
|
||||
| BlogPost Ema.Slug
|
||||
|
||||
data TagRoute
|
||||
= TagListing
|
||||
| Tag Tag
|
||||
|
||||
newtype Tag = Tag Text
|
||||
```
|
||||
|
||||
Defining *hierarchical routes* like this is useful if you want to render *parts* of your HTML as being common to only a subset of your site, such as adding a blog header to all blog pages, but not to tag pages.
|
||||
|
||||
{.last}
|
||||
[Next]{.next}, with our model and routes in place, [we will make them work with Ema](guide/class.md) by defining their static site behaviour.
|
28
docs/index.md
Normal file
28
docs/index.md
Normal file
@ -0,0 +1,28 @@
|
||||
# Ema
|
||||
|
||||
:::{.avatar}
|
||||
![](/favicon.svg){.w-32 .h-32 .float-right}
|
||||
:::
|
||||
|
||||
{.text-xl .mb-8}
|
||||
[Ema](https://github.com/srid/ema) is a next-gen **Haskell** library toolkit for building [jamstack-style](https://jamstack.org/) static sites. Ema sites are *change-aware*; in addition to good ol' static site generation, it provides a live server supporting **fast hot-reload** in the browser on code *or* data change.
|
||||
|
||||
{.text-gray-600}
|
||||
The ultimate goal of ema is to facilitate creating with ease your own [neuron](https://neuron.zettel.page/), or just about any app that creates a browser view of arbitrarily changing data (on disk, database, or whatever). Ema is designed to facilitate creation of apps whose data is normally *edited* via traditional mechanisms (eg: text editor) but *rendered* as a delightful web page - so as to provide an economical read-only view of your data on desktop & mobile.
|
||||
|
||||
:::{.my-8}
|
||||
* [Getting Started](start.md)
|
||||
* [Guide](guide.md)
|
||||
* [Concepts](concepts.md)
|
||||
* [Ema News](https://notes.srid.ca/ema) ([RSS Feed](https://notes.srid.ca/ema.xml))
|
||||
:::
|
||||
|
||||
:::{.flex .justify-center .items-center .mb-8}
|
||||
```{=video}
|
||||
/static/ema-demo.mp4
|
||||
```
|
||||
:::
|
||||
|
||||
:::{.flex .justify-center .items-center}
|
||||
[![FAIR](https://img.shields.io/badge/FAIR-pledge-blue)](https://www.fairforall.org/about/)
|
||||
:::
|
28
docs/index.yaml
Normal file
28
docs/index.yaml
Normal file
@ -0,0 +1,28 @@
|
||||
template:
|
||||
theme: pink
|
||||
iconUrl: /favicon.svg
|
||||
# Disable collapsing of folders since we don't have large number of notes.
|
||||
sidebar:
|
||||
collapsed: false
|
||||
|
||||
pandoc:
|
||||
rewriteClass:
|
||||
# The "Next" navigation in series
|
||||
last: mt-8 border-t-2 border-pink-500 pb-1 pl-1 bg-gray-50 rounded
|
||||
next: py-2 text-xl italic font-bold
|
||||
# For description of items in guide listing
|
||||
item-intro: text-gray-500
|
||||
|
||||
# Put page-specific metadata here. Override them in Markdown frontmatter or
|
||||
# per-folder YAML as necessary.
|
||||
page:
|
||||
siteName: Ema
|
||||
siteTitle: Ema
|
||||
description: |
|
||||
Ema static site generator (Jamstack) in Haskell with live server and reload.
|
||||
headHtml: |
|
||||
<link rel="manifest" href="/static/manifest.json" />
|
||||
<meta name="theme-color" content="#DB2777" />
|
||||
<!-- Syntax highlighting -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/themes/prism-tomorrow.css" rel="stylesheet" />
|
||||
<script src="https://cdn.jsdelivr.net/combine/npm/prismjs@1.23.0/prism.min.js,npm/prismjs@1.23.0/plugins/autoloader/prism-autoloader.min.js"></script>
|
17
docs/start.md
Normal file
17
docs/start.md
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
order: 1
|
||||
---
|
||||
|
||||
# Getting Started
|
||||
|
||||
As first steps, perform the following before proceeding to the tutorial section below:
|
||||
|
||||
1. [Install Nix](https://nixos.org/download.html) (see [platform-specific notes here](https://neuron.zettel.page/install))
|
||||
1. [Enable Flakes](https://nixos.wiki/wiki/Flakes#Installing_flakes)
|
||||
1. Clone [the template repository](https://github.com/srid/ema-template) locally
|
||||
1. Run `bin/run` and access the site at <http://localhost:9001>
|
||||
|
||||
That should start the Ema dev server displaying a simple website. Go ahead and try modifying either the Markdown content in `./content` or the Haskell source in `./src/Main.hs`, and observe how the web view updates [instantly](concepts/hot-reload.md).
|
||||
|
||||
{.last}
|
||||
[Next]{.next}, [in the tutorial](start/tutorial.md) let's try using this template repo to create a basic website.
|
120
docs/start/tutorial.md
Normal file
120
docs/start/tutorial.md
Normal file
@ -0,0 +1,120 @@
|
||||
# Tutorial
|
||||
|
||||
Make sure that you have have followed [the previous section](start.md) in order to have the [template repo](https://github.com/srid/ema-template) checked out and running locally. Here, **our goal** is to replace the source code of the template repo and write a basic site from scratch.
|
||||
|
||||
1. Follow the template repo's [README](https://github.com/srid/ema-template#getting-started) and have it open in Visual Studio Code while running the dev server. Your website should be viewable at <http://localhost:9001/>
|
||||
1. Open `src/Main.hs`
|
||||
1. Delete everything in it, and replace it with the following
|
||||
|
||||
```haskell
|
||||
module Main where
|
||||
|
||||
import qualified Ema
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
let speaker :: Text = "Ema"
|
||||
Ema.runEmaPure $ \_ ->
|
||||
encodeUtf8 $ "<b>Hello</b>, from " <> speaker
|
||||
```
|
||||
|
||||
This is the *minimum* amount of code necessary to run an Ema site. Notice that as you replace and save this file, your browser (which is at <http://locahost:9001>) will [hot reload](concepts/hot-reload.md) to display "Hello, Ema". Congratulations, you just created your first website!
|
||||
|
||||
## Expanding on Hello World
|
||||
|
||||
Okay, but that's just *one* page. But we want to add a second page. And might as well add more content than "Hello, Ema". Let's do that next. The first step is define the [route](guide/routes.md) type that corresponds to our site's pages. Add the following:
|
||||
|
||||
```haskell
|
||||
data Route
|
||||
= Index -- Corresponds to /
|
||||
| About -- Corresponds to /about
|
||||
deriving (Bounded, Enum, Show)
|
||||
```
|
||||
|
||||
Next, let's define a [model](guide/model.md). A model will hold the state of our website used to render its HTML. Let's put the `speaker` variable in it, as that's all we are using:
|
||||
|
||||
```haskell
|
||||
data Model = Model { speaker :: Text }
|
||||
```
|
||||
|
||||
We should now tell Ema how to convert our `Route` to actual URL paths. Let's do that by making an instance of the `Ema` [class](guide/class.md):
|
||||
|
||||
```haskell
|
||||
import Ema (Ema (..))
|
||||
|
||||
instance Ema Model Route where
|
||||
encodeRoute = \case
|
||||
Index -> [] -- To /
|
||||
About -> ["about"] -- To /about
|
||||
decodeRoute = \case
|
||||
[] -> Just Index -- From /
|
||||
["about"] -> Just About -- From /about
|
||||
_ -> Nothing -- Everything else, are bad routes
|
||||
```
|
||||
|
||||
Now, we write the `main` entry point:
|
||||
|
||||
```haskell
|
||||
import Control.Concurrent (threadDelay)
|
||||
import qualified Data.LVar as LVar
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
Ema.runEma render $ \model -> do
|
||||
LVar.set model $ Model "Ema"
|
||||
liftIO $ threadDelay maxBound
|
||||
```
|
||||
|
||||
The `runEma` function is explained [here](guide/class.md), but in brief: it takes a render function (see below) as well as an IO action that allows us to create and update the model [lvar](concepts/lvar.md). Note that `threadDelay maxBound` here? That is because our IO action must not exit; in the dev server mode of real-world websites, you would continue to monitor the external world (such as Markdown files) and update the model, to facilitate [hot reload](concepts/hot-reload.md) of data used by your site.
|
||||
|
||||
On final piece of the puzzle is to write the aforementioned `render` function:
|
||||
|
||||
```haskell
|
||||
import qualified Ema.CLI
|
||||
import qualified Ema.Helper.Tailwind as Tailwind
|
||||
import Text.Blaze.Html5 ((!))
|
||||
import qualified Text.Blaze.Html5 as H
|
||||
import qualified Text.Blaze.Html5.Attributes as A
|
||||
import qualified Text.Blaze.Html.Renderer.Utf8 as RU
|
||||
|
||||
render :: Ema.CLI.Action -> Model -> Route -> LByteString
|
||||
render _emaAction model r = RU.renderHtml $
|
||||
H.html $ do
|
||||
H.head $ do
|
||||
H.title "Basic site"
|
||||
H.body $ do
|
||||
H.div ! A.class_ "container" $ do
|
||||
case r of
|
||||
Index -> do
|
||||
H.toHtml $
|
||||
"You are on the index page. The name is " <> speaker model
|
||||
routeElem About "Go to About"
|
||||
About -> do
|
||||
"You are on the about page. "
|
||||
routeElem Index "Go to Index"
|
||||
where
|
||||
routeElem targetRoute w =
|
||||
H.a
|
||||
! A.style "text-decoration: underline"
|
||||
! A.href (H.toValue $ Ema.routeUrl targetRoute) $ w
|
||||
```
|
||||
|
||||
If everything compiles, you should see the site update in the web browser. A couple of quick points about the `render` function:
|
||||
|
||||
1. It should return the raw HTML as a `ByteString`. Here, we use [blaze-html](https://hackage.haskell.org/package/blaze-html) as HTML DSL. You can also use your own HTML templates of course.
|
||||
1. It uses `Ema.routeUrl` function to create a URL out of our `Route` type. This function uses the [`Ema` typeclass](guide/class.md), so it uses the `encodeRoute` function defined further above.
|
||||
|
||||
On final note, you will note that nothing is actually *generated* so far. This is because Ema has been running in the dev server mode, which is quite useful during development. To actually generate the files, you can use the `gen` command when running the [CLI](concepts/cli.md):
|
||||
|
||||
```sh
|
||||
mkdir ./output
|
||||
nix run . -- -C ./content gen ./output
|
||||
```
|
||||
|
||||
## Exercises
|
||||
|
||||
1. Figure out how to use static assets (images, files) in your static sites (hint: the typeclass)
|
||||
2. What happens if you `throw` an exception or use `error` in the `render` function?
|
||||
|
||||
{.last}
|
||||
[Next]{.next}, checkout the [Guide](guide.md) series for information on specific topics.
|
BIN
docs/static/ema-demo.mp4
vendored
Normal file
BIN
docs/static/ema-demo.mp4
vendored
Normal file
Binary file not shown.
10
docs/static/manifest.json
vendored
Normal file
10
docs/static/manifest.json
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"short_name": "Ema",
|
||||
"name": "Ema static site generator",
|
||||
"description": "Ema static site generator (Jamstack) in Haskell",
|
||||
"start_url": "/",
|
||||
"background_color": "#DB2777",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"theme_color": "#DB2777"
|
||||
}
|
@ -47,12 +47,5 @@
|
||||
|
||||
# Used by `nix develop`
|
||||
devShell = emaProject true;
|
||||
|
||||
# Used by `nix run` (for docs)
|
||||
apps.${name} = flake-utils.lib.mkApp {
|
||||
drv = ema;
|
||||
exePath = "/bin/ema-docs";
|
||||
};
|
||||
defaultApp = apps.${name};
|
||||
});
|
||||
}
|
||||
|
2
hie.yaml
2
hie.yaml
@ -2,5 +2,3 @@ cradle:
|
||||
cabal:
|
||||
- path: "./src"
|
||||
component: "library:ema"
|
||||
- path: "./docs"
|
||||
component: "exe:ema-docs"
|
||||
|
@ -17,12 +17,16 @@ import Ema.Route.Slug (Slug (unSlug), decodeSlug, encodeSlug)
|
||||
import Ema.Route.UrlStrategy
|
||||
( UrlStrategy (..),
|
||||
slugFileWithStrategy,
|
||||
slugUrlWithStrategy,
|
||||
slugRelUrlWithStrategy,
|
||||
)
|
||||
|
||||
-- | Return the relative URL of the given route
|
||||
--
|
||||
-- As the returned URL is relative, you will have to either make it absolute (by
|
||||
-- prepending with `/`) or set the `<base>` URL in your HTML head element.
|
||||
routeUrl :: forall a r. Ema a r => r -> Text
|
||||
routeUrl r =
|
||||
slugUrlWithStrategy def (encodeRoute @a r)
|
||||
slugRelUrlWithStrategy def (encodeRoute @a r)
|
||||
|
||||
routeFile :: forall a r. Ema a r => r -> FilePath
|
||||
routeFile r =
|
||||
|
@ -19,17 +19,17 @@ data UrlStrategy
|
||||
instance Default UrlStrategy where
|
||||
def = UrlStrategy_HtmlOnlySansExt
|
||||
|
||||
slugUrlWithStrategy :: UrlStrategy -> [Slug] -> Text
|
||||
slugUrlWithStrategy strat slugs =
|
||||
slugRelUrlWithStrategy :: UrlStrategy -> [Slug] -> Text
|
||||
slugRelUrlWithStrategy strat slugs =
|
||||
case strat of
|
||||
UrlStrategy_FolderOnly ->
|
||||
"/" <> T.intercalate "/" (encodeSlug <$> slugs)
|
||||
T.intercalate "/" (encodeSlug <$> slugs)
|
||||
UrlStrategy_HtmlOnlySansExt ->
|
||||
case nonEmpty slugs of
|
||||
Nothing ->
|
||||
"/"
|
||||
""
|
||||
Just (removeLastIf (decodeSlug "index") -> slugsWithoutIndex) ->
|
||||
"/" <> T.intercalate "/" (encodeSlug <$> slugsWithoutIndex)
|
||||
T.intercalate "/" (encodeSlug <$> slugsWithoutIndex)
|
||||
where
|
||||
removeLastIf :: Eq a => a -> NonEmpty a -> [a]
|
||||
removeLastIf x xs =
|
||||
|
Loading…
Reference in New Issue
Block a user