Native C CommonMark + GitHub Flavored Markdown parser for PHP. 15-30Γ faster than pure-PHP alternatives (Parsedown, cebe, michelf) with full CommonMark 0.31 compliance: 652/652 spec examples pass. GFM extensions: tables, strikethrough, task lists, autolinks, tagfilter. Installable via PIE (the PHP Foundation's PECL successor); ships as a single .so. PHP 8.3 minimum, OO API with final classes and readonly options.
# PIE (PHP Foundation's extension installer; uses the composer.json
# at the repo root with type: "php-ext")
pie install iliaal/mdparserOn a minimal PHP image (e.g. php:8.x-cli from Docker Hub), PIE needs a few build tools installed first:
# Debian/Ubuntu
sudo apt install -y git bison libtool-bin
# macOS
brew install bison libtoolgit clone https://github.com/iliaal/mdparser.git
cd mdparser
phpize && ./configure --enable-mdparser
make -j
sudo make install
echo 'extension=mdparser.so' | sudo tee /etc/php/conf.d/mdparser.iniPre-built DLLs for PHP 8.3, 8.4, and 8.5 (TS/NTS, x86/x64) are attached to each GitHub release.
use MdParser\Parser;
use MdParser\Options;
// Default parser: safe mode on, GFM extensions on.
$parser = new Parser();
echo $parser->toHtml('# Hello');
// <h1>Hello</h1>
// Custom options via named arguments. All fields readonly.
$parser = new Parser(new Options(
smart: true, // --- -> em dash, -- -> en dash, "..." -> curly
sourcepos: true, // add data-sourcepos to every HTML element
footnotes: true, // enable [^ref] / [^ref]: syntax
unsafe: false, // raw HTML is still stripped (default)
));
echo $parser->toHtml($markdown);
// Three output formats from one parser.
$html = $parser->toHtml($markdown);
$xml = $parser->toXml($markdown); // CommonMark XML, DOCTYPE-wrapped
$ast = $parser->toAst($markdown); // nested arrays, see below
// AST shape is documented in tests/006_ast.phpt. Brief example:
// [
// 'type' => 'document',
// 'children' => [
// ['type' => 'heading', 'level' => 1, 'children' => [
// ['type' => 'text', 'literal' => 'Hello'],
// ]],
// ],
// ]Against the major pure-PHP Markdown libraries, on PHP 8.4 with each parser in its default configuration:
| Parser | Small (200 B) | Medium (1.8 KB) | Large (200 KB) |
|---|---|---|---|
| mdparser | 30447 ops/s | 5697 ops/s | 105 ops/s |
| Parsedown | 1651 ops/s (18x slower) | 325 ops/s (17x) | 6 ops/s (17x) |
| cebe/markdown (GFM) | 1350 ops/s (22x) | 374 ops/s (15x) | 6 ops/s (16x) |
| michelf (Markdown Extra) | 1006 ops/s (30x) | 209 ops/s (27x) | 5 ops/s (19x) |
15-30Γ faster across the board, from small messages to full 200 KB spec documents. See bench/README.md for methodology, corpora, caveats, league/commonmark notes, and how to reproduce these numbers yourself.
Comparison with the major pure-PHP Markdown libraries. "via ext" means the feature exists but requires opting in to a non-default extension; "Extra" means the feature ships in the library's Markdown Extra dialect, not its base mode; "β" means the feature is not supported at all.
| Feature | mdparser | Parsedown | league/cm core | cebe GFM | michelf Extra | Ciconia |
|---|---|---|---|---|---|---|
| CommonMark core | β | partial | β | partial | partial | partial |
| Fenced code blocks | β | β | β | β | β | β |
| GFM tables | β | β | via ext | β | via Extra | β |
| Strikethrough | β | β | via ext | β | β | β |
| Task lists | β | β | via ext | β | β | β |
| Autolinks (bare URL) | β | β | via ext | β | β | β |
<script> tag filter |
β (tagfilter) | β (escaped) | via ext | partial | β | β |
| Smart punctuation | β (Options::smart) |
β | via ext | β | β | β |
| Footnotes | β (Options::footnotes) |
Extra | via ext | β | β Extra | plugin |
| Hardbreaks/nobreaks | β | β | β | β | β | β |
| Sourcepos | β | β | β | β | β | β |
| Heading anchors | β (Options::headingAnchors) |
β | via ext | β | β | β |
rel="nofollow" |
β (Options::nofollowLinks) |
β | via ext | β | β | β |
| HTML output | β | β | β | β | β | β |
| XML output | β | β | β | β | β | β |
| AST output | β (arrays) | β | β (objects) | β | β | β |
mdparser is deliberately scoped to what cmark-gfm supports: CommonMark core plus the five GFM extensions. It does not cover the "Markdown Extra" family of features that Parsedown Extra, michelf Markdown Extra, and league/commonmark's optional extensions offer. If you need any of the following, reach for league/commonmark, the most actively-maintained pure-PHP option for extended Markdown:
- Definition lists (
Term :: definition) - Abbreviations (
*[HTML]: ...) - Attribute syntax (
{.class #id key="val"}) - Permalink anchor markup (we emit heading
idslugs; we don't inject the inner<a class="anchor">element GitHub uses for permalinks) - Table of contents
- YAML front matter
- Mentions (
@user) - LaTeX math (
$$...$$) - Emoji (
:smile:) - Custom admonition containers (
::: warning)
These are real features. They're just not in scope for a CommonMark+GFM core parser, and cmark-gfm doesn't implement them.
Options::unsafe = true tells cmark to pass raw HTML through verbatim instead of escaping or stripping it. The contract for this mode is that you own the input: it is yours, or it comes from a pipeline you trust. Two postprocess interactions are worth knowing if you also turn on headingAnchors or nofollowLinks:
- Heading slug positioning under raw
<hN>. mdparser locates each AST heading in the rendered HTML by rendering it standalone and matching its exact byte sequence. Raw<h1>x</h1>blocks written directly in the markdown source are therefore left untouched and do not consume slugs. The fingerprint search skips over HTML comments, CDATA sections, and raw-text / escapable-raw-text element bodies (script,style,title,textarea,iframe,noscript,xmp,noembed,noframes,plaintext), so a heading-shaped byte sequence inside those regions cannot hijack a slug. The narrow remaining exception is when a raw<hN>...</hN>block in the document body produces bytes byte-identical to a later Markdown heading (same level, same inner text), in which case theidattribute lands on the first match. nofollowLinksis tag-aware. It rewrites every<a href="...">it finds at a real tag-start position. The scan walks tag-by-tag with quote-aware attribute parsing, so anchor-shaped substrings inside another tag's quoted attribute value (e.g.<div title='<a href="x">y</a>'>written directly in the source) are passed through verbatim rather than rewritten. Raw-text element bodies and comment / CDATA bodies are likewise emitted verbatim. In-document fragment anchors (href="#...") are intentionally skipped, so footnote references and backrefs stay clean.
Parser::toAst() returns a structural representation of the parsed document. Link / image url fields and html_block / html_inline literal fields are preserved byte-for-byte; the unsafe, tagfilter, and URL-scheme defenses do not apply to the AST. If you build HTML out of the AST yourself, you own the sanitization: apply a URL scheme allowlist before emitting href, and run HTML through a sanitizer before emitting raw html_block / html_inline literal text. See docs/ast.md for examples.
Companion native PHP extensions for high-throughput PHP workloads:
- php_excel: native Excel I/O. 7-10Γ faster than PhpSpreadsheet, full XLS/XLSX with formulas, formatting, and styling. Powered by LibXL.
- php_clickhouse: native ClickHouse client speaking the wire protocol directly. Picks up where SeasClick left off.
- fastchart: native chart-rendering extension. 19 chart types behind one fluent OO API; composes with caller-owned
\GdImagecanvases.
Full background, design rationale, and benchmark methodology in the launch post: mdparser: A Native CommonMark + GFM Parser for PHP.
- Wrapper code (
mdparser*.c,php_mdparser.h) under BSD 3-Clause. - Embedded cmark-gfm sources under BSD-2-Clause, MIT, and related permissive licenses. See
LICENSEfor aggregated notices.
Follow @iliaa on X β’ Blog β’ If this sped up your stack, β star it!
