mdgriffith / elm-markup / Mark

Building Documents


type alias Document data =
Internal.Description.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>

documentWith : (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 =
Internal.Description.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 Basics.Int

float : Block Basics.Float

bool : Block Basics.Bool

Capture either True or False.

Text


type alias Styles =
{ bold : Basics.Bool
, italic : Basics.Bool
, strike : Basics.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.

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.

textWith : { 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.

Text Replacements


type alias Replacement =
Internal.Parser.Replacement

commonReplacements : List Replacement

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

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: .

balanced : { 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 =
Internal.Description.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 : ( Basics.Int, List Basics.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

parse : Document data -> String -> Outcome (List Error) (Partial Parsed) Parsed


type alias Parsed =
Internal.Description.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 Error.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 : (Edit.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 : Edit.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 Edit.Id