Chapter 37 / 51
Volume 5 · Extensions, Ops & Projects

md4c: Markdown rendering

Two symbols that do one job well: CommonMark parsing and HTML rendering. A small, correct choice — the event stream drives custom renderers, one-shot rendering runs your docs site.

Overview

Positioning: the archetype of a utility extension — unlike sqlite/xllm it carries no business domain, it completes the "controlled content" puzzle. Prerequisite: the build.bat md4c variant.

37.1 Position

md4c is a CommonMark-conformant parser: no DOM, no string allocation — parsing is an event stream. That minimalism fits the XServer scripting world exactly: when input is untrusted content, rendering to controlled HTML is far safer than executing templates (the controlled-block idea of ch. 19). Two imported symbols (md_parse / md_html) are the entire API surface.

37.2 The event-stream model

md_parse calls back in document order: block enter/leave (DOC/P/LI/CODE/TABLE…), span enter/leave (EM/STRONG/CODE_SPAN/LINK/IMG…) and text chunks. The callback structure must be fully populated — null pointers are unchecked (a hard constraint unlike most libraries); any callback returning non-zero aborts the parse. Custom renderers (summary extraction, TOC generation, plain-texting) all build on this layer.

Minimal renderer skeleton
static int enter_block(MD_BLOCKTYPE t, void* d, void* u)
{
    return 0;  /* continue */
}
/* leave_block / enter_span / leave_span / text same shape */
MD_PARSER tParser = { 0, NULL, enter_block, leave_block,
                      enter_span, leave_span, text_cb };
md_parse(sDoc, iLen, &tParser, &tCtx);

37.3 One-shot rendering

md_html carries a complete callback set and renders Markdown to an HTML fragment delivered through your output callback — the same mental shape as md_parse. Flags enable extension dialects: tables, strikethrough, task lists, wikilinks. HTML escaping is on by default (tags in the source get escaped) — the key default that turns untrusted input into safe output.

37.4 Combining with templates

The typical pipeline: request-supplied Markdown → md_html renders into a buffer → injected as a variable into a ch. 19 template's controlled block. Store document fields as Markdown and render at request time — editing experience and safety in one. Static doc sites can render once during Init (the ch. 41 hot-cache discipline).

37.5 The contract

ConcernKey points
Memoryzero-allocation parsing; md_html output is delivered via callback, no library-side buffer
Callbacksstructure must be complete; non-zero return aborts (the error channel)
Characterschar by default; MD_CHAR becomes WCHAR if _UNICODE is defined (TCC scripts default to narrow)
Probing#ifdef XS_USE_MD4C or xsExtensionEnabled("md4c")

Common mistakes

Passing a zeroed callback structure
Symptom
md_parse gets a {0} parser and the process faults.
Cause
Callback pointers are unchecked — the library calls them directly. Fill the complete structure, or use md_html which carries its own.
Pasting render output raw
Symptom
Rendered HTML is echoed straight into a hand-built page; script injection revives.
Cause
md_html escapes tags in the Markdown source; anything you append afterwards still follows the template's escaping rules (ch. 19).

Exercises

BasicRender comparison

Render the same Markdown (tables and code blocks) with default and GFM flags; compare output.

Acceptance: tables only under GFM; tags in the source are escaped.
IntermediateTOC generator

Extract H1-H3 from the md_parse event stream into a TOC tree with anchor jumps.

Acceptance: correct hierarchy on any document; deep nested lists don't interfere.
ChallengeA docs route

GET /docs/:name reads a Markdown file → renders → injects into a template controlled block, cached at Init.

Acceptance: file updates land via hot reload; malicious sample documents produce no executable injection.

Summary

PointConclusion
PositionCommonMark parsing + HTML rendering, 2 symbols total
Event streamzero-allocation callback-style md_parse, the base for custom renderers
One-shotmd_html with built-in callbacks + flag dialects + default escaping
CompositionMarkdown into template controlled blocks; static content rendered at Init
Hard rulecallbacks must be complete; non-zero return aborts
API references used in this chapter
Further reading on xrt