- Added two visualisation tools to
tools/:visualise_svg.jl: SVG bounding-box diagram showing ink extents of each LayoutBox with reference lines for baseline and math axis; Space elements shown as dashed outlinesvisualise_bitmap.jl: FreeType glyph rasterisation onto a PGM/PNG canvas; HRules filled directly; supports PNG output via ImageMagick pipe (convert pgm:- png:$path)
- Bug fixed (
src/layout.jl): Deeply nested fractions had content overlapping the outer fraction rule bar- Root cause:
fraction_numerator_gap_min/fraction_num_display_style_gap_min(and denominator equivalents) were parsed from the MATH table but never applied during fraction layout - Fix: implement TeX Rule 15d/15e — lay out numerator/denominator at y=0 first to measure ink extents, then clamp the initial MATH-table shifts:
num_shift = max(num_shift, axis_em + rule_thickness/2 + num_gap + num_depth) - Also extended
_boxes_top/_boxes_bottomto include HRule extents (previously Glyph-only), so nested fractions-as-numerator are measured correctly - All 511 tests pass after the fix
- Root cause:
Bug 1 & 2: + − = and digits (0–9) were invisible
- Root cause:
_char_glyphinlayout.jlstoredstring(ch)as the PostScript name for non-letter characters. FreeType'srenderface(face, name, size)resolves glyphs by PS name;"+"is not a valid PS name (should be"plus","hyphen","zero","equal", etc.). - Fix: added
glyph_name_by_codepoint(family, cp)tofonts.jlusing_FT.FT_Get_Glyph_Name, then updated_char_glyphto call it and use the returned PS name.
Bug 3: leading √ hook missing from \sqrt
- Root cause: NodeKind.Sqrt branch emitted only body boxes and an HRule bar; the radical glyph was never placed.
- Fix: restructured NodeKind.Sqrt to lay out body at y=0, measure ink, compute
required_du(total vertical span from body bottom to rule top in design units), then call new_layout_radical!helper. _layout_radical!selects the smallest pre-built variant fromvert_constructions["radical"]withadvance >= required_du, or falls back to_layout_radical_assembly!.- Unlike delimiter assemblies (centred on math axis), the radical assembly is TOP-ANCHORED: the top of the top cap aligns with the rule-top em position.
- Body and HRule are then shifted right by
rad_adv(advance width of radical glyph).
- 5 pre-built variants: radical (1001 du), radical.v1 (1201), radical.v2 (1801), radical.v3 (2401), radical.v4 (3001)
- Assembly parts (bottom-to-top): uni23B7 (1820 du, cap), radical.ex (640 du, extender), radical.tp (620 du, cap)
- All assembly parts have y_min=0, y_max=full_advance (top-aligned bounding boxes)
- Base
radicalglyph: y_min=-960, y_max=40 (bar barely above origin; hook extends far below) - Variant glyphs (v1–v4): origin shifts up so hook extends ~350–1250 du below, bar 850–1750 du above
- "Sqrt: radical bar above radicand" test updated to exclude the radical glyph (leftmost x) from radicand_y computation
- All 513 tests pass
- Implemented full TeX atom-class spacing (Knuth Ch. 17 / KaTeX
spacingData.ts). _CHAR_ATOM_CLASS: maps characters (including U+2212 minus, U+2217 asterisk) toord/bin/rel/open/close/punct/inner._CMD_ATOM_CLASS: maps LaTeX command names (all Greek, common operators, arrows, delimiters, etc.) similarly._SPACINGS/_TIGHT_SPACINGS: thin=3/18, medium=4/18, thick=5/18 em lookup tables for Display/Text and Script/ScriptScript styles._atom_class(node): scripted nodes (NodeKind.Superscript/NodeKind.Subscript/NodeKind.Decorated) inherit base atom class; NodeKind.Space is:neutral._interatom_space(prev, next, style): selects tight table for Script/ScriptScript styles.NodeKind.Sequence/NodeKind.Groupbranch now inserts auto-spacingSpaceelements in:mathmode; neutral explicit spaces reset the context preventing double-spacing.- 6 new layout tests (523 total, all passing):
a+bmedium,a=bthick,a,bthin,\sin xthin, Script suppression, explicit-space no double-gap. - Added
tools/visualise_spacing.jl: 13-row grid rendering with auto-spacing gaps shaded. - Open items remaining: limits placement (
\lim_{x}in Display), inter-atom spacing for\text{}/\mbox{}, accent rendering, font switching commands, array/matrix environments.
- Root cause:
_cmd_glyphlooked up PS name"sum"which does not exist in NewCMMath; correct name is"summation","product","integral", etc. - Fix: added
_DISPLAY_OP_CODEPOINTSdict mapping command names to Unicode codepoints;NodeKind.Commandbranch now usesglyph_name_by_codepointto obtain the correct PS name.
- In Display style,
\sum,\prod,\int, etc. select the smallestvert_constructionsvariant withadvance >= display_operator_min_height(1300 du from MATH table). - Operator glyph centred on the math axis (same approach as
\left/\rightdelimiters).
\lim,\limsup,\liminf,\det,\gcd,\inf,\sup,\max,\min,\Prautomatically use limits placement in Display style.\sum,\prod,\coprod,\bigcap,\bigcup, etc. also use limits placement in Display style.\limits/\nolimitsexplicit overrides parsed asNodeKind.LimitsOverrideAST node wrapping the base.- Limits algorithm: lay each part at origin, measure ink extents with
_boxes_top/_boxes_bottom, then apply four MATH constants:upper_limit_gap_min = 200 du— minimum ink gap above base topupper_limit_baseline_rise_min = 111 du— minimum sup baseline above base toplower_limit_gap_min = 167 du— minimum ink gap below base bottomlower_limit_baseline_drop_min = 600 du— minimum sub baseline below base bottom
- Each part centred horizontally over the base; total width = max(base_w, sub_w, sup_w).
- In Text/Script styles (or after
\nolimits), sub/sup remain in normal beside-base positions. limsupandliminfadded to_OPERATOR_NAMESinparser.jl(were previously falling through asNodeKind.Command).- 33 new test assertions across 8
@testsetblocks; 556 total tests, all passing.
- Implemented KaTeX Rule 12 (accents): base in cramped style; vertical placement via
AccentBaseHeight; horizontal viaMathTopAccentAttachment; accent does not widen advance. 11 non-stretchy commands:\hat,\acute,\grave,\ddot,\tilde,\bar,\breve,\check,\dot,\mathring,\vec. - Codepoint choice:
\acute/\grave/\baruse U+00B4/U+0060/U+00AF (Latin-1) rather than KaTeX's Modifier Letter codepoints (U+02CA/02CB/02C9) absent in NewCMMath. - Added
NodeKind.OverUndernode kind (single kind,value = "overline"/"underline") for\overlineand\underline. Rule 9: body in cramped style, HRule above usingOverbarVerticalGap/OverbarRuleThickness. Rule 10: body in current style, HRule below usingUnderbarVerticalGap/UnderbarRuleThickness. - Implemented TeX Rules 5 & 6 (binary atom reclassification) via two-pass algorithm in
_layout_children!: left-to-right (Rule 5) then right-to-left (Rule 6). Neutral atoms (spaces) transparent to both passes. Constants_BIN_LEFT_CANCEL/_BIN_RIGHT_CANCELas module-level tuples. - All planned items (2 → 3 → 4) complete; 649 tests passing.
- Next:
default_font_family()via Artifacts, array/matrix environments, or wide accents (\widehat/\widetilde).
- Added
\widehat => 0x02C6and\widetilde => 0x02DCto_ACCENT_CODEPOINTSinparser.jl. They share codepoints with\hat/\tilde; the layout engine distinguishes them via_WIDE_ACCENT_COMMANDS. horiz_constructionswas already parsed by_parse_math_variantsinmath_table.jl— no binary parsing changes needed.- Added
horiz_constructions::Dict{String,GlyphConstruction}field to_LayoutCtxstruct; updated both constructor call sites (layout()and theNodeKind.FontSwitchbranch). - Added
_WIDE_ACCENT_COMMANDS = Set{String}(["\\widehat", "\\widetilde"])constant. - Added
_layout_wide_accent!helper: tries pre-built variants (smallest whoseadvance >= required_du), then extensible assembly using the existing_min_extender_reps/_expand_assembly_parts/_gap_min_overlaphelpers, then falls back to largest variant. All parts centred over the base viax0 + (base_w - glyph_w) / 2. NodeKind.Accentbranch now dispatches wide accents to_layout_wide_accent!immediately after computingaccent_y; fixed-size path unchanged.- 666 tests passing (added 3 parser tests + 4 layout tests for wide accents).
- Updated
AGENTS.mdfeature table,katex_rules.mdRule 12 status, anddemo_features.jlaccent panel.
- Added
NodeKind.HorizBracenode kind (value = command name,children[1] = body). Six commands:\overbrace(uni23DE),\underbrace(uni23DF),\overbracket(uni23B4),\underbracket(uni23B5),\overparen(uni23DC),\underparen(uni23DD). - Normal
^/_parsing naturally wrapsNodeKind.HorizBraceinNodeKind.Superscript/NodeKind.Subscript/NodeKind.Decorated; the layout engine intercepts these before the standard script algorithm. - Atom class:
NodeKind.HorizBrace → :inner(matching KaTeX'sminner). _layout_horiz_brace!algorithm: body → brace (horizontally stretched via_layout_wide_accent!) → note (primary script, limits-style centered over max(body_w, note_w)) → secondary script (side-placed to the right using standard shift constants).- Brace placement: gap between body ink edge and brace ink edge =
0.1 * scale; gap between brace ink edge and note ink edge =0.2 * scale. Reference glyph y_min/y_max obtained by same variant-selection logic as_layout_wide_accent!to derive brace_y from the chosen glyph's actual metrics. - "Over" braces (overbrace/overbracket/overparen): note is the
sup, placed above. "Under" braces (underbrace/underbracket/underparen): note is thesub, placed below. The opposite script becomes the secondary (side-placed). - 712 tests passing (46 new: 9 parser + 11 layout, plus the existing KaTeX suite was unaffected).
- Updated
AGENTS.mdfeature table; addedhoriz_braces.pngdemo panel todemo_features.jl.
- Added
NodeKind.Matrixnode kind; value encodes"env\x00nrow\x00ncol", children are flat row-major cells (each aNodeKind.Group). _MATRIX_ENVSconstant maps 8 environment names to delimiter glyph names, alignment symbol, and scale factor. Barematrixandsmallmatrixhave no delimiters._read_brace_word!(p)helper reads a brace-delimited name token for\begin/\end._parse_matrix_body!handles&column separators,\\row breaks (with optional[dim]skip),\endstop, and lenient EOF. Usescopy(current_cell)to avoid Julia mutable aliasing pitfall — forgetting this made all cells empty.\begindispatches to_parse_matrix_body!for known environments; unknown environments produceNodeKind.Command. Stray\endproducesNodeKind.Space._layout_matrix!two-pass algorithm: first pass lays out all cells at origin to measure widths/heights/depths; second pass positions cells using computed column/row extents.- Grid centred on math axis via
y_shift; cells always use Text style (not Display), matching TeX array rules. - Column separation
5/18 emper side; extra row gap3/18 em. - Delimiter wrapping via
_layout_delim!; left delimiter width applied as offset before emitting content boxes (not via retroactive splicing). - Key bugs fixed during implementation: (1) Julia mutable aliasing bug with
current_cellreference, (2) wrongy0argument to_layout_delim!(should not pre-add axis height — the function does this internally), (3) sort direction bug in layout test (negating y, not reversing both keys). - 761 tests passing (25 new parser + 14 new layout + 5 new katex smoke tests).
- Updated
AGENTS.mdfeature table (Array/matrix environments: ✗ → ✓).
- Updated
README.md: added matrix/array environments to key features; added Schola/Termes/Bonum pending entries to acknowledgements table. - Bug fixed in
math_table.jl(_cff_top_dict_charset_offset):0x100000000is aUInt64literal;Int64 - UInt64promotes toUInt64, causing anInexactErrorwhen trying to push toVector{Int}. Fixed byv -= Int(0x100000000). Symptom: STIX Two Math crashed on load; all other fonts (NewCMMath, Pagella, Luciole, FiraMath) worked because their CFF Top DICT only uses b==28 (int16) offsets. - New
tools/demo_sheet.jl: generates a comprehensive single-page greyscale PNG demo sheet for any font family. Sections: fractions/roots, scripts/large ops, integrals, delimiters, accents/extensibles, font variants, matrices, array colspec. Headers rendered dark-on-light/white-on-dark via two separate composite functions. Accepts:symbolor/path/to/math.otf; default outputdemo_{symbol}.png. - Demo PNGs generated for all 5 published families: NewCMMath (2047×2300), Pagella, Luciole, FiraMath, STIXTwo (1991×2334). All in
shared/. STIX Two required the CFF fix. tools/prepare_font_artifacts.jl: downloads fonts from CTAN/GitHub, creates Julia artifact tarballs and a draftArtifacts.toml. Covers all 8 families (5 existing + Schola/Termes/Bonum). CTAN paths for TeX Gyre Schola/Termes/Bonum:mirrors.ctan.org/fonts/tex-gyre-math/{schola,termes,bonum}/texgyre{schola,termes,bonum}-math.otf+ text OTFs frommirrors.ctan.org/fonts/tex-gyre/fonts/opentype/public/tex-gyre/.src/fonts.jlscaffolding:_NAMED_ARTIFACTSand_ARTIFACT_LOADERShave placeholder comments for Schola/Termes/Bonum; the@artifact_strloader functions are intentionally absent until artifacts are published (they break precompilation if the Artifacts.toml entries don't exist). Re-add when artifacts are uploaded.- Open question: CTAN URLs for TeX Gyre text fonts — the exact filenames follow
texgyre{name}-{weight}.otfconvention but should be verified before runningprepare_font_artifacts.jl.
- Root cause: FiraMath uses Unicode-style PostScript names ("uni03C0", "uni0028", "uni221A") instead of traditional Adobe Glyph List names ("pi", "parenleft", "radical"). Three symptoms:
- Greek letters (
\alpha,\pi, etc.) rendered nothing —_cmd_glyph("pi")callsFT_Get_Name_Index("pi")→ GID 0 in FiraMath - Square root hook missing —
vert_constructions["radical"]not found (key is "uni221A") - pmatrix/bmatrix brackets missing —
vert_constructions["parenleft"]not found (key is "uni0028")
- Greek letters (
- Fix 1 — Greek letters (
_SYMBOL_CODEPOINTS): Added all 36 Greek letter commands (α–ω + variants + uppercase). The existing codepoint-lookup path (glyph_metrics_by_codepoint+glyph_name_by_codepoint) already returns the font's actual PS name, so NewCM gets "alpha" and FiraMath gets "uni03B1" — both correct for their respective renderers. Note:\epsilon→ U+03F5 (Greek Lunate Epsilon Symbol),\varepsilon→ U+03B5;\phi→ U+03D5 (Greek Phi Symbol),\varphi→ U+03C6. - Fix 2 — construction key translation (
_construction_key,_CANONICAL_CODEPOINTS): Added a helper that, given a canonical PS name ("parenleft"), looks it up invert_constructions; if absent, resolves the codepoint via_CANONICAL_CODEPOINTSand callsglyph_name_by_codepointto get the font's actual PS name, then looks that up. This is called in_layout_radical!,_peek_radical_glyph, and_layout_delim!. - Fix 3 —
_cmd_glyphfallback: Added a codepoint fallback for canonical PS names that fail the primaryFT_Get_Name_Indexlookup (used when no construction variants exist and the base glyph must be placed by name). - Key design decision: kept
vert_constructionskeyed by the font's own PS names (not normalised at parse time). The alternative — normalising "uni0028" → "parenleft" at construction time — would break the large-operator path which usesglyph_name_by_codepointto derive the key dynamically. - All 789 tests pass. FiraMath demo regenerated with sqrt, pi, and brackets all rendering correctly.
- Audit result: 156 commands in
_CMD_ATOM_CLASShave no codepoint entry in_SYMBOL_CODEPOINTSor_DISPLAY_OP_CODEPOINTS. These include extended AMS binary operators (\boxplus,\ltimes,\intercal, …), extended relations (\leqslant,\bowtie,\therefore,\because, all negated relations), extended geometry and misc ordinary symbols (\measuredangle,\triangle,\square,\checkmark,\S,\P,\yen, …), and rare delimiter aliases (\lgroup,\llbracket,\lvert,\lVert, …). - Practical impact: standard fonts (NewCM, Pagella, STIXTwo) work because they use AGL PS names matching the command names. FiraMath and Luciole silently produce blanks for all 156.
- Strategy decision: remove PS-name lookup from
_cmd_glyphand_char_glyphentirely; use codepoints exclusively.- Reason 1 (correctness):
glyph_metrics(family, "x")in NewCMMath returns the upright roman form, not math-italic. The cmap (used byglyph_metrics_by_codepoint) correctly gives math-italic 'x' because math fonts map U+0078 → italic glyph by design. - Reason 2 (portability): eliminates the naming divergence problem across all current and future fonts.
- Remaining PS-name use:
_construction_key(bridging canonical AGL names to font-own MATH-table PS names forvert_constructions/horiz_constructions) — cannot be eliminated because the MATH table itself uses PS names.
- Reason 1 (correctness):
- Multi-codepoint issue: ~10 negated/variant commands (
\nleqslant,\lvertneqq,\varsubsetneq, etc.) have no single Unicode codepoint — they're defined as base + U+0338 (COMBINING SOLIDUS OVERLAY) or U+FE00 (VARIATION SELECTOR). Math fonts don't encode these at a consistent single codepoint. Current state: produce blank space. Fix requires two-glyph overlay (base + combining stroke). Documented in AGENTS.md Known Limitations. - Next step: bulk-expand
_SYMBOL_CODEPOINTS(~140 clean additions), simplify_char_glyphand_cmd_glyphto codepoint-only paths.
- Completed the codepoint-only strategy (continued from earlier session):
_DISPLAY_OP_CODEPOINTS: addediiiint(U+2A0C),oiint(U+222F),oiiint(U+2230)._SYMBOL_CODEPOINTS: added ~85 new entries — AMS binary operators (\boxplus,\ltimes,\curlywedge, etc.), extended relations (\leqslant,\lesssim,\bowtie,\doteqdot,\Subset,\preccurlyeq, etc.), negated relations with single codepoints (\nleq,\nprec,\subsetneq, etc.), ordinary symbols (\measuredangle,\imath,\triangle,\checkmark,\mho,\Finv, etc.), delimiter aliases (\lvert,\lVert,\llbracket,\lgroup,\lmoustache), and punctuation (\colon,\cdotp,\ldotp)._char_glyph: removedisletterPS-name-first block; now always resolves by codepoint. Fixes math-italic letter rendering (PS name "x" → upright roman; cmap U+0078 → italic by font design)._layout_node!NodeKind.Command else-branch: replaced_cmd_glyphfallback withnothing; commands not in_SYMBOL_CODEPOINTSsilently produce no glyph._cmd_glyph: updated comment to document it is now used exclusively for MATH table font-internal names (size variants, assembly parts).
- All 789 tests pass. Committed as
ddad75e. - Remaining open items: two-glyph overlay for multi-codepoint negated relations; Makie integration; Schola/Termes/Bonum artifacts.
- Root cause:
_layout_wide_accent!'s_place()helper centred glyphs byadvance_width/2. For zero-advance combining characters (e.g. New CMcircumflexcmb: adv_w=0, x_min=-446, x_max=-82; STIX Twouni0302: adv_w=0, x_min=-371, x_max=-89), this placed the glyph atx0 + base_w_em/2but the ink lay entirely to the left of that position. - Same bug in the NodeKind.Accent fixed-size fallback (
accent_w = advance_width * s / upm). - Fix: replace
advance_width/2with ink midpoint(x_min + x_max)/(2*upm)*scale. For standard positive-advance glyphs (x_min≈0, x_max≈advance_width), the result is numerically identical to the old formula. - FiraMath limitation: FiraMath has no widehat/widetilde in
horiz_constructions(only has entries foruni23B4/B5anduni23DC–DF). Falls back to a single fixed-size combining circumflex, now correctly centred.
- Root cause:
_layout_assembly!centred the stacked assembly on the math axis usingtotal_du/2(half the sum of cursor advances). For STIX Twobar(y_min=−234, y_max=706, full_advance=941), the ink centre is at 0.236 em, not at 0.258 em (axis). The resulting bar was centred 0.23 em below the axis, making vmatrix bars appear too low relative to enclosed digits. - Fix: look up actual glyph metrics of the first and last assembly parts; compute actual ink bounds (
ink_top_du = cursor_last + g_last.y_max,ink_bot_du = g_first.y_min); centre on(ink_top_du + ink_bot_du)/2. For fonts where y_min=0, y_max=full_advance (e.g. New CM), reduces to the old formula identically. - Note: STIX Two
bardoes have an assembly (1 extender + 1 end piece, bothfull_advance=941, min_overlap=100). For a 2×2 digit matrix the assembly uses n=2 extenders producing ~2523 du for a ~2216 du required span — adequate coverage.
- Root cause: when a pre-built radical variant is larger than the minimum required span, the body was placed at
y0with all excess space appearing below it. This made\sqrt{\pi}show π crammed at the top of an oversized hook (visible in FiraMath demo). - Fix: after
_layout_radical!selects the glyph, peek again with_peek_radical_glyph(ctx, required_du)to determine actual ink spang.y_max - g.y_min. Computebody_shift = max(0, actual_span_du - required_du) / (2*upm) * scale. Shift body boxes DOWN bybody_shift. Rule bar and radical placement unchanged. - Result: excess space is split equally —
body_shiftextra clearance above body (between body top and rule bar) andbody_shiftspace below body (between body bottom and hook tip). For assemblies, peek returns the last variant which has advance < required_du, so body_shift = 0 (no shift for assemblies). For the base glyph fallback, peek also returns without a matching variant, giving body_shift = 0. - All 789 tests pass. Committed as
24f2e7a.
- Centering was already correct after the previous session's fix: numerical check shows
hat_center - base_center = 0.0for all fonts and all tested expressions. The ink-midpoint formula(x_min + x_max) / (2*upm) * scaleis exact for sized variants (x_min=0) and also correctly handles the zero-advance combining base glyph. - Width was the real issue: the demo expression
\widehat{f(x+y)}has a base of ~3.5em, but the largest pre-built hat variants are: NewCM 1.897em, Pagella 1.499em, STIX Two 2.385em, Luciole 3.001em. None of these fonts have a hat assembly (horiz_constructions hasassembly=nothingfor all). The fallback to the largest variant produces a hat that covers only 53%–82% of the base, which is visually wrong. - Fix: changed demo expression from
\widehat{f(x+y)}to\widehat{xyz} + \widetilde{xyz}.xyzhas a base ~1.4–1.6em which fits within all fonts' hat variant sizes (ratios: NewCM 1.04, Pagella 1.04, STIX Two 1.24, Luciole 1.00). STIX Two has coarser variant spacing so the hat is 24% wider than the base — a font property, not a code bug.
- Fira Math has a full Fira Sans companion (regular, italic, bold, bold-italic all present in artifact). The font family IS complete.
- Fira Math lacks widehat/widetilde in its MATH table horiz_constructions (only has overbrace/underbrace variants).
\widehatalways falls back to the fixed-size combining circumflex (uni0302, adv=0), correctly centred by the ink-midpoint formula. - Fira Math also lacks calligraphic and fraktur Unicode math alphabets —
\mathcal{H}renders as upright H,\mathfrak{g}as regular g. Code is correct; this is a Fira Math v0.3.4 font limitation.
- Bug fixed:
\overbrace(and all other horiz brace commands) was silently missing for Luciole because_HORIZ_BRACE_GLYPHSmapped\overbrace→"uni23DE", but Luciole uses the AGL name"overbrace"in itshoriz_constructionstable.- All other fonts (NewCM, Pagella, STIX Two, FiraMath) use
"uni23DE"— Luciole is the outlier. - Same pattern as the existing
_construction_keyissue (FiraMath uses"uni0028"instead of"parenleft"in vert_constructions), but in the opposite direction.
- All other fonts (NewCM, Pagella, STIX Two, FiraMath) use
- Fix: Added
_horiz_construction_key(ctx, uni_name)— mirrors_construction_keyfor horiz_constructions. Resolves"uni{HHHH}"to font's actual PS name viaglyph_name_by_codepoint; covers all six brace commands simultaneously. - Defensive fix: Extended
_cmd_glyphwith a second fallback path: if the name looks like"uni{HHHH}", parse the codepoint and resolve viaglyph_name_by_codepoint. This handles the symmetric case where future code might pass a Unicode-style name to a font using AGL naming. - Audit result: No other unhandled PS name vs codepoint gaps in current code paths. All other callsites either use font-native construction table names, properly-resolved
glyph_name_by_codepointresults, or the existing_CANONICAL_CODEPOINTSmechanism.
- Investigation: KaTeX applies italic correction to limit placement for slanted operators (e.g.
\int). Inop.ts,slant = base.italic ?? 0(the MATH table italic correction). InassembleSupSub.ts, subscripts getmarginLeft: -slantand superscripts getmarginLeft: +slant. KaTeX's comment notes the intent is ±½ slant, with the CSS centering making a full-margin shift achieve that half-shift. - Scale of effect: NewCMMath's
integral.v1(display-size\int) has IC = 459 design units = 0.459 em — nearly half an em. This produces a very visible shift at display size. Symmetric operators (\sum,\prod) have IC = 0, so they are unaffected. - Fix: Added
italic_corrections::Dict{String,Int}to_LayoutCtx(already parsed from the MATH table — no new parsing needed). Added_base_italic_correction_emhelper that reads the first glyph's IC from a box list. Applied±IC/2to all three limits branches (NodeKind.Superscript, NodeKind.Subscript, NodeKind.Decorated): subscripts shift left, superscripts shift right. - OpenType MATH spec basis: Offset limits by ±½ italic correction to track the slanted stroke.
- Bug:
\int_0^\inftyshowed subscript and superscript horizontally aligned — no italic correction effect visible. Root cause:\intuses side placement (sub/sup to the right), not limits placement, so the limits-branch IC fix from the previous session had no effect. - Key distinction:
_use_limitsreturnsfalsefor\int— integrals always use side placement in standard TeX. The limits-placement IC fix (±½IC applied above/below) was correct but irrelevant for integrals. - KaTeX rule for side placement (
supsub.tslines 117–131): for single-symbol bases,marginLeft = makeEm(-italic_correction)is applied only to the subscript. Superscripts are not shifted. This moves the subscript left by the full IC so it sits under the stroke bottom rather than the advance width. - Fix: Applied
ic_em = _base_italic_correction_em(tmp_base, ctx, scale)in both side-placement branches:NodeKind.Subscript:x0 + base_adv - ic_em + b.xNodeKind.Decorated(sub+sup together): subscript atscript_x - ic_em, superscript atscript_xunchanged
- Verification: For
\int_0^\inftywith NewCMMath, subscript (zero) lands at x=0.5400, superscript (infinity) at x=0.9990. Difference = 0.459 em = IC ofintegral.v1(459/1000 du). Correct. - No effect on symmetric operators:
\sum,\prodetc. have IC = 0, so they are unaffected. - All 789 tests pass. Demo sheets regenerated.
- Scope: Audit for idiomatic Julia, abstractions, logic bugs, and KaTeX-equivalence; apply agreed fixes incrementally.
- Bugs found and fixed:
- Lexer UTF-8 unsafe:
src/lexer.jladvanced byte indices withi += 1rather thannextind. Any multi-byte char in math input (e.g.α + β) raisedStringIndexErroron the first non-ASCII codepoint. Fixed; two regression tests added. - Brace fallback inversion in
_layout_horiz_brace!: when the font lacked a brace glyph thebrace_top/brace_botplaceholder values referenced the wrong side of the body in bothis_overand!is_overcases, so any secondary script note would have been mis-positioned. Rare path (none of the bundled fonts hit it) but logic is now consistent with the comment above.
- Lexer UTF-8 unsafe:
- Idiomatic clean-up:
- Deduplicated
_HORIZ_BRACE_COMMANDS(parser) /_HORIZ_BRACE_GLYPHS(layout) — same six-entry dict in two places; reduced the parser-side copy to aSet{String}of recognised commands. NodeKind.Spacenow carries awidth::Float64field instead of round-tripping the value throughString(Node(NodeKind.Space, "0.5")→parse(Float64, sp.value)on every layout). Added aspace_node(w)constructor.- Factored the 22 repeated
for b in tmp; push!(boxes, LayoutBox(b.element, dx+b.x, dy+b.y, b.scale)); endloops into_emit_shifted!(boxes, src, dx, dy). - Added
_with_variant(ctx, variant)helper soNodeKind.FontSwitchdoesn't have to rebuild the whole 10-field_LayoutCtxby hand.
- Deduplicated
- Architectural refactor:
- Split
_layout_node!per kind (src/layout.jl): the 500-line if/elseif chain that handled everyNodeKindis now a thin dispatcher delegating to_layout_X!helpers (one per kind). Behaviour is byte-identical; the goal was readability — each rule now lives in a function of 5–80 lines. glyph_metrics/glyph_metrics_by_codepointnow returnUnion{GlyphMetrics, Nothing}instead of throwing. This is a breaking API change. Every internal caller previously wrapped them intry/catch return nothing end; the new contract removes the exception-driven control flow from the hot path. Test for@test_throws Exceptionreplaced with=== nothing.
- Split
- Considered and rejected:
- Removing the redundant
scaleparameter (which always equalssize_scale(style, mc)): would touch ~40 sites mechanically for no functional gain and remove explicit documentation of the contract. - Reclassifying
NodeKind.HorizBracefrom:innerto:ord: KaTeX'shorizBrace.tsemitsminner, so our current classification matches. - Factoring the 3× repeated
base.kind === NodeKind.HorizBrace && return _layout_horiz_brace!(...)dispatch: only 2 lines per site after the per-kind split, so factoring would add ceremony without saving meaningful code.
- Removing the redundant
- Verification: All 797 tests pass after every commit (originally 789; +8 for new Unicode lexer tests and one nothing-return test). Smoke-tested a 18-input suite including direct-Unicode and empty/malformed inputs.
- Audited all entries in
_CMD_ATOM_CLASSagainst_SYMBOL_CODEPOINTSand_DISPLAY_OP_CODEPOINTS. - The 2026-05-24 session had already added ~85 entries; the residual "genuinely missing" symbols were:
\doteq(U+2250),\Join(U+2A1D),\Bbbk(U+1D55C),\backslash(U+005C). All four added. - All remaining entries in
_CMD_ATOM_CLASSwithout a codepoint are legitimately handled by other code paths (font switch → NodeKind.FontSwitch, accents → NodeKind.Accent,\big*delimiters,\bmod/\pmod/\xleftarrow/\xrightarrow, ellipsis variants), or are negated composites without a single codepoint (documented limitation), or\bigplus(no standard Unicode codepoint). - AGENTS.md Known Limitations updated: "~150 AMS symbols missing" bullet replaced with accurate
\bigplusnote. - 798 tests pass.
-
Goal: Make CairoMakie/GLMakie use TeXLayout's OpenType-aware typesetter when both packages are loaded, as a transparent drop-in replacement for MathTeXEngine's layout engine.
-
Architecture: Julia package extension system (
[weakdeps]+[extensions]in Project.toml). Extensionext/MathTeXEngineExt.jloverridesMathTeXEngine.generate_tex_elementswhen both MathTeXEngine and GeometryBasics are loaded. GeometryBasics is listed as a co-trigger because it is always transitively loaded with MathTeXEngine and the extension needsPoint2f. -
Key constraint —
__precompile__(false): Julia raises"Method overwriting is not permitted during Module precompilation"when an extension replaces an existing method. The extension opts out of precompilation entirely; methods are JIT-compiled at runtime as normal. -
Data flow:
generate_tex_elements(str)receives aLaTeXStringwhose content is"$...$"— strip surrounding$before passing toparse_latex.layout(node, tl_family, Display)returns a flat list ofLayoutBoxvalues.- Each box is converted to
(MTE.TeXChar, Point2f, Float64)or(MTE.HLine, …)/(MTE.VLine, …). - Makie's
texelems_and_glyph_collectionconsumes the result unchanged (it filters onisa MathTeXEngine.TeXCharetc., so real MTE types are mandatory).
-
Glyph resolution:
FreeTypeAbstraction.glyph_index(font, name::String)for PostScript names, with fallbacks for single-char names anduniXXXXencoded names. -
MTE FontFamily construction: Built from TeXLayout's
FontFamilyabsolute paths;MathTeXEngine.FontFamily(Dict(:math => path, ...))passes absolute paths through unchanged. -
Position type: Must be
Point2f(aStaticVector/VecTypes) — Makie'sto_ndimrequiresVecTypes; plain tuples fail. -
Validation: Smoke-tested with
L"x^2"(2 TeXChars at correct positions/scales),L"\frac{a}{b}"(1 HLine), and a CairoMakie figure with five formulae (fraction, sum, integral, sqrt, Greek letters). All rendered correctly. -
Files added/modified:
ext/MathTeXEngineExt.jl(new): full extension with five helper functionsProject.toml: added[weakdeps]and[extensions]sectionstools/demo_makie.jl(new): self-contained CairoMakie demoAGENTS.md: updated "Makie integration" status in Known Limitations
-
Open questions / future work:
- The
font_familyargument to the overriddengenerate_tex_elementsis currently ignored; TeXLayout always usesdefault_font_family(). Could honour caller-specified fonts in future. __precompile__(false)adds ~1 s first-call compilation overhead; a more surgical fix (overloading onstr::LaTeXStringspecifically to avoid the same-signature restriction) could restore precompilation, but requires investigation.
- The
-
\middleauto-sizing (NodeKind.Middle):- Added
NodeKind.MiddletoNodeKindenum; value holds PS glyph name. _parse_delimited_children!now intercepts\middletokens and emitsNodeKind.Middlenodes instead of falling through to the command branch._layout_delimited!refactored to segment children atNodeKind.Middleboundaries; measures combined content height across all segments; then calls_layout_delim!for left, each middle, and right delimiter with identicalrequired_du— all delimiters auto-size to the same height.- Multiple
\middledelimiters per group are supported (n+1 segments for n middles).
- Added
-
\text{}/\mbox{}upright rendering (NodeKind.Text):- Parser previously had
NodeKind.Textin the enum but never produced it;\textfell through to the command error branch. - Added explicit
cmd == "\\text" || cmd == "\\mbox"case in_parse_command!; consumes brace argument, returnsNode(NodeKind.Text, [body]). _with_text_modehelper copies_LayoutCtxwithmode = :text._layout_char!uses_upright_glyphwhenctx.mode === :text— regular font, no italic remapping._layout_text!applies_with_text_modeand delegates to the child node.- Dispatch added:
k === NodeKind.Text && return _layout_text!(...)in_layout_node!. - Inter-atom spacing already suppressed inside text fragments (guard on
ctx.mode === :math).
- Parser previously had
-
All 813 tests pass. Stress test PNG generated for visual verification.
-
Committed as
139bced.
-
Problem: Both
\mathrmand\text{}were routing through_upright_glyph, which is semantically wrong.\mathrmis a math-mode command and must use the math font's own codepoint lookup (_char_glyph), not the text font.\text{}correctly uses_upright_glyph(prefersfamily.regular). -
Fix 1 —
\mathrmuses math font: Removed the:mathrm → _upright_glyphearly-return branch from_variant_glyphinlayout.jl. The function now falls through to_char_glyphfor\mathrm, which uses math-font codepoint lookup. -
Fix 2 — whitespace preserved as
TokenKind.Spacetokens: Lexer previously silently discarded all whitespace runs. Changedlexer.jlto emitToken(TokenKind.Space, " ", i)for each whitespace run (collapsed to a single token). This enables the parser to see spaces and handle them mode-appropriately. -
Fix 3 — math-mode parser skips
TokenKind.Space: AddedTokenKind.Spaceskip guards to:_parse_sequence_children!— skipsTokenKind.Spacein math-mode groups._parse_delimited_children!— skipsTokenKind.Spacebefore\right/\middlecheck._parse_matrix_body!— skipsTokenKind.Spaceas a handled token kind._parse_atom!— already had awhile TokenKind.Spaceskip; reviewed and confirmed correct.
-
Fix 4 — text-mode preserves spaces: Added
_parse_text_sequence_children!which convertsTokenKind.SpacetoNode(NodeKind.Char, " "). Added_parse_text_argument!which uses this for\text{}/\mbox{}brace arguments. -
Fix 5 —
' 'in layout emitsSpaceelement: In_layout_char!, added an early-return branch forctx.mode === :text && ch == ' 'that callsglyph_metrics_upright(ctx.family, ' ')to get the font's word-space advance and emitsLayoutBox(Space(w), x0, y0, scale). -
Semantic distinction documented:
\mathrm→ math font (_char_glyph);\text{}→ regular font (_upright_glyph). Both currently use the math font's PS name in theGlyphstruct — acceptable for matched font families like NewCM. -
6 new tests added; 819 total, all pass.
-
Committed to
TeXLayout.jl.
-
Problem:
Glyphonly stored a PS name; the renderer always usedfamily.mathto resolve it.\text{}glyphs usedfamily.regularfor metrics but the math font for rendering — inconsistent for mismatched families. -
Fix —
font_slotfield onGlyph: Addedfont_slot::Symbol(:math|:regular) as the second field ofGlyph. All glyph builders set this explicitly:_char_glyph,_cmd_glyph,_variant_glyph,_layout_command!,_layout_accent!→:math_upright_glyph→:regularwhenfamily.regular !== nothing(and looks up PS name from the regular font);:mathas fallback.
-
PS name consistency fix: Previously
_upright_glyphgot metrics fromfamily.regularbut the PS name from the math font. Now both come from the same font. Addedglyph_name_by_codepoint(font_path::String, cp::UInt32)overload tofonts.jlto support this. -
Makie extension updated:
_box_to_mtenow takesreg_fontalongsidemath_font; dispatches onel.font_slotto select the correct FreeType face forglyph_indexresolution.generate_tex_elementsloadsreg_font(falling back tomath_fontif no regular font configured). -
Known limitation (pre-existing): Metric values for regular-font glyphs are divided by
ctx.upm(the math font's UPM). Iffamily.regularhas a different UPM thanfamily.math, the em-unit widths will be slightly wrong. In practice, all supported font families use UPM=1000 consistently, so this is not currently an issue. -
821 tests, all pass. Committed.
-
Root causes identified and fixed for three Luciole-specific bugs:
-
Issue #1 & #2 — blank glyphs in \text{} and named operators (\sin etc.)
_upright_glyphcorrectly producesfont_slot = :regularglyphs with PS names fromregular.ttf(e.g. "s", "i", "n")- Both rendering tools (
demo_sheet.jl,stress_test_sheet.jl) calledrenderface(face_math, el.glyph_name, ...)unconditionally — Luciole Math has no glyph named "s" → blank - Fix: load
face_regular = FTFont(family.regular)when available; select render face byel.font_slotat render time - Note: using the regular font for operators is architecturally correct — it matches KaTeX behaviour
-
Issue #3 — Luciole accents dropped (\dot{q}, \tilde{a}, \breve{u}, etc.)
_ACCENT_CODEPOINTSuses spacing modifier codepoints (U+02C6 ˆ, U+02DC ˜, U+02D8 ˘, U+02D9 ˙, U+02C7 ˇ, U+00B4 ´, U+02DA ˚)- Luciole Math carries these at combining-form codepoints (U+0302–U+030C) instead — primary lookup returned "" → silent drop
- Fix: added
_ACCENT_FALLBACK_CODEPOINTSinlayout.jlmapping the 10 most common accent commands to their combining equivalents;_layout_accent!now tries fallback before giving up - Affected accents recovered: \hat, \acute, \tilde, \breve, \check, \dot, \mathring, \ddot, \grave, \bar
-
Incidental fix:
stress_test_sheet.jlwas usingmagick(not installed); aligned withdemo_sheet.jlto useconvert -
All three fixes are in commit
4a22217; verified visually with Luciole stress test and demo sheets, and NewCM regression check passed
- Benchmarked the TeXLayout/Makie integration in a temporary Julia environment using
BenchmarkTools, withMathTeXEngine,LaTeXStrings, andGeometryBasicsloaded so the package extension was active. - Current hot path findings:
src/layout.jl:2504-2515callsload_math_table(family.math)on every layout.src/math_table.jl:757-780reparses the font file every time; there is no MATH-table cache.src/fonts.jl:49-87already caches FreeType font handles and parsedhmtxtables by path, so lower-level font-face reuse exists but higher-level math-font metadata reuse does not.ext/MathTeXEngineExt.jl:147usesresult = Tuple[], producingVector{Tuple}and a type-unstable accumulation path in the Makie-facing conversion stage.
- Benchmark summary on repeated calls with the default font:
- Small expression
x^2+y^2=z^2:parse_latex~0.7 μs;load_math_table~0.97 ms;layout(node, ff, Display)~0.65 ms;MathTeXEngine.generate_tex_elements~0.70 ms. - Larger expression with
\int,\frac, and\sum:parse_latex~5.9 μs;load_math_table~0.95 ms;layout(node, ff, Display)~1.11 ms;MathTeXEngine.generate_tex_elements~1.06 ms. load_math_tablealone allocates ~2.31 MiB per call, essentially dominating repeated-call cost.
- Small expression
- Simulated the effect of a per-font cache by reusing a prebuilt
_LayoutCtx:- Cached lower-level layout: ~24 μs (small) and ~160 μs (large).
- Cached MTE conversion from existing boxes: ~21 μs (small) and ~249 μs (large).
- This suggests a per-font runtime cache could reduce repeated-call latency by roughly an order of magnitude for small formulas and by multiple times for larger ones, while also removing most current allocation pressure.
- Agreed implementation scope for the first four speed-focused items:
- cache parsed
MathTabledata by math-font path, - cache a higher-level Makie runtime bundle by effective
FontFamily, - remove abstract tuple accumulation in the Makie extension,
- cache glyph-name to glyph-index lookups per loaded font.
- cache parsed
- Added execution todos and dependencies in the session SQL tracker:
perf-cache-math-tableperf-cache-makie-runtimedepends onperf-cache-math-tableperf-cache-glyph-indexdepends onperf-cache-makie-runtimeperf-validate-benchmarksdepends on all implementation items
- Plan emphasis: keep the public API unchanged, build on the existing font cache in
src/fonts.jl, and benchmark each step separately so the impact of each optimization remains attributable.
- Implemented
MathTablecaching insrc/math_table.jlvia_MATH_TABLE_CACHE, keyed by math-font path.load_math_tablenow does a cached lookup instead of rereading and reparsing the font on repeated calls. - Implemented a cached Makie runtime bundle in
ext/MathTeXEngineExt.jl, keyed by the effectiveFontFamilypath tuple. The bundle reuses:- loaded math and regular
FTFonthandles, - the derived
MathTeXEngine.FontFamily, - separate glyph-name → glyph-index dictionaries for the math and regular font handles.
- loaded math and regular
- Replaced the old abstract
Tuple[]accumulation with a concrete_MTEElementTuplevector, soMathTeXEngine.generate_tex_elements(::LaTeXString)now infers a concrete return type instead ofVector{Tuple}. - Minor helper cleanup:
_single_charavoidscollect(name)in glyph-name handling, reducing per-call string work in the conversion path. - Added a regression test in
test/test_math_table.jlasserting that repeatedload_math_table(FIXTURE_FONT_PATH)calls return the same cached object. - Validation results:
- test suite: 862/862 passing
parse_latex: still ~0.7 us small / ~6.2 us largeload_math_table: ~60 ns, 0 allocations after warm-uplayout: ~25 us small / ~162 us largeTeXLayout.generate_tex_elements: ~26 us small / ~169 us large
MathTeXEngine.generate_tex_elements: ~48 us small / ~199 us large- Net effect versus the earlier baseline: repeated Makie rendering with the same font now avoids the ~0.95 ms MATH-table parse and multi-megabyte allocation spike on every call, moving the steady-state cost into the tens to low hundreds of microseconds for the benchmarked formulas.
- Ran fresh steady-state
BenchmarkToolsbenchmarks in two separate temporary Julia environments:- Plain MathTeXEngine: loaded
MathTeXEngineandLaTeXStrings, without loadingTeXLayout - TeXLayout Makie path: loaded
TeXLayout,MathTeXEngine,LaTeXStrings, andGeometryBasicsso the package extension was active
- Plain MathTeXEngine: loaded
- Benchmarked the same two formulas in both cases via
MathTeXEngine.generate_tex_elements(::LaTeXString)after warm-up:- small:
x^2+y^2=z^2 - large:
\int_0^\infty e^{-x^2}\,dx = \frac{\sqrt{\pi}}{2} + \sum_{n=1}^{\infty} \frac{(-1)^n}{n^2}
- small:
- Median results:
- Plain MathTeXEngine
- small: 155864 ns, 74160 bytes, 1568 allocs
- large: 1131836 ns, 251256 bytes, 5747 allocs
- TeXLayout extension
- small: 48040 ns, 21528 bytes, 192 allocs
- large: 147548 ns, 51608 bytes, 439 allocs
- Plain MathTeXEngine
- Relative steady-state speedup of the cached TeXLayout path over plain MathTeXEngine:
- small formula: ~3.2x faster, ~3.4x less memory, ~8.2x fewer allocations
- large formula: ~7.7x faster, ~4.9x less memory, ~13.1x fewer allocations
- Caveat: this is a Makie-facing end-to-end comparison, not a claim that the internal layout algorithms are directly equivalent. The formulas and output API were matched, but the two engines make different implementation choices and do not emit identical intermediate structures.
- Added
tools/png_diff.jlto compare two rendered PNGs from TeXLayout runs. - The tool flattens each input onto white, converts to grayscale, and computes a signed pixel delta
after - before. - Output encoding:
- zero delta -> white,
- positive delta -> green tint with intensity proportional to
abs(delta), - negative delta -> red tint with intensity proportional to
abs(delta).
- Dimension mismatches are now handled by padding both inputs onto a white canvas sized to the per-axis maximum of the two images; the tool emits a warning describing the original and padded sizes.
- Implementation uses ImageMagick via whichever binary is available on the host (
magickorconvert), matching the existing toolchain approach.
- Updated
AGENTS.mdto reflect the current tree layout, includingext/MathTeXEngineExt.jl, the broadertools/set, artifact-managed fonts, and repo-level notes files. - Added explicit developer guidance that Julia code should be formatted with Runic.jl, using the
runiccommand in Bash or the global Julia environment@runic. - Documented the current cache layers more accurately: font cache in
src/fonts.jl,MathTablecache insrc/math_table.jl, and the Makie runtime cache inext/MathTeXEngineExt.jl.
- Updated
docs/src/91-developer.mdso the architecture overview no longer claims the pipeline only mutates the font cache; it now documents both the font-handle cache and the parsedMathTablecache. - Added a focused developer-doc note that
load_math_table(path)memoizes the OpenType MATH table by math-font path, which matters for repeated layout and Makie rendering. - Updated
docs/src/03-makie.mdto mention the extension/runtime caching story in steady-state Makie usage.
- New task: add a command-line visualisation tool under
tools/that reproduces the style of the older MathTeXEngine debug view for arbitrary expressions. - Chosen implementation approach: use TeXLayout's own
parse_latex+layoutoutput and render a custom PNG with helper overlays, rather than introducing a CairoMakie dependency into the package project. - Planned visual layers: rendered glyph/rule output, baseline + math-axis guides, and coloured metric overlays inspired by
external/MathTeXEngine.jl/prototype/prototype.jl(left bearing / post-ink advance / above-baseline ink / descender). - Need to update
AGENTS.mdtool list once the script exists and then run formatting plus the relevant Julia checks.
- Added
tools/visualise_metrics.jl, a self-contained command-line visualiser that renders TeXLayout output with MathTeXEngine-style metric overlays. - The tool uses only
TeXLayout+FreeTypeAbstraction, so it stays inside the package project rather than depending on CairoMakie. - Output layers:
- black glyph/rule rendering,
- grey baseline and red math-axis guides,
- yellow origin-to-left-ink region,
- green right-ink-to-advance region,
- red above-baseline ink region,
- blue descender region,
- outline/origin/advance guides per glyph.
- CLI shape:
julia tools/visualise_metrics.jl "expr" [out.png|out.ppm] [:font_symbol|/path/to/font.otf]. - Added a subprocess smoke test in
test/test_tools.jl; because the tool self-activates viausing Pkg, the test restoresJULIA_LOAD_PATH=@:@stdlibbefore launching it.
- New follow-on task: add
tools/visualise_metrics_makie.jlso the expression itself is drawn by Makie'stext!, with metric boxes layered over the top. - Dependency plan: use the existing
examples/environment for CairoMakie, LaTeXStrings, and MathTeXEngine rather than adding Makie to the main package project. - Alignment plan: match the old MathTeXEngine prototype by placing the text at
(0, 0)in data space withalign = (:left, :baseline)and scaling all overlay geometry from TeXLayoutLayoutBoxcoordinates by the chosen font size. - Need to confirm how best to make the local TeXLayout checkout visible from that environment, because the committed
examples/Manifest.tomlstill points at a machine-local dev path.
- Added
tools/visualise_metrics_makie.jl, a CairoMakie-backed companion tovisualise_metrics.jl. - The tool activates the repo's
examples/environment (for CairoMakie / LaTeXStrings / MathTeXEngine) and pushes the local TeXLayout checkout ontoLOAD_PATHso the current workspace code is used. - Important debugging result: the first attempts misaligned the overlays because Makie applies an internal
tex_offsetinsidetexelems_and_glyph_collection; I also accidentally applied that offset to the wholetext!call once, which shifted the rendered formula twice. - Final implementation:
- renders the formula with a single Makie
text!call at(0, 0), - queries Makie's own
texelems_and_glyph_collectionhelper to obtain the exact internaltex_offset, - derives overlay rectangles from the same
MathTeXEngine.TeXCharmetrics that Makie uses to render, - applies the internal offset only to the overlays and guide geometry, not to the
text!anchor.
- renders the formula with a single Makie
- Smoke render for
\\frac{a}{b}now aligns numerator and denominator overlays correctly in the saved PNG.
- Investigated the visible mismatch at the leading join of
\sqrt{...}insrc/layout.jl. - Current implementation in
_layout_radical!places the radical glyph so that its overally_maxaligns with the top of the separately drawnHRule. - For NewCMMath this is not the correct visual anchor: raster inspection of the base
radicalglyph at a large size shows the topmost ink is a short slanted tip, while the long flat vinculum begins several pixels lower. - Relevant metrics for the base radical glyph are
advance_width = 833,x_max = 853,y_min = -960,y_max = 40; the flat rule is therefore currently joined using box metrics that describe the full outline, not the actual hook-to-vinculum transition. - This explains why the hook appears slightly low relative to the top rule even though the layout boxes report exact
y_max/rule-top agreement. - Secondary finding:
RadicalExtraAscenderis parsed from the MATH table (40du in NewCMMath) but is not currently used anywhere in radical layout. This is likely a separate completeness issue for overall radical height / reserved whitespace, but it does not explain the visible join mismatch by itself. - KaTeX avoids this exact problem by drawing the radical and vinculum as one SVG path (
external/KaTeX/src/svgGeometry.ts) rather than splicing a font radical glyph to a separate rectangular rule. - Proposed implementation direction:
- derive a radical-specific vertical join metric from the chosen glyph / top assembly part, and align the rule to that metric rather than to
y_max; - audit the horizontal join point as well, since the current splice uses
advance_widtheven though the glyph ink extends tox_max; - fold
RadicalExtraAscenderinto the radical box height / clearance calculation once the visual anchor is corrected; - add a rendered regression test so this class of mismatch is caught by pixels, not only by layout-box bounds.
- derive a radical-specific vertical join metric from the chosen glyph / top assembly part, and align the rule to that metric rather than to
- Checked
unicode-math/ LuaTeX sources for how square roots are built. unicode-mathdefines radicals in terms of engine primitives (\Uradicaland\Uroot) rather than computing a visible hook/vinculum join from user-level box metrics.- The LaTeX tagging adaptation in
latex3/latex2e(required/latex-lab/latex-lab-unicode-math.dtx) reinforces this: the preferred path for\sqrtand\root...\ofis\tex_Uradical:D/\tex_Uroot:D, with older box constructions used only as fallbacks for some offset cases. - Takeaway for TeXLayout: LuaTeX is useful as a rendering oracle and as evidence that radicals should be treated as a font/engine construction problem. It does not expose a standard public “join metric”; that still appears to need deriving from the selected radical glyph / assembly geometry.
- Updated
src/layout.jlso radical variants are no longer aligned by raw glyphy_max; the rule now joins at a derived radical join heighty_max - RadicalExtraAscender. - Radical selection now treats
RadicalExtraAscenderas part of the required total height, while body-centering and Rule 11 redistribution use the span from the radical bottom up to the rule join. - Changed the horizontal splice so the radicand begins at the radical's right ink bound (
x_max/ assembly maxx_max) and the top rule starts half a rule thickness earlier for a small overlap, following the spirit of the old MathTeXEngine construction. - Extended the
\sqrt{x}layout test to check:- the radicand starts at the radical's right ink bound,
- the rule overlaps the join by half the rule thickness,
- the radical top sits
RadicalExtraAscenderabove the rule top.
- Full test suite passes after the change (867/867), and
\sqrtsmoke tests succeeded across all bundled font families.
- After visual feedback, backed out the vertical
RadicalExtraAscenderanchor change. - The mistake was treating
RadicalExtraAscenderas if it were the visible hook/vinculum join offset. Raster inspection does not support that for NewCMMath: the visible flat top sits much farther belowy_maxthan one extra-ascender step. - Current state:
- keep the original vertical anchoring by glyph
y_max, - keep the horizontal splice fix (body starts at the radical right ink bound; rule overlaps by half a rule thickness),
- keep the stronger layout regression test for the horizontal join.
- keep the original vertical anchoring by glyph
- Full test suite passes after the correction (866/866).
- Fixed the TeXLayout → MathTeXEngine adapter in
ext/MathTeXEngineExt.jlso rule positions are converted between the two geometry conventions:TeXLayout.HRule.yis the rule bottom edge, butMathTeXEngine.HLineexpects the y centreline.TeXLayout.VRule.xis the rule left edge, butMathTeXEngine.VLineexpects the x centreline.
_box_to_mtenow shifts HRule positions by+ thickness/2in y and VRule positions by+ thickness/2in x before constructingMathTeXEngine.HLine/VLine.- Documented this geometry contract in
AGENTS.mdunder the Makie integration notes. - Validation:
- main test suite still passes (866/866),
- CairoMakie/MathTeXEngine-path checks confirm exported
HLineandVLinecoordinates now match the expected centerlines derived from TeXLayout boxes.
- Investigated
\sqrt{1 + \sqrt{1 + \sqrt{1 + \sqrt{1 + x}}}}: the repeated1+drifted slightly right at each larger radical variant. - Root cause in
src/layout.jl:_radical_body_offset_duusedmax(advance_width, x_max), so the radicand started after the radical ink overhang instead of after the radical delimiter box width. - In NewCMMath the prebuilt radical variants have
advance_width = 1000du butx_max = 1020du (radicalis833vs853), so each nested level picked up an unwanted extra20du. - Checked LuaTeX
make_radical(texk/web2c/luatexdir/tex/mlist.c): TeX builds the result as delimiter box followed by the overbar/radicand hlist, so horizontal placement is by delimiter width, not ink bounds. - Fixed
_radical_body_offset_duto useadvance_widthonly and added a regression test that matches each nested radical glyph to its rule bar and asserts the radicand starts exactly one radical advance to the right.
- Follow-up on the nested-radical investigation: the repeated
+glyphs were also drifting downward in\sqrt{1 + \sqrt{1 + \sqrt{1 + \sqrt{1 + x}}}}. - Root cause: after the usual TeX/KaTeX-style clearance adjustment,
_layout_sqrt!applied a secondbody_shiftthat lowered the radicand by half of the selected radical's excess cover. - LuaTeX
make_radicaldoes not do that extra centering step: it increases the clearanceclrwhen needed and then packs the delimiter beside an overbar/radicand vlist. - Removed the extra body shift so nested radicands keep their original baseline, and added a regression test asserting that all four
+glyphs in the deep nested example share the sameyposition.
- Root cause: two compounding bugs in the Makie stress-test layout tool.
em_bboxused the glyph's actual ink extents for vertical bounds, but Makie'sheight_insensitive_boundingbox_with_advanceapplies the font-level ascender/descender as a floor/ceiling for every glyph — making Makie's bounding boxes much taller.- Makie's
text!withalign = (:left, :baseline)forLaTeXStringfalls through to:bottombehaviour (placing the formula's bounding-box bottom at the anchor), not the mathematical baseline. These two errors compounded: formulas were shifted ~30–70 px higher than intended, extending into the section header of the same row.
- Fix (all changes in
tools/stress_test_makie.jl):em_bboxnow takesfont_ascenderandfont_descender(fromFreeTypeAbstraction.ascender/descender) and applies per-glyph vertical clamping:min(font_descender, el.y_min / upm) * box.scale, matching Makie exactly.run_stress_test_makieloads font-level metrics from the math face.- Rendering loop changes
alignto(:left, :bottom)and lowers the anchor by each expression's individualbelow_px_i = round(Int, -by1_i * BASE_PX)so the mathematical baseline ends up at the intended screen y.
- Rows are now correctly sized and expressions sit cleanly within their strips.
- Symptom: with STIX Two,
\left\langle \frac{a}{b} \,\middle|\, \frac{c}{d} \right\ranglerendered the bar ~38% taller than the angle brackets. - Root cause: STIX Two provides only one pre-built bar variant (advance=941). For display-mode fractions the required height is ~1820 DU; the single variant is insufficient, so the glyph assembly is used.
_layout_assembly!used minimum overlaps (= maximum assembly height): withn=2extenders and min overlap=100, total_du = 2623 ≫ required_du ≈ 1820.- The angle brackets have a 13-variant ladder and select the closest one (advance=1907), so they render at the correct height.
- Fix (
src/layout.jl:_layout_assembly!): after selecting the minimumn, increase overlaps uniformly (proportional to each gap's connector capacity) to shrink total_du toward required_du. Each gap is clamped to its per-gap connector limit.- Result after fix: bar ink height ≈ 1819 DU vs langle 1906 DU — near-match (bar is slightly shorter than brackets, which is cosmetically acceptable and far better than 38% overshoot).
- The slight residual discrepancy is because the langle variant overshoots by the quantisation of its size ladder (~87 DU), while the assembly can be sized almost exactly.
- Not affected:
_layout_radical_assembly!(top-anchored, different centering semantics). - All 950 tests pass.
- Goal: lay out general strings mixing styled text,
\\line breaks, inline$…$math, and display environments (align). Auto line-breaking / full justification remain out of scope. Example target:\textbf{Hello} world\\\begin{align}x&=y\\y&=x^2-z\end{align}. - Diagnosis: current engine is single-tree, single-baseline, dimensionless flat output, math-only top level. Missing abstraction = a measured, composable box (TeX hbox/vbox).
- Decision: Option 1 (wrapper layer) now; Option 2 (unified box-and-glue
IR) is the long-term target. Wrote a temporary
future.mddesign draft (not retained in the current tree; seedocs/src/91-developer.mdnow) explaining how Option 1'sTeXBox/vstack/hconcatare the eagerly-shaped degenerate form ofBox/VBox/HBox, so migration is incremental, not a rewrite. - Wrote
text-spec.md: full, self-contained implementation spec. Key points:- New
src/{shaping,document,compose}.jl; no breaking changes; renderer contract (Vector{LayoutBox}) preserved. - Document AST (
Block/Line/Run/TextSpan/TextAttrs) separate from mathNode; text kept as source strings (spans), not NodeKind.Char — required so a future HarfBuzz shaper can kern/ligature whole runs. - Pluggable
TextShaperseam (MetricShaperin core;HarfBuzzShaperin a documented-but-unbuilt extension). Shaper contract: emit PS-nameGlyph(:regular)boxes in em, baseline y=0, so renderer/Makie path is untouched (no GID glyphs in v1). - Decisions locked: y-origin = first baseline; bold/italic/bolditalic font slots
used via new
glyph_metrics_slot; width = widest line by default, optional fixed width; alignment per block (:left default; display blocks :center). - Line height = LaTeX semantics: baseline advance =
max(baselineskip, prevdepth + ascent(next) + lineskip)(defaults 1.2 em / 0.1 em). alignreuses the matrix machinery (add to_MATRIX_ENVS; deriverl…colspec). Known v1 approximation:&uses matrix column gap, not true relation spacing.
- New
- Open/limitations recorded in spec: align spacing approx;
\textsf/\texttt→regular; blank-line paragraph breaks not honoured; HarfBuzz extension not yet implemented.
- Wrote
test/test_text.jlcovering all 10 spec §9 cases plus unit tests for each new layer. - Structure: 6 testsets (font additions, MetricShaper, parser additions, document parser, composition primitives, layout_document integration); ~55 individual testsets, ~120 @test assertions.
- Strategy: all new symbols accessed as
TeXLayout.Xxx(no module-levelusing), so the file can be included without aborting existing tests while implementation is in progress. - Baseline on inclusion before implementation: 951 passed (all existing), 8 failed, 48 errored.
- 8 failures: parser tests (brace leniency, missing align/gather/aligned envs, colspec derivation) — these are assertions that evaluate false rather than throwing.
- 48 errors:
UndefVarErrorfor all new symbols not yet defined in the module.
- Implementation order from text-spec.md §12: fonts.jl → shaping.jl → parser.jl → document.jl → compose.jl → wire exports → iterate to green.
- Implemented all six steps from text-spec.md §12: fonts.jl additions, shaping.jl, parser.jl modifications, document.jl, compose.jl, exports + housekeeping.
- Final test score: 1091/1091 passing (0 failed, 0 errored).
- Key implementation notes:
_codepoint_metricsextracted fromglyph_metrics_by_codepoint;glyph_metrics_slotuses a priority fallback chain (bolditalic→bold→italic→regular→math, de-duplicated).shape_span(MetricShaper) calls_font_upmper glyph to handle per-font UPM when fallback chains cross font boundaries._parse_text_body!+_parse_text_group!are mutually recursive; bare{grouping, font-switch commands, and\text/\mboxall call_parse_text_group!uniformly.- Test fix: Case 3 (tall line forces lineskip) uses
\dfracnot\frac— Text-style fraction denominator (0.345 em) < line_height - lineskip - x_ascent (0.63 em); display- style (\dfrac) denominator (0.686 em) > threshold, correctly triggers lineskip path. - Exports: layout_document, TeXBox, LayoutOptions, TextShaper, MetricShaper.
- Added tools/visualise_text.jl for FreeType rendering of layout_document output.
- Reviewed
future.md,text-spec.md, and the recent refactor notes after thelatex-textmerge. - Current state: Option 1 text/document layer is implemented as eager measured
TeXBoxcomposition (shape_span,hconcat,vstack,layout_document) while the math engine still lays out by mutating flatVector{LayoutBox}scratch buffers. - Structural direction: preserve the public
layout_document/TeXBoxcontract, then introduce an internal box-tree module that first powers text composition and only later absorbs math constructs construct-by-construct. - Codebase guardrail: build on the recent refactor boundaries (
enums.jl,payloads.jl,src/tables/,src/layout/*.jl) instead of adding another parallel layout path. - Near-term plan: harden the existing text wrapper, add richer document/layout regression tests, factor composition semantics into internal HBox/VBox primitives, then migrate selected math constructs where the box abstraction clearly removes scratch-vector measurement logic.
- Left HarfBuzz implementation out of scope and focused on validating the current
MetricShaper/TeXBoxwrapper layer. - Fixed
layout_documentdisplay spacing:abovedisplayskipandbelowdisplayskipare now explicit extra gaps instead of synthetic empty baselines. A display-only document therefore starts near the first real display baseline instead of being pushed down by an empty vskip item. - Added text regression tests for styled span font slots, inline math inside a styled text group, display-only y-origin behavior, and display skip deltas.
- Updated the document layout snapshot for the intentional display-spacing change.
- Fixed FreeType debug renderers (
visualise_text.jl,visualise_metrics.jl,stress_test_freetype.jl) to compareGlyph.font_slotasFontSlotenum values instead of stale symbols. - Validation: full package test suite passes (1121/1121).
visualise_text.jlandvisualise_metrics.jlsmoke renders succeeded after refreshing the tools environment metadata for the local TeXLayout checkout.
- Added
src/boxes.jlwith internal measuredBoxprimitives:ShapedBox,HBox,VBox, and a recursiveshapepass back toVector{LayoutBox}. - Rebased
hconcat,vstack, andlayout_documentstacking through the internal box layer while preserving the publicTeXBoxresult type and renderer contract. - Kept math layout unchanged; existing math
_layout_*!helpers still emit flatLayoutBoxvectors directly. - Added a direct composition test for
HBoxshaping, in addition to the existinghconcat/vstackbehavior tests. - Validation after the internal refactor: full package test suite passes
(1124/1124), and
visualise_text.jl/visualise_metrics.jlsmoke renders still succeed.
- Added
tools/stress_test_text.jl, a PNG stress-test renderer forlayout_documentoutput. - The sheet places literal LaTeX/document source in a left column and the rendered mixed text/math output in a right column.
- Coverage includes plain text, significant spaces, explicit line breaks, fixed
width alignment, text styles/nesting, inline math, display
equation/align/gather, tall inline math, line-height options, malformed inline math, unknown commands, empty styled groups, and display-only input. - Implementation uses FreeType rendering directly so it exercises TeXLayout's document layout path rather than Makie's LaTeXString math-only path.
- Validation:
julia --project=tools tools/stress_test_text.jl :new_cm /tmp/texlayout-text-stress.pngsucceeded and the generated sheet was visually inspected.
- Added focused parser/layout tests for adjacent display blocks, display blocks at document start/end, unknown environments in text mode, display alignment within fixed width, fixed-width overflow, and display-block vertical stacking.
- Found and fixed a document text font-switch bug:
\textsfand\textttwere preserving surrounding bold/italic state, despite the v1 decision that they map to the regular slot._apply_font_switchnow clears bold/italic for those commands, matching\textrm/\textnormal. - Validation: targeted text test run passed (1148/1148 through the package test harness), then the full package suite passed (1148/1148).
- Started step 4 of the text-layout cleanup by centralizing physical font-path
selection for
Glyph.font_slotin_font_path_for_slot(family, slot). - Updated FreeType render/debug tools and docs examples to use the shared helper instead of reimplementing text-slot fallback order locally.
- Added tests covering full bundled text-slot families and math-only fallback behaviour, so future renderer changes should not silently diverge from the layout font-slot contract.
- Strengthened
src/boxes.jlconstructors soShapedBox,HBox, andVBoxvalidate finite non-negative measured extents when constructed. - Moved
VBoxchild/offset/dx length validation from emission time to construction time, making malformed trees fail closer to their source. - Box constructors now copy caller-owned vectors, preventing later mutation of temporary arrays from changing already-built box trees.
- Added composition tests for validation failures, defensive copies, and nested recursive offset shaping.
- Reviewed
math-flat-layout-plan.local.md; the plan is sound for scratch-buffer removal, with snapshot identity as the right guardrail. - Added range-based
_boxes_top,_boxes_bottom,_boxes_vextent,_translate_range!, and_base_italic_correction_emhelpers insrc/layout.jl. - Converted
src/layout/scripts.jlscript and limits placement to emit base/sub/sup boxes into the shared output buffer, measure those emitted ranges, and translate script ranges in place. - Updated
compose.measureto use the same range-extent helpers for consistency. - Focused
test_layout.jl+test_snapshots.jlrun passed after the conversion. - Follow-up: constructs that currently emit delimiters or radicals before measured children (
sqrt,genfrac,delimited, matrix delimiters) need explicit range reordering if converted without changing serialized emission order.
- Relaxed the append-order requirement per user direction and converted
src/layout/constructs.jlto the clearer emit/measure/translate flow for fractions, genfracs, radicals, delimiters, arrows, accents, and over/under rules. - Removed all
LayoutBox[]scratch buffers and_emit_shifted!use fromconstructs.jl; child ranges are emitted into the shared output buffer and translated in place. - Updated layout tests that previously assumed glyph append order to select semantic glyphs by position or axis proximity.
- Changed snapshot serialization to sort rounded box records before hashing, so snapshots guard element geometry and metrics without treating append order as semantic.
- Validation: focused
test_layout.jl+test_snapshots.jlpassed (1168/1168). - Benchmark smoke after this pass:
layout/scripts_fraction51 allocs / 6432 bytes,layout/radical_delimited33 allocs / 3248 bytes,layout/accents_braces_arrows116 allocs / 12800 bytes,layout/matrix_cases139 allocs / 12816 bytes.
- Converted
_layout_horiz_brace!insrc/layout/extensible.jlto emit body, primary note, and secondary note into the shared buffer and translate recorded ranges in place. - Removed the remaining
LayoutBox[]scratch buffers and_emit_shifted!use fromextensible.jl. - Validation: focused
test_layout.jl+test_snapshots.jlpassed (1168/1168).
- Converted
src/layout/matrix.jlto emit cells into the shared output buffer during measurement, recordcell_starts/cell_stops, and translate each cell range after row and column positions are computed. - Removed per-cell scratch
LayoutBoxbuffers from matrix/array layout and deleted the now-unused_emit_shifted!helper fromsrc/layout.jl. - Validation: focused
test_layout.jl+test_snapshots.jlpassed (1168/1168). - Full package validation passed (1168/1168). Final benchmark smoke:
layout/scripts_fraction51 allocs / 6432 bytes,layout/radical_delimited33 allocs / 3248 bytes,layout/accents_braces_arrows112 allocs / 12128 bytes,layout/matrix_cases128 allocs / 11328 bytes,layout_document/document_inline_display671 allocs / 49648 bytes.
- Fixed
tools/stress_test_all.jlreference downloads to use GitHub release asset namesstress_test_output_<font>.png; the previousstress_test_<font>.pngpattern returned 404 for all bundled fonts. julia --project=tools tools/stress_test_all.jlrendered all eight current sheets and downloaded all eight references.- Diff result: all fonts reported
CHANGEDwith max delta 255 and ~27-29% changed pixels. - The large diff is dominated by reference/current sheet size mismatch: current sheets are ~11999-13654 px tall while v0.1.0-stress references are ~7867-9032 px tall, so the comparison pads the shorter reference with white. A fresh reference baseline for the current stress sheet content is needed before this tool can distinguish real layout drift from sheet-content/canvas changes.
- Added
tools/stress_test_suite.jl, a unified stress CLI withgenerate,pack,compare, andallcommands. - The suite renders stable per-case PNG paths grouped by font, suite, and section:
math_freetype,text_freetype, and optionalmakie_cairo. Full math/text/Makie sheets are generated undersheets/for visual inspection but excluded from reference tarballs and comparisons. - Reference packaging uses Julia stdlib
Tarand writesstress_test_reference.tar; comparison accepts either a local tarball or URL. New current images missing from the reference are reported asNEWand do not fail by default, preserving backwards-compatible addition of stress cases. - Added root
justfilehelpers for common test and stress commands. - Updated
tools/stress_test_all.jlto delegate to the unified suite while preserving old usage with bare font-name arguments. - Validation: generated
new_cmmath/text outputs, packed and self-compared a reference tarball (152/152 identical); generated all eight font artifacts without sheets and self-compared the tarball (1216/1216 identical); verified a synthetic new case reportsNEWwithout failing; generated thenew_cmoptional Makie subset (6 PNGs).
- Updated
AGENTS.md/CLAUDE.mdto listtools/stress_test_suite.jl, thestress_test_all.jlcompatibility wrapper, and the rootjustfile. - Documented the canonical stress reference workflow: generate per-case PNGs, pack
stress_test_reference.tarwith Julia'sTarstdlib, compare against a local or downloaded reference, and treat full sheets as visual-only outputs. - Updated
README.mdwith concise developer-tooling instructions and clarified that the linked release images are full-sheet stress-renderer examples. - Refreshed
math-flat-layout-plan.local.mdso its validation steps point at the per-case stress suite rather than the older sheet-only comparison.
- Audited documentation for the range-emission math layout refactor and found stale wording in
AGENTS.md/docs/src/91-developer.md. - Replaced the old "purely additive" invariant with the current contract: layout helpers append boxes, measure just-emitted ranges, and may translate only those ranges in place.
- Documented that snapshot records are sorted before hashing, so append-order changes from allocation-reduction refactors are not treated as layout changes.
- Updated the ignored
math-flat-layout-plan.local.mdworkspace note to stop referring to_emit_shifted!and emission-order identity as current requirements.
- Implemented
\sqrt[n]{x}degree placement in_layout_sqrt!; the parser already produced[degree, body], but layout previously ignored the degree child. - Radical placement helpers now return the radicand offset and actual radical cover height so the degree can use
RadicalKernBeforeDegree,RadicalKernAfterDegree, andRadicalDegreeBottomRaisePercent. - Added a layout regression test checking degree scale, horizontal placement, and MATH-table bottom raise for
\sqrt[3]{x}.
- Adjusted
parse_environment!foralign/alignedso every even column (the cell after an alignment marker) starts with an emptyNodeKind.Group. - The empty group classifies as an ordinary atom and emits no boxes, letting existing math-list spacing produce
ord-rel-ordspacing for cells like=y. - Added parser and layout regression tests for the inserted empty group and the relation-space box before a leading
=. - Updated the
document_inline_displaysnapshot hash for the intentionalaligngeometry change.
- Preserved raw whitespace runs in
TokenKind.Space.valueso document parsing can distinguish ordinary whitespace from blank lines while math parsing continues to ignore space tokens. - Added internal
ParagraphBreakBlockmarkers for top-level blank lines andLayoutOptions.parskip(default0.6em) to control the extra vertical gap. - Updated
layout_documentto fold paragraph-break markers into its existing pending-skip flow, including before display blocks. - Added lexer, parser, and layout tests for raw whitespace preservation, blank-line parsing, default paragraph spacing, and custom
parskip.
- Reworked
_layout_children!to stream TeX Rule 5/6 binary-operator reclassification instead of allocating aVector{Symbol}of atom classes for each child list. - Kept separate left-context and emitted-spacing classifications so right-cancelled binary operators do not accidentally change the left context seen by later atoms.
- Left a collect-based fallback for non-vector iterables; normal parser/layout paths use
Vector{Node}and avoid the extra class vector. - Validation: focused layout/snapshot tests passed (1194/1194). Benchmark smoke showed lower layout allocations on representative math cases:
simple_atom37 allocs,scripts_fraction49,radical_delimited31,accents_braces_arrows108,matrix_cases124.
- Verified all
tools/scripts run against the refactored code (split layout modules + new document/compose/shaping/boxes layers). Package loads cleanly. - Working:
visualise_bitmap,visualise_metrics,visualise_text,stress_test_freetype,stress_test_text,stress_test_latex,stress_test_makie,stress_test_suite(generate/pack/compare round-trip identical),stress_test_all(wrapper passthrough).prepare_font_artifactsis a self-contained downloader (no TeXLayout API), parses/loads fine. - Fixed (Fira headings):
stress_test_freetype.jl render_text!calledrenderface(face, string(ch), px), which resolves by PostScript glyph name. Works on AGL-named fonts (NewCM) but yields.notdeftofu on FiraMath (uni-names) / Luciole. Changed to pass theChar→ cmap lookup, portable across all fonts. The box-glyph path (TeXLayout-supplied glyph names) is unchanged and correct; the text stress tool already used Char. - Fixed (Makie metrics tool):
visualise_metrics_makie.jldidimport Makie, but Makie is only a transitive dep of the tools project → load error. Changed toimport CairoMakie.Makie. - Doc fix: corrected stale arg-order examples in AGENTS.md for
stress_test_freetype.jl/stress_test_makie.jl(all per-font tools take the font spec first; freetype:[:font] [out]; makie:[:font] [fmt] [out]). - Note: font symbols changed to underscore form (
:new_cm, not:newcm).
stress_test_freetype.jl render_text!was vertically centring each header glyph independently (top = H÷2 - by_px÷2 + 2), so glyphs did not share a baseline (period looked raised, descenders/caps misaligned). Replaced with a fixed baselineH÷2 + px÷3andtop = baseline - by_px, matching the baseline logic instress_test_text.jl render_text_line!. Verified on NewCM and FiraMath.
- Reported: too much space before
=in\begin{align}x&=y+z.... Cause: the recent "Add align relation spacing" change inserts an empty ord group so the leading=gets ord-rel spacing (5mu), but the matrix layout was also applying inter-column_MATRIX_COLSEP(2×5mu) between the r/l pair → 15mu before=instead of 5mu. - Fix (
src/layout/matrix.jl): for align/aligned, columns form right/left pairs glued at the alignment point —col_gap(c)=0for even c (inside a pair), full2·_MATRIX_COLSEPonly between pairs (odd c>1); outer margin 0. Relation spacing now matches plain math:=in\begin{align}x&=y\end{align}lands at the same x as inx=y. - Added layout regression test (align
=x == plain=x); updateddocument_inline_displaysnapshot hash. Full suite: 1195 pass.
- Document parser (
src/document.jl) now recognises display math$$…$$and\[…\](→ free-standingDisplayBlock(node, :displaymath), Sequence body laid out in Display style) and inline\(…\)(→MathRun, Text style). A single:displaymathkind is used for both$$and\[(kind is informational;compose.jlignores it). $$vs$ $: disambiguated by peeking —$$is two adjacentMathShifttokens; a spaced$ $has a Space between and stays inline. Display forms are gated on!in_groupso they never open inside{…}/\text{}.- Generalised
_parse_math_until_shift!into_parse_math_until!(p, isstop)+ added_parse_math_until_command!(p, close)and a_peekhelper in parser.jl. - Text-mode token dropping:
^/_/&(and stray top-level}) now render as literal characters instead of being dropped by the catch-allelse. Genuine unknown control sequences in text mode are still dropped (future work: a text-mode command/escape table). - 7 new testsets in test_text.jl; full suite 1213 pass, no snapshot changes.
ext/MathTeXEngineExt.jlgenerate_tex_elements(::LaTeXString)now branches on_is_inline_math(str): a single$…$span (starts/ends with$, exactly two$total) keeps the old behaviour (parse_latex + layout, Display style); everything else goes throughlayout_document(String(str)).boxes. The shared_box_to_mteloop converts either box list.- Verified end-to-end via CairoMakie:
L"x^2+\frac12"(math),"Energy $E=mc^2$ is famous."(mixed text+math), and$$…$$(display) all render correctly. - Docs: README Makie section, docs/src/03-makie.md ("Inline math vs. mixed text and math"), AGENTS.md routing note, CHANGELOG Added entry.
- Limitation kept/documented: the Makie seam still ignores the caller font_family arg, and document-path LayoutOptions aren't exposed (use layout_document directly).
- Added
default_layout_options()/set_default_layout_options!(compose.jl), mirroring thedefault_font_familyRef pattern. Keyword setter merges over the current default via_merge_options(strict: unknown keys throw); positional setter replaces wholesale (LayoutOptions()resets). layout_documentrefactored into two methods: core takesopts::LayoutOptions; the kwarg convenience merges kwargs overdefault_layout_options(). So direct callers and the Makie path both honour the global; output unchanged until set.- Makie extension (document path) now calls
layout_document(str, default_layout_ options(); family). This is the only channel for width/alignment via Makie, since thegenerate_tex_elements(::LaTeXString)signature is fixed. - Exported both names. 10 new tests (merge, per-call override, reset, typo error, Makie pickup). Suite 1223 pass, no snapshot change. Docs: README, 01/03/05 doc pages, AGENTS.md Makie note, CHANGELOG.
- Investigating
examples/eigen_demo.jlshowed the mixed text/math label goes throughMathTeXEngineExt's document path. - Found likely root causes: the Makie adapter resolves every non-math glyph
through the regular FreeType face, so
\textbf{...}cannot render with a bold face; the demo's raw triple-quoted label also preserves indentation as leading spaces, which affects apparent left alignment.
- Fixed
MathTeXEngineExtto resolve glyphs through TeXLayout's per-slot fallback chains and preserve usefulrepresented_charvalues for standard glyph names such asspaceandLambda. - Fixed the eigen demo annotation as one multiline
text!call: no indented triple-quoted source, left-aligned display block,V^{\,-1}spacing, and a standard Julia"\\\\"separator becauseraw"and\\"produced only one trailing backslash and malformed the intended line break. - Rendered
/tmp/eigen_demo.png; the prose is left aligned, bold text is bold, and theV^{-1}superscript is visually separated.
- User review caught that the explanatory prose still had an awkward/misleading
break around
and, making later text look like it had leaked into math mode. - Restored the original wording but made the break explicit as
... \textbf{eigenvectors}+"\\\\"+and $\Lambda$ ..., soandand the following prose are text-mode on the next line while only\Lambdais math. - Regenerated
/tmp/eigen_demo.pngand visually confirmed the prose line now breaks cleanly.
- Replaced stale
future.mdbox-tree architecture note with a focused future work list for matrix vertical spacing helpers. - Captured follow-up items: implement
\strutand phantom-style invisible measured boxes, make matrix row spacing arguments like\\[0.2em]affect layout instead of being skipped, and revisit_MATRIX_ROWGAPonly after comparing against TeX/KaTeX expectations.
- Added future-work notes for dedicated
\textsfand\textttsupport. - Current behavior maps both commands to the regular text slot; future work should add sans-serif and monospace slots/fallbacks, keep nested bold/italic behavior, update Makie extension font caches, and test fallback behavior.
- Added a parser-compatibility note to
future.mdto review leading, trailing, and repeated whitespace handling in math and document text modes against LaTeX conventions.
- Updated
AGENTS.mdfor current session changes: addedfuture.mdto the file tree, clarified that document text uses configured bold/italic text slots while math-mode font switching remains Unicode-variant based, and updated the Makie extension runtime-cache description to cover all slot fallback font paths rather than only math/regular faces. - Added future-work caveats for matrix vertical spacing helpers and whitespace
convention review, pointing developers at
future.md.
- Updated Documenter sources to match the current Makie extension behavior: inline-math vs document routing, font-slot fallback glyph lookup, cached FreeType faces, and session-wide document layout options.
- Clarified public command docs so math-mode font switching is documented as Unicode-variant based while document text styling uses the configured bold/italic/bolditalic text slots.
- Added public/developer limitations for future
\textsf/\textttslots, matrix vertical spacing helpers, and whitespace convention review. - Verified the docs build with
julia --project=docs docs/make.jl.
- Implemented optional
HarfBuzzShapersupport throughext/HarfBuzzExt.jl, emittingGlyphIDelements with exact font paths and final glyph IDs. - Kept
MetricShaperas the default and independently testable path; document layout and math\text{}/\mbox{}can opt into HarfBuzz withshaper = HarfBuzzShaper()after loadingHarfBuzz_jll. - Updated README, Documenter pages,
AGENTS.md,future.md, and changelog coverage forGlyphID, optional HarfBuzz shaping, Makie conversion, and stress test behavior.
- Problem:
\frac(and scripts, big ops) insidealign/gather/etc. rendered at Text-style (script) size, not full display size. Root cause:_layout_matrix!forcedcell_style = Textfor all matrix-family environments (matrix.jl:60), but the amsmath display-alignment environments set each line in display style. - Fix (step 1+2 of the agreed plan):
- Added
_DISPLAY_MATH_ENVS(parser_tables.jl) = {align, aligned, split, gather, gathered, equation}._layout_matrix!now uses Display/CrampedDisplay cells for these, Text for genuine arrays (matrix/array/cases) — so fractions keep full size. - Added
split(≈ aligned) andgathered(≈ gather) as new envs in_MATRIX_ENVS- parser colspec/ordinary-atom branches.
splitalso joins thetight_pairsset.
- parser colspec/ordinary-atom branches.
- Starred forms (
align*,gather*, …) now alias the unstarred via_canonical_env_name(strips trailing*) applied at all three env read sites (parser\begin,parse_environment!, document layer). Equation numbers aren't rendered, so the star has no visual effect. - document.jl
_DISPLAY_ENVSnow points at the shared_DISPLAY_MATH_ENVS.
- Added
- Tests: +2 snapshot cases (align_fraction, gathered_script) lock the display sizing; +2 layout testsets (display-env cell style; starred-alias payload equality). Full suite 1244 pass. Runic-formatted.
- Deferred:
multlinestill a sentinel — does NOT fit the grid model (no&, per-row L/center/R alignment measured against target line width). Needs a separate width-aware path in compose.jl, scoped as future work.
- Implemented LaTeX-style whitespace conventions in document text mode and
%line comments in the lexer. - Lexer (
src/lexer.jl): new%branch. Discards to end of line; if the line ends in%it also drops the newline + next line's leading indent (the whitespace-suppression idiom), unless the following line is blank — then the newline(s) are left for the normal collapser so the paragraph break survives. Escaped\%still lexes as aCommandand never reaches the branch. - Document parser (
src/document.jl): deferred-space model. Addedpending_space/at_line_startto_DocBuilderwith_commit_space!(commit a deferred inter-word space unless at a line start) and_begin_line_boundary!(drop trailing deferred space + suppress next leading whitespace), wired into_end_line!. Top-level spaces are deferred; spaces inside{…}groups stay significant. Result: leading/trailing whitespace at line/block boundaries is trimmed, but single newlines→space, indentation collapse, blank-line→paragraph, and spaces around inline math are unchanged. - Tests: +5 lexer cases, +20 document cases (1244→1269 pass). Snapshots unchanged.
- Docs: CHANGELOG
[Unreleased](Added comment support, Changed whitespace),future.mdwhitespace item updated (math-mode review still open), AGENTS.md limitation note rewritten to describe the implemented behaviour. - Open: pure math-mode (
parse_latex) repeated/leading/trailing whitespace not yet reviewed;\text{…}internal whitespace currently verbatim-significant.
- Reviewed pure math-mode (
parse_latex) and\text{}whitespace against LaTeX and made everything consistent. Three genuine bugs found and fixed. x^ 2/\frac 1 2: a space after^/_or before a command argument was captured as an empty script/arg, pushing the real argument to the baseline. Fixed by skipping ignorable leading whitespace in_parse_argument!.x^ 2now lays out identically tox^2.~,\,\space,\nobreakspacewere dropped entirely in math mode (and\inside\text{}too). KaTeX renders them as the U+00A0 glyph = TeXfontdimen2interword space. Now they emitspace_node(_NORMAL_SPACE_EM)with_NORMAL_SPACE_EM = 6/18em (= 1/3 em).\/\space/\nobreakspaceadded to_SPACE_WIDTHS;~(a Space token, value "~") handled via new_is_ignorable_spacehelper so the four math space-skipping loops skip ordinary whitespace but let~reach_parse_primary!.- Confirmed already-correct + consistent (added tests): math ignores
leading/trailing/repeated ordinary whitespace and blank lines;
\text{}collapses runs and preserves internal/leading/trailing spaces like document{…}groups. - No snapshot churn — no existing snapshot/stress input used
~/\/x^ 2. - Tests: +19 (
test_parser.jl"Math-mode whitespace conventions"); 1269→1288. - Docs: CHANGELOG (Changed: explicit spaces; Fixed:
x^ 2/arg space), future.md item marked done, AGENTS.md whitespace note extended to math mode.
- Follow-up to the math-mode whitespace pass: checked whether
~is handled correctly in document text mode too. It rendered as a space for common cases (Fig.~3→ "Fig. 3") but was treated as ordinary trimmable/collapsible whitespace, so a~at a line/paragraph boundary was dropped (~x→ "x",x~→ "x"). Inconsistent with math mode, where~is now significant. - Fix: added
pending_nbspto_DocBuilder. The Space branch flags a~token as a non-breaking pending space;_commit_space!emits it even at a line start (ordinary leading space is suppressed), and_end_line!commits a trailing pending nbsp before flushing so it survives. A run still collapses to a single space. Now~x→" x",x~→"x ",a~b/a ~ b/Fig.~3→single space, and~survives an explicit\\break. - Note: the document layer does no soft line-wrapping yet (lines break only on
\\and blank-line paragraph breaks), so the non-breaking property has no visible effect on wrapping today — this fix is about~never vanishing and being consistent with math mode. - Tests: +8 in test_text.jl; 1288→1296. Docs: CHANGELOG + AGENTS.md updated.
- Pulled latest
mainfrom GitHub (25f77da..4a720a4); upstream change was a CI workflow tweak, and remote PR branchdependabot/julia/all-julia-packages-f1aa886992became available. - Checked out PR #22, which only widens
Project.tomlcompat fromHarfBuzz_jll = "8.5.1"to"8.5.1, 100.14002". - Local focused probe with
HarfBuzz_jll v100.14002.1+0passed:TeXLayoutandHarfBuzzExtprecompiled,HarfBuzzShapershaped"office"intoGlyphIDboxes with positive dimensions. - The installed artifact's
harfbuzz.pcandhb-version.hidentify the bundled upstream HarfBuzz library as14.2.1; the unusual JLL package version is not the upstream library's semantic version string. - Full
Pkg.test()resolvedHarfBuzz_jll v100.14002.1+0and passed 1308/1308 tests.nm -Dconfirmed the newlibharfbuzz.sostill exports all symbols called directly fromext/HarfBuzzExt.jl.
- Fast-forwarded
mainfrome993ddfto26ec77d; the four upstream commits only update the TagBot workflow. Existing localnotes.mdand.backup-ignorechanges were preserved. \textscis not registered in either the document text-style parser or the math-internal\text{...}path. A focused Termes probe parsed it as an unknown command and emitted ordinary lowercase glyphs inFontSlot.Regular.- The bundled TeX Gyre Termes regular face contains
.scglyphs andsmcp/c2scOpenType feature tags, so true small caps are blocked by TeXLayout's text-attribute/shaper API rather than by the font. - Proper support would carry a small-caps attribute through
TextAttrs, enablesmcp(and likelyc2sc) inHarfBuzzShaper, and define a documentedMetricShaperfallback or limitation.
- Added
src/text_styles.jlas the shared semantic layer for document text and math-internal\text{...}styles.TextAttrs(slot, size)remains compatible; a nestedTextFeaturesvalue now records small caps independently of font slot selection. \textsccomposes with regular/bold/italic slots and restores attributes at group boundaries. Math ASTNodeKind.Text.valuestores nested text commands, which are flattened into styledTextSpans before shaping.- HarfBuzz maps small caps to
smcponly: lowercase letters become designed small capitals while source uppercase letters remain full-height capitals, matching LaTeX\textscsemantics.c2scwould incorrectly force source capitals down to small-cap height. MetricShaperexplicitly rejects feature-bearing spans rather than using synthetic scaled capitals. Users opt into genuine substitutions withHarfBuzzShaper(); Makie uses it viaset_default_layout_options!.- Final formatted runs passed 1317/1317 without HarfBuzz and 1336/1336 through
Pkg.test()with HarfBuzz. The Documenter build passed, and a real CairoMakie Termes render confirmed a full-height initial followed by genuine small-cap glyphs.
- PR #25 merged the small-caps implementation into
mainwith all required tests, Runic, documentation, and Codecov checks passing. - Prepared release branch
codex/release-v0.2.3from the mergedmain. - Updated the Julia package version to
0.2.3; moved the small-caps changelog entries into a datedv0.2.3section and reopened an empty[Unreleased]section. - Updated changelog comparison links for
v0.2.2...v0.2.3andv0.2.3...HEAD. - Release validation passed:
Pkg.project().version == v"0.2.3", 1336/1336 tests with HarfBuzz, and a Documenter build whose inventory reports version0.2.3.
- Added an independent
TextFamilyaxis alongside the existing weight/shapeFontSlotand semanticTextFeatures. Family commands now preserve weight, shape, and small caps;\textnormalis the sole full reset. - Extended
FontFamilycompatibly with optional sans-serif and monospaceTextFontSetvalues. A centralized resolver exhausts the requested family's face fallbacks, then the primary text family, then the math font. - Both text shapers now emit exact-path
GlyphIDvalues. HarfBuzz enumerates GSUB feature tags and rejects a semantic feature when no configured fallback font supports it. - Published shared TeX Gyre Heros and Cursor artifacts from CTAN TeX Gyre 2.501
at the
v0.3.0-fontsGitHub release, avoiding duplicated companion faces in each math-family artifact. - Reduced exports to six Makie-facing configuration names; advanced layout, element, and shaper interfaces remain available through qualified access.
- Stress testing found and fixed two integration issues: the first visual
attempt used doubled LaTeX backslashes, and the Cairo case bounds calculator
needed per-font
GlyphIDUPM handling. The exact Makie title, full Termes text sheet, per-case Cairo generation, and Documenter build now pass.
- Compared a fresh eight-font stress run against a detached worktree at the
current
maincommit to separate feature changes from older release-reference drift. - Found that Makie stress cases leaked their process-wide HarfBuzz default into
later fonts. The renderer now restores both font and layout defaults in a
finallyblock and removes its temporary PNG. - Final comparison: 1248 existing cases identical, 0 changed, 0 missing, and 80 expected new sans/monospace/small-caps and opt-in Cairo cases.
- Set the package version to 0.3.0, closed the changelog release section, and
moved README, developer-guide, and
justfilestress links tov0.3.0-stress. - Replaced stale and completed
future.mdentries with a focused backlog for paragraph shaping, vertical metrics, display environments, symbol composition, Makie integration, and regression tooling. - Generated all eight font families with FreeType math/text sheets and opt-in CairoMakie cases. The packed reference contains 1328 cases across all three suites and compares 1328 identical, with no changed, new, or missing images.
- Fixed full
--include-makiesheet generation by loading the Makie renderer once and restoring its process-wide font default after each sheet. - Published the 15 MB reference archive and 16 preview sheets at
https://github.com/dawbarton/TeXLayout.jl/releases/tag/v0.3.0-stress; the archive SHA-256 isb42d676fe396194ab1b193536dbdcd5ba61543e8f945c9639ab90883e87c74d5.
- Added the shared TeX Gyre Heros/Cursor companion row to the README licence table, matching the font documentation.
- Reviewed grouping the primary regular/bold/italic/bold-italic fields into a
TextFontSet. The symmetry would be cleaner internally, but it would make the documented Makie path awkward (family.regular.regular) and break existing direct field access across user code. Keep the current layout for v0.3.0; reconsider aroman::TextFontSetdesign only with a deliberate accessor API in a future breaking release.
- Updated
mainfromorigin/mainand reproduced missing\%output in both core layout andMathTeXEngine.generate_tex_elements(::LaTeXString). - The lexer correctly distinguished escaped
\%from an unescaped comment, but the parsers left it as an unsupported command: math layout emitted no glyph and document text skipped it. - Treat
\%as a shared escaped literal in math, math-internal text, and document text; retain unescaped%comment behavior. - Core tests pass 1,342/1,342, the full
Pkg.test()target with HarfBuzz passes 1,367/1,367, and direct CairoMakie rendering confirms visible50%output with nonzero glyph IDs through inline-math, document, and\text{…}routes.
- Audited the standard LaTeX special-character escapes through math,
math-internal
\text{…}, document text, and the Makie extension. - Besides the now-fixed
\%, the lexer preserves but the parsers/layout drop\#,\$,\&,\_,\{, and\}in every route. Literal braces do remain usable specifically as delimiter arguments such as\left\{. - Math
\backslashrenders, but document-text\textbackslash,\textasciitilde, and\textasciicircumare unsupported. - Related aliases are inconsistent:
\textdollarrenders in math and math-internal text but not document text, while\textunderscore,\textbraceleft,\textbraceright,\lbrace, and\rbraceare dropped. Ordinary math\vert/\Vertrender, but the\|alias is dropped except when consumed as a delimiter argument. - Makie's
_is_inline_mathcounts escaped\$as a delimiter, so an otherwise single-spanLaTeXStringcontaining a literal dollar is routed through the document path in addition to the missing-glyph problem.
- Split literal handling into a shared escaped-special-character table and
mode-specific math/text alias tables. Math parsing emits
NodeKind.Char; document parsing writes the same characters into text spans; math-internal\text{…}consumes the text-specific table. - Added literal brace atom classes and
\lbrace/\rbracedelimiter aliases; routed ordinary\|through the portable symbol-codepoint table. - Replaced Makie's raw dollar count with an inline-token helper that reuses the
core lexer, so escapes and comments have one source of truth; the resulting
tokens are reused by
parse_latexto avoid a second inline tokenization pass. - Added table-driven core tests, optional-extension tests under
Pkg.test(), and math/text Cairo stress cases covering the full literal set. - Verification passes: dependency-light tests 1,435/1,435; full
Pkg.test()with HarfBuzz and the MathTeXEngine extension 1,518/1,518; Documenter build; New CM math/text Cairo stress generation and visual inspection. Direct probes found no missing literal glyphs across all eight bundled font families.
- Created
fix/escaped-literals-v0.3.1fromorigin/mainfor the escaped literal and Makie routing fixes. - Bumped
Project.tomlto0.3.1, closed the changelog fixes underv0.3.1dated 2026-07-25, and reopened an empty[Unreleased]section. - Full tests (1,518/1,518), the Documenter build, Runic, and diff checks passed;
committed as
8c321aeand opened PR #28.
- Fast-forwarded
maintoorigin/mainat3949f63and reproduced issue #29: a style declaration inside document math consumes the closing math delimiter and all following text because its implicit body uses_parse_sequence_children!, which only stops at}or EOF. - Adding
TokenKind.MathShiftto that helper fixes the reported$…$example, but not the same bug for\(…\)/\[…\], size declarations,\right, or matrix cell/row/environment boundaries. - Recommended fix: propagate the active expression-stop predicate through
_parse_math_until!→ atom/primary/command parsing and reuse it when style or sizing declarations parse their implicit bodies. Explicit braced groups must establish their own}boundary. This follows KaTeX'sbreakOnTokenTextdesign and keeps closing delimiters unconsumed for the owning parser. - Regression coverage should include all document math delimiter forms, retained
trailing text, style and sizing declarations, grouped scope,
\left…\right, and matrix cell/row boundaries.
- Consolidated math expression loops in
_parse_expression_children!and threaded context-specific stop predicates through atom, primary, command, and unbraced-argument parsing. Explicit braced groups continue to reset their boundary to}. - Delimited bodies combine
\rightwith the surrounding boundary; matrix cells combine&,\\, and\endwith the surrounding boundary. Boundary tokens remain unconsumed for their owning parser. - Added parser and document regressions for inline/display delimiters, retained
trailing text, sizing declarations,
\left…\right, and multi-cell matrices. - Dependency-light tests pass 1,484/1,484; full
Pkg.test()passes 1,567/1,567 with HarfBuzz and the MathTeXEngine extension. Runic and diff checks pass. - The quick benchmark smoke completed successfully; results are in
/tmp/texlayout-boundary-bench-smoke.toml.
- Committed the boundary-aware parser fix as
8fda4a9onfix/issue-29-parser-boundariesand opened PR #30.
- After PR #30 merged, full stress output and focused issue-29 renders were
visually clean. An ancillary
tools/visualise_text.jlbug was isolated: its bounds and raster loop handled name-basedGlyphelements but ignored shapedGlyphIDdocument text. - Added exact-font
GlyphIDbounds and rasterization, plus advance/space-aware horizontal measurement. The issue expression now produces a 1257×320 image containing both the display-style integral and trailing text, instead of a cropped 600×320 math-only image. - Prepared v0.3.2 for 2026-07-26: close the issue-29 parser fix and visualiser
fix under the release changelog, bump
Project.toml, verify, and open a PR.
visualise_text.jlrenders the issue expression completely for all eight bundled fonts; output widths range from 1115 to 1340 pixels and visual spot checks cover New CM, Luciole, and Bonum. The mixed bold-text/alignment worked example also renders completely.- Dependency-light tests pass 1,484/1,484 and full
Pkg.test()passes 1,567/1,567 with HarfBuzz and MathTeXEngine extensions at package version 0.3.2. - Runic and diff checks pass. Documenter builds successfully and reports its inventory version as 0.3.2.
- Committed the visualiser fix and release preparation as
39b1a47onfix/visualise-text-glyphid-v0.3.2and opened PR #31.