diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..cd24194 --- /dev/null +++ b/CLAUDE.md @@ -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 diff --git a/Sources/SwiftMath/MathRender/MTMathAtomFactory.swift b/Sources/SwiftMath/MathRender/MTMathAtomFactory.swift index 1278735..f800d46 100644 --- a/Sources/SwiftMath/MathRender/MTMathAtomFactory.swift +++ b/Sources/SwiftMath/MathRender/MTMathAtomFactory.swift @@ -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}"), @@ -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}"), @@ -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}"), @@ -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}"), @@ -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}"), @@ -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 @@ -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}"), diff --git a/Tests/SwiftMathTests/MTMathListBuilderTests.swift b/Tests/SwiftMathTests/MTMathListBuilderTests.swift index 9096a48..6facd56 100644 --- a/Tests/SwiftMathTests/MTMathListBuilderTests.swift +++ b/Tests/SwiftMathTests/MTMathListBuilderTests.swift @@ -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") + } + } + }