1
1
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:
Sridhar Ratnakumar 2021-05-19 13:48:19 -04:00 committed by GitHub
commit d6fd722b0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 741 additions and 19 deletions

22
.github/workflows/publish.yaml vendored Normal file
View 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
View File

@ -23,4 +23,4 @@ cabal.project.local~
.ghc.environment.*
result
result-*
docs-output/
output

View File

@ -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

View File

@ -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
View 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
View 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.

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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}

View 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).

View 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).

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

10
docs/static/manifest.json vendored Normal file
View 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"
}

View File

@ -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};
});
}

View File

@ -2,5 +2,3 @@ cradle:
cabal:
- path: "./src"
component: "library:ema"
- path: "./docs"
component: "exe:ema-docs"

View File

@ -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 =

View File

@ -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 =