Skip to content

Latest commit

 

History

13 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Kirigami

php-mdhtml

A fast CommonMark + GitHub Flavored Markdown PHP extension, built on cmark-gfm.

License: GPL-2.0-or-later PHP 8.5 cmark-gfm 0.29.0 Website


Overview

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.


Table of contents


Why not the existing PECL cmark extension?

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.


Status

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.


API

MDHtml\Render(string $markdown): string

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.

MDHtml\RegisterEmoji(string $shortcode, string $char): void

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>

MDHtml\RegisterPlugin(string $name, callable $callback): void

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.

MDHtml\UnregisterPlugin(string $name): void

Removes a registered plugin.

MDHtml\GetRegisteredPlugins(): array

Returns every currently registered plugin name.


Extended syntax

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.


Building from source

Native build (fast iteration, no Docker/Emscripten):

cd vendor/build && bash stage.sh   # downloads + builds libcmark-gfm
cd ../..
phpize
./configure --with-mdhtml
make

vendor/ is gitignored and rebuilt from source every time — nothing vendored is hand-edited. There is no Emscripten/WASM build yet (see Status).


Requirements

  • PHP >= 8.5
  • A C compiler, phpize, cmake (to build the vendored libcmark-gfm)

Related


License

GPL-2.0-or-later. See LICENSE for the full text.

Author

Maxime Larrivée-Roy

Releases

Packages

Contributors

Languages