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.
This commit is contained in:
@@ -4,10 +4,10 @@ This document lists LaTeX features that are **not yet implemented** in SwiftMath
|
|||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
- **Total Features Tested**: 11
|
- **Total Features Tested**: 12
|
||||||
- **Fully Implemented**: 6 (55%)
|
- **Fully Implemented**: 7 (58%)
|
||||||
- **Partially Implemented**: 0 (0%)
|
- **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
|
### 8. ❌ `\boldsymbol` - Bold Greek Letters
|
||||||
**Status**: ❌ Not Implemented
|
**Status**: ❌ Not Implemented
|
||||||
**Error**: `Invalid command \boldsymbol`
|
**Error**: `Invalid command \boldsymbol`
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ ScrollView {
|
|||||||
This is a list of formula types that the library currently supports:
|
This is a list of formula types that the library currently supports:
|
||||||
|
|
||||||
* Simple algebraic equations
|
* Simple algebraic equations
|
||||||
* Fractions and continued fractions (including `\cfrac`)
|
* Fractions and continued fractions (including `\frac`, `\dfrac`, `\tfrac`, `\cfrac`)
|
||||||
* Exponents and subscripts
|
* Exponents and subscripts
|
||||||
* Trigonometric formulae
|
* Trigonometric formulae
|
||||||
* Square roots and n-th roots
|
* Square roots and n-th roots
|
||||||
|
|||||||
@@ -702,6 +702,34 @@ public struct MTMathListBuilder {
|
|||||||
frac.numerator = self.buildInternal(true)
|
frac.numerator = self.buildInternal(true)
|
||||||
frac.denominator = self.buildInternal(true)
|
frac.denominator = self.buildInternal(true)
|
||||||
return frac;
|
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" {
|
} else if command == "binom" {
|
||||||
// A binom command has 2 arguments
|
// A binom command has 2 arguments
|
||||||
let frac = MTFraction(hasRule: false)
|
let frac = MTFraction(hasRule: false)
|
||||||
@@ -1048,6 +1076,34 @@ public struct MTMathListBuilder {
|
|||||||
frac.numerator = self.buildInternal(true)
|
frac.numerator = self.buildInternal(true)
|
||||||
frac.denominator = self.buildInternal(true)
|
frac.denominator = self.buildInternal(true)
|
||||||
return frac
|
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" {
|
} else if command == "binom" {
|
||||||
let frac = MTFraction(hasRule: false)
|
let frac = MTFraction(hasRule: false)
|
||||||
frac.numerator = self.buildInternal(true)
|
frac.numerator = self.buildInternal(true)
|
||||||
|
|||||||
@@ -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 {
|
func testBoldsymbol() throws {
|
||||||
// Test \boldsymbol for bold Greek letters
|
// Test \boldsymbol for bold Greek letters
|
||||||
let testCases = [
|
let testCases = [
|
||||||
|
|||||||
Reference in New Issue
Block a user