php-mdhtml parses CommonMark + GFM (tables, tasklists, strikethrough,
autolinks, footnotes) via cmark-gfm
and exposes it to PHP as a single namespaced function,
\MDHtml\Render(), plus a small set of extras — heading anchors, emoji
shortcodes, a {% plugin %} system with real PHP callbacks, GFM-style
alerts, definition lists, and ==highlight==/^sup^/~sub~ — all done
in C, not PHP.
It exists to accelerate MD::toHtml(), the Markdown renderer in
kirigami/php-prepros,
whose CommonMark/GFM structural parsing is currently ~800 lines of
hand-written regex. php-mdhtml replaces that structural core with a
real C parser while keeping the same output.
Part of the Kirigami project ecosystem.
- php-mdhtml
krakjoe/cmark already wraps plain
cmark for PHP, but it's unmaintained since 2019, wraps upstream cmark
rather than cmark-gfm (no tables/tasklists/strikethrough/autolink), and
hand-mimics PHP's internal zend_object struct with hardcoded field
offsets — a pattern that silently breaks (heap corruption, a crash on
object destruction) whenever PHP's real zend_object layout changes,
which it did between 2019 and PHP 8.5. Patching that fork further would
mean repeating the same fragile trick for every GFM node type it would
need. php-mdhtml is a small extension written from scratch against
cmark-gfm directly, with an idiomatic object model (in practice: no
object model at all — see API) instead of a hand-rolled one.
See docs/CONTEXT.md for the full investigation and docs/DECISIONS.md for every decision made along the way.
Builds natively and under Emscripten: statically linked into
@kirigami/php-wasm
by php-wasm-compiler,
where kirigami/php-prepros's MD::toHtml() delegates to it. See
docs/STATUS.md for what's verified and
docs/TODO.md for known gaps.
Parses and renders Markdown to HTML. Covers the CommonMark+GFM structural core (headings, emphasis, lists, blockquotes, code, links, images, thematic breaks, tables, tasklists, strikethrough, autolinks, footnotes) plus everything under Extended syntax.
echo \MDHtml\Render("# Hello\n\n- one\n- two\n");
// <h1 id="hello">Hello</h1>
// <ul>
// <li>one</li>
// <li>two</li>
// </ul>Raw HTML in the source passes through a whitelist sanitizer (same
tag/attribute allow-list as MD::sanitizeHtmlTag()) rather than being
escaped or stripped outright.
Registers (or overrides) a :shortcode: emoji for the rest of the
request, like a MD::registerEmoji() userland static. Register again in
each request that renders it (Kirigami does, on every page). Over
300 are built in already, ported from MD::$emojiMap.
\MDHtml\RegisterEmoji('kirigami', '📐');
echo \MDHtml\Render('Made with :kirigami:');
// <p>Made with 📐</p>Registers a {% name args %} (inline) / {% name args\nbody\n%} (block)
tag. The callback receives (array $args, string $body) and returns the
HTML to splice in; a block-style plugin's output is correctly unwrapped
from the paragraph cmark would otherwise wrap it in.
\MDHtml\RegisterPlugin('codepen', function (array $args, string $body): string {
$id = htmlspecialchars($args[0] ?? '', ENT_QUOTES, 'UTF-8');
return "<iframe src=\"https://codepen.io/embed/{$id}\"></iframe>";
});
echo \MDHtml\Render('{% codepen abc123 %}');
// <p><iframe src="https://codepen.io/embed/abc123"></iframe></p>An unregistered tag name is left as literal text.
Removes a registered plugin.
Returns every currently registered plugin name.
None of these are CommonMark or GFM — they're conventions
kirigami/php-prepros's MD::toHtml() already supports, reproduced here
in C:
| Syntax | Renders as |
|---|---|
`{#custom-id}` after a heading |
<h2 id="custom-id">…</h2> instead of an auto-generated slug |
==highlighted== |
<mark>highlighted</mark> |
^superscript^ |
<sup>superscript</sup> |
~subscript~ |
<sub>subscript</sub> |
> [!NOTE] / [!TIP] / [!IMPORTANT] / [!WARNING] / [!CAUTION] |
GitHub-style <div class="markdown-alert markdown-alert-*"> |
Term⏎: Definition |
<dl><dt>Term</dt><dd>Definition</dd></dl> |
External links get target="_blank" rel="noopener noreferrer"; images
get loading="lazy"; a GFM task list's <ul> gets class="task-list"
and each <li> gets class="task-item" — matching MD::toHtml()'s own
output shape, not cmark-gfm's bare defaults.
Native build (fast iteration, no Docker/Emscripten):
cd vendor/build && bash stage.sh # downloads + builds libcmark-gfm
cd ../..
phpize
./configure --with-mdhtml
makevendor/ is gitignored and rebuilt from source every time — nothing
vendored is hand-edited. There is no Emscripten/WASM build yet (see
Status).
- PHP
>= 8.5 - A C compiler,
phpize,cmake(to build the vendoredlibcmark-gfm)
cmark-gfm— the C library this extension wraps- kirigami/php-prepros —
MD::toHtml(), the renderer this extension accelerates - php-wasm-compiler —
builds the
php.wasmruntime this extension will eventually target
GPL-2.0-or-later. See LICENSE for the full text.
Maxime Larrivée-Roy