On this page:
document
3.1 Blocks and Flows
block-element?
3.2 Inline elements
inline-element?
3.3 Custom elements
3.3.1 Custom element conveniences

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.

 (require punct/doc) package: punct-lib

The bindings provided by this module are also provided by punct/core.

struct

(struct document (metas body footnotes)
    #:extra-constructor-name make-document
    #:prefab)
  metas : hash-eq?
  body : (listof block-element?)
  footnotes : (listof block-element?)
A Punct source file evaluates to a document struct that includes a metas hash table containing any metadata defined using the Metadata block, ? or set-meta; and body and footnotes, both of which are lists of block elements.

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🔗

procedure

(block-element? v)  boolean?

  v : any/c
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.

> (block-element? '(paragraph "Block party!"))

#t

> (block-element? "simple string")

#f

A flow is a list of block elements. The Markdown parser produces three block elements that may contain flows: blockquote, item, and footnote-definition.

txexpr

(heading [[level lev-str]] content ...)

 
  lev-str : (or/c "1" "2" "3" "4" "5" "6")
  content : xexpr?

txexpr

(paragraph content ...)

 
  content : xexpr?

txexpr

(itemization [[style style-str] [start maybe-start]] item ...)

 
  style-str : (or/c "loose" "tight")
  maybe-start : (or/c "" string?)
  item : xexpr?

txexpr

(item block ...)

 
  block : xexpr?

txexpr

(blockquote block ...)

 
  block : block-element?

txexpr

(code-block [[info info-str]] content ...)

 
  info-str : string?
  content : xexpr?

txexpr

(html-block content ...)

 
  content : xexpr?

txexpr

(footnote-definition [[label lbl] [ref-count rcount]] content ...)

 
  lbl : string?
  rcount : string?
  content : block-element?

txexpr

(thematic-break)

3.2 Inline elements🔗

procedure

(inline-element? v)  boolean?

  v : any/c
An inline element in Punct is a string, or any tagged X-expression that is not counted as a block element.

> (inline-element? "simple string")

#t

> (inline-element? '(italic "emphasis"))

#t

> (inline-element? '(made-up-element "x"))

#t

> (inline-element? '(paragraph "Block party!"))

#f

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.

txexpr

(italic content ...)

 
  content : inline-element?

txexpr

(bold content ...)

 
  content : inline-element?

txexpr

(link [[dest href] [title title-str]] content ...)

 
  href : string?
  title-str : string?
  content : inline-element?

txexpr

(code content ...)

 
  content : inline-element?

txexpr

(image [[src source] [title title-str] [desc description]])

 
  source : string?
  title-str : string?
  description : string?

txexpr

(html content ...)

 
  content : inline-element?

txexpr

(footnote-reference [[label lbl] [defn-num dnum] [ref-num rnum]])

 
  lbl : string?
  dnum : string?
  rnum : string?

txexpr

(line-break)

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, and which was produced by inline Racket code rather than by parsed Markdown syntax.

If you think “custom elements” sound like Pollen “tags”, you are correct. I use “custom elements” rather than “tags” or “X-expressions” to distinguish them from from Markdown-generated elements; also, unlike Pollen tags, custom elements may be treated differently depending on their block attributes.

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 (abbr term . elems)
   `(abbreviation [[term ,term]] ,@elems))
 
Writing documentation in Javascript? abbr["Laugh out loud"]{LOL}.

Produces:

'#s(document #hasheq((here-path . "7-unsaved-editor"))
             ((paragraph
               "Writing documentation in Javascript? "
               (abbreviation ((term "Laugh out loud")) "LOL")
               "."))
             ())

By default, Punct will treat custom elements as inline elements: they will be wrapped inside paragraph elements if they occur on their own lines.

You can set a custom element’s block attribute to force Punct to treat it as a block element (that is, to avoid having it auto-wrapped inside a paragraph element): simply give it a 'block attribute with a value of either "root" or "single":

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.3.1 Custom element conveniences🔗

If you make much use of custom elements, you will probably find yourself writing several functions that do nothing but lightly rearrange the arguments into an X-expression:

(define (abbr term . elems)
   `(abbrevation [[term ,term]] ,@elems))

To cut down on the repetition, you can use default-element-function to create a function that automatically parses keyword arguments into attributes:

#lang punct
(define abbr (default-element-function 'abbreviation))
 
abbr[#:term "Laugh Out Loud"]{LOL}  •; -→ '(abbreviation ((term "Laugh Out Loud")) "LOL")

You can simplify this even further with define-element:

#lang punct
(define-element abbr)
 
abbr[#:term "Laugh Out Loud"]{LOL}  •; -→ '(abbr ((term "Laugh Out Loud")) "LOL")

Both default-element-function and define-element allow shorthand for setting default block attributes and defaults for the 'class attribute. See their entries in module reference for more details.

#lang punct
(define-element note box§.warning #:title "WATCH IT")
 
note{Wet floor!}
•; -→ '(box ((block "root") (class "warning") (title "WATCH IT") "Wet floor!")