diff --git a/iosMath/lib/MTMathList.m b/iosMath/lib/MTMathList.m index 901916b..5363f25 100644 --- a/iosMath/lib/MTMathList.m +++ b/iosMath/lib/MTMathList.m @@ -1707,8 +1707,10 @@ - (MTMathList *)finalized break; case kMTMathAtomNumber: - // combine numbers together - if (prevNode && prevNode.type == kMTMathAtomNumber && !prevNode.subScript && !prevNode.superScript) { + // combine numbers together, but never across a font-style change: + // fuse: keeps the first atom's fontStyle and would corrupt the other's. + if (prevNode && prevNode.type == kMTMathAtomNumber && !prevNode.subScript && !prevNode.superScript + && prevNode.fontStyle == newNode.fontStyle) { [prevNode fuse:newNode]; // skip the current node, we are done here. continue; diff --git a/iosMath/render/internal/MTTypesetter.m b/iosMath/render/internal/MTTypesetter.m index a448d22..1b3f831 100644 --- a/iosMath/render/internal/MTTypesetter.m +++ b/iosMath/render/internal/MTTypesetter.m @@ -550,8 +550,11 @@ + (NSArray*) preprocessMathList:(MTMathList*) ml if (atom.type == kMTMathAtomOrdinary) { // This is Rule 14 to merge ordinary characters. - // combine ordinary atoms together + // combine ordinary atoms together, but only within one font style -- + // the \mathit face is stamped per single-style run (and TeX's own Rule 14 + // fuses only within a family). if (prevNode && prevNode.type == kMTMathAtomOrdinary && !prevNode.subScript && !prevNode.superScript + && prevNode.fontStyle == atom.fontStyle && ![prevNode isKindOfClass:[MTLargeDelimiter class]] && ![atom isKindOfClass:[MTLargeDelimiter class]]) { [prevNode fuse:atom]; diff --git a/iosMathTests/MTMathListTest.m b/iosMathTests/MTMathListTest.m index a1a3d71..216c967 100644 --- a/iosMathTests/MTMathListTest.m +++ b/iosMathTests/MTMathListTest.m @@ -426,6 +426,48 @@ - (void)testArrayTableFactoryNormalizesVerticalLines XCTAssertEqualObjects(table.verticalLines, (@[ @1, @0, @0 ])); } +- (void) testFinalizedDoesNotFuseDigitsAcrossFontStyles +{ + // Without the guard, fuse: keeps the first atom's style: 1\mathit{2} + // silently drops the italic, \mathit{1}2 spreads it onto a plain digit. + MTMathList* list = [MTMathListBuilder buildFromString:@"1\\mathit{2}"].finalized; + XCTAssertEqual(list.atoms.count, 2); + XCTAssertEqualObjects(list.atoms[0].nucleus, @"1"); + XCTAssertEqual(list.atoms[0].fontStyle, kMTFontStyleDefault); + XCTAssertEqualObjects(list.atoms[1].nucleus, @"2"); + XCTAssertEqual(list.atoms[1].fontStyle, kMTFontStyleItalic); + + list = [MTMathListBuilder buildFromString:@"\\mathit{1}2"].finalized; + XCTAssertEqual(list.atoms.count, 2); + XCTAssertEqual(list.atoms[0].fontStyle, kMTFontStyleItalic); + XCTAssertEqual(list.atoms[1].fontStyle, kMTFontStyleDefault); + + // Style boundaries split the run; the uniform-style middle still fuses. + list = [MTMathListBuilder buildFromString:@"1\\mathit{23}4"].finalized; + XCTAssertEqual(list.atoms.count, 3); + XCTAssertEqualObjects(list.atoms[0].nucleus, @"1"); + XCTAssertEqualObjects(list.atoms[1].nucleus, @"23"); + XCTAssertEqualObjects(list.atoms[2].nucleus, @"4"); + + // The same guard repairs \mathbf on mixed-style digit runs (LLD §2.6). + list = [MTMathListBuilder buildFromString:@"1\\mathbf{2}"].finalized; + XCTAssertEqual(list.atoms.count, 2); + XCTAssertEqual(list.atoms[0].fontStyle, kMTFontStyleDefault); + XCTAssertEqual(list.atoms[1].fontStyle, kMTFontStyleBold); +} + +- (void) testFinalizedStillFusesUniformStyleDigits +{ + MTMathList* list = [MTMathListBuilder buildFromString:@"1234"].finalized; + XCTAssertEqual(list.atoms.count, 1); + XCTAssertEqualObjects(list.atoms[0].nucleus, @"1234"); + + list = [MTMathListBuilder buildFromString:@"\\mathit{12}"].finalized; + XCTAssertEqual(list.atoms.count, 1); + XCTAssertEqualObjects(list.atoms[0].nucleus, @"12"); + XCTAssertEqual(list.atoms[0].fontStyle, kMTFontStyleItalic); +} + @end @interface MTMathAtomTest : XCTestCase diff --git a/iosMathTests/MTTypesetterTest.m b/iosMathTests/MTTypesetterTest.m index 3e21979..dc46935 100644 --- a/iosMathTests/MTTypesetterTest.m +++ b/iosMathTests/MTTypesetterTest.m @@ -17,6 +17,10 @@ #import "MTMathAtomFactory.h" #import "MTMathListBuilder.h" +@interface MTTypesetter (FusionTesting) ++ (NSArray*) preprocessMathList:(MTMathList*) ml; +@end + @interface MTTypesetterTest : XCTestCase @property (nonatomic) MTFont* font; @@ -3607,4 +3611,54 @@ - (void)testArrayRuleGeometryIsDeterministic XCTAssertEqualWithAccuracy(botLine, contentBot - padding, 0.01); } +- (void) testPreprocessDoesNotFuseAcrossFontStyles +{ + // f\mathit{x}: without the guard both fuse into one Default atom and the + // italic on x is lost (LLD §2.6). + NSArray* atoms = [MTTypesetter preprocessMathList: + [MTMathListBuilder buildFromString:@"f\\mathit{x}"].finalized]; + XCTAssertEqual(atoms.count, 2); + XCTAssertEqual(atoms[0].fontStyle, kMTFontStyleDefault); + XCTAssertEqual(atoms[1].fontStyle, kMTFontStyleItalic); + + atoms = [MTTypesetter preprocessMathList: + [MTMathListBuilder buildFromString:@"\\mathit{f}x"].finalized]; + XCTAssertEqual(atoms.count, 2); + XCTAssertEqual(atoms[0].fontStyle, kMTFontStyleItalic); + XCTAssertEqual(atoms[1].fontStyle, kMTFontStyleDefault); + + // The bold digit that site A now preserves reaches its bold code point. + atoms = [MTTypesetter preprocessMathList: + [MTMathListBuilder buildFromString:@"1\\mathbf{2}"].finalized]; + XCTAssertEqual(atoms.count, 2); + XCTAssertEqualObjects(atoms[0].nucleus, @"1"); + XCTAssertEqualObjects(atoms[1].nucleus, @"\U0001D7D0"); +} + +- (void) testPreprocessStillFusesUniformStyleAtoms +{ + // Same style still merges... + NSArray* atoms = [MTTypesetter preprocessMathList: + [MTMathListBuilder buildFromString:@"\\mathit{fg}"].finalized]; + XCTAssertEqual(atoms.count, 1); + + // ...including Variable+Number pairs, which meet only at site B because + // site A fuses same-type atoms only (LLD §3.3 B). + atoms = [MTTypesetter preprocessMathList: + [MTMathListBuilder buildFromString:@"\\mathit{a1}"].finalized]; + XCTAssertEqual(atoms.count, 1); + XCTAssertEqual(atoms[0].type, kMTMathAtomOrdinary); + XCTAssertEqual(atoms[0].fontStyle, kMTFontStyleItalic); +} + +- (void) testPreprocessUnstyledListUnchanged +{ + NSArray* atoms = [MTTypesetter preprocessMathList: + [MTMathListBuilder buildFromString:@"xyz+1"].finalized]; + XCTAssertEqual(atoms.count, 3); + XCTAssertEqualObjects(atoms[0].nucleus, @"\U0001D465\U0001D466\U0001D467"); + XCTAssertEqualObjects(atoms[1].nucleus, @"+"); + XCTAssertEqualObjects(atoms[2].nucleus, @"1"); +} + @end