diff --git a/iosMath/render/internal/MTFontMathTable.m b/iosMath/render/internal/MTFontMathTable.m index 3b66bf4..bb1e5c5 100644 --- a/iosMath/render/internal/MTFontMathTable.m +++ b/iosMath/render/internal/MTFontMathTable.m @@ -427,9 +427,12 @@ - (CGFloat) stretchStackTopShiftUp { NSString* glyphName = [self.font getGlyphName:glyph]; NSArray* variantGlyphs = (NSArray*) variants[glyphName]; NSMutableArray* glyphArray = [NSMutableArray arrayWithCapacity:variantGlyphs.count]; - if (!variantGlyphs) { - // There are no extra variants, so just add the current glyph to it. - CGGlyph glyph = [self.font getGlyphWithName:glyphName]; + if (variantGlyphs.count == 0) { + // No sized variants for this glyph. This covers two cases: the glyph has no + // MathGlyphConstruction entry at all (variantGlyphs == nil), and assembly-only + // glyphs whose construction has a GlyphAssembly but zero variant records (an + // empty array, e.g. XITS's stretchy arrows). In both cases the glyph itself is + // its only variant, so callers can rely on a non-empty result. [glyphArray addObject:@(glyph)]; return glyphArray; } diff --git a/iosMathExample/example/ViewController.m b/iosMathExample/example/ViewController.m index b3c622b..e3a9ef4 100644 --- a/iosMathExample/example/ViewController.m +++ b/iosMathExample/example/ViewController.m @@ -350,9 +350,11 @@ - (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComp case 1: [self.controller termesButtonPressed:nil]; + break; case 2: [self.controller xitsButtonPressed:nil]; + break; default: break; diff --git a/iosMathTests/MTTypesetterTest.m b/iosMathTests/MTTypesetterTest.m index 2f66a52..7ff5a38 100644 --- a/iosMathTests/MTTypesetterTest.m +++ b/iosMathTests/MTTypesetterTest.m @@ -2001,6 +2001,74 @@ - (void)testOverrightarrowNarrow XCTAssertGreaterThanOrEqual(display.width + 0.01, stack.over.width); } +// Regression: XITS encodes the stretchy arrows (U+2190/2192/2194) as assembly-only +// glyphs — their OpenType MathGlyphConstruction has a GlyphAssembly but zero variant +// records, so h_variants is an empty list. Typesetting an \overrightarrow with such a +// font must not trip the "numVariants > 0" assertion; it should fall through to the +// horizontal glyph assembly. +- (void)testStretchyArrowAssemblyOnlyFont +{ + MTFont* xits = [MTFontManager.fontManager xitsFontWithSize:20]; + XCTAssertNotNil(xits); + + for (NSString* latex in @[@"\\overrightarrow{x}", @"\\overrightarrow{ABCD}", + @"\\overleftarrow{y}", @"\\overleftrightarrow{ABC}"]) { + MTMathList* list = [MTMathListBuilder buildFromString:latex]; + XCTAssertNotNil(list, @"%@", latex); + MTMathListDisplay* display = [MTTypesetter createLineForMathList:list font:xits style:kMTLineStyleDisplay]; + XCTAssertNotNil(display, @"%@", latex); + XCTAssertEqual(display.subDisplays.count, 1u, @"%@", latex); + + MTDisplay* sub0 = display.subDisplays[0]; + XCTAssertTrue([sub0 isKindOfClass:[MTStackDisplay class]], @"%@", latex); + MTStackDisplay* stack = (MTStackDisplay*)sub0; + XCTAssertNotNil(stack.over, @"%@", latex); + XCTAssertNil(stack.under, @"%@", latex); + // The over-row must cover the base width. + XCTAssertGreaterThanOrEqual(stack.over.width + 0.01, stack.base.width, @"%@", latex); + } +} + +// Vertical twin of the regression above. XITS encodes the stretchy vertical arrows +// (U+2191/2193/2195) as assembly-only glyphs — empty v_variants but a populated +// v_assembly. These are reachable as \left/\right delimiters (\uparrow, \downarrow, +// \updownarrow). Unlike the horizontal path, -findGlyph:withHeight: has no assertion +// guarding numVariants > 0: with an empty list it read glyphs[-1] (out-of-bounds). +// Treating the empty variant list as absent makes the boundary fall through to the +// vertical glyph assembly instead. +- (void)testStretchyVerticalArrowAssemblyOnlyFont +{ + MTFont* xits = [MTFontManager.fontManager xitsFontWithSize:20]; + XCTAssertNotNil(xits); + + // Tall content (a fraction) forces the boundary delimiter to stretch, exercising + // the variant lookup and then the glyph assembly. + for (NSString* latex in @[@"\\left\\uparrow \\frac{1}{2} \\right\\downarrow", + @"\\left\\updownarrow \\frac{a}{b} \\right\\updownarrow", + @"\\left\\downarrow \\frac{x}{y} \\right\\uparrow"]) { + MTMathList* list = [MTMathListBuilder buildFromString:latex]; + XCTAssertNotNil(list, @"%@", latex); + MTMathListDisplay* display = [MTTypesetter createLineForMathList:list font:xits style:kMTLineStyleDisplay]; + XCTAssertNotNil(display, @"%@", latex); + XCTAssertEqual(display.subDisplays.count, 1u, @"%@", latex); + + MTDisplay* sub0 = display.subDisplays[0]; + XCTAssertTrue([sub0 isKindOfClass:[MTInnerDisplay class]], @"%@", latex); + MTInnerDisplay* inner = (MTInnerDisplay*)sub0; + // No pre-built variant fits the tall content, so the empty variant list must + // fall through to the vertical glyph assembly rather than crash. + XCTAssertTrue([inner.leftDelimiter isKindOfClass:[MTGlyphConstructionDisplay class]], @"%@", latex); + XCTAssertTrue([inner.rightDelimiter isKindOfClass:[MTGlyphConstructionDisplay class]], @"%@", latex); + // The stretched delimiters cover the inner content's height, up to the + // allowed 5pt delimiter shortfall (kDelimiterShortfallPoints). + CGFloat innerHeight = inner.inner.ascent + inner.inner.descent; + XCTAssertGreaterThanOrEqual(inner.leftDelimiter.ascent + inner.leftDelimiter.descent + 5.01, + innerHeight, @"%@", latex); + XCTAssertGreaterThanOrEqual(inner.rightDelimiter.ascent + inner.rightDelimiter.descent + 5.01, + innerHeight, @"%@", latex); + } +} + - (void)testOverrightarrowWide { MTMathListDisplay* display = [self displayForLaTeX:@"\\overrightarrow{ABCD}"];