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
57 changes: 57 additions & 0 deletions iosMath/lib/MTMathList.h
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,39 @@ typedef NS_ENUM(NSUInteger, MTTextStyle)

@end

/**
@typedef MTFractionStyle
@brief Explicit math style for a fraction (the AMSMath \genfrac style digit).

- kMTFractionStyleAuto: Honor the surrounding style via TeX Rule 15a
(one step smaller for operands; same style as parent for the bar/metrics).
This is the default and corresponds to plain \frac/\binom/\atop/\over.
- kMTFractionStyleDisplay: Force display style for this fraction
(\dfrac, \dbinom, \cfrac).
- kMTFractionStyleText: Force text style (\tfrac, \tbinom).
- kMTFractionStyleScript / kMTFractionStyleScriptScript: Reserved for
future \genfrac support. Not produced by any command in this LLD's
scope, but valid values for forward use.
*/
typedef NS_ENUM(NSUInteger, MTFractionStyle) {
kMTFractionStyleAuto = 0,
kMTFractionStyleDisplay,
kMTFractionStyleText,
kMTFractionStyleScript,
kMTFractionStyleScriptScript,
};

/**
@typedef MTFractionAlignment
@brief Horizontal alignment of the numerator within the fraction column.
Only \cfrac[l]/[c]/[r] sets a non-center value.
*/
typedef NS_ENUM(NSUInteger, MTFractionAlignment) {
kMTFractionAlignmentCenter = 0,
kMTFractionAlignmentLeft,
kMTFractionAlignmentRight,
};

/** An atom of type fraction. This atom has a numerator and denominator. */
@interface MTFraction : MTMathAtom

Expand All @@ -228,6 +261,30 @@ typedef NS_ENUM(NSUInteger, MTTextStyle)
/** An optional delimiter for a fraction on the right. */
@property (nonatomic, nullable) NSString* rightDelimiter;

/** Optional explicit style override for this fraction. Default
kMTFractionStyleAuto means: honor the surrounding style via TeX Rule 15a.
\dfrac and \dbinom set this to kMTFractionStyleDisplay; \tfrac and \tbinom
set this to kMTFractionStyleText; \cfrac sets this to kMTFractionStyleDisplay. */
@property (nonatomic) MTFractionStyle styleOverride;

/** True for \cfrac. The typesetter uses this flag to apply AMSMath's
strut equivalents to both operand displays and to wrap the rendered
fraction with surrounding 3mu thin space. Does not affect the style
decision, which is encoded in styleOverride.

Not persisted by appendLaTeXToString:/+[MTMathListBuilder mathListToString:]:
\cfrac serializes as \frac{\displaystyle{...}}{\displaystyle{...}}, so a
latex -> MTMathList -> latex round trip drops this flag. */
@property (nonatomic) BOOL isContinuedFraction;

/** Numerator alignment within max(numWidth, denWidth). Default
kMTFractionAlignmentCenter. Only \cfrac[l]/[r] sets a non-default value.

Not persisted by appendLaTeXToString:/+[MTMathListBuilder mathListToString:]:
the [l]/[r] optional argument is not emitted, so a round trip drops this
alignment. */
@property (nonatomic) MTFractionAlignment numeratorAlignment;

@end

/** An atom of type radical (square root). */
Expand Down
26 changes: 24 additions & 2 deletions iosMath/lib/MTMathList.m
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,9 @@ - (id)copyWithZone:(NSZone *)zone
frac->_hasRule = self.hasRule;
frac.leftDelimiter = [self.leftDelimiter copyWithZone:zone];
frac.rightDelimiter = [self.rightDelimiter copyWithZone:zone];
frac.styleOverride = self.styleOverride;
frac.isContinuedFraction = self.isContinuedFraction;
frac.numeratorAlignment = self.numeratorAlignment;
return frac;
}

Expand All @@ -378,12 +381,31 @@ - (instancetype)finalized

- (void)appendLaTeXToString:(NSMutableString *)str
{
NSString* numLatex = [MTMathListBuilder mathListToString:self.numerator];
NSString* denLatex = [MTMathListBuilder mathListToString:self.denominator];
NSString* (^wrap)(NSString*) = ^NSString*(NSString* inner) {
switch (self.styleOverride) {
case kMTFractionStyleDisplay:
return [NSString stringWithFormat:@"\\displaystyle{%@}", inner];
case kMTFractionStyleText:
return [NSString stringWithFormat:@"\\textstyle{%@}", inner];
case kMTFractionStyleScript:
return [NSString stringWithFormat:@"\\scriptstyle{%@}", inner];
case kMTFractionStyleScriptScript:
return [NSString stringWithFormat:@"\\scriptscriptstyle{%@}", inner];
case kMTFractionStyleAuto:
default:
return inner;
}
};
NSString* numWrapped = wrap(numLatex);
NSString* denWrapped = wrap(denLatex);
if (self.hasRule) {
[str appendFormat:@"\\frac{%@}{%@}", [MTMathListBuilder mathListToString:self.numerator], [MTMathListBuilder mathListToString:self.denominator]];
[str appendFormat:@"\\frac{%@}{%@}", numWrapped, denWrapped];
return;
}
NSString* command = fractionCommandForDelimiterPair(self.leftDelimiter, self.rightDelimiter);
[str appendFormat:@"{%@ \\%@ %@}", [MTMathListBuilder mathListToString:self.numerator], command, [MTMathListBuilder mathListToString:self.denominator]];
[str appendFormat:@"{%@ \\%@ %@}", numWrapped, command, denWrapped];
}
Comment on lines +386 to 409
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The current implementation of appendLaTeXToString: for MTFraction has several issues:

  1. Semantic Correctness (TeX Rule 15): Wrapping operands in \displaystyle{...} is not equivalent to setting the fraction's style. In TeX, if a fraction is in Display style, its operands are typeset in Text style (Rule 15). Forcing operands to \displaystyle results in incorrect sizing and placement of sub-elements (like limits on sums) within the fraction.
  2. Loss of Metadata: The isContinuedFraction flag and numeratorAlignment (used by \cfrac) are completely lost during serialization. This breaks the "round-trip" goal mentioned in the PR description.
  3. Non-standard LaTeX: \displaystyle{...} is a non-standard grouping (it's a switch, not a command taking an argument), although some parsers accept it. Canonical LaTeX would be {\displaystyle ...}.
  4. Macro Preservation: Since the parser now supports \dfrac, \tfrac, \cfrac, \dbinom, and \tbinom, the serializer should use these macros when the properties match. This ensures a faithful round-trip of the data model.

I suggest refactoring this method to use the specific macros based on the atom's properties.

- (void)appendLaTeXToString:(NSMutableString *)str
{
    if (self.isContinuedFraction) {
        [str appendString:@"\\cfrac"];
        if (self.numeratorAlignment == kMTFractionAlignmentLeft) {
            [str appendString:@"[l]"];
        } else if (self.numeratorAlignment == kMTFractionAlignmentRight) {
            [str appendString:@"[r]"];
        }
        [str appendFormat:@"{%@}{%@}", [MTMathListBuilder mathListToString:self.numerator], [MTMathListBuilder mathListToString:self.denominator]];
        return;
    }

    if (self.hasRule) {
        if (self.styleOverride == kMTFractionStyleDisplay) {
            [str appendString:@"\\dfrac"];
        } else if (self.styleOverride == kMTFractionStyleText) {
            [str appendString:@"\\tfrac"];
        } else {
            [str appendString:@"\\frac"];
        }
        [str appendFormat:@"{%@}{%@}", [MTMathListBuilder mathListToString:self.numerator], [MTMathListBuilder mathListToString:self.denominator]];
        return;
    }

    if ([self.leftDelimiter isEqualToString:@"("] && [self.rightDelimiter isEqualToString:@")"]) {
        if (self.styleOverride == kMTFractionStyleDisplay) {
            [str appendFormat:@"\\dbinom{%@}{%@}", [MTMathListBuilder mathListToString:self.numerator], [MTMathListBuilder mathListToString:self.denominator]];
            return;
        } else if (self.styleOverride == kMTFractionStyleText) {
            [str appendFormat:@"\\tbinom{%@}{%@}", [MTMathListBuilder mathListToString:self.numerator], [MTMathListBuilder mathListToString:self.denominator]];
            return;
        }
    }

    NSString* command = fractionCommandForDelimiterPair(self.leftDelimiter, self.rightDelimiter);
    [str appendFormat:@"{%@ \\%@ %@}", [MTMathListBuilder mathListToString:self.numerator], command, [MTMathListBuilder mathListToString:self.denominator]];
}


@end
Expand Down
132 changes: 118 additions & 14 deletions iosMath/lib/MTMathListBuilder.m
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,60 @@ - (void) unlookCharacter
_currentChar--;
}

// Reads an optional [l|c|r] argument for \cfrac. If the next character is '[',
// consumes one letter (l|c|r), then ']', writes the corresponding
// MTFractionAlignment to *outAlignment, returns YES. If the bracket body is
// anything else, calls -setError: and still returns YES (consumption happened);
// the caller should bail on _error. If the next character is not '[', restores
// the position and returns NO.
- (BOOL) readOptionalAlignment:(MTFractionAlignment*)outAlignment
{
if (![self hasCharacters]) {
return NO;
}
unichar ch = [self getNextCharacter];
if (ch != '[') {
[self unlookCharacter];
return NO;
}
// Read one alignment letter
if (![self hasCharacters]) {
[self setError:MTParseErrorInvalidCommand
message:@"Unterminated optional alignment for \\cfrac"];
return YES;
}
unichar letter = [self getNextCharacter];
MTFractionAlignment alignment;
switch (letter) {
case 'l': alignment = kMTFractionAlignmentLeft; break;
case 'c': alignment = kMTFractionAlignmentCenter; break;
case 'r': alignment = kMTFractionAlignmentRight; break;
default: {
NSString* errorMessage = [NSString stringWithFormat:
@"Invalid alignment for \\cfrac: '%C' (expected l, c, or r)", letter];
[self setError:MTParseErrorInvalidCommand message:errorMessage];
return YES;
}
}
// Require closing ']'
if (![self hasCharacters]) {
[self setError:MTParseErrorInvalidCommand
message:@"Unterminated optional alignment for \\cfrac"];
return YES;
}
unichar close = [self getNextCharacter];
if (close != ']') {
NSString* errorMessage = [NSString stringWithFormat:
@"Expected ']' to close \\cfrac alignment, got '%C'", close];
[self setError:MTParseErrorInvalidCommand message:errorMessage];
return YES;
}
if (outAlignment) {
*outAlignment = alignment;
}
return YES;
}

- (MTMathList *)build
{
MTMathList* list = [self buildInternal:false];
Expand Down Expand Up @@ -653,25 +707,42 @@ - (MTMathAtom*) atomForCommand:(NSString*) command
mathClass:mathClass
size:size];
}
NSDictionary<NSString*, NSDictionary*>* fracTable = [MTMathListBuilder fractionMacroCommands];
NSDictionary* fracSpec = fracTable[command];
if (fracSpec) {
BOOL hasRule = [fracSpec[@"hasRule"] boolValue];
MTFractionStyle style = (MTFractionStyle)[fracSpec[@"style"] unsignedIntegerValue];
MTFraction* frac = hasRule ? [MTFraction new] : [[MTFraction alloc] initWithRule:NO];
frac.styleOverride = style;
if ([fracSpec[@"acceptsAlign"] boolValue]) {
MTFractionAlignment alignment = kMTFractionAlignmentCenter;
if ([self readOptionalAlignment:&alignment]) {
if (_error) {
return nil;
}
frac.numeratorAlignment = alignment;
}
}
if ([fracSpec[@"continued"] boolValue]) {
frac.isContinuedFraction = YES;
}
frac.numerator = [self buildInternal:true];
frac.denominator = [self buildInternal:true];
NSString* leftDelim = fracSpec[@"leftDelim"];
NSString* rightDelim = fracSpec[@"rightDelim"];
if (leftDelim) {
frac.leftDelimiter = leftDelim;
}
if (rightDelim) {
frac.rightDelimiter = rightDelim;
}
return frac;
}
MTAccent* accent = [MTMathAtomFactory accentWithName:command];
if (accent) {
// The command is an accent
accent.innerList = [self buildInternal:true];
return accent;
} else if ([command isEqualToString:@"frac"]) {
// A fraction command has 2 arguments
MTFraction* frac = [MTFraction new];
frac.numerator = [self buildInternal:true];
frac.denominator = [self buildInternal:true];
return frac;
} else if ([command isEqualToString:@"binom"]) {
// A binom command has 2 arguments
MTFraction* frac = [[MTFraction alloc] initWithRule:NO];
frac.numerator = [self buildInternal:true];
frac.denominator = [self buildInternal:true];
frac.leftDelimiter = @"(";
frac.rightDelimiter = @")";
return frac;
} else if ([command isEqualToString:@"sqrt"]) {
// A sqrt command with one argument
MTRadical* rad = [MTRadical new];
Expand Down Expand Up @@ -959,6 +1030,39 @@ + (NSDictionary*) spaceToCommands
return largeDelimiterCommands;
}

+ (NSDictionary<NSString*, NSDictionary*>*) fractionMacroCommands
{
static NSDictionary<NSString*, NSDictionary*>* fractionMacroCommands = nil;
static dispatch_once_t fractionOnceToken;
dispatch_once(&fractionOnceToken, ^{
fractionMacroCommands = @{
@"frac" : @{ @"hasRule": @YES,
@"style": @(kMTFractionStyleAuto) },
@"binom" : @{ @"hasRule": @NO,
@"leftDelim": @"(",
@"rightDelim": @")",
@"style": @(kMTFractionStyleAuto) },
@"dfrac" : @{ @"hasRule": @YES,
@"style": @(kMTFractionStyleDisplay) },
@"tfrac" : @{ @"hasRule": @YES,
@"style": @(kMTFractionStyleText) },
@"dbinom" : @{ @"hasRule": @NO,
@"leftDelim": @"(",
@"rightDelim": @")",
@"style": @(kMTFractionStyleDisplay) },
@"tbinom" : @{ @"hasRule": @NO,
@"leftDelim": @"(",
@"rightDelim": @")",
@"style": @(kMTFractionStyleText) },
@"cfrac" : @{ @"hasRule": @YES,
@"style": @(kMTFractionStyleDisplay),
@"continued": @YES,
@"acceptsAlign":@YES },
};
});
return fractionMacroCommands;
}

+ (NSDictionary*) styleToCommands
{
static NSDictionary* styleToCommands = nil;
Expand Down
Loading
Loading