From d2df078dc9c783660c5af6343a8078b430ed3e9c Mon Sep 17 00:00:00 2001 From: Nicolas Guillot Date: Mon, 3 Nov 2025 10:23:49 +0100 Subject: [PATCH] Add support for dfrac and tfrac LaTeX commands Add display-style (dfrac) and text-style (tfrac) fraction commands to SwiftMath's LaTeX parser. These commands force fractions to render in specific styles regardless of context. Implementation: - Add dfrac parsing to prepend displaystyle to numerator/denominator - Add tfrac parsing to prepend textstyle to numerator/denominator - Implement in both parser functions in MTMathListBuilder.swift Testing: - Add testDisplayStyleFraction() for dfrac validation - Add testTextStyleFraction() for tfrac validation - Add testDisplayAndTextStyleFractions() for complex expressions - All 180 tests pass on macOS and iOS simulator Documentation: - Update MISSING_FEATURES.md (7/12 features now implemented, 58%) - Update README.md feature list to include dfrac and tfrac Fixes issue where equations like y'=-\dfrac{2}{x^{3}} would fail to parse with "Invalid command dfrac" error. This was blocking the StepByStep feature preview rendering. --- MISSING_FEATURES.md | 25 +++- README.md | 2 +- .../MathRender/MTMathListBuilder.swift | 56 +++++++++ .../MTMathListBuilderTests.swift | 112 ++++++++++++++++++ 4 files changed, 191 insertions(+), 4 deletions(-) diff --git a/MISSING_FEATURES.md b/MISSING_FEATURES.md index 9ae210a..d215d07 100644 --- a/MISSING_FEATURES.md +++ b/MISSING_FEATURES.md @@ -4,10 +4,10 @@ This document lists LaTeX features that are **not yet implemented** in SwiftMath ## Summary -- **Total Features Tested**: 11 -- **Fully Implemented**: 6 (55%) +- **Total Features Tested**: 12 +- **Fully Implemented**: 7 (58%) - **Partially Implemented**: 0 (0%) -- **Not Implemented**: 5 (45%) +- **Not Implemented**: 5 (42%) --- @@ -126,6 +126,25 @@ x \, y \: z \; w % mixed spacing --- +### 7b. ✅ `\dfrac` and `\tfrac` - Display/Text Style Fractions - **IMPLEMENTED** +**Status**: ✅ Working +**Description**: Fractions with forced display or text style + +**Test Results**: All tests passed +- `\dfrac{1}{2}` - ✅ Works (display-style fraction) +- `\tfrac{a}{b}` - ✅ Works (text-style fraction) +- `y'=-\dfrac{2}{x^{3}}` - ✅ Works (complex expression) +- Nested `\dfrac` and `\tfrac` - ✅ Works + +**Use Case**: +- `\dfrac` forces display style (larger, more readable fractions) +- `\tfrac` forces text style (smaller, inline fractions) +- Useful when you want consistent fraction appearance regardless of context + +**Implementation**: Prepends style atoms to numerator and denominator to force rendering style. + +--- + ### 8. ❌ `\boldsymbol` - Bold Greek Letters **Status**: ❌ Not Implemented **Error**: `Invalid command \boldsymbol` diff --git a/README.md b/README.md index cb7f0fe..827144b 100644 --- a/README.md +++ b/README.md @@ -284,7 +284,7 @@ ScrollView { This is a list of formula types that the library currently supports: * Simple algebraic equations -* Fractions and continued fractions (including `\cfrac`) +* Fractions and continued fractions (including `\frac`, `\dfrac`, `\tfrac`, `\cfrac`) * Exponents and subscripts * Trigonometric formulae * Square roots and n-th roots diff --git a/Sources/SwiftMath/MathRender/MTMathListBuilder.swift b/Sources/SwiftMath/MathRender/MTMathListBuilder.swift index d7a7c97..f26d3ea 100644 --- a/Sources/SwiftMath/MathRender/MTMathListBuilder.swift +++ b/Sources/SwiftMath/MathRender/MTMathListBuilder.swift @@ -702,6 +702,34 @@ public struct MTMathListBuilder { frac.numerator = self.buildInternal(true) frac.denominator = self.buildInternal(true) return frac; + } else if command == "dfrac" { + // Display-style fraction command has 2 arguments + let frac = MTFraction() + let numerator = self.buildInternal(true) + let denominator = self.buildInternal(true) + + // Prepend \displaystyle to force display mode rendering + let displayStyle = MTMathStyle(style: .display) + numerator?.insert(displayStyle, at: 0) + denominator?.insert(displayStyle, at: 0) + + frac.numerator = numerator + frac.denominator = denominator + return frac; + } else if command == "tfrac" { + // Text-style fraction command has 2 arguments + let frac = MTFraction() + let numerator = self.buildInternal(true) + let denominator = self.buildInternal(true) + + // Prepend \textstyle to force text mode rendering + let textStyle = MTMathStyle(style: .text) + numerator?.insert(textStyle, at: 0) + denominator?.insert(textStyle, at: 0) + + frac.numerator = numerator + frac.denominator = denominator + return frac; } else if command == "binom" { // A binom command has 2 arguments let frac = MTFraction(hasRule: false) @@ -1048,6 +1076,34 @@ public struct MTMathListBuilder { frac.numerator = self.buildInternal(true) frac.denominator = self.buildInternal(true) return frac + } else if command == "dfrac" { + // Display-style fraction command has 2 arguments + let frac = MTFraction() + let numerator = self.buildInternal(true) + let denominator = self.buildInternal(true) + + // Prepend \displaystyle to force display mode rendering + let displayStyle = MTMathStyle(style: .display) + numerator?.insert(displayStyle, at: 0) + denominator?.insert(displayStyle, at: 0) + + frac.numerator = numerator + frac.denominator = denominator + return frac + } else if command == "tfrac" { + // Text-style fraction command has 2 arguments + let frac = MTFraction() + let numerator = self.buildInternal(true) + let denominator = self.buildInternal(true) + + // Prepend \textstyle to force text mode rendering + let textStyle = MTMathStyle(style: .text) + numerator?.insert(textStyle, at: 0) + denominator?.insert(textStyle, at: 0) + + frac.numerator = numerator + frac.denominator = denominator + return frac } else if command == "binom" { let frac = MTFraction(hasRule: false) frac.numerator = self.buildInternal(true) diff --git a/Tests/SwiftMathTests/MTMathListBuilderTests.swift b/Tests/SwiftMathTests/MTMathListBuilderTests.swift index b051a38..2a5aef2 100644 --- a/Tests/SwiftMathTests/MTMathListBuilderTests.swift +++ b/Tests/SwiftMathTests/MTMathListBuilderTests.swift @@ -2330,6 +2330,118 @@ final class MTMathListBuilderTests: XCTestCase { } } + func testDisplayStyleFraction() throws { + // Test \dfrac - display-style fraction + let str = "\\dfrac{1}{2}" + var error: NSError? = nil + let list = MTMathListBuilder.build(fromString: str, error: &error) + let desc = "Error for string: \(str)" + + XCTAssertNil(error, desc) + let unwrappedList = try XCTUnwrap(list, desc) + XCTAssertEqual(unwrappedList.atoms.count, 1, desc) + + let frac = try XCTUnwrap(unwrappedList.atoms[0] as? MTFraction, desc) + XCTAssertEqual(frac.type, .fraction, desc) + XCTAssertTrue(frac.hasRule, desc) + + // Check numerator + let numerator = try XCTUnwrap(frac.numerator, desc) + XCTAssertTrue(numerator.atoms.count >= 1, "Numerator should have at least style atom") + + // First atom should be displaystyle + if numerator.atoms.count > 1 { + let styleAtom = numerator.atoms[0] as? MTMathStyle + XCTAssertNotNil(styleAtom, "First atom should be style atom") + XCTAssertEqual(styleAtom?.style, .display, "Should be display style") + } + + // Check denominator + let denominator = try XCTUnwrap(frac.denominator, desc) + XCTAssertTrue(denominator.atoms.count >= 1, "Denominator should have at least style atom") + + if denominator.atoms.count > 1 { + let styleAtom = denominator.atoms[0] as? MTMathStyle + XCTAssertNotNil(styleAtom, "First atom should be style atom") + XCTAssertEqual(styleAtom?.style, .display, "Should be display style") + } + } + + func testTextStyleFraction() throws { + // Test \tfrac - text-style fraction + let str = "\\tfrac{a}{b}" + var error: NSError? = nil + let list = MTMathListBuilder.build(fromString: str, error: &error) + let desc = "Error for string: \(str)" + + XCTAssertNil(error, desc) + let unwrappedList = try XCTUnwrap(list, desc) + XCTAssertEqual(unwrappedList.atoms.count, 1, desc) + + let frac = try XCTUnwrap(unwrappedList.atoms[0] as? MTFraction, desc) + XCTAssertEqual(frac.type, .fraction, desc) + XCTAssertTrue(frac.hasRule, desc) + + // Check numerator + let numerator = try XCTUnwrap(frac.numerator, desc) + XCTAssertTrue(numerator.atoms.count >= 1, "Numerator should have at least style atom") + + if numerator.atoms.count > 1 { + let styleAtom = numerator.atoms[0] as? MTMathStyle + XCTAssertNotNil(styleAtom, "First atom should be style atom") + XCTAssertEqual(styleAtom?.style, .text, "Should be text style") + } + + // Check denominator + let denominator = try XCTUnwrap(frac.denominator, desc) + XCTAssertTrue(denominator.atoms.count >= 1, "Denominator should have at least style atom") + + if denominator.atoms.count > 1 { + let styleAtom = denominator.atoms[0] as? MTMathStyle + XCTAssertNotNil(styleAtom, "First atom should be style atom") + XCTAssertEqual(styleAtom?.style, .text, "Should be text style") + } + } + + func testDisplayAndTextStyleFractions() throws { + // Test the original LaTeX from the user's issue + let str = "y'=-\\dfrac{2}{x^{3}}" + var error: NSError? = nil + let list = MTMathListBuilder.build(fromString: str, error: &error) + let desc = "Error for string: \(str)" + + XCTAssertNil(error, desc) + let unwrappedList = try XCTUnwrap(list, desc) + XCTAssertTrue(unwrappedList.atoms.count >= 4, "Should have y, ', =, -, and fraction") + + // Find the fraction atom + var foundFraction = false + for atom in unwrappedList.atoms { + if atom.type == .fraction { + foundFraction = true + let frac = atom as! MTFraction + + // Check that numerator has displaystyle + if let numerator = frac.numerator, numerator.atoms.count > 0 { + let firstAtom = numerator.atoms[0] + if let styleAtom = firstAtom as? MTMathStyle { + XCTAssertEqual(styleAtom.style, .display, "Should force display style") + } + } + break + } + } + + XCTAssertTrue(foundFraction, "Should find fraction in the expression") + + // Test nested dfrac and tfrac + let nestedStr = "\\dfrac{\\tfrac{a}{b}}{c}" + error = nil + let nestedList = MTMathListBuilder.build(fromString: nestedStr, error: &error) + XCTAssertNil(error, "Should parse nested dfrac/tfrac") + XCTAssertNotNil(nestedList, "Should parse nested dfrac/tfrac") + } + func testBoldsymbol() throws { // Test \boldsymbol for bold Greek letters let testCases = [