Skip to content

Route \mathit through the companion face - #271

Merged
kostub merged 5 commits into
masterfrom
feature/mathit-routing-pr3
Aug 6, 2026
Merged

Route \mathit through the companion face#271
kostub merged 5 commits into
masterfrom
feature/mathit-routing-pr3

Conversation

@kostub

@kostub kostub commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Plan: docs/plans/2026-08-04-mathit-text-italic-routing.md (PR 3 of 3, items 5-7)
LLD: docs/lld/2026-07-27-mathit-text-italic-routing.md

Goal

\mathit stops being a no-op. Routable characters — A-Z, a-z, 0-9, and capital Greek minus the unassigned U+03A2 — keep their plain code points and are drawn from mathitCTFont via per-range font attributes, matching pdflatex's cmti10 behaviour. Everything else inside \mathit is unchanged.

\mathit{f} was byte-identical to f (U+1D453, math font, 0.490 em); it now draws U+0066 from the companion at 0.307 em, against cmti10's 0.30667 em.

Three things worth review attention:

  • The routing domain is exactly TeX's class-7 mathchar set (LLD §2.1), derived independently a second time as the set where iosMath diverges from pdflatex (LLD §2.2). (, +, ), lowercase Greek, \sin and the symbol families are deliberately excluded — iosMath already agrees with pdflatex on all of them, and italicising them is MathJax's behaviour, not LaTeX's.
  • Font attribution moved from flush to append. The whole-line stamp in addDisplayLine is deleted rather than made range-aware: every character now gets its font at append time, so no later pass can silently overwrite a routed range. This is exact, not approximate — there is one append site into _currentLine and _styleFont is assigned once (LLD §3.3 C/D).
  • Routability is tested per character, not per atom. Fusion merges a whole \mathit{…} group into one atom, so \mathit{a\theta b} is a single atom whose nucleus mixes routable and non-routable characters — theta must stay in the math font, the only font that has it.

Italic correction is deliberately not addressed here; it is LLD 2's subject. \mathit{f} is now correct in face and code point but still short by the correction, with a small documented lookup drift (LLD §3.3 E).

Commits

  1. [item 5] Keep routable characters plain ASCII under \mathit
  2. [item 6] Draw \mathit routable runs with the companion face
  3. [item 7] Pin \mathit geometry and shaping with integration tests

Full swift test green (466 tests).

Stack

  1. Never fuse atoms across a font-style change #269 — Never fuse atoms across a font-style change
  2. \mathit companion font on MTFont #270\mathit companion font on MTFont
  3. This PR

Summary by CodeRabbit

  • Bug Fixes
    • Improved \mathit rendering for Latin letters, digits, and supported Greek characters.
    • Preserved correct character appearance while applying italic styling consistently across mixed content.
    • Improved spacing, kerning, ligatures, delimiters, and style transitions in italic math expressions.
    • Ensured repeated typesetting produces stable, consistent results.
  • Tests
    • Added comprehensive regression coverage for italic rendering, mixed characters, font attribution, and text layout.

@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5ba1f3f4-7a18-4399-8221-74f4f847bbe5

📥 Commits

Reviewing files that changed from the base of the PR and between a33c04b and 759d798.

📒 Files selected for processing (1)
  • iosMath/render/internal/MTTypesetter.m
🚧 Files skipped from review as they are similar to previous changes (1)
  • iosMath/render/internal/MTTypesetter.m

📝 Walkthrough

Walkthrough

MTTypesetter preserves routable \mathit characters as plain Unicode code points and assigns the math-italic companion font to contiguous ranges. Tests cover font attribution, widths, idempotence, delimiters, kerning, ligatures, and style boundaries.

Changes

Math italic routing

Layer / File(s) Summary
Routable character preprocessing
iosMath/render/internal/MTTypesetter.m, iosMathTests/MTTypesetterTest.m
Classifies routable Latin letters, digits, and assigned capital Greek characters. Preserves their code points during \mathit preprocessing. Tests mixed mappings and repeated typesetting.
Per-range companion-font assignment
iosMath/render/internal/MTTypesetter.m, iosMathTests/MTTypesetterTest.m
Applies the current style font to each math run, then assigns the companion font to contiguous routable ranges. Tests attribution, widths, delimiters, operators, and retention of the regular math font for unrouted characters.
Shaping boundary validation
iosMathTests/MTTypesetterTest.m
Tests kerning and ligatures within companion-font ranges. Verifies that shaping does not cross math-font boundaries.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MTTypesetter
  participant RenderedAtom
  participant CoreText
  MTTypesetter->>RenderedAtom: preprocess routable math-italic characters
  MTTypesetter->>RenderedAtom: assign regular and companion font ranges
  RenderedAtom->>CoreText: shape mixed font runs
Loading

Possibly related PRs

  • kostub/iosMath#269: Both changes modify MTTypesetter preprocessing for \mathit font-style boundaries.
  • kostub/iosMath#270: This change uses the math-italic companion font introduced by that PR.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes routing \mathit through the companion italic font face, which is the main change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/mathit-routing-pr3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kostub kostub left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the diff of this PR only, against LLD §3 / §3.3 A/C/D/G. I verified the two load-bearing claims empirically on the PR head (swift test: 466 pass), plus a sweep asserting no character in any MTCTLineDisplay is left without kCTFontAttributeName across 22 inputs (\dfrac/\tfrac style overrides, \left…\right, matrices, \color/\colorbox, \text, nested scripts, \hat{\mathit{f}}, \sqrt[\mathit{n}]{…}). No unstamped ranges. The UTF-16 run arithmetic in applyMathitFontToRoutableCharactersInRange: is correct — no surrogate code unit (U+D800–U+DFFF) falls in the routable domain, so \mathit{a\theta b} closes runs at {0,1} and {3,1} exactly as the test asserts.

Two things.


1. NSAssert(atom.type == kMTMathAtomOrdinary, …) fires on a documented public API — and the fix is shorter than the assert (MTTypesetter.m:1079)

MTMathAtomFactory addLatexSymbol:value: is public and documented in the header. Registering any non-Ordinary atom whose value contains a Latin letter, digit or capital Greek and then using it under \mathit aborts every Debug build:

[MTMathAtomFactory addLatexSymbol:@"bmod"
                            value:[MTMathAtom atomWithType:kMTMathAtomBinaryOperator value:@"mod"]];
// \mathit{a \bmod b}

Verified on PR head with [MTMathAtom atomWithType:kMTMathAtomRelation value:@"R"] and a \mathit{\myrel} b:

MTTypesetter.m:1079: Routable character in non-Ordinary atom: Relation: R (NSInternalInconsistencyException)

LLD §3.3 C justifies the assert with "no LaTeX input produces a non-Ordinary atom with a routable nucleus". That is true of the built-in table — I decoded all 290 entries in supportedLatexSymbols and every routable-nucleus atom is kMTMathAtomVariable, which preprocessMathList: rewrites to Ordinary — but addLatexSymbol: lets a caller add one, and \bmod is a plausible thing to add given iosMath doesn't ship it.

Making it the third condition instead of an assert is a net deletion — the forAtom: parameter exists only to feed the assert:

// at the append site
if (atom.fontStyle == kMTFontStyleItalic && atom.type == kMTMathAtomOrdinary) {
    [self applyMathitFontToRoutableCharactersInRange:appendedRange];
}

and drop forAtom:(MTMathAtom*) atom from the signature and line 1079 from the loop. Four lines and a parameter out, and it's also the behaviour TeX gives: \mathit selects a family for class-7 mathchars, so a class-3 relation should keep the math font rather than crash.


2. The stated invariant behind deleting the addDisplayLine stamp is wrong; the real one is different (no behaviour bug)

The PR body and LLD §3.3 C/D justify the deletion with "_styleFont is assigned once per typesetter and never reassigned [verified: MTTypesetter.m:593 is its only assignment]". That line is the body of - (void) setStyle:(MTLineStyle) style (MTTypesetter.m:608-611 here), which makeFraction calls for \dfrac/\tfrac style overrides and restores afterwards — so _styleFont is reassigned mid-typesetter.

The deletion is still safe, for a different reason: the fraction case flushes _currentLine before makeFraction (MTTypesetter.m:827-830), and setStyle: has no other caller, so _styleFont is constant for the lifetime of any single _currentLine. My \dfrac{\mathit{ab}}{\mathit{cd}} / \tfrac{\mathit{ab}}{x} sweep cases confirm it. Worth correcting the wording, since the deletion's whole safety argument rests on it and the next person to touch this will re-derive from what's written.

The "one append site" half of the claim is exact: MTTypesetter.m:1014 is the only appendAttributedString: into _currentLine.


Everything else in the diff looks right to me. §3.3 A, C, D and G land as designed, the exclusion set matches the LLD's contract table, and the \mathit{f}i / \mathit{fi} pair is a good way to pin that the attribution actually partitions CoreText shaping.

@kostub
kostub force-pushed the feature/mathit-routing-pr2 branch from d4b2927 to 894f8de Compare August 4, 2026 13:19
@kostub
kostub force-pushed the feature/mathit-routing-pr3 branch from a00f775 to 87ac799 Compare August 4, 2026 13:19
kostub added 3 commits August 5, 2026 01:29
styleCharacter's italic case no longer remaps Latin letters, digits, and
capital Greek into the math-italic Unicode block. Those are TeX's class-7
mathchars (LLD 2026-07-27 mathit-text-italic-routing.md §2.1) and now stay
plain code points so they can be drawn from a text-italic companion face
in the next item; getItalicized still handles lowercase Greek and Greek
symbol variants, which the companion face cannot serve (LLD §2.3).
Every nucleus append now stamps _styleFont.ctFont over its own range, then
applyMathitFontToRoutableCharactersInRange:forAtom: overrides maximal runs
of routable characters within an Italic atom with _styleFont.mathitCTFont.
The whole-line font stamp in addDisplayLine is deleted since every
character already carries a font by the time flush runs, so nothing can
silently overwrite a \mathit range (LLD §3.3 C-D).
Integration tests for items 5-6: companion advances for f and 1, unrouted
categories left byte-identical to plain, companion pair-kerning on AV,
the f_i ligature shaping to one glyph, and no ligature crossing a
\mathit{f}i style boundary. Written after the implementation per plan;
all pass immediately.
@kostub
kostub force-pushed the feature/mathit-routing-pr3 branch from 87ac799 to 3604aac Compare August 4, 2026 20:01
@kostub
kostub changed the base branch from feature/mathit-routing-pr2 to master August 4, 2026 20:01

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@iosMath/render/internal/MTTypesetter.m`:
- Around line 385-395: Update getItalicized so its capital-Greek handling
explicitly excludes U+03A2 before applying the math-italic offset, matching
MTIsMathItalicRoutable’s existing exclusion. Preserve the current mapping for
assigned capital Greek letters and leave U+03A2 unchanged or rejected according
to the existing fallback behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 57e6a62b-2292-45a6-b5a4-57f93cb5184e

📥 Commits

Reviewing files that changed from the base of the PR and between 32e3e9a and 3604aac.

📒 Files selected for processing (2)
  • iosMath/render/internal/MTTypesetter.m
  • iosMathTests/MTTypesetterTest.m

Comment thread iosMath/render/internal/MTTypesetter.m
+addLatexSymbol:value: is public, so a caller can register a non-Ordinary
atom whose nucleus is routable — e.g. a BinaryOperator with value "mod".
Under \mathit that reached an NSAssert and aborted every Debug build.

Make the atom type a third routing condition instead. This is also what
TeX gives: \mathit selects a family for class-7 mathchars, so a class-3
relation keeps the math font. changeFont/styleCharacter run only for
variables and numbers, so such an atom's nucleus is not remapped either
and it renders exactly as it does without \mathit.

The forAtom: parameter existed only to feed the assert; drop it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmCWfSMYeLRJmUEvSd5XoT
@kostub

kostub commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

Both points actioned in a33c04b.

1. The assert on a public API. Confirmed and fixed as suggested — atom type is now the third routing condition, the assert and the forAtom: parameter are gone. Net −4 lines.

One thing worth adding: the fix is behaviour-preserving, not just crash-preserving. changeFont/styleCharacter run only for Variable and Number atoms (MTTypesetter.m:553), so a Relation or BinaryOperator nucleus is never passed through styleCharacter — its code points are untouched regardless. With the type condition, such an atom now renders exactly as it does without \mathit, rather than being routed to the companion. testMathitLeavesCustomNonOrdinaryAtomInMathFont pins it, and it fails on the companion font name if the condition is removed.

2. The _styleFont invariant. You are right, and the correction is in the LLD (§3.3 C and the §9 risk row). setStyle: is the only assignment, makeFraction:1389 calls it, so _styleFont is reassigned mid-typesetter. §3.3 C now states the real invariant — _styleFont is constant for the lifetime of any single _currentLine, because the fraction case flushes at MTTypesetter.m:828-830 before makeFraction and setStyle: has no other caller.

The §9 risk row also needed rewriting rather than just correcting: it described the atom-type check as a Debug-only assert that does not fire in Release. That is now a real condition in both configurations, so the residual risk it named is gone.

swift test: 467 pass.

CodeRabbit’s U+03A2 comment is declined on the thread — the offset arithmetic is correct by Unicode’s design (U+1D6F3 is MATHEMATICAL ITALIC CAPITAL THETA SYMBOL, occupying exactly that slot), and its suggested fix would shift Sigma through Omega by one.

MTIsMathItalicRoutable excluded U+03A2, the unassigned code point in the
Greek capital block, so it fell through to getItalicized and was mapped to
U+1D6F3 — MATHEMATICAL ITALIC CAPITAL THETA SYMBOL, a real glyph every
bundled math font has. An unassigned input rendered as a different, real
letter.

Route it with the rest instead. The companion has no glyph for it, so it
draws .notdef, which is a visible failure and matches what the character
already gets without \mathit: getDefaultStyle returns capital Greek
unchanged and no bundled math font carries U+03A2 either.

The exclusion was also guarding an input the parser cannot produce --
atomForCharacter: returns nil for every non-ASCII literal, so U+03A2
reaches a math list only via direct MTMathAtom construction.

Lets the function reuse IS_CAPITAL_GREEK instead of open-coding the range.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmCWfSMYeLRJmUEvSd5XoT
@kostub
kostub merged commit a5cb28d into master Aug 6, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant