Markdown and JSON
A project's docs are often a folder of Markdown files, like seal's API reference. chimera reads Markdown into the same objects it builds pages from, so you can add search, themes and components to those docs and write them back out as HTML, Markdown or JSON. JSON gives other tools the same tree without an HTML parser of their own.
Reading Markdown
Document.from_markdown reads a Markdown file into a whole page. Its title comes from the front matter's title or else the first level 1 heading, and its description from description:
const page = Document.from_markdown(fs.readfile("install.md"))
fs.writefile("install.html", page:to_html(true))
For the nodes alone, a MarkdownReader parses Markdown into one node per block. It reads headings, paragraphs, emphasis, links, images, lists, quotes, code, tables and HTML:
Heading<h1>
└─ "Install"
Paragraph<p>
├─ "Run "
├─ Code.chimera-code data-language
├─ ", then read the "
├─ Link<a> href
│ └─ "guide"
└─ "."
UnorderedList<ul>
├─ ListItem<li>
│ ├─ "Clone "
│ └─ Strong
│ └─ "chimera"
└─ ListItem<li>
└─ "Point an alias at it"
const nodes = MarkdownReader():parse(CodeBlock.dedent([==[
# Install
Run `seal setup`, then read the [guide](guide.md).
- Clone **chimera**
- Point an alias at it
]==]))
Inline code is Luwu unless the reader's code_language says otherwise. Fenced code takes the language after the fence, and copyable or line-numbers after that turn on those settings. Links to .md files point at the .html pages built from them, so the guide link above goes to guide.html. Problems that don't stop reading, like HTML the parser can't place, end up in the reader's warnings. Pass a configured reader to from_markdown to read a page its way.
A folder of Markdown
add_markdown adds a page for every Markdown file in a folder. index.md becomes index.html and a subfolder's files become pages under the page of the same name, so guide.md gets the pages in guide/. Pages sort by their front matter's order, then by name. Page.from_markdown reads a single page:
Site("My docs")
:search()
:add_markdown("docs")
:build("site")
HTML inside Markdown
HTML in Markdown goes through the Parser, so components written as HTML come back as components. A tag alone on its line wraps the Markdown up to its closing tag. seal's docs put <details> around each function's documentation that way, and even leave a </summary unclosed:
<details>
<summary> See the docs </summary
Reads the file at `path` to string.
## Errors
- if `path` is not valid utf-8
</details>
chimera reads it into a Details with its Summary, the way a browser would:
Details
├─ Summary
│ └─ " See the docs "
├─ Paragraph<p>
│ ├─ "Reads the file at "
│ ├─ Code.chimera-code data-language
│ └─ " to string."
├─ Heading<h2>
│ └─ "Errors"
└─ UnorderedList<ul>
└─ ListItem<li>
├─ "if "
├─ Code.chimera-code data-language
└─ " is not valid utf-8"
Writing Markdown
Every node has to_markdown. Parts Markdown has no syntax for, like a component or an element with a style class, keep their HTML. A container of blocks writes its tags on lines of their own with Markdown between them, so the text stays readable and reads back as the same component:
<div data-element="Card" class="chimera-card">
### Fast
Renders **thousands** of pages a second.
- No server
- No build step
</div>
const card = Card("Fast", {
Paragraph({ "Renders ", Strong("thousands"), " of pages a second." }),
UnorderedList({ "No server", "No build step" }),
})
const markdown = card:to_markdown()
Your own elements write Markdown in a markdown method. It gets a MarkdownWriter for its children: out:inline(...) for text inside a line and out:blocks(...) for blocks. The callout further down writes itself as a quote.
JSON
Every node also has to_json. It writes the tree the node's HTML has, and Element.nodes_from_json reads it back into the same nodes, components included. json_tree gives the tree as tables for tools written in Luwu, and Document.from_json reads a whole page:
{
"children": [
{
"text": "Hi "
},
{
"children": [
{
"text": "there"
}
],
"tag": "strong"
}
],
"tag": "p"
}
const text = Paragraph({ "Hi ", Strong("there") }):to_json(true)
const again = Element.nodes_from_json(text)
Each element has its tag, its component in element and its attributes in the order it renders them. Reading the JSON back gives the same HTML: true.
Components with their own JSON
A tree of tags is exact, but it says little about what a component means. A component that implements Json writes the fields that describe it in json and reads them back in its own from_json. to_json asks it wherever it appears, and reading calls its from_json for every node that names its class:
{
"children": [
{
"text": "Status: "
},
{
"element": "Badge",
"label": "Beta"
}
],
"tag": "p"
}
class Badge implements Json
function tag(): string
return "span"
end
function __init(self, label: string?)
self:attr("data-label", label)
end
function label(self): string
return tostring(self:get_attr("data-label") or "")
end
function render(self, out: Writer)
self:render_open(out)
out:write(`{Text.escape(self:label())}</span>`)
end
-- The text comes from the label, so parsed text is dropped.
function adopt(self, node: Markup): boolean
return true
end
function json(self): JsonNode
return { label = self:label() }
end
function from_json(data: JsonNode): Json?
return if type(data.label) == "string" then Badge(data.label) else nil
end
end
const status = Paragraph({ "Status: ", Badge("Beta") }):to_json(true)
chimera adds the element name for you. Badge.from_json also works on its own, and returning nil tells the reader to treat the JSON as a plain tree. A Json component still needs HTML that parses back as itself, which is why Badge keeps its label in an attribute.
Recognizing your components
Markdown from someone else won't say which of your components it means. A quote that starts with **Note:** is a callout to you and a plain quote to the Markdown reader. A Shape is a component that recognizes itself in what the readers read. Its recognize gets each parsed element, children first, and returns the component that element stands for. Markdown, HTML and JSON all try every shape:
Callout<aside>.callout
└─ Paragraph<p>
└─ "Back up first."
Callout<aside>.callout
└─ Paragraph<p>
└─ "Careful."
class Callout implements Shape, Container
function tag(): string
return "aside"
end
function __init(self, content: Content?)
self
:add_style_class("callout")
:add(content)
end
-- A quote that starts with **Note:**, or another site's <div class="alert">.
function recognize(node: Element): Shape?
if
class.isinstance(node, BlockQuote)
when const first = node.children[1]
when class.isinstance(first, Paragraph)
when const lead = first.children[1]
when class.isinstance(lead, Strong)
when text_of(lead) == "Note:"
then
table.remove(first.children, 1)
if
const rest = first.children[1]
when class.isinstance(rest, Text)
then
rest.content = string.gsub(rest.content, "^%s+", "")
end
return Callout(node.children)
elseif class.isinstance(node, Div) and node:has_style_class("alert") then
return Callout(node.children)
end
return nil
end
function markdown(self, out: MarkdownWriter): string
const quoted = `**Note:** {out:blocks(self.children)}`
return "> " .. string.gsub(quoted, "\n", "\n> ")
end
end
const from_markdown = MarkdownReader():parse("> **Note:** Back up first.")
const from_html = Parser():parse(`<div class="alert"><p>Careful.</p></div>`)
Both came back as callouts. Its markdown method writes the quote again, so a callout survives a trip through Markdown: > **Note:** Back up first..
Fenced code blocks can become components too. A reader's fence takes a language and a function that builds the content for each block in it:
const reader = MarkdownReader():fence("mermaid", function(source)
return Diagram(source)
end)
const page = Document.from_markdown(fs.readfile("design.md"), reader)
For whole pages of HTML, see Parsing HTML.