Options

Every renderer, plus markdown->ast, takes a cmark-options record as its options argument. It's an immutable value: make-cmark-options builds one, cmark-options-with builds an updated copy, and nothing ever mutates one in place. See usage.md for the entry points that consume it.

The defaults

KeyDefault
extensions(autolink strikethrough table tagfilter tasklist)
validate-utf8?#t
source-positions?#f
hardbreaks?#f
nobreaks?#f
smart?#f
unsafe-html?#f
max-input-bytes5242880 (5 MiB)
max-nodes250000
max-depth1000

(default-cmark-options) builds exactly this record. It's defined as (make-cmark-options) called with no overrides at all, not as a second copy of these ten values, so the two constructors cannot drift apart.

markdown->ast, called with just a markdown string, uses a different default: default-ast-options, built as (cmark-options-with (make-cmark-options) 'source-positions? #t) — every field the same as the table above except source-positions?. Positions are worth having in a parsed tree and not worth paying for in rendered markup, so the default is chosen per entry point rather than the record carrying a third "unset" state. markdown->ast called with an explicit options record honours it verbatim, default-cmark-options included. Details are in ast.md.

Constructing and updating

(make-cmark-options 'extensions '(table) 'smart? #t)
;; A new record: extensions '(table), smart? #t, and the other eight
;; fields at their defaults from The defaults above.

(default-cmark-options)
;; Equivalent to (make-cmark-options) with no plist at all.

(cmark-options-with (make-cmark-options 'extensions '(table)) 'smart? #t)
;; A THIRD record. The argument to cmark-options-with is untouched --
;; there is no setter anywhere in this API.

cmark-options? is the type predicate. cmark-options-with, every renderer, and markdown->ast all check it on their options argument — passing something else raises &cmark-invalid-option with key #f and reason invalid-value. That check runs before anything else for every entry point except one: the three-argument form of markdown->commonmark and markdown->plaintext checks its width argument first, so a call that gets both the width and the options record wrong raises on width (reason invalid-width), not on the options record — see Wrap width.

Both constructors run their result through the same validation, so nothing built by cmark-options-with can carry a value the original construction path would have refused. (cmark gfm options) imports no native library — not even transitively — so every one of these checks runs in pure Scheme, and a rejected plist never reaches native code at all:

ProblemReason
Plist has an odd number of elementsmalformed-plist
A key is not one of the ten fieldsunknown-key
The same key appears twice in one callduplicate-key
A value is the wrong type for its fieldinvalid-value
extensions names something outside the five known extensionsunknown-extension
Both hardbreaks? and nobreaks? are #tcontradictory

Each is &cmark-invalid-option, with cmark-invalid-option-key naming the field (#f for malformed-plist, which is a problem with the plist as a whole, not one field) and cmark-invalid-option-reason giving the symbol above:

(guard (e ((cmark-invalid-option? e)
           (list (cmark-invalid-option-key e) (cmark-invalid-option-reason e))))
  (make-cmark-options 'extensiosn '(table)))
;; => (extensiosn unknown-key)

See errors.md for the complete condition hierarchy, including the conditions renderers and markdown->ast raise that construction never does.

hardbreaks? and nobreaks?

Both default to #f, and setting both to #t is refused — &cmark-invalid-option, key hardbreaks?, reason contradictory — rather than silently resolved to one of them:

(guard (e ((cmark-invalid-option? e) (cmark-invalid-option-reason e)))
  (make-cmark-options 'hardbreaks? #t 'nobreaks? #t))
;; => contradictory

This is not a defence against undefined behaviour: cmark's own renderers give hardbreaks? precedence over nobreaks? when both option bits are set, so the combination is well-defined at the C level. Refusing it here is policy — a caller who set both explicitly made a mistake, and silently honouring one request while discarding the other would hide that mistake instead of surfacing it.

validate-utf8?

Defaults to #t, and is a real cmark option bit — but has no effect reachable through this binding. Markdown input here is always a Scheme string, encoded internally with string->utf8, which cannot produce invalid UTF-8 from a valid Scheme string. cmark's validate-utf8? exists to replace invalid bytes with U+FFFD for callers who hand it raw, untrusted bytes; this binding never does, so there is nothing for the flag to correct.

What is rejected, regardless of validate-utf8?, is an embedded NUL character — at any position in the string, not only a leading or trailing one:

(guard (e ((cmark-invalid-input? e) (cmark-invalid-input-reason e)))
  (markdown->html (string #\a #\nul #\b) (default-cmark-options)))
;; => embedded-nul

This is a property of the string, not of validate-utf8?'s setting: cmark's own accessors return NUL-terminated C strings, and an embedded NUL would silently truncate rather than error, so it is refused outright before anything reaches native code.

Resource limits

Two ceilings bound how much a parse can produce, independent of max-input-bytes: five megabytes of adversarial Markdown can still parse to millions of nodes, so the input-size limit alone does not bound the Scheme-side copy.

KeyDefaultBounds
max-input-bytes5242880 (5 MiB)the UTF-8 bytevector parsed
max-nodes250000total nodes in the copied tree
max-depth1000nesting depth of the copied tree

Exceeding any of the three raises &cmark-resource-limit — a subtype of &cmark-invalid-input, carrying the ceiling that was exceeded via cmark-resource-limit-value, and discriminated from the other two by cmark-invalid-input-reason: too-large for max-input-bytes, too-many-nodes for max-nodes, too-deep for max-depth. Full behaviour, including where in a parse each one is checked, is in ast.md.