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 =
--...
}
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.
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
.
{ 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.
/italic/
results in italics*bold*
results in bold~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.
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.
view
is the function to render an individual fragment of text.Mark.text
does, so it should seem familiar.replacements
will replace characters before rendering....
with the real ellipses unicode character, …
.inlines
are custom inline blocks. You can use these to render things like links or emojis :D.Internal.Parser.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 (
). This is not for manually increasing space (sequential <>
tokens will only render as one
), 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: …
.
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 = ( "\"", "”" )
}
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`.
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
.
oneOf : List (Block a) -> Block a
manyOf : List (Block a) -> Block (List a)
Many blocks that are all at the same indentation level.
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
]
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, [])
{ 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
Internal.Description.Parsed
toString : Parsed -> String
render : Document data -> Parsed -> Outcome (List Error) (Partial data) data
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