Skip to content
Closed
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
95 changes: 95 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## First Principles

### 1. Respect Existing Code Patterns

**Philosophy**: This is a fork intended for upstream contribution. Changes must blend seamlessly with the existing codebase.

**Implementation implications**:
- Follow existing naming conventions, formatting, and code style exactly
- Add new symbols to `supportedLatexSymbols` using the same pattern as existing entries
- Use existing atom types (`.relation`, `.binaryOperator`, `.ordinary`, `.variable`) appropriately
- Maintain thread-safety patterns (use `NSLock` where existing code does)
- Do not refactor existing code unless absolutely required for a feature
- Preserve all existing tests and ensure they continue to pass

### 2. Feature-by-Feature Development with Documentation

**Philosophy**: Each feature should be a self-contained, reviewable unit ready for upstream PR.

**Implementation implications**:
- Work on one logical feature at a time (e.g., "add slanted inequalities", "add Greek variants")
- Each feature must include: code changes, unit tests, documentation updates
- Update `MISSING_FEATURES.md` when implementing features listed there
- Update `README.md` when adding user-facing capabilities
- Create atomic commits for each logical change
- Create PRs that can be reviewed and merged independently
- PR format: Summary with bullet points, test plan checklist, generated-by footer

## Build & Test Commands

```bash
# Build the package
swift build

# Run all tests
swift test

# Run a single test class
swift test --filter MTMathListBuilderTests

# Run a specific test method
swift test --filter MTMathListBuilderTests.testBuilder
```

## Architecture Overview

ExtendedSwiftMath is a Swift implementation of LaTeX math rendering for iOS (11+) and macOS (12+). The package name is `ExtendedSwiftMath` but the product/module is `SwiftMath` for drop-in compatibility.

### Core Processing Pipeline

**LaTeX String → MTMathList → MTDisplay → Rendered Output**

1. **MTMathListBuilder** (`MathRender/MTMathListBuilder.swift`) - Parses LaTeX strings into an abstract syntax tree (`MTMathList`). Handles math delimiters (`$...$`, `$$...$$`, `\(...\)`, `\[...\]`), commands, environments.

2. **MTMathList** (`MathRender/MTMathList.swift`) - The AST representation. Contains `MTMathAtom` objects representing mathematical elements (variables, operators, fractions, radicals, etc.). Each atom has a `MTMathAtomType` that determines rendering and spacing.

3. **MTTypesetter** (`MathRender/MTTypesetter.swift`) - Converts `MTMathList` to `MTDisplay` tree using TeX typesetting rules. Handles inter-element spacing, script positioning, and line breaking.

4. **MTDisplay** (`MathRender/MTMathListDisplay.swift`) - The display tree that knows how to draw itself via CoreText/CoreGraphics.

5. **MTMathUILabel** (`MathRender/MTMathUILabel.swift`) - The UIView/NSView that hosts the rendered math. Entry point for most usage.

### Font System

Located in `MathBundle/`:

- **MathFont** (`MathFont.swift`) - Enum of 12 bundled OTF math fonts with thread-safe loading via `BundleManager`
- **MTFont** (`MathRender/MTFont.swift`) - Font wrapper with math metrics access
- **MTFontMathTable** (`MathRender/MTFontMathTable.swift`) - Parses OpenType MATH table data from `.plist` files

Each font has a `.otf` file and a companion `.plist` containing math metrics (generated via included Python script).

### Key Classes

- **MTMathAtomFactory** (`MathRender/MTMathAtomFactory.swift`) - Factory for creating atoms, includes command mappings (`aliases`, `delimiters`, `accents`, `supportedLatexSymbols`)
- **MTFontManager** (`MathRender/MTFontManager.swift`) - Manages font instances and defaults

### Platform Abstraction

Cross-platform types defined in `MathRender/`:
- `MTBezierPath` - UIBezierPath/NSBezierPath
- `MTColor` - UIColor/NSColor
- `MTView` - UIView/NSView (via `#if os(iOS)` conditionals)

### Line Wrapping

The typesetter supports automatic line breaking via `preferredMaxLayoutWidth` on `MTMathUILabel`. Uses interatom breaking (breaks between atoms) as primary mechanism, with Unicode word boundary breaking as fallback.

## Task Master AI Instructions

**Import Task Master's development workflow commands and guidelines, treat as if import is in the main CLAUDE.md file.**
@./.taskmaster/CLAUDE.md
22 changes: 21 additions & 1 deletion Sources/SwiftMath/MathRender/MTMathAtomFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ public class MTMathAtomFactory {
"omicron" : MTMathAtom(type: .variable, value: "\u{03BF}"),
"pi" : MTMathAtom(type: .variable, value: "\u{03C0}"),
"rho" : MTMathAtom(type: .variable, value: "\u{03C1}"),
"varsigma" : MTMathAtom(type: .variable, value: "\u{03C1}"),
"varsigma" : MTMathAtom(type: .variable, value: "\u{03C2}"),
"sigma" : MTMathAtom(type: .variable, value: "\u{03C3}"),
"tau" : MTMathAtom(type: .variable, value: "\u{03C4}"),
"upsilon" : MTMathAtom(type: .variable, value: "\u{03C5}"),
Expand All @@ -177,6 +177,9 @@ public class MTMathAtomFactory {
"phi" : MTMathAtom(type: .ordinary, value: "\u{0001D719}"),
"varrho" : MTMathAtom(type: .ordinary, value: "\u{0001D71A}"),
"varpi" : MTMathAtom(type: .ordinary, value: "\u{0001D71B}"),
"varkappa" : MTMathAtom(type: .ordinary, value: "\u{03F0}"),
"digamma" : MTMathAtom(type: .variable, value: "\u{03DD}"),
"Digamma" : MTMathAtom(type: .variable, value: "\u{03DC}"),

// Capital greek characters
"Gamma" : MTMathAtom(type: .variable, value: "\u{0393}"),
Expand Down Expand Up @@ -227,11 +230,16 @@ public class MTMathAtomFactory {
"Longleftarrow" : MTMathAtom(type: .relation, value: "\u{27F8}"),
"Longrightarrow" : MTMathAtom(type: .relation, value: "\u{27F9}"),
"Longleftrightarrow" : MTMathAtom(type: .relation, value: "\u{27FA}"),
"longmapsto" : MTMathAtom(type: .relation, value: "\u{27FC}"),
"hookrightarrow" : MTMathAtom(type: .relation, value: "\u{21AA}"),
"hookleftarrow" : MTMathAtom(type: .relation, value: "\u{21A9}"),


// Relations
"leq" : MTMathAtom(type: .relation, value: UnicodeSymbol.lessEqual),
"geq" : MTMathAtom(type: .relation, value: UnicodeSymbol.greaterEqual),
"leqslant" : MTMathAtom(type: .relation, value: "\u{2A7D}"),
"geqslant" : MTMathAtom(type: .relation, value: "\u{2A7E}"),
"neq" : MTMathAtom(type: .relation, value: UnicodeSymbol.notEqual),
"in" : MTMathAtom(type: .relation, value: "\u{2208}"),
"notin" : MTMathAtom(type: .relation, value: "\u{2209}"),
Expand All @@ -250,6 +258,8 @@ public class MTMathAtomFactory {
"ll" : MTMathAtom(type: .relation, value: "\u{226A}"),
"prec" : MTMathAtom(type: .relation, value: "\u{227A}"),
"succ" : MTMathAtom(type: .relation, value: "\u{227B}"),
"preceq" : MTMathAtom(type: .relation, value: "\u{2AAF}"),
"succeq" : MTMathAtom(type: .relation, value: "\u{2AB0}"),
"subset" : MTMathAtom(type: .relation, value: "\u{2282}"),
"supset" : MTMathAtom(type: .relation, value: "\u{2283}"),
"subseteq" : MTMathAtom(type: .relation, value: "\u{2286}"),
Expand All @@ -259,6 +269,9 @@ public class MTMathAtomFactory {
"sqsubseteq" : MTMathAtom(type: .relation, value: "\u{2291}"),
"sqsupseteq" : MTMathAtom(type: .relation, value: "\u{2292}"),
"models" : MTMathAtom(type: .relation, value: "\u{22A7}"),
"vdash" : MTMathAtom(type: .relation, value: "\u{22A2}"),
"dashv" : MTMathAtom(type: .relation, value: "\u{22A3}"),
"bowtie" : MTMathAtom(type: .relation, value: "\u{22C8}"),
"perp" : MTMathAtom(type: .relation, value: "\u{27C2}"),
"implies" : MTMathAtom(type: .relation, value: "\u{27F9}"),

Expand Down Expand Up @@ -288,6 +301,7 @@ public class MTMathAtomFactory {
"odot" : MTMathAtom(type: .binaryOperator, value: "\u{2299}"),
"star" : MTMathAtom(type: .binaryOperator, value: "\u{22C6}"),
"cdot" : MTMathAtom(type: .binaryOperator, value: "\u{22C5}"),
"diamond" : MTMathAtom(type: .binaryOperator, value: "\u{22C4}"),
"amalg" : MTMathAtom(type: .binaryOperator, value: "\u{2A3F}"),

// No limit operators
Expand Down Expand Up @@ -388,19 +402,25 @@ public class MTMathAtomFactory {
"Re" : MTMathAtom(type: .ordinary, value: "\u{211C}"),
"mho" : MTMathAtom(type: .ordinary, value: "\u{2127}"),
"aleph" : MTMathAtom(type: .ordinary, value: "\u{2135}"),
"beth" : MTMathAtom(type: .ordinary, value: "\u{2136}"),
"gimel" : MTMathAtom(type: .ordinary, value: "\u{2137}"),
"daleth" : MTMathAtom(type: .ordinary, value: "\u{2138}"),
"forall" : MTMathAtom(type: .ordinary, value: "\u{2200}"),
"exists" : MTMathAtom(type: .ordinary, value: "\u{2203}"),
"nexists" : MTMathAtom(type: .ordinary, value: "\u{2204}"),
"emptyset" : MTMathAtom(type: .ordinary, value: "\u{2205}"),
"varnothing" : MTMathAtom(type: .ordinary, value: "\u{2205}"),
"nabla" : MTMathAtom(type: .ordinary, value: "\u{2207}"),
"infty" : MTMathAtom(type: .ordinary, value: "\u{221E}"),
"angle" : MTMathAtom(type: .ordinary, value: "\u{2220}"),
"measuredangle" : MTMathAtom(type: .ordinary, value: "\u{2221}"),
"top" : MTMathAtom(type: .ordinary, value: "\u{22A4}"),
"bot" : MTMathAtom(type: .ordinary, value: "\u{22A5}"),
"vdots" : MTMathAtom(type: .ordinary, value: "\u{22EE}"),
"cdots" : MTMathAtom(type: .ordinary, value: "\u{22EF}"),
"ddots" : MTMathAtom(type: .ordinary, value: "\u{22F1}"),
"triangle" : MTMathAtom(type: .ordinary, value: "\u{25B3}"),
"Box" : MTMathAtom(type: .ordinary, value: "\u{25A1}"),
"imath" : MTMathAtom(type: .ordinary, value: "\u{0001D6A4}"),
"jmath" : MTMathAtom(type: .ordinary, value: "\u{0001D6A5}"),
"upquote" : MTMathAtom(type: .ordinary, value: "\u{0027}"),
Expand Down
167 changes: 167 additions & 0 deletions Tests/SwiftMathTests/MTMathListBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2737,5 +2737,172 @@ final class MTMathListBuilderTests: XCTestCase {
// }
// }

// MARK: - Priority 1 Symbol Tests

func testGreekVariants() throws {
let variants = ["varkappa", "digamma", "Digamma", "varepsilon", "vartheta", "varpi", "varrho", "varsigma", "varphi"]

for variant in variants {
var error: NSError? = nil
let str = "$\\\(variant)$"
let list = MTMathListBuilder.build(fromString: str, error: &error)

let unwrappedList = try XCTUnwrap(list, "Should parse \\\(variant)")
XCTAssertNil(error, "Should not error on \\\(variant): \(error?.localizedDescription ?? "")")
XCTAssertTrue(unwrappedList.atoms.count >= 1, "\\\(variant) should have at least one atom")
}
}

func testVarsigmaCorrectUnicode() throws {
var error: NSError? = nil
let str = "\\varsigma"
let list = MTMathListBuilder.build(fromString: str, error: &error)

let unwrappedList = try XCTUnwrap(list, "Should parse \\varsigma")
XCTAssertNil(error)
XCTAssertEqual(unwrappedList.atoms.count, 1)

// Verify it's the correct Unicode character (U+03C2, final sigma ς)
let atom = unwrappedList.atoms[0]
XCTAssertEqual(atom.nucleus, "\u{03C2}", "varsigma should map to U+03C2 (final sigma ς), not U+03C1 (rho ρ)")
}

func testNewArrows() throws {
let arrows = ["longmapsto", "hookrightarrow", "hookleftarrow"]

for arrow in arrows {
var error: NSError? = nil
let str = "$a \\\(arrow) b$"
let list = MTMathListBuilder.build(fromString: str, error: &error)

let unwrappedList = try XCTUnwrap(list, "Should parse \\\(arrow)")
XCTAssertNil(error, "Should not error on \\\(arrow): \(error?.localizedDescription ?? "")")

var foundArrow = false
for atom in unwrappedList.atoms {
if atom.type == .relation {
foundArrow = true
break
}
}
XCTAssertTrue(foundArrow, "Should find arrow relation for \\\(arrow)")
}
}

func testSlantedInequalities() throws {
let inequalities = ["leqslant", "geqslant"]

for ineq in inequalities {
var error: NSError? = nil
let str = "$a \\\(ineq) b$"
let list = MTMathListBuilder.build(fromString: str, error: &error)

let unwrappedList = try XCTUnwrap(list, "Should parse \\\(ineq)")
XCTAssertNil(error, "Should not error on \\\(ineq): \(error?.localizedDescription ?? "")")

var foundRel = false
for atom in unwrappedList.atoms {
if atom.type == .relation {
foundRel = true
break
}
}
XCTAssertTrue(foundRel, "Should find relation for \\\(ineq)")
}
}

func testPrecedenceRelations() throws {
let relations = ["preceq", "succeq", "prec", "succ"]

for rel in relations {
var error: NSError? = nil
let str = "$a \\\(rel) b$"
let list = MTMathListBuilder.build(fromString: str, error: &error)

let unwrappedList = try XCTUnwrap(list, "Should parse \\\(rel)")
XCTAssertNil(error, "Should not error on \\\(rel): \(error?.localizedDescription ?? "")")

var foundRel = false
for atom in unwrappedList.atoms {
if atom.type == .relation {
foundRel = true
break
}
}
XCTAssertTrue(foundRel, "Should find relation for \\\(rel)")
}
}

func testTurnstileRelations() throws {
let relations = ["vdash", "dashv", "bowtie", "models"]

for rel in relations {
var error: NSError? = nil
let str = "$a \\\(rel) b$"
let list = MTMathListBuilder.build(fromString: str, error: &error)

let unwrappedList = try XCTUnwrap(list, "Should parse \\\(rel)")
XCTAssertNil(error, "Should not error on \\\(rel): \(error?.localizedDescription ?? "")")

var foundRel = false
for atom in unwrappedList.atoms {
if atom.type == .relation {
foundRel = true
break
}
}
XCTAssertTrue(foundRel, "Should find relation for \\\(rel)")
}
}

func testDiamondOperator() throws {
var error: NSError? = nil
let str = "$a \\diamond b$"
let list = MTMathListBuilder.build(fromString: str, error: &error)

let unwrappedList = try XCTUnwrap(list, "Should parse \\diamond")
XCTAssertNil(error, "Should not error on \\diamond")

var foundOp = false
for atom in unwrappedList.atoms {
if atom.type == .binaryOperator {
foundOp = true
break
}
}
XCTAssertTrue(foundOp, "Should find binary operator for \\diamond")
}

func testHebrewLetters() throws {
let letters = ["aleph", "beth", "gimel", "daleth"]

for letter in letters {
var error: NSError? = nil
let str = "\\\(letter)"
let list = MTMathListBuilder.build(fromString: str, error: &error)

let unwrappedList = try XCTUnwrap(list, "Should parse \\\(letter)")
XCTAssertNil(error, "Should not error on \\\(letter): \(error?.localizedDescription ?? "")")
XCTAssertEqual(unwrappedList.atoms.count, 1, "\\\(letter) should have exactly one atom")

let atom = unwrappedList.atoms[0]
XCTAssertEqual(atom.type, .ordinary, "\\\(letter) should be ordinary type")
}
}

func testMiscSymbols() throws {
let symbols = ["varnothing", "emptyset", "Box", "measuredangle", "angle", "triangle"]

for symbol in symbols {
var error: NSError? = nil
let str = "$\\\(symbol)$"
let list = MTMathListBuilder.build(fromString: str, error: &error)

let unwrappedList = try XCTUnwrap(list, "Should parse \\\(symbol)")
XCTAssertNil(error, "Should not error on \\\(symbol): \(error?.localizedDescription ?? "")")
XCTAssertTrue(unwrappedList.atoms.count >= 1, "\\\(symbol) should have at least one atom")
}
}

}