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
35 changes: 35 additions & 0 deletions iosMath/lib/MTMathList.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ typedef NS_ENUM(NSUInteger, MTMathAtomType)
/// \textit, \textsf, \texttt. Captured raw at parse time; the body cannot
/// contain math.
kMTMathAtomText = 19,
/// A box atom (phantom/smash/lap family). Script-capable (< kMTMathAtomBoundary).
kMTMathAtomBox = 20,

// Atoms after this point do not support subscripts or superscripts

Expand Down Expand Up @@ -611,6 +613,39 @@ typedef NS_ENUM(NSUInteger, MTMathStackConstructionKind) {

@end

/** Horizontal alignment of a box's child relative to the box origin (drives the lap draw offset). */
typedef NS_ENUM(NSUInteger, MTBoxHAlign) {
kMTBoxHAlignLeft = 0, ///< \rlap: child left edge at origin, offset 0
kMTBoxHAlignCenter, ///< \clap: offset -childWidth/2
kMTBoxHAlignRight, ///< \llap: child right edge at origin, offset -childWidth
};

/** An atom representing a box element: the phantom/smash/lap family.
@note As with `MTMathColorbox`, the usual nucleus/script fields are unused;
the content lives in `innerList` and the flags below select the variant. */
@interface MTMathBox : MTMathAtom

/// Creates an empty box atom (type = kMTMathAtomBox).
- (instancetype) init NS_DESIGNATED_INITIALIZER;

/// Throws unless type == kMTMathAtomBox.
- (instancetype) initWithType:(MTMathAtomType)type value:(NSString *)value;

/// The wrapped math content.
@property (nonatomic, nullable) MTMathList* innerList;
/// Report the child's width (YES) or zero width (NO).
@property (nonatomic) BOOL keepWidth;
/// Report the child's ascent (YES) or zero ascent (NO).
@property (nonatomic) BOOL keepHeight;
/// Report the child's descent (YES) or zero descent (NO).
@property (nonatomic) BOOL keepDepth;
/// Draw the measured child (YES) or suppress drawing entirely (NO, phantom).
@property (nonatomic) BOOL drawChild;
/// Horizontal draw offset applied when keepWidth == NO (laps).
@property (nonatomic) MTBoxHAlign hAlign;

@end

/** An atom representing an table element. This atom is not like other
atoms and is not present in TeX. We use it to represent the `\halign` command
in TeX with some simplifications. This is used for matrices, equation
Expand Down
80 changes: 79 additions & 1 deletion iosMath/lib/MTMathList.m
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ static BOOL isNotBinaryOperator(MTMathAtom* prevNode)
return @"Stack";
case kMTMathAtomText:
return @"Text";
case kMTMathAtomBox:
return @"Box";
case kMTMathAtomBoundary:
return @"Boundary";
case kMTMathAtomSpace:
Expand Down Expand Up @@ -153,9 +155,12 @@ + (instancetype)atomWithType:(MTMathAtomType)type value:(NSString *)value
return [[MTTextAtom alloc] initWithText:value ?: @""
style:kMTTextStyleRoman];

case kMTMathAtomBox:
return [[MTMathBox alloc] init];

case kMTMathAtomSpace:
return [[MTMathSpace alloc] initWithSpace:0];

case kMTMathAtomColor:
return [[MTMathColor alloc] init];

Expand Down Expand Up @@ -997,6 +1002,79 @@ - (instancetype)finalized
@end


#pragma mark - MTMathBox

@implementation MTMathBox

- (instancetype)init
{
self = [super initWithType:kMTMathAtomBox value:@""];
return self;
}

- (instancetype)initWithType:(MTMathAtomType)type value:(NSString *)value
{
if (type == kMTMathAtomBox) {
return [self init];
}
@throw [NSException exceptionWithName:@"InvalidMethod"
reason:@"[MTMathBox initWithType:value:] cannot be called. Use [MTMathBox init] instead."
userInfo:nil];
}

// Lossy by design (LLD §3.4): pick the closest LaTeX command from the flag matrix.
- (NSString *)stringValue
{
NSString* cmd;
NSString* inner = self.innerList.stringValue ?: @"";
if (!self.drawChild) {
// phantom family
if (self.keepWidth && self.keepHeight && self.keepDepth) cmd = @"\\phantom";
else if (self.keepWidth) cmd = @"\\hphantom";
else cmd = @"\\vphantom"; // covers \mathstrut -> \vphantom{(}
} else if (!self.keepWidth) {
// lap family
switch (self.hAlign) {
case kMTBoxHAlignRight: cmd = @"\\llap"; break;
case kMTBoxHAlignCenter: cmd = @"\\clap"; break;
case kMTBoxHAlignLeft:
default: cmd = @"\\rlap"; break;
}
} else {
// smash family
if (!self.keepHeight && !self.keepDepth) cmd = @"\\smash";
else if (self.keepDepth && !self.keepHeight) cmd = @"\\smash[t]";
else cmd = @"\\smash[b]";
}
return [NSString stringWithFormat:@"%@{%@}", cmd, inner];
}

- (void)appendLaTeXToString:(NSMutableString *)str
{
[str appendString:self.stringValue];
}

- (id)copyWithZone:(NSZone *)zone
{
MTMathBox* op = [super copyWithZone:zone];
op.innerList = [self.innerList copyWithZone:zone];
op->_keepWidth = self.keepWidth;
op->_keepHeight = self.keepHeight;
op->_keepDepth = self.keepDepth;
op->_drawChild = self.drawChild;
op->_hAlign = self.hAlign;
return op;
}

- (instancetype)finalized
{
MTMathBox *newBox = [super finalized];
newBox.innerList = newBox.innerList.finalized;
return newBox;
}

@end

#pragma mark - MTMathTable

@interface MTMathTable ()
Expand Down
75 changes: 74 additions & 1 deletion iosMath/lib/MTMathListBuilder.m
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,30 @@ - (MTMathAtom*) getBoundaryAtom:(NSString*) delimiterType
return boundary;
}

// Maps each phantom/smash/lap command to its MTMathBox flag set.
// keys: kW=keepWidth, kH=keepHeight, kD=keepDepth, draw=drawChild, hAlign, acceptsTB, synthParen
+ (NSDictionary<NSString*, NSDictionary*>*) boxCommands
{
static NSDictionary* commands = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
commands = @{
@"phantom": @{@"kW":@YES, @"kH":@YES, @"kD":@YES, @"draw":@NO},
@"hphantom": @{@"kW":@YES, @"kH":@NO, @"kD":@NO, @"draw":@NO},
@"vphantom": @{@"kW":@NO, @"kH":@YES, @"kD":@YES, @"draw":@NO},
@"mathstrut": @{@"kW":@NO, @"kH":@YES, @"kD":@YES, @"draw":@NO, @"synthParen":@YES},
@"smash": @{@"kW":@YES, @"kH":@NO, @"kD":@NO, @"draw":@YES, @"acceptsTB":@YES},
@"llap": @{@"kW":@NO, @"kH":@YES, @"kD":@YES, @"draw":@YES, @"hAlign":@(kMTBoxHAlignRight)},
@"rlap": @{@"kW":@NO, @"kH":@YES, @"kD":@YES, @"draw":@YES, @"hAlign":@(kMTBoxHAlignLeft)},
@"clap": @{@"kW":@NO, @"kH":@YES, @"kD":@YES, @"draw":@YES, @"hAlign":@(kMTBoxHAlignCenter)},
@"mathllap": @{@"kW":@NO, @"kH":@YES, @"kD":@YES, @"draw":@YES, @"hAlign":@(kMTBoxHAlignRight)},
@"mathrlap": @{@"kW":@NO, @"kH":@YES, @"kD":@YES, @"draw":@YES, @"hAlign":@(kMTBoxHAlignLeft)},
@"mathclap": @{@"kW":@NO, @"kH":@YES, @"kD":@YES, @"draw":@YES, @"hAlign":@(kMTBoxHAlignCenter)},
};
});
return commands;
}

- (MTMathAtom*) atomForCommand:(NSString*) command
{
MTMathAtom* atom = [MTMathAtomFactory atomForLatexSymbolName:command];
Expand Down Expand Up @@ -923,7 +947,56 @@ - (MTMathAtom*) atomForCommand:(NSString*) command
mathColorbox.colorString = colorStr;
mathColorbox.innerList = [self buildInternal:true];
return mathColorbox;
} else {
}

NSDictionary* boxSpec = [MTMathListBuilder boxCommands][command];
if (boxSpec) {
MTMathBox* box = [MTMathBox new];
box.keepWidth = [boxSpec[@"kW"] boolValue];
box.keepHeight = [boxSpec[@"kH"] boolValue];
box.keepDepth = [boxSpec[@"kD"] boolValue];
box.drawChild = [boxSpec[@"draw"] boolValue];
box.hAlign = (MTBoxHAlign)[boxSpec[@"hAlign"] unsignedIntegerValue];

if ([boxSpec[@"synthParen"] boolValue]) {
// \mathstrut: no argument; synthetic inner list with a single open paren.
MTMathList* inner = [MTMathList new];
MTMathAtom* paren = [MTMathAtomFactory atomForCharacter:'('];
[inner addAtom:paren];
box.innerList = inner;
return box;
}

if ([boxSpec[@"acceptsTB"] boolValue] && [self hasCharacters]) {
// \smash[t]/[b]: optional [t]/[b] before the {X} argument (\sqrt[…] pattern).
unichar ch = [self getNextCharacter];
if (ch == '[') {
NSMutableString* opt = [NSMutableString string];
BOOL foundClose = NO;
while ([self hasCharacters]) {
unichar c = [self getNextCharacter];
if (c == ']') { foundClose = YES; break; }
[opt appendString:[NSString stringWithCharacters:&c length:1]];
}
if (!foundClose) {
// Mirror \sqrt[…]: a missing ']' is a parse error, not a silent recovery.
[self setError:MTParseErrorCharacterNotFound message:@"Expected character not found: ]"];
return nil;
}
NSString* o = [opt stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if ([o isEqualToString:@"t"]) { box.keepHeight = NO; box.keepDepth = YES; }
else if ([o isEqualToString:@"b"]) { box.keepHeight = YES; box.keepDepth = NO; }
// any other value: ignore, leave smash-both flags (no crash).
Comment on lines +973 to +989

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

When parsing the optional argument [t] or [b] for \smash, if the closing bracket ] is missing (e.g., \smash[t at the end of the input), the parser currently continues silently without raising an error. To be consistent with other optional argument parsing (like \sqrt) and standard LaTeX syntax, we should verify that the closing bracket was successfully found and raise a MTParseErrorCharacterNotFound error if it is missing.

            if (ch == '[') {
                NSMutableString* opt = [NSMutableString string];
                BOOL foundClose = NO;
                while ([self hasCharacters]) {
                    unichar c = [self getNextCharacter];
                    if (c == ']') {
                        foundClose = YES;
                        break;
                    }
                    [opt appendString:[NSString stringWithCharacters:&c length:1]];
                }
                if (!foundClose) {
                    [self setError:MTParseErrorCharacterNotFound message:@"Expected character not found: ]"];
                    return nil;
                }
                NSString* o = [opt stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
                if ([o isEqualToString:@"t"]) { box.keepHeight = NO; box.keepDepth = YES; }
                else if ([o isEqualToString:@"b"]) { box.keepHeight = YES; box.keepDepth = NO; }
                // any other value: ignore, leave smash-both flags (no crash).

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.

Fixed in 0a8395f. The smash optional-arg loop now tracks the closing bracket and raises MTParseErrorCharacterNotFound when it is missing, matching the \sqrt[…] precedent. Added \smash[t to the parse-error test table.

} else {
[self unlookCharacter];
}
}

box.innerList = [self buildInternal:true];
return box;
}

{
NSString* errorMessage = [NSString stringWithFormat:@"Invalid command \\%@", command];
[self setError:MTParseErrorInvalidCommand message:errorMessage];
return nil;
Expand Down
5 changes: 5 additions & 0 deletions iosMath/render/MTMathListDisplay.h
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,11 @@ typedef NS_ENUM(unsigned int, MTLinePosition) {

@end

/** Display for the box family (phantom/smash/lap). Reports geometry selected by
the keep* flags and either draws or suppresses its measured child. */
@interface MTMathBoxDisplay : MTDisplay
@end

/// Rendering of an list with delimiters
@interface MTInnerDisplay : MTDisplay

Expand Down
67 changes: 67 additions & 0 deletions iosMath/render/MTMathListDisplay.m
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,73 @@ - (void)dealloc

@end

#pragma mark - MTMathBoxDisplay

@implementation MTMathBoxDisplay

- (instancetype) initWithChild:(MTMathListDisplay*) child
keepWidth:(BOOL) keepWidth
keepHeight:(BOOL) keepHeight
keepDepth:(BOOL) keepDepth
drawChild:(BOOL) drawChild
hAlign:(MTBoxHAlign) hAlign
range:(NSRange) range
{
self = [super init];
if (self) {
_child = child;
_drawChild = drawChild;
_keepWidth = keepWidth;
_hAlign = hAlign;
self.width = keepWidth ? child.width : 0;
self.ascent = keepHeight ? child.ascent : 0;
self.descent = keepDepth ? child.descent : 0;
self.range = range;
}
return self;
}

- (void)setTextColor:(MTColor *)textColor
{
[super setTextColor:textColor];
self.child.textColor = textColor; // forward so smash/lap inherit label color
}

- (void) setPosition:(CGPoint)position
{
super.position = position;
[self updateChildPosition];
}

- (void) updateChildPosition
{
// Push an absolute position down to the child (mirrors MTRadicalDisplay /
// MTInnerDisplay) so draw: never has to mutate child state or juggle the CTM.
CGFloat offset = 0;
if (!self.keepWidth) {
switch (self.hAlign) {
case kMTBoxHAlignRight: offset = -self.child.width; break; // \llap
case kMTBoxHAlignCenter: offset = -self.child.width / 2; break; // \clap
case kMTBoxHAlignLeft:
default: offset = 0; break; // \rlap
}
}
self.child.position = CGPointMake(self.position.x + offset, self.position.y);
}

- (void)draw:(CGContextRef)context
{
[super draw:context]; // base draws only localBackgroundColor (a no-op here)
if (!self.drawChild) {
return; // phantom: geometry already flowed up at measure time
}
// Child holds its own absolute position (set in setPosition:); it translates
// the CTM by that position itself, so there is nothing to do here but draw it.
[self.child draw:context];
}

@end

#pragma mark - MTInnerDisplay

@implementation MTInnerDisplay {
Expand Down
17 changes: 17 additions & 0 deletions iosMath/render/internal/MTMathListDisplayInternal.h
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,23 @@ NS_ASSUME_NONNULL_BEGIN
@end


@interface MTMathBoxDisplay ()

- (instancetype) initWithChild:(MTMathListDisplay*) child
keepWidth:(BOOL) keepWidth
keepHeight:(BOOL) keepHeight
keepDepth:(BOOL) keepDepth
drawChild:(BOOL) drawChild
hAlign:(MTBoxHAlign) hAlign
range:(NSRange) range NS_DESIGNATED_INITIALIZER;

@property (nonatomic) MTMathListDisplay* child;
@property (nonatomic) BOOL drawChild;
@property (nonatomic) BOOL keepWidth;
@property (nonatomic) MTBoxHAlign hAlign;

@end

@interface MTInnerDisplay ()

- (instancetype) initWithInner:(MTMathListDisplay*) inner leftDelimiter:(MTDisplay*) leftDelimiter rightDelimiter:(MTDisplay*) rightDelimiter atIndex:(NSUInteger) index NS_DESIGNATED_INITIALIZER;
Expand Down
29 changes: 29 additions & 0 deletions iosMath/render/internal/MTTypesetter.m
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,35 @@ - (void) createDisplayAtoms:(NSArray*) preprocessed
break;
}

case kMTMathAtomBox: {
// stash the existing layout
if (_currentLine.length > 0) {
[self addDisplayLine];
}
// Box spacing class is Ordinary: reclassify before any inter-element lookup
// (mirrors overline/accent), so it never reaches the getInterElementSpace default assert.
[self addInterElementSpace:prevNode currentType:kMTMathAtomOrdinary];
atom.type = kMTMathAtomOrdinary;

MTMathBox* boxAtom = (MTMathBox*) atom;
MTMathListDisplay* child = [MTTypesetter createLineForMathList:boxAtom.innerList font:_font style:_style];
MTMathBoxDisplay* display = [[MTMathBoxDisplay alloc] initWithChild:child
keepWidth:boxAtom.keepWidth
keepHeight:boxAtom.keepHeight
keepDepth:boxAtom.keepDepth
drawChild:boxAtom.drawChild
hAlign:boxAtom.hAlign
range:atom.indexRange];
display.position = _currentPosition;
_currentPosition.x += display.width; // 0 for vphantom/laps
[_displayAtoms addObject:display];

if (atom.subScript || atom.superScript) {
[self makeScripts:atom display:display index:atom.indexRange.location delta:0];
}
break;
}

case kMTMathAtomText: {
// Flush any pending math run so the text block stands alone.
if (_currentLine.length > 0) {
Expand Down
Loading
Loading