On this page:
4.1 Rendering to HTML
4.1.1 A complete HTML rendering example
4.1.2 Handling custom elements in HTML
4.2 Rendering to Typst
4.2.1 A complete Typst rendering example
4.2.2 Handling custom elements in Typst
4.2.3 Using the default Typst rules
4.3 Extending the Renderer
4.3.1 Extending an existing renderer
4.3.2 Creating a custom renderer from scratch
9.3

4 Rendering Strategies🔗ℹ

A Punct document is format-independent; its AST can be converted to any output format you need. Punct provides built-in renderers for HTML, Typst, and plain text.

This section explains how to use these renderers effectively, how to handle custom elements in your output, and when you might want to extend the base renderer for your own needs.

4.1 Rendering to HTML🔗ℹ

HTML is the most common output format for web publishing. To render a Punct document to HTML, use doc->html or doc->html-xexpr:

#lang racket
 
(require punct/fetch
         punct/render/html)
 
;; Render a document to an HTML string
(define html-output (doc->html (get-doc "my-article.rkt")))
 
;; Or get the intermediate X-expression for further processing
(define html-xexpr (doc->html-xexpr (get-doc "my-article.rkt")))

The HTML renderer wraps the document body in an <article> element. Footnotes, if present, are collected into a <section class="footnotes"> at the end.

4.1.1 A complete HTML rendering example🔗ℹ

Here’s a simplistic example that renders a Punct document to a standalone HTML file:

#lang racket
 
(require punct/fetch
         punct/render/html
         xml)
 
(define (render-page source-path output-path title)
  (define doc (get-doc source-path))
  (define body-html (doc->html doc))
 
  (define full-html
    (string-append
      "<!DOCTYPE html>\n"
      "<html lang=\"en\">\n"
      "<head>\n"
      "  <meta charset=\"utf-8\">\n"
      "  <title>" title "</title>\n"
      "  <link rel=\"stylesheet\" href=\"style.css\">\n"
      "</head>\n"
      "<body>\n"
      body-html
      "\n</body>\n"
      "</html>"))
 
  (call-with-output-file output-path
    (lambda (out) (display full-html out))
    #:exists 'replace))
 
;; Usage:
;; (render-page "my-article.rkt" "my-article.html" "My Article")

4.1.2 Handling custom elements in HTML🔗ℹ

When your Punct document contains custom elements, you need to tell the HTML renderer how to convert them. Provide a rules function as the second argument to doc->html:

#lang racket
 
(require punct/fetch
         punct/render/html)
 
;; Define how to render custom elements. Any other element goes to default-html-tag.
(define-html-rules (my-html-rules tag attrs elems)
  ;; Convert 'abbreviation to HTML <abbr>, using its term attribute
  [(abbreviation term)
   `(abbr [[title ,term]] ,@elems)]
 
  ;; Convert 'note to <div class="note">
  [note #:as div.note]
 
  ;; Convert 'youtube to an iframe
  [(youtube id)
   `(iframe [[src ,(string-append "https://www.youtube.com/embed/" id)]
             [width "560"]
             [height "315"]
             [frameborder "0"]
             [allowfullscreen ""]])])
 
(define (render-with-custom-elements source-path)
  (doc->html (get-doc source-path) my-html-rules))

See rules for the full syntax. A rules function is an ordinary function, so you can also write one yourself, for example with match.

4.2 Rendering to Typst🔗ℹ

Typst is a modern typesetting system for producing PDF documents. Punct includes a Typst renderer which converts your document to Typst markup, which you can then compile with the typst command-line tool.

#lang racket
 
(require punct/fetch
         punct/render/typst)
 
(define typst-output (doc->typst (get-doc "my-article.rkt")))

4.2.1 A complete Typst rendering example🔗ℹ

Here’s a complete workflow for rendering Punct documents to PDF via Typst:

#lang racket
 
(require punct/fetch
         punct/render/typst
         racket/system)
 
(define (render-to-pdf source-path output-pdf)
  (define doc (get-doc source-path))
  (define typst-path (path-replace-extension output-pdf ".typ"))
 
  ;; Add Typst preamble for document setup
  (define typst-content
    (string-append
      "#set page(paper: \"us-letter\", margin: 1in)\n"
      "#set text(font: \"Linux Libertine\", size: 11pt)\n"
      "#set par(justify: true)\n\n"
      (doc->typst doc)))
 
  ;; Write the .typ file
  (call-with-output-file typst-path
    (lambda (out) (display typst-content out))
    #:exists 'replace)
 
  ;; Compile to PDF using Typst CLI
  (system* (find-executable-path "typst")
           "compile" typst-path output-pdf))
 
;; Usage:
;; (render-to-pdf "my-article.rkt" "my-article.pdf")

4.2.2 Handling custom elements in Typst🔗ℹ

Just as with HTML, you can provide a rules function for custom elements:

#lang racket
 
(require punct/fetch
         punct/render/typst)
 
;; Any other element goes to default-typst-tag.
(define-typst-rules (my-typst-rules tag attrs elems)
  ;; Render 'note as a Typst callout box
  [note
   (string-append "#block(fill: luma(230), inset: 1em, radius: 4pt)[\n"
                  (string-join elems)
                  "\n]")]
 
  ;; Render 'abbreviation with an underline
  [(abbreviation term)
   (string-append "#underline[" (string-join elems) "]")]
 
  ;; Render 'margin-note by calling the Typst function aside
  [margin-note #:as aside])
 
(define (render-typst-with-custom source-path)
  (doc->typst (get-doc source-path) my-typst-rules))

Note that in Typst rules, the elems have already been escaped and rendered to strings, so you typically join them with string-join or string-append. Attribute values are not escaped: use escape-typst-string or escape-typst-text on them.

4.2.3 Using the default Typst rules🔗ℹ

The default rules function (default-typst-tag) converts custom elements directly to Typst function calls. For example, a custom element like '(note "Important!") becomes #note[Important!] in the output. Attributes become named arguments: '(note [[class "info"]] "Text") becomes #note(class: "info")[Text].

This means you can define corresponding functions in your Typst template, and your custom elements will automatically call them:

#lang racket
 
(require punct/fetch
         punct/render/typst
         racket/system)
 
(define (render-to-pdf source-path output-pdf)
  (define doc (get-doc source-path))
  (define typst-path (path-replace-extension output-pdf ".typ"))
 
  ;; Typst preamble with custom function definitions
  (define typst-preamble #<<PREAMBLE
#set page(paper: "us-letter", margin: 1in)
#set text(font: "Linux Libertine", size: 11pt)
 
// Define a function that matches your custom Punct element.
// Named arguments come from element attributes; content from elements.
#let note(type: "info", body) = {
  let fills = (info: luma(230), warning: rgb("#fff3cd"))
  block(
    fill: fills.at(type),
    inset: 1em,
    radius: 4pt,
    body
  )
}
 
PREAMBLE
)
 
  (define typst-content
    (string-append typst-preamble "\n" (doc->typst doc)))
 
  (call-with-output-file typst-path
    (lambda (out) (display typst-content out))
    #:exists 'replace)
 
  (system* (find-executable-path "typst")
           "compile" typst-path output-pdf))

With this setup, a Punct source like:

#lang punct
 
•(define-element note)
 
•note{Remember to save}
 
•note[#:type "warning"]{This action cannot be undone.}

...will render as #note[Remember to save] and #note(type: "warning")[This action cannot be undone.], calling the note function defined in the Typst preamble with the appropriate arguments.

4.3 Extending the Renderer🔗ℹ

Sometimes providing a rules function isn’t enough. You might need to:

  • Change how standard elements (like headings or links) are rendered

  • Add wrapper content around the rendered body

  • Maintain state across the rendering process

  • Target a format not covered by the built-in renderers (e.g., LaTeX, Markdown, or a custom XML format)

In these cases, you can extend punct-abstract-render%, the base class that all Punct renderers inherit from.

4.3.1 Extending an existing renderer🔗ℹ

The simplest approach is to extend one of the built-in renderers. Here’s an example that customizes the HTML renderer to add automatic IDs to headings:

#lang racket
 
(require punct/render/html
         punct/doc
         racket/class
         racket/string)
 
(define punct-html-with-ids%
  (class punct-html-render%
    (super-new)
 
    ;; Override heading rendering to add an id attribute
    (define/override (render-heading level elems)
      (define tag (string->symbol (format "h~a" level)))
      (define text (string-join (map ~a elems)))
      (define id (string-downcase
                   (regexp-replace* #rx"[^a-zA-Z0-9]+" text "-")))
      `(,tag [[id ,id]] ,@elems))))
 
(define (doc->html-with-ids doc [rules default-html-tag])
  (send (new punct-html-with-ids% [doc doc] [render-fallback rules])
        render-document))

When extending an existing renderer, you need only override the methods whose behavior you want to change.

4.3.2 Creating a custom renderer from scratch🔗ℹ

For a new output format, extend punct-abstract-render% directly and implement all the abstract methods. Here’s a minimal example that renders to a simple Markdown-like format:

#lang racket
 
(require punct/render/base
         punct/doc
         racket/class
         racket/string)
 
(define punct-simple-markdown%
  (class punct-abstract-render%
    (super-new)
 
    ;; Helper to join rendered elements
    (define (join elems) (string-append* (map ~a elems)))
 
    ;; Block elements
    (define/override (render-heading level elems)
      (string-append (make-string (string->number level) #\#)
                     " " (join elems) "\n\n"))
 
    (define/override (render-paragraph elems)
      (string-append (join elems) "\n\n"))
 
    (define/override (render-thematic-break)
      "---\n\n")
 
    (define/override (render-code-block info elems)
      (string-append "```" info "\n" (join elems) "\n```\n\n"))
 
    (define/override (render-blockquote elems)
      (define lines (string-split (join elems) "\n"))
      (string-append
        (string-join (map (lambda (l) (string-append "> " l)) lines) "\n")
        "\n\n"))
 
    (define/override (render-itemization style start elems)
      (define marker (if (non-empty-string? start) "1. " "- "))
      (string-append
        (string-join
          (for/list ([e (in-list elems)])
            (string-append marker e))
          "\n")
        "\n\n"))
 
    (define/override (render-item elems)
      (string-trim (join elems)))
 
    (define/override (render-html-block elem) elem)
 
    ;; Inline elements
    (define/override (render-bold elems)
      (string-append "**" (join elems) "**"))
 
    (define/override (render-italic elems)
      (string-append "_" (join elems) "_"))
 
    (define/override (render-code elems)
      (string-append "`" (join elems) "`"))
 
    (define/override (render-link dest title elems)
      (string-append "[" (join elems) "](" dest ")"))
 
    (define/override (render-image src title desc elems)
      (string-append "![" desc "](" src ")"))
 
    (define/override (render-line-break) "\n")
 
    (define/override (render-html elem) elem)
 
    (define/override (render-footnote-reference label defnum refnum)
      (string-append "[^" label "]"))
 
    (define/override (render-footnote-definition label refcount elems)
      (string-append "[^" label "]: " (join elems) "\n"))
 
    ;; Override render-document to return a single string
    (define/override (render-document)
      (define-values [body footnotes] (super render-document))
      (string-append (string-append* body)
                     (if (null? footnotes) "" "\n")
                     (string-append* footnotes)))))
 
(define (doc->simple-markdown doc [rules (lambda (t a e) (string-append* e))])
  (send (new punct-simple-markdown% [doc doc] [render-fallback rules])
        render-document))

See the Rendering section in the Module Reference for complete documentation of punct-abstract-render% and the methods you need to implement.