This engine implements TeX's typesetting in two halves. Horizontally: the Knuth–Plass line breaker, which considers every feasible sequence of breakpoints and minimises total demerits instead of filling each line greedily. Vertically: blocks contribute vertical material, the list is broken into pages, and the whole layout re-runs until a pass reads exactly what it wrote — references, citations, the table of contents (LaTeX's .aux, minus the file, and iterated to a fixed point rather than a fixed count). Math layout is TeX's with one substitution: where TeX reads TFM fontdimens (\sigma1–22, \xi1–13), this reads the OpenType MATH table's MathConstants through ttf-parser, so the engine's measurements and the renderer's painting cannot drift apart.
The rules that carry the visual weight
| Rule | What it decides |
|---|---|
| Inter-atom spacing | The 8×8 atom-class table (thin / medium / thick, suppressed in script styles), applied after a Bin atom that binds nothing on its left — or is followed by a relation, closer or punctuation — is demoted to Ord |
| Scripts | Shift-up and shift-down with the drop-from-nucleus, bottom-min, top-max, gap-min and bottom-max-with-subscript constraints, so x_i^2 never collides; a symbol nucleus carries its scripts off its own baseline, a composite one drops them relative to its extent |
| Fractions | Numerator and denominator shifts per style, clearances measured to the rule's edges (or one gap between the two boxes when there is no rule, as in \atop and \binom), everything straddling the math axis |
| Radicals | The surd chosen from the face's vertical variant chain to cover the radicand plus the gap, the rule and the glyph's own ascender; the degree raised by the face's degree-bottom-raise percentage |
| Delimiters | \left…\right sized by TeX's \delimiterfactor / \delimitershortfall rule, grown through the variant chain, centred on the axis, and scaled only when the face runs out of variants |
| Large operators | Display-style operators taken at the face's display size and centred on the axis; limits stacked above and below with the MATH gap and baseline-rise minimums |
Alphabets are code points, not styling
\mathbb{R} is U+211D, not an R in a different font. Every alphabet maps into the real Unicode math blocks, including the letterlike holes (C H N P Q R Z for double-struck, the script and fraktur exceptions, and math italic's h = U+210E PLANCK CONSTANT). Any face carrying the glyphs works, and copied text stays meaningful.
The render tree
Output is a RenderDoc: page geometry in inches, matching the zoffice render model field for field so one frontend renderer serves both. A document comes back as pages — each a sheet of absolutely positioned glyph runs and rules, already broken into lines and pages by the engine, so the frontend lays out nothing. A bare formula takes the short path and comes back as one equation block instead: a box (width, baseline, height, in points) of the same runs. Every run names the face it was measured in — roman, italic, bold, bold-italic, sans, mono, math — and carries the byte span of the source that produced it, which is what lets a preview click map back to an editor selection.
Export
| Format | How it is drawn | Self-contained |
|---|---|---|
| SVG | Glyph outlines as <path>, rules as <rect> | Yes, with a face loaded |
| PNG | The same outlines rasterized (tiny-skia) | Yes |
| Outlines as filled paths in the content stream (lopdf) | Yes — and it carries its LaTeX source in the info dictionary |
No font is embedded in the exports, because none is needed: the glyphs are already curves. The source riding inside the PDF is what makes a dropped file reopen as an editable equation rather than paste as a picture.
Test strategy
Two layers, on purpose. The unit tests run metric-less — a nominal box for every glyph — so they assert structure: the spacing-table entries against the TeXbook's, the style transitions against Appendix G, that a superscript raises the box and a subscript deepens it, that \atop draws no rule where \frac draws one, that a leading - is reclassified, that an unknown control sequence still previews. They also cover the line breaker directly: that total-fit beats greedy on the classic case, that a forced penalty always breaks, that an unbreakable run wider than the measure still yields every word — and the hyphenator against TeX itself, since the expected break positions are tex \showhyphens{…} output rather than a memory of it, exceptions included. The integration tests run against the vendored Latin Modern faces and pin what only real faces can: the MATH-constant field mapping (re-read independently from the same table and compared), per-character advances, delimiter growth, outline-based export, a PDF source round-trip, and a whole paper — title, contents, sections, math, lists, a table, references, bibliography — where no line may run past the right margin and no reference may print ??.
Honest limits
| Limit | Why |
|---|---|
| Hyphenation is American English only | Liang's algorithm runs against TeX's own vendored hyph-en-us pattern file; another language needs its own patterns, and nothing loads a second set yet |
| Packages are mined, never executed | A .sty is read for the \newcommand / \def / \newenvironment definitions it makes; the primitives a real LaTeX kernel would run come back as skipped. A document using a drawing package typesets everything else and marks what it left out |
| Faces without a MATH table guess | Layout falls back to generic em fractions and delimiters scale instead of stepping through a variant chain |
| Floats take the next page, not the best page | A float that does not fit in what is left of a page moves whole to the top of the next one; LaTeX's [htbp] preferences, float pages and \topfraction limits are not implemented |
\multicolumn spans, one column | A \multicolumn fills the columns it spans so every later cell stays where the source meant, but the cells are not visually merged; twocolumn is parsed into the class options and ignored |
| No line breaking inside an equation | A long formula produces one wide box; only text paragraphs go through the line breaker |