An introduction to elm-markup by way of Elmstatic
2019-05-21

mdgriffith/elm-markup is an interesting package that allows you to take marked up text, and transform it into the output format of your choosing. In this post, I’d like to give you a short introduction to it, and also show you how it can be used in conjunction with my static site generator, Elmstatic.

What does it look like?

Here is some (slightly modified) markup from the package README:

| Title
    My fancy cat blog article


Welcome!  Have you heard about /cats/?  They're great.

| Image
    src = http://placekitten/200/500
    description = Here's a great picture of my cat, pookie.

How much do I like cats?  *A bunch*.

Let me begin my {Highlight| 87 part argument } in favor of ~dogs~ cats.

There are three types of markup:

  • Block markup, like the |Title block at the start
  • Inline markup, like {Highlight| 87 part argument } on the last line
  • Basic text markup, like italicised /cats/ , bold *A bunch*, and also crossed out ~dogs~.

There are two key ideas behind elm-markup:

  • It’s customisable, in that you’re able to define your own blocks and inline markup
  • Rendering the output is up to you (although the package includes an elm-ui-based renderer).

This makes it very flexible and suitable for many different purposes.

Where does Elmstatic come into this?

Elmstatic is my Elm-based static site generator. It allows you to define page layout in Elm, and write content for posts and pages in Markdown.

I’ve been asked whether Elmstatic could support elm-markup in addition to Markdown. Well, it turns out that it already does!

With Elmstatic, you have a lot of freedom for generating HTML and CSS from Elm. Effectively, all it does is compile your Elm code and then set it loose on an empty HTML document. It places a few constraints on the directory structure and the like, but you can use any way you like to produce Html msg values.

Since rendering of elm-markup documents is up to us, we can turn them into HTML – more specifically, into Html msg values, which means we can plug in rendering results into Elmstatic.

But where are the source strings going to come from? Well, the content of .md files following the frontmatter block is passed into the Elm code as a string, so we can simply write elm-markup instead of Markdown in .md files. It’s a bit weird, but fine for a proof of concept.

Side note: in a regular Elm application, it’s possible to use markup blocks to create dynamic or interactive regions. However, Elmstatic generates static content so that’s not something we can do here.

So how do we render elm-markup documents?

With elm-markup, we need to define a renderer, which is a value of type Mark.Document result. For HTML, this becomes Mark.Document (Html msg). Then, we use it like this:

main : Elmstatic.Layout
main =
    Elmstatic.layout Elmstatic.decodePage <|
        \content ->
            let
                htmlContent =
                    case Mark.parse document content.markdown of
                        Ok html ->
                            html

                        Err _ ->
                            Html.text "Error parsing elm-markup"
            in
            layout content.title [ htmlContent ]

Note the Mark.parse call in the snippet above. This main definition comes straight from an Elmstatic scaffold, except instead of passing [ Page.markdown content.markdown ] to layout on the last line, we instead use [ htmlContent ] which is the result of calling Mark.parse.

This renders the page layout and the content:

Rendered page

What about the document definition?

We passed document to Mark.parse, so let’s see how it’s defined:

document : Mark.Document (Html msg)
document =
    Mark.document
        (Html.article [])
    <|
        Mark.manyOf
            [ Mark.block "Title" (Html.h3 []) docText
            , Mark.record2 "Image"
                (\src description ->
                    Html.img [ Attr.src src, alt description ] []
                )
                (Mark.field "src" Mark.string)
                (Mark.field "description" Mark.string)

            -- top level Text
            , Mark.map (Html.p []) docText
            ]

This describes how the custom blocks (Title and Image) should be converted into Html msg values. It also refers to docText in a couple of places. docText defines how to deal with inline elements in the text:

docText =
    let
        toStyles style =
            case style of
                Mark.Bold ->
                    Attr.style "font-weight" "bold"

                Mark.Italic ->
                    Attr.style "font-style" "italic"

                Mark.Strike ->
                    Attr.style "text-decoration" "line-through"

        textToHtml (Mark.Text styles str) =
            Html.span (List.map toStyles styles) [ Html.text str ]
    in
    Mark.text
        { view = textToHtml

        -- custom inline blocks
        , inlines =
            [ Mark.inline "Highlight"
                (\texts ->
                    Html.span [ Attr.class "highlight" ]
                        (List.map textToHtml texts)
                )
                |> Mark.inlineText
            ]

        -- pre-parsing string replacements
        , replacements = [ Mark.replacement "..." "…" ]
        }

This deals with both the custom inline block (Highlight) and the built-in syntax elements for italics, bold and strikethrough, which we’re also free to style any way we like.

Why use elm-markup instead of Markdown?

The advantage of Markdown is that it’s easy to render with elm-explorations/markdown (and it’s built into the default Elmstatic scaffold). Markdown also allows you to insert bits of HTML when you need more complex page elements.

That’s very convenient when your pages are mostly text aside from headers, footers and other layout elements.

However, if you have complex page content where text is interspersed with HTML, and maybe where you are repeating blocks of HTML (such as forms) multiple times within the text, it becomes somewhat inconvenient to manage in Elmstatic. You have to create a custom page layout, break up the Markdown into pieces in Elm, and then combine those pieces with the other content.

In these situations, you might want to consider elm-markup.

Conveniently, there’s nothing stopping you from using both Markdown and elm-markup because Elmstatic allows you to define any number of page layouts. You can define one page layout that handles Markdown, and another one that renders elm-markup.

Further reading

Would you like to dive further into Elm?
📢 My book
Practical Elm
skips the basics and gets straight into the nuts-and-bolts of building non-trivial apps.
🛠 Things like building out the UI, communicating with servers, parsing JSON, structuring the application as it grows, testing, and so on.
Practical Elm