3 Document Structure🔗
Because it uses the commonmark parser as a starting point, Punct documents come with
a default structure that is fairly opinionated. You can augment this structure if you understand how
the pieces fit together.
The bindings provided by this module are also provided by punct/core.
Behind the scenes: the commonmark parser produces a body and footnote definitions in
the form of nested structs. Punct converts both of these into lists of tagged X-expressions, to
allow for greater flexibility in adding Custom elements.
Changed in version 1.0 of package punct-lib: body and footnotes now guaranteed to be valid
X-expressions and not simply lists.
3.1 Blocks and Flows🔗
A
block element in Punct is a tagged X-expression which counts as a structural part of a
document: it starts with one of
'heading,
'paragraph,
'itemization,
'item,
'blockquote,
'code-block,
'html-block,
'footnote-definition or
'thematic-break. At the highest level, a document is a
sequence of these block elements.
A flow is a list of block elements. The Markdown parser produces three block
elements that may contain flows: blockquote, item, and
footnote-definition.
(heading [[level lev-str]] content ...)
| | | lev-str | | : | | (or/c "1" "2" "3" "4" "5" "6") |
|
|
|
(itemization [[style style-str] [start maybe-start]] item ...)
| | | style-str | | : | | (or/c "loose" "tight") |
|
|
|
|
(code-block [[info info-str]] content ...)
| | |
|
|
(footnote-definition [[label lbl] [ref-count rcount]] content ...)
| | |
|
|
3.2 Inline elements🔗
An
inline element in Punct is a string, or any tagged X-expression that is not counted as
a
block element.
Inline elements that appear on a line by themselves (i.e., not marked up within block elements) are
automatically wrapped in paragraph elements.
Below is a list of the inline elements that can be produced by the Markdown parser.
|
|
(link [[dest href] [title title-str]] content ...)
| | |
|
|
(image [[src source] [title title-str] [desc description]])
| | |
|
|
(footnote-reference [[label lbl] [defn-num dnum] [ref-num rnum]])
| | |
|
|
3.3 Custom elements🔗
You can use Racket code to introduce new elements to the document’s structure.
A custom element is any list that begins with a symbol other than those produced by the
Markdown parser.
A custom element may optionally have a set of attributes, which is a list of key/value
pairs that appears as the second item in the list.
Here is an example of a function that produces a custom abbreviation element with a term
attribute:
#lang punct |
|
•(define (a term . elems) |
`(abbrevation [[term ,term]] ,@elems)) |
|
Writing documentation in Javascript? •a["Laugh out loud"]{LOL}. |
Produces:
'#s(document #hasheq((here-path . "7-unsaved-editor")) |
((paragraph |
"Writing documentation in Javascript? " |
(abbrevation ((term "Laugh out loud")) "LOL") |
".")) |
()) |
By default, Punct will treat custom elements as inline content.
If you want a custom element to count as a block (that is, to avoid having it auto-wrapped inside
a paragraph element), you must give it a 'block attribute with a value of either
"root" or "single":
"root" should be used for blocks that might contain other block elements.
Limitations: "root"-type blocks cannot be contained inside Markdown-created
flows (such as block quotations notated using >); if found inside such a flow, they
will “escape” out to the root level of the document.
"single" should be used for block elements that might need to be contained within
Markdown-created flows. Limitations: "single"-type blocks must appear on
their own line or lines in order to be counted as blocks.
If that seems complicated, think of it this way: there are three kinds of flows that you can
notate with Markdown: block quotes, list items, and footnote definitions. If your custom block
element might appear as a child of any of those three Markdown notations, you should probably start
by giving it the '(block "single") attribute.
You’ll never get an error for using the wrong 'block type on your custom elements; you’ll
just get unexpected results in the structure of your document.
Under the hood: The two types of blocks above correspond to two methods Punct uses to trick
the CommonMark parser into treating custom elements as blocks. With "root"-type blocks,
Punct inserts extra line breaks (which is what causes these blocks to “escape” out of Markdown
blockquotes to the document’s root level, just as it would if you typed two linebreaks in your
source). With "single"-type blocks, Punct allows CommonMark to wrap the element in
a paragraph, then looks for any paragraph that contains only a "single"-type
block and “shucks” them out of their containing paragraphs. The need for such tricks comes from
a design decision to use the commonmark package exactly as published, without
forking or customizing it in any way.
3.4 Rendering custom elements🔗
When rendering your document to a specific output format (such as HTML) you’ll want to provide
a fallback procedure to the renderer that can convert those elements into that specific format.
Your fallback function will be given three arguments: the tag, a list of attributes, and a list of
sub-elements found inside your element. The elements will already have been fully processed by
Punct.
Here’s an example pair of functions for rendering documents containing the custom abbreviation
element (from the examples above) into HTML:
(define (custom-html tag attrs elems) |
(match `(,tag ,attrs) |
[`(abbreviation [[term ,term]]) `(abbr [[title ,term]] ,@elems)])) |
|
(define (my-html-renderer source-path) |
(doc->html (get-doc source-path) custom-html)) |