These are the TeXbook "rules" referenced in KaTeX's source, collected here for quick lookup during TeXLayout.jl development. Page numbers refer to the TeXbook (Knuth, 5th edition). The OpenType equivalents are noted where they differ from the original TeX metric names.
The Status field for each rule describes TeXLayout.jl's implementation relative to what KaTeX does. "Matches KaTeX" means the algorithm agrees modulo the OpenType/TeX metric naming differences; deviations are noted explicitly.
Source: src/functions/styling.ts
When a style command (\displaystyle, \textstyle, \scriptstyle,
\scriptscriptstyle) is encountered, switch to the specified style for the
remainder of the group. The font size multiplier changes accordingly.
Status: Matches KaTeX — TexStyle propagated through _layout_node!.
Source: src/buildHTML.ts (preamble, before the rule-number comments)
A mbin atom is demoted to mord if:
- it appears at the start of a list, or immediately after
mbin,mopen,mrel,mop, ormpunct(Rule 5 / left-canceller), or - it appears immediately before
mrel,mclose, ormpunct(Rule 6 / right-canceller).
Status — matches KaTeX.
- Two-pass reclassification in
_layout_children!: left-to-right (Rule 5) then right-to-left (Rule 6). ✓ - Neutral atoms (
:neutral— spaces,NodeKind.Space) are transparent to both passes. ✓ _BIN_LEFT_CANCEL = (:bin, :open, :rel, :op, :punct)and_BIN_RIGHT_CANCEL = (:rel, :close, :punct)defined as module-level constants. ✓
Source: src/functions/overline.ts
Build the body in the cramped style. Place a rule above it with:
gap = 3 × defaultRuleThickness (between body top and rule bottom)
rule height = defaultRuleThickness
OpenType equivalent: defaultRuleThickness ≈ FractionRuleThickness from the
MATH table. The OpenType MATH table provides purpose-built constants
OverbarVerticalGap and OverbarRuleThickness that supersede the KaTeX
formula.
Status — matches KaTeX.
- Body built in
cramp_style(style)as required. ✓ - Gap and rule thickness read from
OverbarVerticalGap/OverbarRuleThicknessrather than the KaTeX3 × defaultRuleThicknessheuristic. ✓ - HRule emitted above the body with bottom edge at
body_top + gap. ✓
Source: src/functions/underline.ts
Build the body in the current style (not cramped). Place a rule below it with:
gap = 3 × defaultRuleThickness (between rule top and body bottom)
rule height = defaultRuleThickness
OpenType equivalents: UnderbarVerticalGap and UnderbarRuleThickness.
Status — matches KaTeX.
- Body built in the current (uncramped) style as required. ✓
- Gap and rule thickness read from
UnderbarVerticalGap/UnderbarRuleThickness. ✓ - HRule emitted below the body with top edge at
body_bottom − gap. ✓
Source: src/functions/accent.ts
- Build the base in the cramped style.
- Compute the clearance:
where
clearance = min(base.height, xHeight)xHeightis the OpenType constantAccentBaseHeight. For tall bases the accent floats abovexHeight; for short ones it sits at the formula baseline. - Place the accent glyph at vertical position
base.top − clearance(i.e. the accent baseline is atmax(0, base.height − xHeight)above the formula baseline). - Align horizontally using
MathTopAccentAttachmentrecords from the MATH table:- If both the base glyph and the accent glyph have attachment records,
accent_x = base_attach_x − accent_attach_x. - Otherwise, centre the accent over the base:
accent_x = (base_w − accent_w) / 2.
- If both the base glyph and the accent glyph have attachment records,
- The accent does not contribute to the overall advance width; the box width equals the base width.
OpenType equivalent: xHeight → AccentBaseHeight; attachment records in
MathTopAccentAttachment subtable of the MATH table.
Status — matches KaTeX.
- Rule 12 steps 1–5 fully implemented for 11 non-stretchy accent commands:
\hat,\acute,\grave,\ddot,\tilde,\bar,\breve,\check,\dot,\mathring,\vec. ✓ MathTopAccentAttachmentalignment used when both glyphs have records; falls back to centering for complex multi-glyph bases. ✓- Wide/stretchy accents (
\widehat,\widetilde) implemented: share codepoints with their fixed-size counterparts; layout dispatches to_layout_wide_accent!which selects the smallest pre-built variant fromhoriz_constructionswide enough to cover the base, or assembles one from extensible parts. ✓ - Codepoint note: KaTeX's
symbols.tsmaps\acute/\grave/\barto Modifier Letter codepoints (U+02CA/U+02CB/U+02C9) absent in most OpenType math fonts. TeXLayout.jl uses Latin-1/ASCII equivalents (U+00B4/U+0060/U+00AF) which are present in NewCMMath and render to the same glyphs.
Source: src/functions/sqrt.ts
- Build the body in the cramped style.
- Compute the initial clearance:
- Display style:
phi = xHeight,lineClearance = ruleThickness + xHeight / 4 - Otherwise:
phi = ruleThickness,lineClearance = ruleThickness + ruleThickness / 4
- Display style:
- Select the radical delimiter with minimum height
= body.height + body.depth + lineClearance + ruleThickness. - Gap adjustment — if
delimDepth > body.height + body.depth + lineClearance:wherelineClearance = (lineClearance + delimDepth − body.height − body.depth) / 2delimDepthis the depth of the radical glyph below the rule arm. This distributes the excess space equally above and below the body rather than leaving it all below, preventing an oversized hook for small radicands. - Position the radical so its rule arm aligns with
body.top + lineClearance.
Status — matches KaTeX.
- Gap adjustment (step 4) implemented. ✓
- Step 2 uses
radical_vertical_gap/radical_display_style_vertical_gapfrom the OpenType MATH table instead of the KaTeX formula; these are purpose-built constants that subsume theruleThickness + phi/4calculation. ✓ - Body is built in
cramp_style(style)as required. ✓
Source: src/functions/genfrac.ts
| Style | With rule | Without rule |
|---|---|---|
| Display | num1, denom1, clearance=3×rule |
num1, denom1, clearance=7×rule |
| Non-display | num2, denom2, clearance=1×rule |
num3, denom2, clearance=3×rule |
OpenType equivalents: num1/num2 → FractionNumeratorDisplayStyleShiftUp /
FractionNumeratorShiftUp; denom1/denom2 → FractionDenominatorDisplayStyleShiftDown /
FractionDenominatorShiftDown.
candidateClearance = (numShift − num.depth) − (denom.height − denomShift)
if candidateClearance < clearance:
numShift += 0.5 × (clearance − candidateClearance)
denomShift += 0.5 × (clearance − candidateClearance)
# Numerator must not come too close to the rule top:
if (numShift − num.depth) − (axisHeight + 0.5 × ruleWidth) < clearance:
numShift += clearance − gap_above_rule
# Denominator must not come too close to the rule bottom:
if (axisHeight − 0.5 × ruleWidth) − (denom.height − denomShift) < clearance:
denomShift += clearance − gap_below_rule
Clearance is 3 × ruleWidth in Display, 1 × ruleWidth otherwise.
Minimum delimiter height:
- Display:
delim1 - ScriptScript:
delim2(at Script size) - Otherwise:
delim2
Status — matches KaTeX for \frac and \binom.
Rules 15b and 15d are implemented using the OpenType MATH table constants
FractionNumeratorGapMin / FractionDenominatorGapMin and their Display-style
variants, which encode the clearance directly.
Rule 15c is implemented for NodeKind.Genfrac (\binom/\dbinom/\tbinom) via
_layout_genfrac!. KaTeX uses num3 (no-rule non-display shift) which has no
OpenType equivalent; TeXLayout uses FractionNumeratorShiftUp (num2) instead.
The visual difference is negligible because the gap clamping still guarantees a
reasonable minimum gap via FractionNumeratorGapMin.
Rule 15e (\genfrac arbitrary delimiters) and \atop (no-rule, no delimiters)
are not implemented.
Source: src/functions/supsub.ts
For bases that are not a single character box:
supShift = base.height − supDrop × script_size_multiplier
subShift = base.depth + subDrop × script_size_multiplier
supDrop (σ₁₈) and subDrop (σ₁₉) are the OpenType constants
SuperscriptBaselineDropMax and SubscriptBaselineDropMin.
Status — matches KaTeX. The _is_char_box helper mirrors KaTeX's
isCharacterBox: true for NodeKind.Char, for NodeKind.Command nodes that are not large
operators (Greek letters, etc.), and recursively for NodeKind.FontSwitch wrapping a
single character. Large operators (\int, \sum, …), named operators (\sin,
…), fractions, and groups all return false, triggering the supDrop/subDrop clamp.
subShift = max(subShift, sub1, subm.height − 0.8 × xHeight)
sub1 → SubscriptShiftDown.
Status — matches KaTeX. All three terms are applied using the OpenType
constant SubscriptTopMax in place of 0.8 × xHeight. ✓
supShift = max(supShift, minSupShift, supm.depth + 0.25 × xHeight)
minSupShift (three cases):
- Display style:
sup1 - Cramped style:
sup3(SuperscriptShiftUpCramped) - Otherwise:
sup2(SuperscriptShiftUp)
Status — one deviation (structural).
- The
supm.depth + 0.25 × xHeightclamp is implemented using the OpenType constantSuperscriptBottomMin. ✓ - KaTeX uses three minSupShift cases; TeXLayout.jl uses two (cramped vs
not-cramped), matching the two OpenType constants available. The Display
case (
sup1) has no OpenType equivalent — the font designer is expected to encode the appropriate value inSuperscriptShiftUp. This structural difference is intentional and not treated as a bug.
Subsumed in the max(…) of Rule 18c above.
supShift = max(supShift, minSupShift, supm.depth + 0.25 × xHeight)
subShift = max(subShift, sub2) # sub2 ≈ SubscriptShiftDown
# Ensure minimum gap between sup bottom and sub top:
maxWidth = 4 × defaultRuleThickness
if (supShift − supm.depth) − (subm.height − subShift) < maxWidth:
subShift = maxWidth − (supShift − supm.depth) + subm.height
psi = 0.8 × xHeight − (supShift − supm.depth)
if psi > 0:
supShift += psi
subShift -= psi
Status — matches KaTeX. The gap clamp and psi redistribution are
implemented using OpenType constants SubSuperscriptGapMin (min gap) and
SuperscriptBottomMaxWithSubscript (psi threshold). ✓
Source: mentioned in src/buildHTML.ts preamble.
Defines the inter-atom spacing table for atom class pairs (ord/op/bin/rel/open/close/punct/inner). KaTeX uses this to insert thin, medium, or thick spaces (or no space) between adjacent atoms.
Status: Matches KaTeX. Inter-atom spacing implemented in _interatom_space
in layout.jl. Binary reclassification (Rules 5 & 6) applied via two-pass
_layout_children! — see the Rule 5 & 6 entry above.
Features not covered by a numbered TeX/KaTeX rule. NodeKind names and key constants are noted for quick lookup.
| Feature | NodeKind | Key implementation detail |
|---|---|---|
\left/\right auto-sized delimiters |
NodeKind.Delimited |
Smallest variant from vert_constructions that clears the inner content height; centred on math axis |
\middle delimiter |
NodeKind.Middle |
Auto-sized to match the enclosing \left/\right pair; multiple per group supported |
\bigl/\bigr/\big families |
NodeKind.BigDelim |
4 fixed tiers (1.2/1.8/2.4/3.0 em × upm); size is scale-independent |
Named operators (\sin, \lim, …) |
NodeKind.Operator |
Upright glyphs via glyph_metrics_upright; 27 operators; Display-style limits for operators in _LIMITS_OPERATORS |
Large operators (\sum, \int, …) |
NodeKind.Command |
Display-size variant from vert_constructions using display_operator_min_height; codepoints in _DISPLAY_OP_CODEPOINTS |
| Limits placement | NodeKind.Decorated |
Sub/sup centred below/above in Display style; uses UpperLimitGapMin, LowerLimitGapMin, UpperLimitBaselineRiseMin, LowerLimitBaselineDropMin |
\limits/\nolimits override |
NodeKind.LimitsOverride |
Wraps the preceding base; checked before script dispatch |
Horizontal extensibles (\widehat, \widetilde) |
NodeKind.Accent |
Smallest pre-built variant from horiz_constructions that covers the base; extensible assembly if no variant fits |
Horizontal braces (\overbrace, \underbrace, …) |
NodeKind.HorizBrace |
Widest-fitting variant or assembly from horiz_constructions; limits-style note placement for sub/superscripts; 6 commands |
Extensible arrows (\xrightarrow, …) |
NodeKind.XArrow |
18 commands; arrow stretched to cover labels; labels at _XARROW_KERN (0.111 em) clearance |
Font switching (\mathbf, \mathrm, …) |
NodeKind.FontSwitch |
Maps Latin/Greek/symbols to Unicode math-variant codepoints; propagates through sub/superscripts via ctx.font_variant |
Style switches (\displaystyle, …) |
NodeKind.StyleOverride |
Consumes rest of current group; resets both style and scale absolutely (see AGENTS.md encoding note) |
Font sizing (\large, \tiny, …) |
NodeKind.Sizing |
Multiplier stored as decimal string in value; multiplies current scale; 10 levels from 0.5× to 2.488× |
\dfrac, \tfrac |
NodeKind.StyleOverride wrapping NodeKind.Frac |
Forces Display or Text style with absolute scale reset |
\binom, \dbinom, \tbinom |
NodeKind.Genfrac |
Rule 15c gap clamping; auto-sized () delimiters via _layout_delim! |
| Array/matrix environments | NodeKind.Matrix |
8 named environments + \begin{array}{colspec}; per-column l/c/r alignment; single and double ` |
\text{}, \mbox{} |
NodeKind.Text |
Switches to _with_text_mode; upright glyphs from regular font slot; spaces preserved; inter-atom spacing suppressed |
Text styles (\textbf, \textit, \textsc, …) |
NodeKind.Text inside math; TextAttrs in documents |
Shared command semantics in text_styles.jl; \textsc becomes a semantic feature and HarfBuzz applies OpenType smcp |
Escaped literal characters (\#, \$, \%, …) |
NodeKind.Char in math; text buffer characters in documents |
Shared special-character table plus mode-specific math/text aliases in parser_tables.jl; escaped dollars are excluded from Makie's math-shift count |
default_font_family() / set_default_font_family!() |
— | Session-wide default; lazy artifact download; Makie extension picks up changes automatically |
Explicit spacing (\, \; \quad \kern …) |
NodeKind.Space |
Width in node.width (em); negative spaces supported; 1 mu = 1/18 em |