Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions iosMath/render/internal/MTFontMathTable.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 2 additions & 0 deletions iosMathExample/example/ViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
68 changes: 68 additions & 0 deletions iosMathTests/MTTypesetterTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -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}"];
Expand Down
Loading