the guide
A document, whole and in pieces.
Install the package, read one document, look at what came out, then read one that is still arriving. Every output below is what the library printed for the code above it.
§01
Install
moon add moonbitstack/moonmd
The import alias is a convention, not a requirement. parse takes a StringView, so a String is handed over with [:].
import {
"moonbitstack/moonmd" @moonmd,
}
§02
A document
One call reads the text and returns a Document. It holds the blocks and the flavor it was read with; nothing is rendered until asked.
let document = @moonmd.parse("# Notes\n\nA *small* page.\n"[:])
println(document.html(safe=false))
<h1>Notes</h1>
<p>A <em>small</em> page.</p>
A document that is empty, or holds only blank lines, has no blocks at all. A last line without a line ending is still a line.
§03
The tree
Blocks hold blocks; a paragraph, a heading and a table cell hold inlines. There are nine block kinds and ten inline kinds, and walk visits each node once, containers before their contents.
let document = @moonmd.parse("# Notes\n\n- one\n- two\n"[:])
@moonmd.walk(document, fn(node) {
match node {
@moonmd.Block(@moonmd.Heading(heading)) =>
println("heading, level \{heading.level}")
@moonmd.Block(@moonmd.List(list)) =>
println("list of \{list.items.length()}")
@moonmd.Inline(@moonmd.Text(text)) => println("text: \{text}")
_ => ()
}
})
heading, level 1
text: Notes
list of 2
text: one
text: two
A list item carries a task marker of its own, so a checkbox is data rather than a string that has to be recognised again later.
§04
Where a block came from
A block knows the lines it was read from, counted from one and in the order of blocks(). That is what cutting a document by heading needs, and what points a reader back at the text behind a node.
let document = @moonmd.parse("# Notes\n\ntwo\nthree\n"[:])
for span in document.lines() {
println("lines \{span.first}-\{span.last}")
}
lines 1-1
lines 3-4
Blank lines between blocks belong to neither. A container reports the lines of everything inside it, a fenced block keeps its blank lines and its closing fence, and a table keeps its delimiter row. A document assembled with of_blocks was not read from text, so it reports no lines.
§05
HTML
html takes one flag. With the default safe=true, raw HTML is escaped and a destination that would run script keeps its text and loses its link.
let page = @moonmd.parse("<b>bold</b> [x](javascript:alert(1))\n"[:])
println(page.html())
println(page.html(safe=false))
<p><b>bold</b> x</p>
<p><b>bold</b> <a href="javascript:alert(1)">x</a></p>
The flag is a rendering choice, not a parsing one: the tree is the same either way, so a viewer can offer both without reading the document twice. xhtml is the other one, on by default: it closes the elements that carry nothing, and xhtml=false ends them at the angle bracket, the way a document written to HTML5 does.
§06
Markdown
The same tree writes itself back out. The writer escapes what would otherwise start a block, and grows a fence past any run of its own characters inside the code, so the result reads back to the same HTML.
let document = @moonmd.parse("# Notes\n\nA *small* page.\n"[:])
println(document.markdown())
# Notes
A *small* page.
The output is not the input: a table written with uneven pipes comes back aligned, an indented code block becomes a fence. What survives is the document, not its whitespace.
§07
Footnotes
gfm also reads footnotes: a [^label] in the text is a reference, and a line that opens [^label]: is its definition. The reference becomes a numbered marker; the definitions gather at the end as a notes list, each with a link back to where it was cited.
let document = @moonmd.parse("A claim[^note]\n\n[^note]: the support\n"[:])
println(document.html(safe=false))
<p>A claim<sup class="footnote-ref"><a href="#fn-note" id="fnref-note" data-footnote-ref>1</a></sup></p>
<section class="footnotes" data-footnotes>
<ol>
<li id="fn-note">
<p>the support <a href="#fnref-note" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
</li>
</ol>
</section>
The number follows the first reference, not the definition, so a document can define its notes in any order. markdown writes both the reference and the definition back out, and a reference with no definition stays the text it was written as.
§08
Streaming
feed takes the next chunk and returns the blocks that no later text can change. finish returns what is left. A chunk may end anywhere, including in the middle of a line.
let stream = @moonmd.Stream::new()
println(stream.feed("# Notes\n\nA *sm"[:]).length())
println(stream.feed("all* page.\n\n"[:]).length())
println(stream.finish().length())
1
1
0
The heading settles when the blank line after it arrives; the paragraph settles when its own blank line does. Nothing is handed out twice and nothing is taken back.
§09
Flavors
gfm is the default: CommonMark plus tables, strikethrough, bare links, task list markers, footnotes and the tag filter. commonmark turns all six off, and a Flavor literal turns them off one at a time.
let plain = @moonmd.parse("~~gone~~\n"[:], flavor=@moonmd.commonmark)
println(plain.html(safe=false))
<p>~~gone~~</p>
A flavor is a plain record, so a caller can take gfm and change one field without copying the rest by hand.
One field is not an extension. smart writes the text the way a typesetter would: quotes curl towards what they wrap, two hyphens become a dash and three dots an ellipsis. It is off in both presets, and a code span, an escaped character, an entity and a destination keep what they were written with.
let typed : @moonmd.Flavor = { ..@moonmd.gfm, smart: true }
let page = @moonmd.parse("\"quoted\" -- and...\n"[:], flavor=typed)
println(page.html(safe=false))
<p>“quoted” – and…</p>
A second field reads the line ending itself: breaks makes a newline inside a paragraph a hard break, as a message box does, instead of the soft break the specification gives it.