mdgriffith/elm-markup/3.0.1

Mark

Building Documents

type alias Document data =
Document data
document : (child -> result) -> Block child -> Document result

Create a markup Document. You're first goal is to describe a document in terms of the blocks you're expecting.

Here's an overly simple document that captures one block, a Title, and wraps it in some Html

document : Mark.Document (Html msg)
document =
    Mark.document
        (\title -> Html.main [] [ title ])
        (Mark.block "Title"
            (Html.h1 [])
            Mark.string
        )

will parse the following markup:

|> Title
    Here is my title!

and ultimately render it as

<main>
    <h1>Here is my title!</h1>
</main>
(metadata -> body -> document)
-> { metadata : Block metadata
, body : Block body
}
-> Document document

Capture some metadata at the start of your document, followed by the body.

import Mark.Record as Record


Mark.documentWith
    (\metadata body ->
        { metadata = metadata
        , body = body
        }
    )
    { metadata =
        Record.record
            (\author publishedAt ->
                { author = author
                , publishedAt = publishedAt
                }
            )
            |> Record.field "author" Mark.string
            |> Record.field "publishedAt" Mark.string
            |> Record.toBlock
    , body =
        --...
    }
type alias Block data =
Block data
block : String -> (child -> result) -> Block child -> Block result

A named block.

Mark.block "MyBlock"
    Html.text
    Mark.string

Will parse the following and render it using Html.text

|> MyBlock
    Here is an unformatted string!

Note block names should be capitalized. In the future this may be enforced.

Primitives

string : Block String

This will capture a multiline string.

For example:

Mark.block "Poem"
    (\str -> str)
    Mark.string

will capture

|> Poem
    Whose woods these are I think I know.
    His house is in the village though;
    He will not see me stopping here
    To watch his woods fill up with snow.

Where str in the above function will be

"""Whose woods these are I think I know.
His house is in the village though;
He will not see me stopping here
To watch his woods fill up with snow."""

Note If you're looking for styled text, you probably want Mark.text or Mark.textWith.

int : Block Int
float : Block Float
bool : Block Bool

Capture either True or False.

Text

type alias Styles =
{ bold : Bool
, italic : Bool
, strike : Bool
}
text : (Styles -> String -> text) -> Block (List text)

One of the first things that's interesting about a markup language is how to handle styled text.

In elm-markup there are only a limited number of special characters for formatting text.

  • /italic/ results in italics
  • *bold* results in bold
  • and ~strike~ results in strike

Here's an example of how to convert markup text into Html using Mark.text:

Mark.text
    (\styles string ->
        Html.span
            [ Html.Attributes.classList
                [ ( "bold", styles.bold )
                , ( "italic", styles.italic )
                , ( "strike", styles.strike )
                ]
            ]
            [ Html.text string ]
    )

Though you might be thinking that bold, italic, and strike are not nearly enough!

And you're right, this is just to get you started. Your next stop is Mark.textWith, which is more involved to use but can represent everything you're used to having in a markup language.

Note: Text blocks stop when two consecutive newline characters are encountered.

{ view : Styles -> String -> rendered
, replacements : List Replacement
, inlines : List (Record rendered)
}
-> Block (List rendered)

Handling formatted text is a little more involved than may be initially apparent, but have no fear!

textWith is where a lot of things come together. Let's check out what these fields actually mean.

  • view is the function to render an individual fragment of text.
    • This is mostly what Mark.text does, so it should seem familiar.
  • replacements will replace characters before rendering.
    • For example, we can replace ... with the real ellipses unicode character, ….
  • inlines are custom inline blocks. You can use these to render things like links or emojis :D.

Text Replacements

type alias Replacement =
Replacement
commonReplacements : List Replacement

This is a set of common character replacements with some typographical niceties.

  • ... is converted to the ellipses unicode character(…).
  • " Straight double quotes are replaced with curly quotes (“, ”)
  • ' Single Quotes are replaced with apostrophes(’).
  • -- is replaced with an en-dash(–).
  • --- is replaced with an em-dash(—).
  • <> also known as "glue", will create a non-breaking space (&nbsp;). This is not for manually increasing space (sequential <> tokens will only render as one &nbsp;), but to signify that the space between two words shouldn't break when wrapping. Like glueing two words together!
  • // will change to /. Normally / starts italic formatting. To escape this, we'd normally do \/, though that looks pretty funky. // just feels better!

Note this is included by default in Mark.text

replacement : String -> String -> Replacement

Replace a string with another string. This can be useful to have shortcuts to unicode characters.

For example, we could use this to replace ... with the unicode ellipses character: ….

{ start : ( String, String )
, end : ( String, String )
}
-> Replacement

A balanced replacement. This is used for replacing parentheses or to do auto-curly quotes.

Mark.balanced
    { start = ( "\"", "“" )
    , end = ( "\"", "”" )
    }

Text Annotations

Along with basic styling and replacements, we also have a few ways to annotate text.

annotation : String -> (List ( Styles, String ) -> result) -> Record result

An annotation is some text, a name, and zero or more attributes.

So, we can make a link that looks like this in markup:

Here is my [*cool* sentence]{link| url = website.com }.

and rendered in elm-land via:

link =
    Mark.annotation "link"
        (\styles url ->
            Html.a
                [ Html.Attributes.href url ]
                (List.map renderStyles styles)
        )
        |> Record.field "url" Mark.string
verbatim : String -> (String -> result) -> Record result

A verbatim annotation is denoted by backticks(`) and allows you to capture a literal string.

Just like token and annotation, a verbatim can have a name and attributes attached to it.

Let's say we wanted to embed an inline piece of elm code. We could write

inlineElm =
    Mark.verbatim "elm"
        (\str ->
            Html.span
                [ Html.Attributes.class "elm-code" ]
                [ Html.text str ]
        )

Which would capture the following

Here's an inline function: `\you -> Awesome`{elm}.

Note A verbatim can be written without a name or attributes and will capture the contents as a literal string, ignoring any special characters.

Let's take a look at `http://elm-lang.com`.

Records

type alias Record a =
Record a
record : String -> data -> Record data

Parse a record with any number of fields.

Mark.record "Image"
    (\src description ->
        Html.img
            [ Html.Attributes.src src
            , Html.Attributes.alt description
            ]
            []
    )
    |> Mark.field "src" Mark.string
    |> Mark.field "description" Mark.string
    |> Mark.toBlock

would parse the following markup:

|> Image
    src = http://placekitten/200/500
    description = What a cutie.

Fields can be in any order in the markup. Also, by convention field names should be camelCase. This might be enforced in the future.

field : String -> Block value -> Record (value -> result) -> Record result
toBlock : Record a -> Block a

Convert a Record to a Block.

Higher Level

oneOf : List (Block a) -> Block a
manyOf : List (Block a) -> Block (List a)

Many blocks that are all at the same indentation level.

Trees

tree : String -> (Enumerated item -> result) -> Block item -> Block result

Would you believe that a markdown list is actually a tree?

Here's an example of a nested list in elm-markup:

|> List
    1.  This is definitely the first thing.

        With some additional content.

    --- Another thing.

        And some more content

        1.  A sublist

            With it's content

            And some other content

        --- Second item

Note As before, the indentation is always a multiple of 4.

In elm-markup you can make a nested section either Bulleted or Numbered by having the first element of the section start with - or 1..

The rest of the icons at that level are ignored. So this:

|> List
    1. First
    -- Second
    -- Third

Is a numbered list. And this:

|> List
    -- First
        1. sublist one
        -- sublist two
        -- sublist three
    -- Second
    -- Third

is a bulleted list with a numbered list inside of it.

Note You can use as many dashes(-) as you want to start an item. This can be useful to make the indentation match up. Similarly, you can also use spaces after the dash or number.

Here's how to render the above list:

import Mark


myTree =
    Mark.tree "List" renderList text


-- Note: we have to define this as a separate function because
-- `Items` and `Node` are a pair of mutually recursive data structures.
-- It's easiest to render them using two separate functions:
-- renderList and renderItem
renderList (Mark.Enumerated list) =
    let
        group =
            case list.icon of
                Mark.Bullet ->
                    Html.ul


                Mark.Number ->
                    Html.ol
    in
    group []
        (List.map renderItem list.items)


renderItem (Mark.Item item) =
    Html.li []
        [ Html.div [] item.content
        , renderList item.children
        ]
type Enumerated item
= Enumerated
{ icon : Icon
, items : List (Item item)
}
type Item item
= Item
{ index : ( Int, List Int )
, content : List item
, children : Enumerated item
}

Note index is our position within the nested list.

The first Int in the tuple is our current position in the current sub list.

The List Int that follows are the indices for the parent list.

For example, given this list

|> List
    1. First element
    -- Second Element
        1. Element #2.1
            -- Element #2.1.1
        -- Element #2.2
    -- Third Element

here are the indices:

1. (1, [])
-- (2, [])
    1. (1, [2])
        -- (1, [1,2])
    -- (2, [2])
-- (3, [])
type Icon
= Bullet
| Number

Rendering

type Outcome failure almost success
= Success success
| Almost almost
| Failure failure
type alias Partial data =
{ errors : List Error
, result : data
}
compile : Document data -> String -> Outcome (List Error) (Partial data) data
Document data
-> String
-> Outcome (List Error) (Partial Parsed) Parsed
type alias Parsed =
Parsed
toString : Parsed -> String
render : Document data -> Parsed -> Outcome (List Error) (Partial data) data

Constraining and Recovering Blocks

map : (a -> b) -> Block a -> Block b

Change the result of a block by applying a function to it.

verify : (a -> Result Custom b) -> Block a -> Block b

Mark.verify lets you put constraints on a block.

Let's say you don't just want a Mark.string, you actually want a date.

So, you install the ISO8601 and you write something that looks like:

import Iso8601
import Mark
import Mark.Error
import Time


date : Mark.Block Time.Posix
date =
    Mark.verify
        (\str ->
            str
                |> Iso8601.toTime
                |> Result.mapError
                    (\_ -> illformatedDateMessage)
        )
        Mark.string


illformatedDateMessage =
    Mark.Error.custom
        { title = "Bad Date"
        , message =
            [ "I was trying to parse a date, but this format looks off.\n\n"
            , "Dates should be in ISO 8601 format:\n\n"
            , "YYYY-MM-DDTHH:mm:ss.SSSZ"
            ]
        }

Now you can use date whever you actually want dates and the error message will be shown if something goes wrong.

More importantly, you now know if a document parses successfully, that all your dates are correctly formatted.

Mark.verify is a very nice way to extend your markup however you'd like.

You could use it to

How exciting! Seriously, I think this is pretty cool.

onError : a -> Block a -> Block a

Parsing any given Block can fail.

However sometimes we don't want the whole document to be unable to render just because there was a small error somewhere.

So, we need some way to say "Hey, if you run into an issue, here's a placeholder value to use."

And that's what Mark.onError does.

Mark.int
    |> Mark.onError 5

This means if we fail to parse an integer (let's say we added a decimal), that this block would still be renderable with a default value of 5.

Note If there is an error that is fixed using onError, we'll get a Partial when we render the document. This will let us see the full rendered document, but also see the error that actually occurred.

withId : (Id -> a -> b) -> Block a -> Block b

Get an Id associated with a Block, which can be used to make updates through Mark.Edit.

Mark.withId
    (\id str ->
        Html.span
            [ onClick (Mark.Edit.delete id) ]
            [ Html.text str ]
    )
    Mark.string
idToString : Id -> String

It may be necessary to convert an Id to a String and back in order attach it as an Html.Attributes.id and read it back.

See the editor example for more details.

Note be aware that the actual string format of an Id is an implementation detail and may change even on patch releases of a library.

stringToId : String -> Maybe Id