On this page:
5.5.1 Rendering custom elements
5.5.2 HTML
doc->html
doc->html-xexpr
default-html-tag
html-rules
define-html-rules
punct-html-render%
5.5.3 Typst
doc->typst
default-typst-tag
escape-typst-text
escape-typst-string
typst-rules
define-typst-rules
punct-typst-render%
5.5.4 Plain text
doc->plaintext
make-plaintext-fallback
5.5.5 Base renderer
punct-abstract-render%
new
render-document
render-string
render-thematic-break
render-heading
render-code-block
render-html-block
render-paragraph
render-blockquote
render-itemization
render-item
render-line-break
render-bold
render-italic
render-code
render-link
render-image
render-html
render-footnote-reference
render-footnote-definition
5.5.6 Base rules
rules
define-rules

5.5 Rendering🔗ℹ

Punct currently includes renderers for HTML, Typst, and plain-text. These are based on a "base" renderer class that you can extend to customize the rendering process or to target new output formats.

5.5.1 Rendering custom elements🔗ℹ

When rendering your document to a specific output format (such as HTML) Punct has to decide how to render any custom elements introduced by your code. When Punct encounters a custom element, it passes it to a rules function, which receives three arguments: the custom element’s tag, its attributes, and a list of sub-elements found inside the element. The sub-elements will already have been fully processed by Punct.

A separate rules function is needed for each output format you want to target. Each of Punct’s renderers has a default: for example, when targeting HTML, Punct uses default-html-tag, which simply converts custom elements to HTML elements. If you want more customized behavior, provide your own rules function to the renderer.

The easiest way to make a rules function is with define-html-rules, define-typst-rules, or (for any other format) define-rules. Here’s a pair of functions for rendering documents containing the custom abbreviation element (from the examples above) into HTML:

(define-html-rules (custom-html tag attrs elems)
  [(abbreviation term) `(abbr [[title ,term]] ,@elems)])
 
(define (my-html-renderer source-path)
  (doc->html (get-doc source-path) custom-html))

Any element that no rule covers goes to default-html-tag. See rules for the full syntax. A rules function is an ordinary function, so you can also write one yourself, for example with match.

5.5.2 HTML🔗ℹ

 (require punct/render/html) package: punct-lib

procedure

(doc->html pdoc [rules]) → string?

  pdoc : document?
  rules : 
(-> symbol?
    (listof (list/c symbol? string?))
    (listof xexpr?)
    xexpr?)
   = default-html-tag
Renders pdoc into a string containing HTML markup. Each custom element is passed to rules, which must return an X-expression.

This function uses xexpr->string to generate the HTML string. This function will blindly escape characters inside <script> and <style> tags, which may introduce errors. For HTML output that is friendlier and more correct, consider using the html-printer package in concert with doc->html-xexpr.

For more information on using the rules argument to render custom elements, see Rendering custom elements.

procedure

(doc->html-xexpr pdoc [rules]) → xexpr?

  pdoc : document?
  rules : 
(-> symbol?
    (listof (list/c symbol? string?))
    (listof xexpr?)
    xexpr?)
   = default-html-tag
Renders pdoc into HTML, but in X-expression form rather than as a string. Each custom element is passed to rules, which must itself return an X-expression.

For more information on using the rules argument to render custom elements, see Rendering custom elements.

procedure

(default-html-tag tag attributes elements) → xexpr?

  tag : symbol?
  attributes : (listof (list/c symbol? string?))
  elements : (listof xexpr?)
Returns an X-expression comprised of tag, attributes and elements. This is the default rules function for doc->html, and the default for any element that no rule covers in html-rules.

Examples:
> (require punct/render/html)
> (default-html-tag 'kbd '() '("Enter"))

'(kbd "Enter")

> (default-html-tag 'a '((href "http://example.com")) '("Link"))

'(a ((href "http://example.com")) "Link")

syntax

(html-rules (tag-id attrs-id elems-id)
            clause ...
            maybe-else)

syntax

(define-html-rules (id tag-id attrs-id elems-id)
                   clause ...
                   maybe-else)
The html-rules form produces a rules function for doc->html and doc->html-xexpr. It is the same as (rules (tag-id attrs-id elems-id) #:default default-html-tag clause ... maybe-else); see rules for the syntax of each clause.

The define-html-rules form binds id to such a function. It is the same as (define id (html-rules (tag-id attrs-id elems-id) clause ... maybe-else)).

Examples:
> (require punct/render/html)
> (define-html-rules (my-html tag attrs elems)
    [margin-note #:as aside.margin]
    [(or webversion unsubscribe) ""]
    [(youtube video [title "Video"])
     `(iframe [[src ,(string-append "https://www.youtube.com/embed/" video)]
               [title ,title]])]
    [(abbreviation term) `(abbr [[title ,term]] ,@elems)])
> (my-html 'margin-note '((block "root")) '("A note"))

'(aside ((class "margin")) "A note")

> (my-html 'unsubscribe '() '("Unsubscribe"))

""

> (my-html 'youtube '((video "dQw4w9WgXcQ")) '())

'(iframe ((src "https://www.youtube.com/embed/dQw4w9WgXcQ") (title "Video")))

> (my-html 'abbreviation '((term "HyperText Markup Language")) '("HTML"))

'(abbr ((title "HyperText Markup Language")) "HTML")

> (my-html 'abbreviation '() '("HTML"))

'(abbreviation "HTML")

> (my-html 'kbd '() '("Enter"))

'(kbd "Enter")

Added in version 1.6 of package punct-lib.

The HTML renderer class. Extends punct-abstract-render% to produce HTML output. You can extend this class to customize HTML rendering behavior.
See Extending the Renderer for examples of extending the HTML renderer.

5.5.3 Typst🔗ℹ

 (require punct/render/typst) package: punct-lib

Typst is a modern typesetting system designed as an alternative to LaTeX. This renderer produces Typst markup from Punct documents.

procedure

(doc->typst pdoc [rules]) → string?

  pdoc : document?
  rules : (symbol? (listof (listof symbol? string?)) list? . -> . string?)
   = default-typst-tag
Renders pdoc into a string containing Typst markup. Any custom elements are passed to rules, which must return a string.

Markdown/Punct elements are mapped to Typst syntax as follows:

Punct element

Typst output

Headings

= Heading (with levels as = count)

Bold

*text*

Italic

_text_

Inline code

`code`

Code blocks

```lang ... ```

Links

#link("url")[text]

Images

#image("path")

Bullet lists

- item

Numbered lists

+ item

Block quotes

#quote(block: true)[...]

Footnotes

#footnote[...] (inline)

Typst special characters (*, _, `, #, @, $, [, ]) in text content are automatically escaped, allowing the same Punct source to target both HTML and Typst without manual escaping. Content inside code blocks and inline code is not escaped, as Typst renders these literally.

For more information on using the rules argument to render custom elements, see Rendering custom elements.

Added in version 1.4 of package punct-lib.

procedure

(default-typst-tag tag attributes elements) → string?

  tag : symbol?
  attributes : (listof (listof symbol? string?))
  elements : list?
Returns a Typst function call string using tag, attributes, and elements. This is the default rules function for doc->typst, and the default for any element that no rule covers in typst-rules.

The output format is #tag(attr: "val", ...)[content], where attributes become named arguments with string values, and elements become a trailing content block. When there are no attributes, the parentheses are omitted: #tag[content]. When there are no elements, the content block is omitted: #tag(attr: "val").

Hyphens in tag and attribute names are converted to underscores to produce valid Typst identifiers (e.g., 'my-element becomes my_element).

Note that elements are already escaped when passed to a rules function. Attribute values are automatically escaped and quoted as Typst strings.

Examples:
> (require punct/render/typst)
> (default-typst-tag 'note '() '("Important!"))

"#note[Important!]"

> (default-typst-tag 'note '((class "info")) '("Important!"))

"#note(class: \"info\")[Important!]"

> (default-typst-tag 'my-spacer '((line-height "2em")) '())

"#my_spacer(line_height: \"2em\")"

Added in version 1.4 of package punct-lib.

procedure

(escape-typst-text str) → string?

  str : string?
Escapes special Typst characters (*, _, `, #, @, $, [, ], \) in str by prefixing them with backslashes. Use this for text content in your own rules functions.

Added in version 1.4 of package punct-lib.

procedure

(escape-typst-string str) → string?

  str : string?
Escapes backslashes and double quotes in str for use within quoted Typst strings (e.g., URLs or file paths in function arguments like #link("...") or #image("...")).

Added in version 1.4 of package punct-lib.

syntax

(typst-rules (tag-id attrs-id elems-id)
             clause ...
             maybe-else)

syntax

(define-typst-rules (id tag-id attrs-id elems-id)
                    clause ...
                    maybe-else)
The typst-rules form produces a rules function for doc->typst. It is the same as (rules (tag-id attrs-id elems-id) #:default default-typst-tag clause ... maybe-else); see rules for the syntax of each clause.

The define-typst-rules form binds id to such a function. It is the same as (define id (typst-rules (tag-id attrs-id elems-id) clause ... maybe-else)).

The elems-id is bound to strings that are already rendered and escaped. Attribute values are not escaped: use escape-typst-string or escape-typst-text on them. An #:as clause gives its class names to default-typst-tag as a class argument, so the Typst function must accept one.

Examples:
> (require punct/render/typst)
> (define-typst-rules (my-typst tag attrs elems)
    [margin-note #:as aside]
    [warning #:as callout.alert]
    [(abbreviation term)
     (format "#abbr(\"~a\")[~a]" (escape-typst-string term) (apply string-append elems))])
> (my-typst 'margin-note '((block "root")) '("A note"))

"#aside[A note]"

> (my-typst 'warning '() '("Hot!"))

"#callout(class: \"alert\")[Hot!]"

> (my-typst 'abbreviation '((term "HyperText Markup Language")) '("HTML"))

"#abbr(\"HyperText Markup Language\")[HTML]"

> (my-typst 'kbd '() '("Enter"))

"#kbd[Enter]"

Added in version 1.6 of package punct-lib.

The Typst renderer class. Extends punct-abstract-render% to produce Typst markup output. You can extend this class to customize Typst rendering behavior.

Added in version 1.4 of package punct-lib.

5.5.4 Plain text🔗ℹ

 (require punct/render/plaintext) package: punct-lib

Sometimes you want to convert a document into a text format that is even plainer than Markdown, such as when generating the plaintext version of an email newsletter.

procedure

(doc->plaintext pdoc line-width [rules]) → string?

  pdoc : document?
  line-width : exact-nonnegative-integer?
  rules : 
(-> symbol?
    (listof (list/c symbol? string?))
    (listof xexpr?)
    xexpr?)
   = (make-plaintext-fallback line-width)
Renders pdoc into a string of plain text, hard-wrapped to line-width characters (except for block-quotes, which are hard-wrapped to a length approximately 75% as long as line-width). Any custom elements are passed to rules, which must return a string.

The function applies very rudimentary text formatting which usually looks as you would expect, but which often discards information.

  • Level 1 headings are underlined with =, and all other headings are underlined with -.

  • Link destination URLs are given inside parentheses directly following the link text.

  • Code blocks are indented with four spaces, and the language name, if any, is discarded.

  • Images are replaced with their "alt" text, prefixed by "Image: " and wrapped in parentheses; the source URL and title are discarded.

For more information on using the rules argument to render custom elements, see Rendering custom elements.

Examples:
> (require punct/render/plaintext)
> (define email (parse-markup-elements (hasheq) '("# Issue No. 1\n\nHowdy!")))
> (display (doc->plaintext email 72))

Issue No. 1

===========

Howdy!

Returns a function that accepts three arguments (the tag, attributes and elements of an X-expression). Mainly used to create the default rules function for doc->plaintext. To make your own rules for plain text, use define-rules with this function as the #:default.

Examples:
> (define foo (make-plaintext-fallback 72))
> (foo 'kbd '() '("Enter"))

"[kbd] Enter\n\n"

> (foo 'a '((href "http://example.com")) '("Link"))

"[a] Link\n\n"

5.5.5 Base renderer🔗ℹ

 (require punct/render/base) package: punct-lib

class

punct-abstract-render% : class?

  superclass: object%

The abstract base class for all Punct renderers. To create a custom renderer, extend this class and implement all abstract methods. The built-in HTML, Typst, and plaintext renderers all extend this class.

constructor

(new punct-abstract-render% 
    [doc doc] 
    [render-fallback render-fallback]) 
 → (is-a?/c punct-abstract-render%)
  doc : document?
  render-fallback : 
(-> symbol?
    (listof (list/c symbol? string?))
    list?
    any/c)
Creates a new renderer instance. The doc argument is the Punct document to render. The render-fallback procedure is the rules function: it is called for any custom elements encountered during rendering.

method

(send a-punct-abstract-render render-document) → any/c

The main entry point for rendering. Calls the appropriate rendering methods for each element in the document body and footnotes.

The default implementation returns two values: a list of rendered body elements and a list of rendered footnotes. Subclasses typically override this to combine these into a single output value (e.g., a string or X-expression).

method

(send a-punct-abstract-render render-string s) → any/c

  s : string?
Called for every string in the document. Override this to transform raw text (e.g., for escaping special characters in the target format). The default implementation returns the string unchanged.A

Added in version 1.4 of package punct-lib.

To create a custom renderer, extend this class and implement all of the abstract methods below.

See Extending the Renderer for examples of extending the base renderer.

method

(send a-punct-abstract-render render-thematic-break) → any/c

method

(send a-punct-abstract-render render-heading level 
  rendered-elems) 
 → any/c
  level : string?
  rendered-elems : list?

method

(send a-punct-abstract-render render-code-block info-string 
  raw-elems) 
 → any/c
  info-string : string?
  raw-elems : list?

method

(send a-punct-abstract-render render-html-block raw-content)

 → any/c
  raw-content : any/c

method

(send a-punct-abstract-render render-paragraph rendered-elems)

 → any/c
  rendered-elems : list?

method

(send a-punct-abstract-render render-blockquote rendered-elems)

 → any/c
  rendered-elems : list?

method

(send a-punct-abstract-render render-itemization 
  style 
  start 
  rendered-items) 
 → any/c
  style : string?
  start : string?
  rendered-items : list?

method

(send a-punct-abstract-render render-item rendered-elems)

 → any/c
  rendered-elems : list?

method

(send a-punct-abstract-render render-line-break) → any/c

method

(send a-punct-abstract-render render-bold rendered-elems)

 → any/c
  rendered-elems : list?

method

(send a-punct-abstract-render render-italic rendered-elems)

 → any/c
  rendered-elems : list?

method

(send a-punct-abstract-render render-code raw-elems) → any/c

  raw-elems : list?

method

(send a-punct-abstract-render render-link dest 
  title 
  rendered-elems) 
 → any/c
  dest : string?
  title : string?
  rendered-elems : list?

method

(send a-punct-abstract-render render-image src    
  title    
  desc    
  raw-elems) → any/c
  src : string?
  title : string?
  desc : string?
  raw-elems : list?

method

(send a-punct-abstract-render render-html raw-content) → any/c

  raw-content : any/c

method

(send a-punct-abstract-render render-footnote-reference 
  label 
  defn-num 
  ref-num) 
 → any/c
  label : string?
  defn-num : string?
  ref-num : string?

method

(send a-punct-abstract-render render-footnote-definition 
  label 
  ref-count 
  rendered-elems) 
 → any/c
  label : string?
  ref-count : string?
  rendered-elems : list?

5.5.6 Base rules🔗ℹ

syntax

(rules (tag-id attrs-id elems-id) #:default default-expr
       clause ...
       maybe-else)

syntax

(define-rules (id tag-id attrs-id elems-id) #:default default-expr
              clause ...
              maybe-else)
 
clause = [head maybe-when #:as new-tag]
  | [head maybe-when body ...+]
     
head = tag
  | (tag attr-spec ...)
  | (or tag ...+)
     
attr-spec = attr-id
  | [attr-id attr-default-expr]
     
maybe-when = 
  | #:when guard-expr
     
maybe-else = 
  | [else body ...+]
The rules form produces a rules function for any renderer. The define-rules form binds id to such a function: it is the same as (define id (rules (tag-id attrs-id elems-id) #:default default-expr clause ... maybe-else)). Most of the time you will use html-rules or typst-rules (or their define- forms), which supply default-expr for you.

The function takes three arguments, bound to tag-id, attrs-id and elems-id inside every clause, and tries each clause in order. The first one whose head matches the element (and whose guard-expr, if any, is true) gives the result.

A head matches an element when:

  • it is the element’s tag;

  • it is (tag attr-spec ...), the element’s tag is tag, and the element has every attribute named by a plain attr-id. Each attr-id is bound to the value of that attribute. An [attr-id attr-default-expr] binds attr-id to attr-default-expr when the attribute is missing;

  • it is (or tag ...+) and the element’s tag is one of the tags.

An element that is missing a required attribute does not match, and the next clause is tried.

The default-expr is evaluated once, when the rules function is made, and must produce a rules function. A clause with #:as new-tag calls it with a new tag, the element’s children, and no other attributes. The new-tag is a tag name, optionally followed by class names that each start with a period: for example, #:as aside.note.wide gives the same result as (default 'aside '((class "note wide")) elems). A clause with body forms gives the result of the last body.

An element that matches no clause goes to the else clause, or to the default if there is no else clause.

Examples:
> (require punct/render/base punct/render/plaintext)
> (define-rules (my-text tag attrs elems)
    #:default (make-plaintext-fallback 72)
    [(or webversion unsubscribe) ""]
    [margin-note #:as note]
    [(abbreviation term) (format "~a (~a)" (apply string-append elems) term)])
> (my-text 'unsubscribe '() '("Unsubscribe"))

""

> (my-text 'margin-note '() '("A note"))

"[note] A note\n\n"

> (my-text 'abbreviation '((term "HyperText Markup Language")) '("HTML"))

"HTML (HyperText Markup Language)"

Added in version 1.6 of package punct-lib.