Supported MathML Features

PeachPDF renders inline <math> elements — MathML embedded directly in HTML markup — as real vector PDF content: glyphs are drawn through the same text pipeline as ordinary HTML text, fraction bars and radical rules are native PDF path/fill operators, and stretchy operators (parentheses, brackets, radical signs, …) are built from the font’s own OpenType MATH table glyph data. Nothing is rasterized to a bitmap.

The layout algorithm targets MathML Core — the W3C/browser-vendor specification that gives MathML 3’s presentation markup a fully concrete box model and layout algorithm (the parent MathML 3 specification leaves layout details implementation-defined). This page documents exactly what is and is not supported; where a feature is only partially supported, the specific gaps are noted.

A <math> element needs a font with a real MATH table to render structurally correct fraction bars, radicals, script sizing, and stretchy operators — see Fonts for how PeachPDF resolves fonts, and Fallback when no MATH table is present below for what happens without one.


Root Element

Element MDN Reference Notes
math math The display attribute ("block"/"inline", MathML Core §2.1) sets the formula’s initial displaystyle. This only affects MathML Core’s own sizing rules (e.g. whether a fraction’s numerator/denominator shrink) — it does not change the CSS box-level display of the element itself; a <math> element always participates in ordinary HTML inline flow, the same way an inline <svg> does. Genuine CSS properties — color, font-family, font-size, background — cascade into <math> content normally, from the surrounding document. An explicit CSS width/height (including a percentage, resolved against the containing block) resizes the element’s own box independently of its content — per MathML Core, <math> is not a replaced element the way <img>/<svg> are, so the formula’s internal typesetting is never rescaled to fit; content can overflow a box smaller than its natural size, or leave extra space in a larger one, exactly like an ordinary block whose content doesn’t fill its author-specified size

Token Elements

Element MDN Reference Notes
mi mi Identifier text, drawn via the ordinary text pipeline
mn mn Numeric literal text
mo mo Operator/fence/separator text. form (prefix/infix/postfix), stretchy, fence, separator, lspace/rspace, minsize/maxsize are read when the author sets them explicitly. Any of form/stretchy/symmetric/largeop/movablelimits/lspace/rspace the author leaves unset falls back to MathML Core’s own operator dictionary (Appendix B) default for that character, rather than a flat approximation — e.g. a bare <mo>(</mo> stretches by default with no explicit stretchy/fence attribute needed. accent is parsed but not consulted for a default (see Script and Limit Schemata’s munder/mover/munderover row)
mtext mtext Plain text, not italicized by default (per spec)
ms ms String literal text
mspace mspace width/height/depth — full MathML length grammar, including the named space keywords (thinmathspaceveryverythickmathspace, and their negative... variants)
mathvariant mathvariant A single-character mi is automatically italicized by substituting its Unicode “math italic” codepoint (MathML Core §4.2’s text-transform: math-auto, Appendix C.1) — e.g. a lone variable x renders as the slanted 𝑥, while a multi-character identifier like exp does not. mathvariant="normal" cancels this. No other mathvariant value has any effect, which matches MathML Core itself: it defines no bold/italic/double-struck/etc. mapping at all, and instead expects authors to use the corresponding Unicode Mathematical Alphanumeric Symbols characters directly

General Layout Schemata

Element MDN Reference Notes
mrow mrow Horizontal concatenation of its children. Inter-element spacing around an mo with no explicit lspace/rspace comes from the full operator dictionary (MathML Core Appendix B)
mfrac mfrac Numerator/denominator stacked around a real, measured fraction rule, using the font’s own MATH table constants (FractionRuleThickness, FractionNumeratorGapMin/DenominatorGapMin, AxisHeight). linethickness override is supported. bevelled="true" is parsed but renders as an ordinary (non-slanted) fraction — an accepted gap
msqrt / mroot msqrt / mroot A real, measured vinculum (radical rule) over the radicand, plus the radical sign itself drawn from the font’s MATH table MathVariants vertical constructions (stretched to match the radicand’s height) when the font provides one, reserving that chosen glyph’s own real advance width (from the font’s hmtx table) rather than an approximation. Falls back to a fixed fraction of the current font size only when the font has no MATH table or no vertical construction for the radical sign at all. mroot’s index is positioned and sized per MathML Core (two script levels smaller, kerned before/after)
mstyle mstyle displaystyle, scriptlevel, mathsize, mathcolor are honored for the subtree; mathbackground and the deprecated MathML 2 presentation attributes it also accepted are not rendered
mspace (see Token Elements above)  
mphantom mphantom Reserves its content’s exact size; draws nothing
mpadded mpadded width/height/depth/lspace/voffset (each a plain CSS <length-percentage> — a percentage value falls back to that attribute’s own default, per spec) override the box’s reported size and reposition its content within it, without rescaling the content itself. height/depth both default to the content’s own natural ascent when absent (MathML Core’s own default, not a bug)
mfenced mfenced (deprecated in MathML Core; still supported here) Desugared at parse time into an mrow of synthesized fence/separator mo elements (open/close/separators attributes, cyclically repeating the separator list) — the same shape hand-authoring the equivalent mrow would produce
menclose menclose (not in MathML Core; supported here for MathML 3 compatibility) Parsed (including the notation attribute), but renders as a plain pass-through of its content — no enclosure mark is drawn. An accepted gap
merror merror Renders its content plainly, like an mrow — MathML Core drops the “highlight as a syntax error” presentational suggestion

Script and Limit Schemata

Element MDN Reference Notes
msub / msup / msubsup msub / msup / msubsup Shift-up/shift-down and script-size scaling (ScriptPercentScaleDown) come from the font’s MATH table constants, with a minimum-clearance check between a simultaneous sub- and superscript (SubSuperscriptGapMin). Per-glyph corner kerning (MathKernInfo) is not applied — an accepted gap; scripts are still correctly shifted and sized, just without the extra fine-grained horizontal kerning a MathKernInfo-aware engine would add
munder / mover / munderover munder / mover / munderover An explicit accent/accentunder="true" renders that script at full size (not shrunk), matching an accent placed directly over/under the base; otherwise it’s treated as a moving limit and shrunk one script level, like a sub/superscript. This is MathML Core’s own default (§3.4.2: an absent or invalid accent/accentunder attribute is simply false) — it does not consult the base’s core operator, unlike full MathML 3
mmultiscripts mmultiscripts Pre- and post-script pairs (<mprescripts/>-separated), including the <none/> placeholder for an omitted half of a pair

Tables and Matrices

Element MDN Reference Notes
mtable mtable A real grid: each column sized to its widest cell, each row to its tallest, centered on the table’s own vertical center. Per-column/per-row columnalign/rowalign are parsed but every cell is currently centered regardless of the requested alignment — an accepted gap
mtr / mlabeledtr mtr / mlabeledtr mlabeledtr’s label cell is parsed and carried on the row but not yet drawn (equation numbering is a rare feature for a v1) — an accepted gap
mtd mtd Cell content lays out at displaystyle="false" unless overridden, per MathML Core’s own table default

Operator Dictionary

An <mo>’s form/stretchy/symmetric/largeop/movablelimits/lspace/rspace — whichever the author doesn’t set explicitly — resolve from MathML Core’s own operator dictionary (Appendix B): the character’s form classifies it into one of 13 categories, each with its own spacing and property defaults (e.g. +/- get a slightly tighter space than a relational operator like =; parentheses and other fence characters default to stretchy/symmetric with zero space). An operator matching no category (most identifiers used as mo, or an unrecognized character) falls back to a flat default matching MathML’s own thickmathspace.

Stretchy Operators

A single-character mo that’s stretchy — stretchy="true"/fence="true" explicit, or (per the operator dictionary above) a character the dictionary marks stretchy by default, like (/)/{/} — inside an mrow stretches vertically to match the tallest non-stretchy sibling in that row, per MathML Core §5.3.2’s “shape a stretchy glyph” algorithm: the font’s MATH table MathVariants pre-sized glyph variants (e.g. a font’s several progressively taller parenthesis glyphs) are tried first, and if none is tall enough, a taller shape is assembled from the font’s GlyphAssembly parts (a top cap, a repeatable extender, and a bottom cap, for a typical fence) — the same construction real browsers use, so arbitrarily tall content (deeply nested fractions inside parentheses, a large matrix, …) stretches the fence to match rather than clamping at the font’s largest pre-sized step.

Fallback When No MATH Table Is Present

A font with no MATH table (the common case — only dedicated math fonts like STIX Two Math or Latin Modern Math carry one) still renders a formula: every constant the layout algorithm needs falls back to a fixed ratio of the current font size (e.g. axis height ≈ 0.25em, fraction rule thickness ≈ 0.04em) — MathML Core’s own documented fallback strategy for this case. Structural correctness (fractions, radicals, scripts, tables) is preserved; stretchy operators fall back to their plain, unstretched glyph, since MathVariants data doesn’t exist to stretch from.

Elementary Math

mstack/mlongdiv/msgroup/msrow/mscarries/mscarry/msline (MathML 3 §3.5, dropped entirely by MathML Core) are not supported — arithmetic-stack/long-division layout is a large, separate algorithm with no browser reference implementation to check against, and is rare outside elementary-education material. An unrecognized element degrades to a plain row of its children.

Actions and Semantic Annotations

Element MDN Reference Notes
maction maction Renders only its selected child (selection, 1-based, default 1) or its first child — MathML Core itself reduces maction to this minimal behavior; PDF has no notion of the toggle/tooltip/statusline interactivity MathML 3 otherwise defines for it
semantics semantics Renders only its first child (the presentation-markup branch)
annotation / annotation-xml annotation Never laid out or painted — semantic/source-format metadata only

Content MathML

Content MathML (<apply>, <ci>, <cn>, and the rest of the semantic/functional markup vocabulary) is not supported at all. It’s rarely authored directly (most tools that produce it also produce an equivalent presentation-MathML rendering, typically via a <semantics> wrapper — see Actions and Semantic Annotations above), and implementing it would require an entirely separate semantic-to-visual layout layer.


Accepted Gaps

Each gap noted above with “an accepted gap” is tracked in this repository’s internal engineering notes (.claude/accepted-gaps/) alongside the reasoning for leaving it out of v1 and, where it’s a genuine spec deviation, a tracked issue for closing it later. In summary: