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
pdoc : document?
rules :
(-> symbol? (listof (list/c symbol? string?)) (listof xexpr?) xexpr?) = default-html-tag
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
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?)
> (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 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)).
> (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.
|
superclass: punct-abstract-render% |
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
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?
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.
> (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?
Added in version 1.4 of package punct-lib.
procedure
(escape-typst-string str) → string?
str : string?
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 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.
> (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.
|
superclass: punct-abstract-render% |
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)
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.
> (require punct/render/plaintext) > (define email (parse-markup-elements (hasheq) '("# Issue No. 1\n\nHowdy!"))) > (display (doc->plaintext email 72))
Issue No. 1
===========
Howdy!
procedure
(make-plaintext-fallback width)
→ (symbol? (listof (listof symbol? string?)) list? . -> . string?) width : exact-nonnegative-integer?
> (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 |
|
superclass: object% |
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.AAdded 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
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 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.
> (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.