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
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.
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
| Concern | Key points |
|---|---|
| Memory | zero-allocation parsing; md_html output is delivered via callback, no library-side buffer |
| Callbacks | structure must be complete; non-zero return aborts (the error channel) |
| Characters | char 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
- 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.
- 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
Render the same Markdown (tables and code blocks) with default and GFM flags; compare output.
Extract H1-H3 from the md_parse event stream into a TOC tree with anchor jumps.
GET /docs/:name reads a Markdown file → renders → injects into a template controlled block, cached at Init.
Summary
| Point | Conclusion |
|---|---|
| Position | CommonMark parsing + HTML rendering, 2 symbols total |
| Event stream | zero-allocation callback-style md_parse, the base for custom renderers |
| One-shot | md_html with built-in callbacks + flag dialects + default escaping |
| Composition | Markdown into template controlled blocks; static content rendered at Init |
| Hard rule | callbacks must be complete; non-zero return aborts |
- xrt · template — the composition partner for controlled blocks