From 4284b243026c3488c06c6a8ef26f50716af22602 Mon Sep 17 00:00:00 2001 From: Kostub D Date: Fri, 12 Jun 2026 06:44:27 +0530 Subject: [PATCH 1/2] fix(REN-3): \color and \colorbox atoms now receive inter-element spacing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this fix, `kMTMathAtomColor` and `kMTMathAtomColorbox` cases in `-createDisplayAtoms:` never called `addInterElementSpace:prevNode currentType:atom.type`, so a colored group lost its leading inter-element gap. For example, the medium binary-operator space in `x+\color{red}y` was dropped, causing the colored `y` to abut the `+` with no gap. The fix adds the missing `addInterElementSpace:` call to each case (after the line flush, before `display.position = _currentPosition`), mirroring the text-case and inner-case patterns. Both atom types already map to index 0 (Ordinary) in `getInterElementSpaceArrayIndexForType`, so no table changes are needed. Three new typesetter tests cover: - `\color` before a binary operator gap (RED → GREEN) - `\colorbox` before a binary operator gap (RED → GREEN) - regression guard that spacing after a `\color` group is preserved (already GREEN) Co-Authored-By: Claude Sonnet 4.6 --- iosMath/render/internal/MTTypesetter.m | 4 ++ iosMathTests/MTTypesetterTest.m | 91 ++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/iosMath/render/internal/MTTypesetter.m b/iosMath/render/internal/MTTypesetter.m index 6aa2b55..fda2dfc 100644 --- a/iosMath/render/internal/MTTypesetter.m +++ b/iosMath/render/internal/MTTypesetter.m @@ -635,6 +635,8 @@ - (void) createDisplayAtoms:(NSArray*) preprocessed if (_currentLine.length > 0) { [self addDisplayLine]; } + // Color is spaced as Ord (see getInterElementSpaceArrayIndexForType). + [self addInterElementSpace:prevNode currentType:atom.type]; MTMathColor* colorAtom = (MTMathColor*) atom; MTDisplay* display = [MTTypesetter createLineForMathList:colorAtom.innerList font:_font style:_style]; display.localTextColor = [MTColor colorFromHexString:colorAtom.colorString]; @@ -649,6 +651,8 @@ - (void) createDisplayAtoms:(NSArray*) preprocessed if (_currentLine.length > 0) { [self addDisplayLine]; } + // Colorbox is spaced as Ord (see getInterElementSpaceArrayIndexForType). + [self addInterElementSpace:prevNode currentType:atom.type]; MTMathColorbox* colorboxAtom = (MTMathColorbox*) atom; MTDisplay* display = [MTTypesetter createLineForMathList:colorboxAtom.innerList font:_font style:_style]; diff --git a/iosMathTests/MTTypesetterTest.m b/iosMathTests/MTTypesetterTest.m index af6df42..a9d1165 100644 --- a/iosMathTests/MTTypesetterTest.m +++ b/iosMathTests/MTTypesetterTest.m @@ -2725,4 +2725,95 @@ - (void)testOversetRowRendersAtScriptScriptWhenNestedInSuperscript XCTAssertLessThan(nestedStack.over.ascent, baselineStack.over.ascent); } +// REN-3: \color atoms must receive inter-element spacing before the colored sub-display. +// Before the fix, the colored group abutted the preceding binary operator with no gap. +// After the fix, the medium binary-operator→ordinary gap (4 mu) separates them. + +- (void)testColorReceivesInterElementSpacingBeforeIt { + // x + \color{red}y — the colored sub-display follows the binary operator +. + // The gap between the end of "x+" and the start of the colored group must equal + // the medium space (4mu = 4 * font.mathTable.muUnit) at display style. + MTMathListDisplay* display = [self displayForLaTeX:@"x+\\color{#ff0000}{y}"]; + XCTAssertNotNil(display); + XCTAssertEqual(display.subDisplays.count, 2u, + @"Expected CTLine for 'x+' and a colored sub-display"); + + MTDisplay* sub0 = display.subDisplays[0]; + XCTAssertTrue([sub0 isKindOfClass:[MTCTLineDisplay class]], + @"First sub-display should be a CTLine for 'x+'"); + MTCTLineDisplay* xPlusLine = (MTCTLineDisplay*)sub0; + + MTDisplay* sub1 = display.subDisplays[1]; + XCTAssertTrue([sub1 isKindOfClass:[MTMathListDisplay class]], + @"Second sub-display should be the colored MTMathListDisplay"); + MTMathListDisplay* colorSub = (MTMathListDisplay*)sub1; + XCTAssertNotNil(colorSub.localTextColor, @"Colored display must carry a localTextColor"); + + // The medium binary-operator gap is 4 mu = 4 * muUnit (display style, non-script). + CGFloat expectedGap = 4.0 * self.font.mathTable.muUnit; + CGFloat actualGap = colorSub.position.x - (xPlusLine.position.x + xPlusLine.width); + XCTAssertEqualWithAccuracy(actualGap, expectedGap, 0.01, + @"Expected medium binary-op gap of %.4f pt before \\color, got %.4f pt", + expectedGap, actualGap); +} + +- (void)testColorboxReceivesInterElementSpacingBeforeIt { + // x + \colorbox{red}y — same as testColorReceivesInterElementSpacingBeforeIt + // but for \colorbox (kMTMathAtomColorbox). + MTMathListDisplay* display = [self displayForLaTeX:@"x+\\colorbox{#ff0000}{y}"]; + XCTAssertNotNil(display); + XCTAssertEqual(display.subDisplays.count, 2u, + @"Expected CTLine for 'x+' and a colorbox sub-display"); + + MTDisplay* sub0 = display.subDisplays[0]; + XCTAssertTrue([sub0 isKindOfClass:[MTCTLineDisplay class]], + @"First sub-display should be a CTLine for 'x+'"); + MTCTLineDisplay* xPlusLine = (MTCTLineDisplay*)sub0; + + MTDisplay* sub1 = display.subDisplays[1]; + XCTAssertTrue([sub1 isKindOfClass:[MTMathListDisplay class]], + @"Second sub-display should be the colorbox MTMathListDisplay"); + MTMathListDisplay* colorboxSub = (MTMathListDisplay*)sub1; + XCTAssertNotNil(colorboxSub.localBackgroundColor, + @"Colorbox display must carry a localBackgroundColor"); + + CGFloat expectedGap = 4.0 * self.font.mathTable.muUnit; + CGFloat actualGap = colorboxSub.position.x - (xPlusLine.position.x + xPlusLine.width); + XCTAssertEqualWithAccuracy(actualGap, expectedGap, 0.01, + @"Expected medium binary-op gap of %.4f pt before \\colorbox, got %.4f pt", + expectedGap, actualGap); +} + +- (void)testSpacingAfterColorGroupIsPreserved { + // Regression guard: spacing AFTER a \color group already worked via the spacing table. + // \color{red}{x} + z — the binary-operator gap after the colored group must still be present. + // The display structure is: [colored MTMathListDisplay, CTLine for "+z"]. + MTMathListDisplay* display = [self displayForLaTeX:@"\\color{#ff0000}{x}+z"]; + XCTAssertNotNil(display); + XCTAssertEqual(display.subDisplays.count, 2u, + @"Expected colored sub-display and a CTLine for '+z'"); + + MTDisplay* sub0 = display.subDisplays[0]; + XCTAssertTrue([sub0 isKindOfClass:[MTMathListDisplay class]], + @"First sub-display should be the colored MTMathListDisplay"); + MTMathListDisplay* colorSub = (MTMathListDisplay*)sub0; + XCTAssertNotNil(colorSub.localTextColor); + + MTDisplay* sub1 = display.subDisplays[1]; + XCTAssertTrue([sub1 isKindOfClass:[MTCTLineDisplay class]], + @"Second sub-display should be CTLine for '+z'"); + MTCTLineDisplay* plusZLine = (MTCTLineDisplay*)sub1; + + // The gap between end of the color group and the start of "+z" is the + // binary-operator gap (4 mu) because the typesetter sees color (Ord) then BinOp. + // But because + is a BinaryOperator and z is Ordinary, the actual inter-element + // space between color (Ord) and + (BinOp, left of the line) is medium (4mu). + // The "+z" CTLine starts at the position where the typesetter placed it. + CGFloat expectedGap = 4.0 * self.font.mathTable.muUnit; + CGFloat actualGap = plusZLine.position.x - (colorSub.position.x + colorSub.width); + XCTAssertEqualWithAccuracy(actualGap, expectedGap, 0.01, + @"Spacing after \\color group must be preserved (%.4f pt), got %.4f pt", + expectedGap, actualGap); +} + @end From f40c063e3e7f05feda3b040ec7defb7508d94249 Mon Sep 17 00:00:00 2001 From: Kostub D Date: Fri, 12 Jun 2026 12:20:37 +0530 Subject: [PATCH 2/2] test: tidy rambling comment in testSpacingAfterColorGroupIsPreserved Addresses non-blocking review nit. The previous comment block re-derived the Ord->BinOp spacing three times with a self-contradicting "But because" pivot. Replaced with one accurate sentence. No behavioral change. Co-Authored-By: Claude Opus 4.8 --- iosMathTests/MTTypesetterTest.m | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/iosMathTests/MTTypesetterTest.m b/iosMathTests/MTTypesetterTest.m index a9d1165..282e06d 100644 --- a/iosMathTests/MTTypesetterTest.m +++ b/iosMathTests/MTTypesetterTest.m @@ -2804,11 +2804,8 @@ - (void)testSpacingAfterColorGroupIsPreserved { @"Second sub-display should be CTLine for '+z'"); MTCTLineDisplay* plusZLine = (MTCTLineDisplay*)sub1; - // The gap between end of the color group and the start of "+z" is the - // binary-operator gap (4 mu) because the typesetter sees color (Ord) then BinOp. - // But because + is a BinaryOperator and z is Ordinary, the actual inter-element - // space between color (Ord) and + (BinOp, left of the line) is medium (4mu). - // The "+z" CTLine starts at the position where the typesetter placed it. + // The color group is Ord and '+' is a BinaryOperator, so the gap between them + // is the Ord->BinOp inter-element space: medium (4 mu). CGFloat expectedGap = 4.0 * self.font.mathTable.muUnit; CGFloat actualGap = plusZLine.position.x - (colorSub.position.x + colorSub.width); XCTAssertEqualWithAccuracy(actualGap, expectedGap, 0.01,