diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..410ff6c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +# editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0abdc73 --- /dev/null +++ b/Makefile @@ -0,0 +1,48 @@ +IOS_VERSION = 26.0 +TVOS_VERSION = 26.0 +WATCHOS_VERSION = 26.0 +VISIONOS_VERSION = 26.0 + +PLATFORM_IOS = iOS Simulator,id=$(call udid_for,iOS $(IOS_VERSION),iPhone \d\+ Pro [^M]) +PLATFORM_MACOS = macOS +PLATFORM_TVOS = tvOS Simulator,id=$(call udid_for,tvOS $(TVOS_VERSION),TV) +PLATFORM_WATCHOS = watchOS Simulator,id=$(call udid_for,watchOS $(WATCHOS_VERSION),Watch) +PLATFORM_VISIONOS = visionOS Simulator,id=$(call udid_for,visionOS $(VISIONOS_VERSION),Vision) + +default: test + +test: test-macos test-ios test-tvos test-watchos test-visionos + +test-macos: + @echo "Testing macOS..." + xcodebuild test -scheme swiftui-math -destination platform="$(PLATFORM_MACOS)" + +test-ios: + @echo "Testing iOS $(IOS_VERSION)..." + xcodebuild test -scheme swiftui-math -destination platform="$(PLATFORM_IOS)" + +test-tvos: + @echo "Testing tvOS $(TVOS_VERSION)..." + xcodebuild test -scheme swiftui-math -destination platform="$(PLATFORM_TVOS)" + +test-watchos: + @echo "Testing watchOS $(WATCHOS_VERSION)..." + xcodebuild test -scheme swiftui-math -destination platform="$(PLATFORM_WATCHOS)" + +test-visionos: + @echo "Testing visionOS $(PLATFORM_VISIONOS)..." + xcodebuild test -scheme swiftui-math -destination platform="$(PLATFORM_VISIONOS)" + +format: + swift format \ + --ignore-unparsable-files \ + --in-place \ + --parallel \ + --recursive \ + ./Package.swift ./Sources ./Tests + +.PHONY: format test + +define udid_for +$(shell xcrun simctl list devices available '$(1)' | grep '$(2)' | sort -r | head -1 | awk -F '[()]' '{ print $$(NF-3) }') +endef diff --git a/Package.swift b/Package.swift index a861cf2..ceeb038 100755 --- a/Package.swift +++ b/Package.swift @@ -1,27 +1,29 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 6.0 import PackageDescription let package = Package( - name: "swiftui-math", - defaultLocalization: "en", - platforms: [.iOS("11.0"), .macOS("12.0")], - products: [ - .library( - name: "SwiftUIMath", - targets: ["SwiftUIMath"]), - ], - dependencies: [ - ], - targets: [ - .target( - name: "SwiftUIMath", - dependencies: [], - resources: [ - .copy("mathFonts.bundle") - ]), - .testTarget( - name: "SwiftUIMathTests", - dependencies: ["SwiftUIMath"]), - ] + name: "swiftui-math", + platforms: [ + .macOS(.v12), + .iOS(.v15), + .tvOS(.v15), + .watchOS(.v8), + .visionOS(.v1), + ], + products: [ + .library(name: "SwiftUIMath", targets: ["SwiftUIMath"]) + ], + dependencies: [], + targets: [ + .target( + name: "SwiftUIMath", + dependencies: [], + resources: [.copy("mathFonts.bundle")] + ), + .testTarget( + name: "SwiftUIMathTests", + dependencies: ["SwiftUIMath"] + ), + ] ) diff --git a/README.md b/README.md index 4845787..ffc00c1 100644 --- a/README.md +++ b/README.md @@ -1,681 +1 @@ -# SwiftMath - -`SwiftMath` provides a full Swift implementation of [iosMath](https://travis-ci.org/kostub/iosMath) -for displaying beautifully rendered math equations in iOS and MacOS applications. It typesets formulae written -using LaTeX in a `UILabel` equivalent class. It uses the same typesetting rules as LaTeX and -so the equations are rendered exactly as LaTeX would render them. - -Please also check out [SwiftMathDemo](https://github.com/mgriebling/SwiftMathDemo.git) for examples of how to use `SwiftMath` -from SwiftUI. - -`SwiftMath` is similar to [MathJax](https://www.mathjax.org) or -[KaTeX](https://github.com/Khan/KaTeX) for the web but for native iOS or MacOS -applications without having to use a `UIWebView` and Javascript. More -importantly, it is significantly faster than using a `UIWebView`. - -`SwiftMath` is a Swift translation of the latest `iosMath` v0.9.5 release but includes bug fixes -and enhancements like a new \lbar (lambda bar) character and cyrillic alphabet support. -The original `iosMath` test suites have also been translated to Swift and run without errors. -Note: Error test conditions are ignored to avoid tagging everything with silly `throw`s. -Please let me know of any bugs or bug fixes that you find. - -`SwiftMath` prepackages everything needed for direct access via the Swift Package Manager. - -## Examples -Here are screenshots of some formulae that were rendered with this library: - -```LaTeX -x = \frac{-b \pm \sqrt{b^2-4ac}}{2a} -``` - -![Quadratic Formula](img/quadratic-light.png#gh-light-mode-only) -![Quadratic Formula](img/quadratic-dark.png#gh-dark-mode-only) - -```LaTeX -f(x) = \int\limits_{-\infty}^\infty\!\hat f(\xi)\,e^{2 \pi i \xi x}\,\mathrm{d}\xi -``` - -![Calculus](img/calculus-light.png#gh-light-mode-only) -![Calculus](img/calculus-dark.png#gh-dark-mode-only) - -```LaTeX -\frac{1}{n}\sum_{i=1}^{n}x_i \geq \sqrt[n]{\prod_{i=1}^{n}x_i} -``` - -![AM-GM](img/amgm-light.png#gh-light-mode-only) -![AM-GM](img/amgm-dark.png#gh-dark-mode-only) - -```LaTex -\frac{1}{\left(\sqrt{\phi \sqrt{5}}-\phi\\right) e^{\frac25 \pi}} -= 1+\frac{e^{-2\pi}} {1 +\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\cdots} } } } -``` - -![Ramanujan Identity](img/ramanujan-light.png#gh-light-mode-only) -![Ramanujan Identity](img/ramanujan-dark.png#gh-dark-mode-only) - -More examples are included in [EXAMPLES](EXAMPLES.md) - -## Fonts -Here are previews of the included fonts: - -![](img/FontsPreview.png#gh-dark-mode-only) -![](img/FontsPreviewLight.png#gh-light-mode-only) - -## Requirements -`SwiftMath` works on iOS 11+ or MacOS 12+. It depends -on the following Apple frameworks: - -* Foundation.framework -* CoreGraphics.framework -* QuartzCore.framework -* CoreText.framework - -Additionally for iOS it requires: -* UIKit.framework - -Additionally for MacOS it requires: -* AppKit.framework - -## Installation - -### Swift Package - -`SwiftMath` is available from [SwiftMath](https://github.com/mgriebling/SwiftMath.git). -To use it in your code, just add the https://github.com/mgriebling/SwiftMath.git path to -XCode's package manager. - -## Usage - -The library provides a class `MTMathUILabel` which is a `UIView` that -supports rendering math equations. To display an equation simply create -an `MTMathUILabel` as follows: - -```swift - -import SwiftMath - -let label = MTMathUILabel() -label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}" - -``` -Adding `MTMathUILabel` as a sub-view of your `UIView` will render the -quadratic formula example shown above. - -The following code creates a SwiftUI component called `MathView` encapsulating the MTMathUILabel: - -```swift -import SwiftUI -import SwiftMath - -struct MathView: UIViewRepresentable { - var equation: String - var font: MathFont = .latinModernFont - var textAlignment: MTTextAlignment = .center - var fontSize: CGFloat = 30 - var labelMode: MTMathUILabelMode = .text - var insets: MTEdgeInsets = MTEdgeInsets() - - func makeUIView(context: Context) -> MTMathUILabel { - let view = MTMathUILabel() - view.setContentHuggingPriority(.required, for: .vertical) - view.setContentCompressionResistancePriority(.required, for: .vertical) - return view - } - - func updateUIView(_ view: MTMathUILabel, context: Context) { - view.latex = equation - let font = MTFontManager().font(withName: font.rawValue, size: fontSize) - font?.fallbackFont = UIFont.systemFont(ofSize: fontSize) - view.font = font - view.textAlignment = textAlignment - view.labelMode = labelMode - view.textColor = MTColor(Color.primary) - view.contentInsets = insets - view.invalidateIntrinsicContentSize() - } - - func sizeThatFits(_ proposal: ProposedViewSize, uiView: MTMathUILabel, context: Context) -> CGSize? { - // Enable line wrapping by passing proposed width to the label - if let width = proposal.width, width.isFinite, width > 0 { - uiView.preferredMaxLayoutWidth = width - let size = uiView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude)) - return size - } - return nil - } -} -``` - -For code that works with SwiftUI running natively under MacOS use the following: - -```swift -import SwiftUI -import SwiftMath - -struct MathView: NSViewRepresentable { - var equation: String - var font: MathFont = .latinModernFont - var textAlignment: MTTextAlignment = .center - var fontSize: CGFloat = 30 - var labelMode: MTMathUILabelMode = .text - var insets: MTEdgeInsets = MTEdgeInsets() - - func makeNSView(context: Context) -> MTMathUILabel { - let view = MTMathUILabel() - view.setContentHuggingPriority(.required, for: .vertical) - view.setContentCompressionResistancePriority(.required, for: .vertical) - return view - } - - func updateNSView(_ view: MTMathUILabel, context: Context) { - view.latex = equation - let font = MTFontManager().font(withName: font.rawValue, size: fontSize) - font?.fallbackFont = NSFont.systemFont(ofSize: fontSize) - view.font = font - view.textAlignment = textAlignment - view.labelMode = labelMode - view.textColor = MTColor(Color.primary) - view.contentInsets = insets - view.invalidateIntrinsicContentSize() - } - - func sizeThatFits(_ proposal: ProposedViewSize, nsView: MTMathUILabel, context: Context) -> CGSize? { - // Enable line wrapping by passing proposed width to the label - if let width = proposal.width, width.isFinite, width > 0 { - nsView.preferredMaxLayoutWidth = width - let size = nsView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude)) - return size - } - return nil - } -} -``` - -### Automatic Line Wrapping - -`SwiftMath` supports automatic line wrapping (multiline display) for mathematical content. The implementation uses **interatom line breaking** which breaks equations at atom boundaries (between mathematical elements) rather than within them, preserving the semantic structure of the mathematics. - -#### Using Line Wrapping with UIKit/AppKit - -For direct `MTMathUILabel` usage, set the `preferredMaxLayoutWidth` property: - -```swift -let label = MTMathUILabel() -label.latex = "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5" -label.font = MTFontManager.fontManager.defaultFont - -// Enable line wrapping by setting a maximum width -label.preferredMaxLayoutWidth = 235 -``` - -You can also use `sizeThatFits` to calculate the size with a width constraint: - -```swift -let constrainedSize = label.sizeThatFits(CGSize(width: 235, height: .greatestFiniteMagnitude)) -``` - -#### Using Line Wrapping with SwiftUI - -The `MathView` examples above include `sizeThatFits()` which automatically enables line wrapping when SwiftUI proposes a width constraint. No additional configuration is needed: - -```swift -VStack(alignment: .leading, spacing: 8) { - MathView( - equation: "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5", - fontSize: 17, - labelMode: .text - ) -} -.frame(maxWidth: 235) // The equation will break across multiple lines -``` - -#### Line Wrapping Behavior and Capabilities - -SwiftMath implements **two complementary line breaking mechanisms**: - -##### 1. Interatom Line Breaking (Primary) -Breaks equations **between atoms** (mathematical elements) when content exceeds the width constraint. This is the preferred method as it maintains semantic integrity. - -##### 2. Universal Line Breaking (Fallback) -For very long text within single atoms, breaks at Unicode word boundaries using Core Text with number protection (prevents splitting numbers like "3.14"). - -#### Fully Supported Cases - -These atom types work perfectly with interatom line breaking: - -**✅ Variables and ordinary text:** -```swift -label.latex = "a b c d e f g h i j k l m n o p" -label.preferredMaxLayoutWidth = 150 -// Breaks between individual variables at natural boundaries -``` - -**✅ Binary operators (+, -, ×, ÷):** -```swift -label.latex = "a+b+c+d+e+f+g+h" -label.preferredMaxLayoutWidth = 100 -// Breaks cleanly: "a+b+c+d+" -// "e+f+g+h" -``` - -**✅ Relations (=, <, >, ≤, ≥, etc.):** -```swift -label.latex = "a=1, b=2, c=3, d=4, e=5" -label.preferredMaxLayoutWidth = 120 -// Breaks after commas and operators -``` - -**✅ Mixed text and simple math:** -```swift -label.latex = "\\text{Calculer }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1" -label.preferredMaxLayoutWidth = 200 -// Breaks between text and math atoms naturally -``` - -**✅ Punctuation (commas, periods):** -```swift -label.latex = "\\text{First, second, third, fourth, fifth}" -label.preferredMaxLayoutWidth = 150 -// Breaks at commas and spaces -``` - -**✅ Brackets and parentheses (simple):** -```swift -label.latex = "(a+b)+(c+d)+(e+f)" -label.preferredMaxLayoutWidth = 120 -// Breaks between parenthesized groups -``` - -**✅ Greek letters and symbols:** -```swift -label.latex = "\\alpha+\\beta+\\gamma+\\delta+\\epsilon+\\zeta" -label.preferredMaxLayoutWidth = 150 -// Breaks between Greek letters -``` - -**✅ Fractions (NEW!):** -```swift -label.latex = "a+\\frac{1}{2}+b+\\frac{3}{4}+c" -label.preferredMaxLayoutWidth = 150 -// Fractions stay inline if they fit, break to new line only when needed -// Example: "a + ½ + b" stays on one line if it fits -``` - -**✅ Radicals/Square roots (NEW!):** -```swift -label.latex = "x+\\sqrt{2}+y+\\sqrt{3}+z" -label.preferredMaxLayoutWidth = 150 -// Radicals stay inline if they fit, break to new line only when needed -// Example: "x + √2 + y" stays on one line if it fits -``` - -**✅ Mixed fractions and radicals (NEW!):** -```swift -label.latex = "a+\\frac{1}{2}+\\sqrt{3}+b" -label.preferredMaxLayoutWidth = 200 -// Intelligently breaks between complex mathematical elements -``` - -#### Limited Support Cases - -These cases work but with some constraints: - -**⚠️ Atoms with superscripts/subscripts:** -```swift -label.latex = "a^{2}+b^{2}+c^{2}+d^{2}+e^{2}" -label.preferredMaxLayoutWidth = 150 -// Works, but uses fallback breaking mechanism -// May not break at the most optimal positions -``` -**Note**: Scripted atoms (with superscripts/subscripts) trigger the universal breaking mechanism which breaks within accumulated text rather than at atom boundaries. This still works but may not be as clean as pure interatom breaking. - -**⚠️ Very long single text atoms:** -```swift -label.latex = "\\text{This is an extremely long piece of text within a single text command}" -label.preferredMaxLayoutWidth = 200 -// Uses Unicode word boundary breaking with Core Text -// Protects numbers from being split (e.g., "3.14" stays together) -``` - -#### Remaining Unsupported Cases - -These atom types still force line breaks (not yet optimized): - -**⚠️ Large operators (∑, ∫, ∏, lim):** -```swift -label.latex = "\\sum_{i=1}^{n} x_i + \\int_{0}^{1} f(x)dx" -// Each operator forces a new line -``` - -**⚠️ Matrices and tables:** -```swift -label.latex = "A = \\begin{pmatrix} 1 & 2 \\\\ 3 & 4 \\end{pmatrix}" -// Matrix always on own line -``` - -**⚠️ Delimited expressions (\left...\right):** -```swift -label.latex = "\\left(\\frac{a}{b}\\right) + c" -// The parenthesized group forces line breaks -``` - -**⚠️ Colored expressions:** -```swift -label.latex = "a + \\color{red}{b} + c" -// Colored portion causes line break -``` - -**⚠️ Math accents:** -```swift -label.latex = "\\hat{x} + \\tilde{y} + \\bar{z}" -// Accents may cause line breaks -``` - -#### Best Practices - -**DO:** -- Use interatom breaking for simple equations with operators and relations -- Use for mixed text and math where you want natural breaks -- Use for long sequences of variables, numbers, and operators -- Set appropriate `preferredMaxLayoutWidth` based on your layout needs - -**DON'T:** -- Expect natural breaking in expressions with large operators (∑, ∫, etc. - not yet optimized) -- Expect natural breaking in expressions with \left...\right delimiters (not yet optimized) -- Use extremely narrow widths (less than ~80pt) which may cause poor breaks - -#### Examples - -**Excellent use case (discriminant formula):** -```swift -label.latex = "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5" -label.preferredMaxLayoutWidth = 235 -// ✅ Breaks naturally at good points between atoms -``` - -**Good use case (simple arithmetic):** -```swift -label.latex = "5+10+15+20+25+30+35+40+45+50" -label.preferredMaxLayoutWidth = 150 -// ✅ Breaks between operators cleanly -``` - -**Excellent use case (fractions inline - NEW!):** -```swift -label.latex = "a+\\frac{1}{2}+b+\\frac{3}{4}+c" -label.preferredMaxLayoutWidth = 200 -// ✅ Fractions stay inline when they fit! -// Breaks intelligently: "a + ½ + b" on line 1, "+ ¾ + c" on line 2 -``` - -**Excellent use case (radicals inline - NEW!):** -```swift -label.latex = "x+\\sqrt{2}+y+\\sqrt{3}+z" -label.preferredMaxLayoutWidth = 150 -// ✅ Radicals stay inline when they fit! -// Example: "x + √2 + y" on line 1, "+ √3 + z" on line 2 -``` - -**Alternative for complex expressions:** -```swift -// Instead of trying to break this: -label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}" -// Consider it as a single display equation without width constraint -label.preferredMaxLayoutWidth = 0 // No breaking -``` - -#### Technical Details - -- **Line spacing**: New lines are positioned at `fontSize × 1.5` below the previous line -- **Breaking algorithm**: Greedy - breaks immediately when projected width exceeds constraint -- **Width calculation**: Includes inter-element spacing according to TeX spacing rules -- **Number protection**: Numbers in patterns like "3.14", "1,000", etc. are kept intact -- **Supports locales**: English, French, Swiss number formats - -### Included Features -This is a list of formula types that the library currently supports: - -* Simple algebraic equations -* Fractions and continued fractions (including `\frac`, `\dfrac`, `\tfrac`, `\cfrac`) -* Exponents and subscripts -* Trigonometric formulae -* Square roots and n-th roots -* Calculus symbols - limits, derivatives, integrals (including `\iint`, `\iiint`, `\iiiint`) -* Big operators (e.g. product, sum) -* Big delimiters (using `\left` and `\right`) -* Greek alphabet -* Combinatorics (`\binom`, `\choose` etc.) -* Geometry symbols (e.g. angle, congruence etc.) -* Ratios, proportions, percentages -* Math spacing -* Overline and underline -* Math accents -* Matrices (including `\smallmatrix` and starred variants like `pmatrix*` with alignment) -* Multi-line subscripts and limits (`\substack`) -* Equation alignment -* Change bold, roman, caligraphic and other font styles (`\bf`, `\text`, etc.) -* Style commands (`\displaystyle`, `\textstyle`) -* Most commonly used math symbols -* Colors for both text and background -* **Inline and display math mode delimiters** (see below) - -### LaTeX Math Delimiters - -`SwiftMath` now supports all standard LaTeX math delimiters for both inline and display modes. The parser automatically detects and handles these delimiters: - -#### Inline Math (Text Style) -Use these delimiters for inline math within text, which renders more compactly: - -```swift -// Dollar signs (TeX style) -label.latex = "$E = mc^2$" - -// Parentheses (LaTeX style) -label.latex = "\\(\\sum_{i=1}^{n} x_i\\)" - -// Cases environment in inline mode -label.latex = "\\(\\begin{cases} x + y = 5 \\\\ 2x - y = 1 \\end{cases}\\)" -``` - -#### Display Math (Display Style) -Use these delimiters for standalone equations with larger operators and limits: - -```swift -// Double dollar signs (TeX style) -label.latex = "$$\\int_{0}^{\\infty} e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$" - -// Square brackets (LaTeX style) -label.latex = "\\[\\sum_{k=1}^{n} k^2 = \\frac{n(n+1)(2n+1)}{6}\\]" - -// Equation environment -label.latex = "\\begin{equation} x^2 + y^2 = z^2 \\end{equation}" - -// Cases environment in display mode -label.latex = "\\begin{cases} x + y = 5 \\\\ 2x - y = 1 \\end{cases}" -``` - -**Note:** The difference between inline and display modes: -- **Inline mode** (`$...$` or `\(...\)`) renders compactly, suitable for math within text -- **Display mode** (`$$...$$`, `\[...\]`, or environments) renders with larger operators and limits positioned above/below - -All delimiters are automatically stripped during parsing, and the math mode is set appropriately. No additional configuration is needed! - -#### Backward Compatibility -Equations without explicit delimiters continue to work as before, defaulting to display mode: - -```swift -label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}" // Works as always -``` - -Note: SwiftMath only supports the commands in LaTeX's math mode. There is -also no language support for other than west European langugages and some -Cyrillic characters. There would be two ways to support more languages: - -1) Find a math font compatible with `SwiftMath` that contains all the glyphs -for that language. -2) Add support to `SwiftMath` for standard Unicode fonts that contain all -langauge glyphs. - -Of these two, the first is much easier. However, if you want a challenge, -try to tackle the second option. - -### Example - -The [SwiftMathDemo](https://github.com/mgriebling/SwiftMathDemo) is a SwiftUI version -of the Objective-C demo included in `iosMath` that uses `SwiftMath` as a Swift package dependency. - -### Advanced configuration - -`MTMathUILabel` supports some advanced configuration options: - -##### Math mode - -You can change the mode of the `MTMathUILabel` between Display Mode -(equivalent to `$$` or `\[` in LaTeX) and Text Mode (equivalent to `$` -or `\(` in LaTeX). The default style is Display. To switch to Text -simply: - -```swift -label.labelMode = .text -``` - -##### Text Alignment -The default alignment of the equations is left. This can be changed to -center or right as follows: - -```swift -label.textAlignment = .center -``` - -##### Font size -The default font-size is 30pt. You can change it as follows: - -```swift -label.fontSize = 25 -``` -##### Font -The default font is *Latin Modern Math*. This can be changed as: - -```swift -label.font = MTFontManager.fontmanager.termesFont(withSize:20) -``` - -This project has 12 fonts bundled with it, but you can use any OTF math -font. A python script is included that generates the `.plist` files -required for an `.otf` font to work with `SwiftMath`. If you generate -(and test) any other fonts please contribute them back to this project for -others to benefit. - - - -Note: The `KpMath-Light`, `KpMath-Sans`, `Asana` fonts currently incorrectly -render very large radicals. It appears that the font files do -not properly define the offsets required to typeset these glyphs. If -anyone can fix this, it would be greatly appreciated. - -##### Text Color -The default color of the rendered equation is black. You can change -it to any other color as follows: - -```swift -label.textColor = .red -``` - -It is also possible to set different colors for different parts of the -equation. Just access the `displayList` field and set the `textColor` -of the underlying displays of which you want to change the color. - -##### Fallback Font for Unicode Text -By default, math fonts only support a limited set of characters (Latin, Greek, common math symbols). -To display other Unicode characters like Chinese, Japanese, Korean, emoji, or other scripts in `\text{}` -commands, you can configure a fallback font: - -```swift -let mathFont = MTFontManager().font(withName: MathFont.latinModernFont.rawValue, size: 30) - -// Set a fallback font for unsupported characters (defaults to nil) -#if os(iOS) || os(visionOS) -let systemFont = UIFont.systemFont(ofSize: 30) -mathFont?.fallbackFont = CTFontCreateWithName(systemFont.fontName as CFString, 30, nil) -#elseif os(macOS) -let systemFont = NSFont.systemFont(ofSize: 30) -mathFont?.fallbackFont = CTFontCreateWithName(systemFont.fontName as CFString, 30, nil) -#endif - -label.font = mathFont -label.latex = "\\text{Hello 世界 🌍}" // English, Chinese, and emoji -``` - -When the main math font doesn't contain a glyph for a character, the fallback font will be used automatically. -This is particularly useful for: -- Chinese text: `\text{中文}` -- Japanese text: `\text{日本語}` -- Korean text: `\text{한국어}` -- Emoji: `\text{Math is fun! 🎉📐}` -- Mixed scripts: `\text{Equation: 方程式}` - -**Note**: The fallback font only applies to characters within `\text{}` commands, not regular math mode. - -##### Custom Commands -You can define your own commands that are not already predefined. This is -similar to macros is LaTeX. To define your own command use: - -```swift -MTMathAtomFactory.addLatexSymbol("lcm", value: MTMathAtomFactory.operator(withName: "lcm", limits: false)) -``` - -This creates an `\lcm` command that can be used in the LaTeX. - -##### Content Insets -The `MTMathUILabel` has `contentInsets` for finer control of placement of the -equation in relation to the view. - -If you need to set it you can do as follows: - -```swift -label.contentInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 20) -``` - -##### Error handling - -If the LaTeX text given to `MTMathUILabel` is -invalid or if it contains commands that aren't currently supported then -an error message will be displayed instead of the label. - -This error can be programmatically retrieved as `label.error`. If you -prefer not to display anything then set: - -```swift -label.displayErrorInline = true -``` - -## Future Enhancements - -Note this is not a complete implementation of LaTeX math mode. There are -some important pieces that are missing and will be included in future -updates. This includes: - -* Support for explicit big delimiters (`\big`, `\Big`, `\bigg`, `\Bigg`, etc.) -* `\middle` delimiter for use between `\left` and `\right` -* Fine spacing commands (`\,`, `\:`, `\;`, `\!`) -* Bold symbol command (`\boldsymbol`) -* Addition of missing plain TeX commands - -For a complete list of missing features and their implementation status, see [MISSING_FEATURES.md](MISSING_FEATURES.md). - -## License - -`SwiftMath` is available under the MIT license. See the [LICENSE](./LICENSE) -file for more info. - -### Fonts -This distribution contains the following fonts. These fonts are -licensed as follows: -* Latin Modern Math: - [GUST Font License](GUST-FONT-LICENSE.txt) -* Tex Gyre Termes: - [GUST Font License](GUST-FONT-LICENSE.txt) -* [XITS Math](https://github.com/khaledhosny/xits-math): - [Open Font License](OFL.txt) -* [KpMath Light/KpMath Sans](http://scripts.sil.org/OFL): - [SIL Open Font License](OFL.txt) +# SwiftUIMath diff --git a/Sources/SwiftUIMath/Untitled.swift b/Sources/SwiftUIMath/Untitled.swift new file mode 100644 index 0000000..15fdc1e --- /dev/null +++ b/Sources/SwiftUIMath/Untitled.swift @@ -0,0 +1,6 @@ +// +// Untitled.swift +// swiftui-math +// +// Created by Guille Gonzalez on 31/12/25. +// diff --git a/EXAMPLES.md b/SwiftMath/EXAMPLES.md similarity index 100% rename from EXAMPLES.md rename to SwiftMath/EXAMPLES.md diff --git a/MISSING_FEATURES.md b/SwiftMath/MISSING_FEATURES.md similarity index 100% rename from MISSING_FEATURES.md rename to SwiftMath/MISSING_FEATURES.md diff --git a/MULTILINE_IMPLEMENTATION_NOTES.md b/SwiftMath/MULTILINE_IMPLEMENTATION_NOTES.md similarity index 100% rename from MULTILINE_IMPLEMENTATION_NOTES.md rename to SwiftMath/MULTILINE_IMPLEMENTATION_NOTES.md diff --git a/SwiftMath/README.md b/SwiftMath/README.md new file mode 100644 index 0000000..4845787 --- /dev/null +++ b/SwiftMath/README.md @@ -0,0 +1,681 @@ +# SwiftMath + +`SwiftMath` provides a full Swift implementation of [iosMath](https://travis-ci.org/kostub/iosMath) +for displaying beautifully rendered math equations in iOS and MacOS applications. It typesets formulae written +using LaTeX in a `UILabel` equivalent class. It uses the same typesetting rules as LaTeX and +so the equations are rendered exactly as LaTeX would render them. + +Please also check out [SwiftMathDemo](https://github.com/mgriebling/SwiftMathDemo.git) for examples of how to use `SwiftMath` +from SwiftUI. + +`SwiftMath` is similar to [MathJax](https://www.mathjax.org) or +[KaTeX](https://github.com/Khan/KaTeX) for the web but for native iOS or MacOS +applications without having to use a `UIWebView` and Javascript. More +importantly, it is significantly faster than using a `UIWebView`. + +`SwiftMath` is a Swift translation of the latest `iosMath` v0.9.5 release but includes bug fixes +and enhancements like a new \lbar (lambda bar) character and cyrillic alphabet support. +The original `iosMath` test suites have also been translated to Swift and run without errors. +Note: Error test conditions are ignored to avoid tagging everything with silly `throw`s. +Please let me know of any bugs or bug fixes that you find. + +`SwiftMath` prepackages everything needed for direct access via the Swift Package Manager. + +## Examples +Here are screenshots of some formulae that were rendered with this library: + +```LaTeX +x = \frac{-b \pm \sqrt{b^2-4ac}}{2a} +``` + +![Quadratic Formula](img/quadratic-light.png#gh-light-mode-only) +![Quadratic Formula](img/quadratic-dark.png#gh-dark-mode-only) + +```LaTeX +f(x) = \int\limits_{-\infty}^\infty\!\hat f(\xi)\,e^{2 \pi i \xi x}\,\mathrm{d}\xi +``` + +![Calculus](img/calculus-light.png#gh-light-mode-only) +![Calculus](img/calculus-dark.png#gh-dark-mode-only) + +```LaTeX +\frac{1}{n}\sum_{i=1}^{n}x_i \geq \sqrt[n]{\prod_{i=1}^{n}x_i} +``` + +![AM-GM](img/amgm-light.png#gh-light-mode-only) +![AM-GM](img/amgm-dark.png#gh-dark-mode-only) + +```LaTex +\frac{1}{\left(\sqrt{\phi \sqrt{5}}-\phi\\right) e^{\frac25 \pi}} += 1+\frac{e^{-2\pi}} {1 +\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\cdots} } } } +``` + +![Ramanujan Identity](img/ramanujan-light.png#gh-light-mode-only) +![Ramanujan Identity](img/ramanujan-dark.png#gh-dark-mode-only) + +More examples are included in [EXAMPLES](EXAMPLES.md) + +## Fonts +Here are previews of the included fonts: + +![](img/FontsPreview.png#gh-dark-mode-only) +![](img/FontsPreviewLight.png#gh-light-mode-only) + +## Requirements +`SwiftMath` works on iOS 11+ or MacOS 12+. It depends +on the following Apple frameworks: + +* Foundation.framework +* CoreGraphics.framework +* QuartzCore.framework +* CoreText.framework + +Additionally for iOS it requires: +* UIKit.framework + +Additionally for MacOS it requires: +* AppKit.framework + +## Installation + +### Swift Package + +`SwiftMath` is available from [SwiftMath](https://github.com/mgriebling/SwiftMath.git). +To use it in your code, just add the https://github.com/mgriebling/SwiftMath.git path to +XCode's package manager. + +## Usage + +The library provides a class `MTMathUILabel` which is a `UIView` that +supports rendering math equations. To display an equation simply create +an `MTMathUILabel` as follows: + +```swift + +import SwiftMath + +let label = MTMathUILabel() +label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}" + +``` +Adding `MTMathUILabel` as a sub-view of your `UIView` will render the +quadratic formula example shown above. + +The following code creates a SwiftUI component called `MathView` encapsulating the MTMathUILabel: + +```swift +import SwiftUI +import SwiftMath + +struct MathView: UIViewRepresentable { + var equation: String + var font: MathFont = .latinModernFont + var textAlignment: MTTextAlignment = .center + var fontSize: CGFloat = 30 + var labelMode: MTMathUILabelMode = .text + var insets: MTEdgeInsets = MTEdgeInsets() + + func makeUIView(context: Context) -> MTMathUILabel { + let view = MTMathUILabel() + view.setContentHuggingPriority(.required, for: .vertical) + view.setContentCompressionResistancePriority(.required, for: .vertical) + return view + } + + func updateUIView(_ view: MTMathUILabel, context: Context) { + view.latex = equation + let font = MTFontManager().font(withName: font.rawValue, size: fontSize) + font?.fallbackFont = UIFont.systemFont(ofSize: fontSize) + view.font = font + view.textAlignment = textAlignment + view.labelMode = labelMode + view.textColor = MTColor(Color.primary) + view.contentInsets = insets + view.invalidateIntrinsicContentSize() + } + + func sizeThatFits(_ proposal: ProposedViewSize, uiView: MTMathUILabel, context: Context) -> CGSize? { + // Enable line wrapping by passing proposed width to the label + if let width = proposal.width, width.isFinite, width > 0 { + uiView.preferredMaxLayoutWidth = width + let size = uiView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude)) + return size + } + return nil + } +} +``` + +For code that works with SwiftUI running natively under MacOS use the following: + +```swift +import SwiftUI +import SwiftMath + +struct MathView: NSViewRepresentable { + var equation: String + var font: MathFont = .latinModernFont + var textAlignment: MTTextAlignment = .center + var fontSize: CGFloat = 30 + var labelMode: MTMathUILabelMode = .text + var insets: MTEdgeInsets = MTEdgeInsets() + + func makeNSView(context: Context) -> MTMathUILabel { + let view = MTMathUILabel() + view.setContentHuggingPriority(.required, for: .vertical) + view.setContentCompressionResistancePriority(.required, for: .vertical) + return view + } + + func updateNSView(_ view: MTMathUILabel, context: Context) { + view.latex = equation + let font = MTFontManager().font(withName: font.rawValue, size: fontSize) + font?.fallbackFont = NSFont.systemFont(ofSize: fontSize) + view.font = font + view.textAlignment = textAlignment + view.labelMode = labelMode + view.textColor = MTColor(Color.primary) + view.contentInsets = insets + view.invalidateIntrinsicContentSize() + } + + func sizeThatFits(_ proposal: ProposedViewSize, nsView: MTMathUILabel, context: Context) -> CGSize? { + // Enable line wrapping by passing proposed width to the label + if let width = proposal.width, width.isFinite, width > 0 { + nsView.preferredMaxLayoutWidth = width + let size = nsView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude)) + return size + } + return nil + } +} +``` + +### Automatic Line Wrapping + +`SwiftMath` supports automatic line wrapping (multiline display) for mathematical content. The implementation uses **interatom line breaking** which breaks equations at atom boundaries (between mathematical elements) rather than within them, preserving the semantic structure of the mathematics. + +#### Using Line Wrapping with UIKit/AppKit + +For direct `MTMathUILabel` usage, set the `preferredMaxLayoutWidth` property: + +```swift +let label = MTMathUILabel() +label.latex = "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5" +label.font = MTFontManager.fontManager.defaultFont + +// Enable line wrapping by setting a maximum width +label.preferredMaxLayoutWidth = 235 +``` + +You can also use `sizeThatFits` to calculate the size with a width constraint: + +```swift +let constrainedSize = label.sizeThatFits(CGSize(width: 235, height: .greatestFiniteMagnitude)) +``` + +#### Using Line Wrapping with SwiftUI + +The `MathView` examples above include `sizeThatFits()` which automatically enables line wrapping when SwiftUI proposes a width constraint. No additional configuration is needed: + +```swift +VStack(alignment: .leading, spacing: 8) { + MathView( + equation: "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5", + fontSize: 17, + labelMode: .text + ) +} +.frame(maxWidth: 235) // The equation will break across multiple lines +``` + +#### Line Wrapping Behavior and Capabilities + +SwiftMath implements **two complementary line breaking mechanisms**: + +##### 1. Interatom Line Breaking (Primary) +Breaks equations **between atoms** (mathematical elements) when content exceeds the width constraint. This is the preferred method as it maintains semantic integrity. + +##### 2. Universal Line Breaking (Fallback) +For very long text within single atoms, breaks at Unicode word boundaries using Core Text with number protection (prevents splitting numbers like "3.14"). + +#### Fully Supported Cases + +These atom types work perfectly with interatom line breaking: + +**✅ Variables and ordinary text:** +```swift +label.latex = "a b c d e f g h i j k l m n o p" +label.preferredMaxLayoutWidth = 150 +// Breaks between individual variables at natural boundaries +``` + +**✅ Binary operators (+, -, ×, ÷):** +```swift +label.latex = "a+b+c+d+e+f+g+h" +label.preferredMaxLayoutWidth = 100 +// Breaks cleanly: "a+b+c+d+" +// "e+f+g+h" +``` + +**✅ Relations (=, <, >, ≤, ≥, etc.):** +```swift +label.latex = "a=1, b=2, c=3, d=4, e=5" +label.preferredMaxLayoutWidth = 120 +// Breaks after commas and operators +``` + +**✅ Mixed text and simple math:** +```swift +label.latex = "\\text{Calculer }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1" +label.preferredMaxLayoutWidth = 200 +// Breaks between text and math atoms naturally +``` + +**✅ Punctuation (commas, periods):** +```swift +label.latex = "\\text{First, second, third, fourth, fifth}" +label.preferredMaxLayoutWidth = 150 +// Breaks at commas and spaces +``` + +**✅ Brackets and parentheses (simple):** +```swift +label.latex = "(a+b)+(c+d)+(e+f)" +label.preferredMaxLayoutWidth = 120 +// Breaks between parenthesized groups +``` + +**✅ Greek letters and symbols:** +```swift +label.latex = "\\alpha+\\beta+\\gamma+\\delta+\\epsilon+\\zeta" +label.preferredMaxLayoutWidth = 150 +// Breaks between Greek letters +``` + +**✅ Fractions (NEW!):** +```swift +label.latex = "a+\\frac{1}{2}+b+\\frac{3}{4}+c" +label.preferredMaxLayoutWidth = 150 +// Fractions stay inline if they fit, break to new line only when needed +// Example: "a + ½ + b" stays on one line if it fits +``` + +**✅ Radicals/Square roots (NEW!):** +```swift +label.latex = "x+\\sqrt{2}+y+\\sqrt{3}+z" +label.preferredMaxLayoutWidth = 150 +// Radicals stay inline if they fit, break to new line only when needed +// Example: "x + √2 + y" stays on one line if it fits +``` + +**✅ Mixed fractions and radicals (NEW!):** +```swift +label.latex = "a+\\frac{1}{2}+\\sqrt{3}+b" +label.preferredMaxLayoutWidth = 200 +// Intelligently breaks between complex mathematical elements +``` + +#### Limited Support Cases + +These cases work but with some constraints: + +**⚠️ Atoms with superscripts/subscripts:** +```swift +label.latex = "a^{2}+b^{2}+c^{2}+d^{2}+e^{2}" +label.preferredMaxLayoutWidth = 150 +// Works, but uses fallback breaking mechanism +// May not break at the most optimal positions +``` +**Note**: Scripted atoms (with superscripts/subscripts) trigger the universal breaking mechanism which breaks within accumulated text rather than at atom boundaries. This still works but may not be as clean as pure interatom breaking. + +**⚠️ Very long single text atoms:** +```swift +label.latex = "\\text{This is an extremely long piece of text within a single text command}" +label.preferredMaxLayoutWidth = 200 +// Uses Unicode word boundary breaking with Core Text +// Protects numbers from being split (e.g., "3.14" stays together) +``` + +#### Remaining Unsupported Cases + +These atom types still force line breaks (not yet optimized): + +**⚠️ Large operators (∑, ∫, ∏, lim):** +```swift +label.latex = "\\sum_{i=1}^{n} x_i + \\int_{0}^{1} f(x)dx" +// Each operator forces a new line +``` + +**⚠️ Matrices and tables:** +```swift +label.latex = "A = \\begin{pmatrix} 1 & 2 \\\\ 3 & 4 \\end{pmatrix}" +// Matrix always on own line +``` + +**⚠️ Delimited expressions (\left...\right):** +```swift +label.latex = "\\left(\\frac{a}{b}\\right) + c" +// The parenthesized group forces line breaks +``` + +**⚠️ Colored expressions:** +```swift +label.latex = "a + \\color{red}{b} + c" +// Colored portion causes line break +``` + +**⚠️ Math accents:** +```swift +label.latex = "\\hat{x} + \\tilde{y} + \\bar{z}" +// Accents may cause line breaks +``` + +#### Best Practices + +**DO:** +- Use interatom breaking for simple equations with operators and relations +- Use for mixed text and math where you want natural breaks +- Use for long sequences of variables, numbers, and operators +- Set appropriate `preferredMaxLayoutWidth` based on your layout needs + +**DON'T:** +- Expect natural breaking in expressions with large operators (∑, ∫, etc. - not yet optimized) +- Expect natural breaking in expressions with \left...\right delimiters (not yet optimized) +- Use extremely narrow widths (less than ~80pt) which may cause poor breaks + +#### Examples + +**Excellent use case (discriminant formula):** +```swift +label.latex = "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5" +label.preferredMaxLayoutWidth = 235 +// ✅ Breaks naturally at good points between atoms +``` + +**Good use case (simple arithmetic):** +```swift +label.latex = "5+10+15+20+25+30+35+40+45+50" +label.preferredMaxLayoutWidth = 150 +// ✅ Breaks between operators cleanly +``` + +**Excellent use case (fractions inline - NEW!):** +```swift +label.latex = "a+\\frac{1}{2}+b+\\frac{3}{4}+c" +label.preferredMaxLayoutWidth = 200 +// ✅ Fractions stay inline when they fit! +// Breaks intelligently: "a + ½ + b" on line 1, "+ ¾ + c" on line 2 +``` + +**Excellent use case (radicals inline - NEW!):** +```swift +label.latex = "x+\\sqrt{2}+y+\\sqrt{3}+z" +label.preferredMaxLayoutWidth = 150 +// ✅ Radicals stay inline when they fit! +// Example: "x + √2 + y" on line 1, "+ √3 + z" on line 2 +``` + +**Alternative for complex expressions:** +```swift +// Instead of trying to break this: +label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}" +// Consider it as a single display equation without width constraint +label.preferredMaxLayoutWidth = 0 // No breaking +``` + +#### Technical Details + +- **Line spacing**: New lines are positioned at `fontSize × 1.5` below the previous line +- **Breaking algorithm**: Greedy - breaks immediately when projected width exceeds constraint +- **Width calculation**: Includes inter-element spacing according to TeX spacing rules +- **Number protection**: Numbers in patterns like "3.14", "1,000", etc. are kept intact +- **Supports locales**: English, French, Swiss number formats + +### Included Features +This is a list of formula types that the library currently supports: + +* Simple algebraic equations +* Fractions and continued fractions (including `\frac`, `\dfrac`, `\tfrac`, `\cfrac`) +* Exponents and subscripts +* Trigonometric formulae +* Square roots and n-th roots +* Calculus symbols - limits, derivatives, integrals (including `\iint`, `\iiint`, `\iiiint`) +* Big operators (e.g. product, sum) +* Big delimiters (using `\left` and `\right`) +* Greek alphabet +* Combinatorics (`\binom`, `\choose` etc.) +* Geometry symbols (e.g. angle, congruence etc.) +* Ratios, proportions, percentages +* Math spacing +* Overline and underline +* Math accents +* Matrices (including `\smallmatrix` and starred variants like `pmatrix*` with alignment) +* Multi-line subscripts and limits (`\substack`) +* Equation alignment +* Change bold, roman, caligraphic and other font styles (`\bf`, `\text`, etc.) +* Style commands (`\displaystyle`, `\textstyle`) +* Most commonly used math symbols +* Colors for both text and background +* **Inline and display math mode delimiters** (see below) + +### LaTeX Math Delimiters + +`SwiftMath` now supports all standard LaTeX math delimiters for both inline and display modes. The parser automatically detects and handles these delimiters: + +#### Inline Math (Text Style) +Use these delimiters for inline math within text, which renders more compactly: + +```swift +// Dollar signs (TeX style) +label.latex = "$E = mc^2$" + +// Parentheses (LaTeX style) +label.latex = "\\(\\sum_{i=1}^{n} x_i\\)" + +// Cases environment in inline mode +label.latex = "\\(\\begin{cases} x + y = 5 \\\\ 2x - y = 1 \\end{cases}\\)" +``` + +#### Display Math (Display Style) +Use these delimiters for standalone equations with larger operators and limits: + +```swift +// Double dollar signs (TeX style) +label.latex = "$$\\int_{0}^{\\infty} e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$" + +// Square brackets (LaTeX style) +label.latex = "\\[\\sum_{k=1}^{n} k^2 = \\frac{n(n+1)(2n+1)}{6}\\]" + +// Equation environment +label.latex = "\\begin{equation} x^2 + y^2 = z^2 \\end{equation}" + +// Cases environment in display mode +label.latex = "\\begin{cases} x + y = 5 \\\\ 2x - y = 1 \\end{cases}" +``` + +**Note:** The difference between inline and display modes: +- **Inline mode** (`$...$` or `\(...\)`) renders compactly, suitable for math within text +- **Display mode** (`$$...$$`, `\[...\]`, or environments) renders with larger operators and limits positioned above/below + +All delimiters are automatically stripped during parsing, and the math mode is set appropriately. No additional configuration is needed! + +#### Backward Compatibility +Equations without explicit delimiters continue to work as before, defaulting to display mode: + +```swift +label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}" // Works as always +``` + +Note: SwiftMath only supports the commands in LaTeX's math mode. There is +also no language support for other than west European langugages and some +Cyrillic characters. There would be two ways to support more languages: + +1) Find a math font compatible with `SwiftMath` that contains all the glyphs +for that language. +2) Add support to `SwiftMath` for standard Unicode fonts that contain all +langauge glyphs. + +Of these two, the first is much easier. However, if you want a challenge, +try to tackle the second option. + +### Example + +The [SwiftMathDemo](https://github.com/mgriebling/SwiftMathDemo) is a SwiftUI version +of the Objective-C demo included in `iosMath` that uses `SwiftMath` as a Swift package dependency. + +### Advanced configuration + +`MTMathUILabel` supports some advanced configuration options: + +##### Math mode + +You can change the mode of the `MTMathUILabel` between Display Mode +(equivalent to `$$` or `\[` in LaTeX) and Text Mode (equivalent to `$` +or `\(` in LaTeX). The default style is Display. To switch to Text +simply: + +```swift +label.labelMode = .text +``` + +##### Text Alignment +The default alignment of the equations is left. This can be changed to +center or right as follows: + +```swift +label.textAlignment = .center +``` + +##### Font size +The default font-size is 30pt. You can change it as follows: + +```swift +label.fontSize = 25 +``` +##### Font +The default font is *Latin Modern Math*. This can be changed as: + +```swift +label.font = MTFontManager.fontmanager.termesFont(withSize:20) +``` + +This project has 12 fonts bundled with it, but you can use any OTF math +font. A python script is included that generates the `.plist` files +required for an `.otf` font to work with `SwiftMath`. If you generate +(and test) any other fonts please contribute them back to this project for +others to benefit. + + + +Note: The `KpMath-Light`, `KpMath-Sans`, `Asana` fonts currently incorrectly +render very large radicals. It appears that the font files do +not properly define the offsets required to typeset these glyphs. If +anyone can fix this, it would be greatly appreciated. + +##### Text Color +The default color of the rendered equation is black. You can change +it to any other color as follows: + +```swift +label.textColor = .red +``` + +It is also possible to set different colors for different parts of the +equation. Just access the `displayList` field and set the `textColor` +of the underlying displays of which you want to change the color. + +##### Fallback Font for Unicode Text +By default, math fonts only support a limited set of characters (Latin, Greek, common math symbols). +To display other Unicode characters like Chinese, Japanese, Korean, emoji, or other scripts in `\text{}` +commands, you can configure a fallback font: + +```swift +let mathFont = MTFontManager().font(withName: MathFont.latinModernFont.rawValue, size: 30) + +// Set a fallback font for unsupported characters (defaults to nil) +#if os(iOS) || os(visionOS) +let systemFont = UIFont.systemFont(ofSize: 30) +mathFont?.fallbackFont = CTFontCreateWithName(systemFont.fontName as CFString, 30, nil) +#elseif os(macOS) +let systemFont = NSFont.systemFont(ofSize: 30) +mathFont?.fallbackFont = CTFontCreateWithName(systemFont.fontName as CFString, 30, nil) +#endif + +label.font = mathFont +label.latex = "\\text{Hello 世界 🌍}" // English, Chinese, and emoji +``` + +When the main math font doesn't contain a glyph for a character, the fallback font will be used automatically. +This is particularly useful for: +- Chinese text: `\text{中文}` +- Japanese text: `\text{日本語}` +- Korean text: `\text{한국어}` +- Emoji: `\text{Math is fun! 🎉📐}` +- Mixed scripts: `\text{Equation: 方程式}` + +**Note**: The fallback font only applies to characters within `\text{}` commands, not regular math mode. + +##### Custom Commands +You can define your own commands that are not already predefined. This is +similar to macros is LaTeX. To define your own command use: + +```swift +MTMathAtomFactory.addLatexSymbol("lcm", value: MTMathAtomFactory.operator(withName: "lcm", limits: false)) +``` + +This creates an `\lcm` command that can be used in the LaTeX. + +##### Content Insets +The `MTMathUILabel` has `contentInsets` for finer control of placement of the +equation in relation to the view. + +If you need to set it you can do as follows: + +```swift +label.contentInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 20) +``` + +##### Error handling + +If the LaTeX text given to `MTMathUILabel` is +invalid or if it contains commands that aren't currently supported then +an error message will be displayed instead of the label. + +This error can be programmatically retrieved as `label.error`. If you +prefer not to display anything then set: + +```swift +label.displayErrorInline = true +``` + +## Future Enhancements + +Note this is not a complete implementation of LaTeX math mode. There are +some important pieces that are missing and will be included in future +updates. This includes: + +* Support for explicit big delimiters (`\big`, `\Big`, `\bigg`, `\Bigg`, etc.) +* `\middle` delimiter for use between `\left` and `\right` +* Fine spacing commands (`\,`, `\:`, `\;`, `\!`) +* Bold symbol command (`\boldsymbol`) +* Addition of missing plain TeX commands + +For a complete list of missing features and their implementation status, see [MISSING_FEATURES.md](MISSING_FEATURES.md). + +## License + +`SwiftMath` is available under the MIT license. See the [LICENSE](./LICENSE) +file for more info. + +### Fonts +This distribution contains the following fonts. These fonts are +licensed as follows: +* Latin Modern Math: + [GUST Font License](GUST-FONT-LICENSE.txt) +* Tex Gyre Termes: + [GUST Font License](GUST-FONT-LICENSE.txt) +* [XITS Math](https://github.com/khaledhosny/xits-math): + [Open Font License](OFL.txt) +* [KpMath Light/KpMath Sans](http://scripts.sil.org/OFL): + [SIL Open Font License](OFL.txt) diff --git a/Sources/SwiftUIMath/MathBundle/MTFontMathTableV2.swift b/SwiftMath/Sources/SwiftMath/MathBundle/MTFontMathTableV2.swift similarity index 100% rename from Sources/SwiftUIMath/MathBundle/MTFontMathTableV2.swift rename to SwiftMath/Sources/SwiftMath/MathBundle/MTFontMathTableV2.swift diff --git a/Sources/SwiftUIMath/MathBundle/MTFontV2.swift b/SwiftMath/Sources/SwiftMath/MathBundle/MTFontV2.swift similarity index 100% rename from Sources/SwiftUIMath/MathBundle/MTFontV2.swift rename to SwiftMath/Sources/SwiftMath/MathBundle/MTFontV2.swift diff --git a/Sources/SwiftUIMath/MathBundle/MathFont.swift b/SwiftMath/Sources/SwiftMath/MathBundle/MathFont.swift similarity index 100% rename from Sources/SwiftUIMath/MathBundle/MathFont.swift rename to SwiftMath/Sources/SwiftMath/MathBundle/MathFont.swift diff --git a/Sources/SwiftUIMath/MathBundle/MathImage.swift b/SwiftMath/Sources/SwiftMath/MathBundle/MathImage.swift similarity index 100% rename from Sources/SwiftUIMath/MathBundle/MathImage.swift rename to SwiftMath/Sources/SwiftMath/MathBundle/MathImage.swift diff --git a/Sources/SwiftUIMath/MathRender/MTBezierPath.swift b/SwiftMath/Sources/SwiftMath/MathRender/MTBezierPath.swift similarity index 100% rename from Sources/SwiftUIMath/MathRender/MTBezierPath.swift rename to SwiftMath/Sources/SwiftMath/MathRender/MTBezierPath.swift diff --git a/Sources/SwiftUIMath/MathRender/MTColor.swift b/SwiftMath/Sources/SwiftMath/MathRender/MTColor.swift similarity index 100% rename from Sources/SwiftUIMath/MathRender/MTColor.swift rename to SwiftMath/Sources/SwiftMath/MathRender/MTColor.swift diff --git a/Sources/SwiftUIMath/MathRender/MTConfig.swift b/SwiftMath/Sources/SwiftMath/MathRender/MTConfig.swift similarity index 100% rename from Sources/SwiftUIMath/MathRender/MTConfig.swift rename to SwiftMath/Sources/SwiftMath/MathRender/MTConfig.swift diff --git a/Sources/SwiftUIMath/MathRender/MTFont.swift b/SwiftMath/Sources/SwiftMath/MathRender/MTFont.swift similarity index 100% rename from Sources/SwiftUIMath/MathRender/MTFont.swift rename to SwiftMath/Sources/SwiftMath/MathRender/MTFont.swift diff --git a/Sources/SwiftUIMath/MathRender/MTFontManager.swift b/SwiftMath/Sources/SwiftMath/MathRender/MTFontManager.swift similarity index 100% rename from Sources/SwiftUIMath/MathRender/MTFontManager.swift rename to SwiftMath/Sources/SwiftMath/MathRender/MTFontManager.swift diff --git a/Sources/SwiftUIMath/MathRender/MTFontMathTable.swift b/SwiftMath/Sources/SwiftMath/MathRender/MTFontMathTable.swift similarity index 100% rename from Sources/SwiftUIMath/MathRender/MTFontMathTable.swift rename to SwiftMath/Sources/SwiftMath/MathRender/MTFontMathTable.swift diff --git a/Sources/SwiftUIMath/MathRender/MTLabel.swift b/SwiftMath/Sources/SwiftMath/MathRender/MTLabel.swift similarity index 100% rename from Sources/SwiftUIMath/MathRender/MTLabel.swift rename to SwiftMath/Sources/SwiftMath/MathRender/MTLabel.swift diff --git a/Sources/SwiftUIMath/MathRender/MTMathAtomFactory.swift b/SwiftMath/Sources/SwiftMath/MathRender/MTMathAtomFactory.swift similarity index 100% rename from Sources/SwiftUIMath/MathRender/MTMathAtomFactory.swift rename to SwiftMath/Sources/SwiftMath/MathRender/MTMathAtomFactory.swift diff --git a/Sources/SwiftUIMath/MathRender/MTMathImage.swift b/SwiftMath/Sources/SwiftMath/MathRender/MTMathImage.swift similarity index 100% rename from Sources/SwiftUIMath/MathRender/MTMathImage.swift rename to SwiftMath/Sources/SwiftMath/MathRender/MTMathImage.swift diff --git a/Sources/SwiftUIMath/MathRender/MTMathList.swift b/SwiftMath/Sources/SwiftMath/MathRender/MTMathList.swift similarity index 100% rename from Sources/SwiftUIMath/MathRender/MTMathList.swift rename to SwiftMath/Sources/SwiftMath/MathRender/MTMathList.swift diff --git a/Sources/SwiftUIMath/MathRender/MTMathListBuilder.swift b/SwiftMath/Sources/SwiftMath/MathRender/MTMathListBuilder.swift similarity index 100% rename from Sources/SwiftUIMath/MathRender/MTMathListBuilder.swift rename to SwiftMath/Sources/SwiftMath/MathRender/MTMathListBuilder.swift diff --git a/Sources/SwiftUIMath/MathRender/MTMathListDisplay.swift b/SwiftMath/Sources/SwiftMath/MathRender/MTMathListDisplay.swift similarity index 100% rename from Sources/SwiftUIMath/MathRender/MTMathListDisplay.swift rename to SwiftMath/Sources/SwiftMath/MathRender/MTMathListDisplay.swift diff --git a/Sources/SwiftUIMath/MathRender/MTMathListIndex.swift b/SwiftMath/Sources/SwiftMath/MathRender/MTMathListIndex.swift similarity index 100% rename from Sources/SwiftUIMath/MathRender/MTMathListIndex.swift rename to SwiftMath/Sources/SwiftMath/MathRender/MTMathListIndex.swift diff --git a/Sources/SwiftUIMath/MathRender/MTMathUILabel.swift b/SwiftMath/Sources/SwiftMath/MathRender/MTMathUILabel.swift similarity index 100% rename from Sources/SwiftUIMath/MathRender/MTMathUILabel.swift rename to SwiftMath/Sources/SwiftMath/MathRender/MTMathUILabel.swift diff --git a/Sources/SwiftUIMath/MathRender/MTTypesetter.swift b/SwiftMath/Sources/SwiftMath/MathRender/MTTypesetter.swift similarity index 100% rename from Sources/SwiftUIMath/MathRender/MTTypesetter.swift rename to SwiftMath/Sources/SwiftMath/MathRender/MTTypesetter.swift diff --git a/Sources/SwiftUIMath/MathRender/MTUnicode.swift b/SwiftMath/Sources/SwiftMath/MathRender/MTUnicode.swift similarity index 100% rename from Sources/SwiftUIMath/MathRender/MTUnicode.swift rename to SwiftMath/Sources/SwiftMath/MathRender/MTUnicode.swift diff --git a/Sources/SwiftUIMath/MathRender/RWLock.swift b/SwiftMath/Sources/SwiftMath/MathRender/RWLock.swift similarity index 100% rename from Sources/SwiftUIMath/MathRender/RWLock.swift rename to SwiftMath/Sources/SwiftMath/MathRender/RWLock.swift diff --git a/Tests/SwiftUIMathTests/ConcurrencyThreadsafeTests.swift b/SwiftMath/Tests/SwiftMathTests/ConcurrencyThreadsafeTests.swift similarity index 100% rename from Tests/SwiftUIMathTests/ConcurrencyThreadsafeTests.swift rename to SwiftMath/Tests/SwiftMathTests/ConcurrencyThreadsafeTests.swift diff --git a/Tests/SwiftUIMathTests/MTFontMathTableV2Tests.swift b/SwiftMath/Tests/SwiftMathTests/MTFontMathTableV2Tests.swift similarity index 100% rename from Tests/SwiftUIMathTests/MTFontMathTableV2Tests.swift rename to SwiftMath/Tests/SwiftMathTests/MTFontMathTableV2Tests.swift diff --git a/Tests/SwiftUIMathTests/MTFontV2Tests.swift b/SwiftMath/Tests/SwiftMathTests/MTFontV2Tests.swift similarity index 100% rename from Tests/SwiftUIMathTests/MTFontV2Tests.swift rename to SwiftMath/Tests/SwiftMathTests/MTFontV2Tests.swift diff --git a/Tests/SwiftUIMathTests/MTMathListBuilderTests.swift b/SwiftMath/Tests/SwiftMathTests/MTMathListBuilderTests.swift similarity index 100% rename from Tests/SwiftUIMathTests/MTMathListBuilderTests.swift rename to SwiftMath/Tests/SwiftMathTests/MTMathListBuilderTests.swift diff --git a/Tests/SwiftUIMathTests/MTMathListTests.swift b/SwiftMath/Tests/SwiftMathTests/MTMathListTests.swift similarity index 100% rename from Tests/SwiftUIMathTests/MTMathListTests.swift rename to SwiftMath/Tests/SwiftMathTests/MTMathListTests.swift diff --git a/Tests/SwiftUIMathTests/MTMathUILabelLineWrappingTests.swift b/SwiftMath/Tests/SwiftMathTests/MTMathUILabelLineWrappingTests.swift similarity index 100% rename from Tests/SwiftUIMathTests/MTMathUILabelLineWrappingTests.swift rename to SwiftMath/Tests/SwiftMathTests/MTMathUILabelLineWrappingTests.swift diff --git a/Tests/SwiftUIMathTests/MTTypesetterTests.swift b/SwiftMath/Tests/SwiftMathTests/MTTypesetterTests.swift similarity index 100% rename from Tests/SwiftUIMathTests/MTTypesetterTests.swift rename to SwiftMath/Tests/SwiftMathTests/MTTypesetterTests.swift diff --git a/Tests/SwiftUIMathTests/MathFontTests.swift b/SwiftMath/Tests/SwiftMathTests/MathFontTests.swift similarity index 100% rename from Tests/SwiftUIMathTests/MathFontTests.swift rename to SwiftMath/Tests/SwiftMathTests/MathFontTests.swift diff --git a/Tests/SwiftUIMathTests/MathImageTests.swift b/SwiftMath/Tests/SwiftMathTests/MathImageTests.swift similarity index 100% rename from Tests/SwiftUIMathTests/MathImageTests.swift rename to SwiftMath/Tests/SwiftMathTests/MathImageTests.swift diff --git a/img/FontsPreview.png b/SwiftMath/img/FontsPreview.png similarity index 100% rename from img/FontsPreview.png rename to SwiftMath/img/FontsPreview.png diff --git a/img/FontsPreviewLight.png b/SwiftMath/img/FontsPreviewLight.png similarity index 100% rename from img/FontsPreviewLight.png rename to SwiftMath/img/FontsPreviewLight.png diff --git a/img/amgm-dark.png b/SwiftMath/img/amgm-dark.png similarity index 100% rename from img/amgm-dark.png rename to SwiftMath/img/amgm-dark.png diff --git a/img/amgm-light.png b/SwiftMath/img/amgm-light.png similarity index 100% rename from img/amgm-light.png rename to SwiftMath/img/amgm-light.png diff --git a/img/calculus-dark.png b/SwiftMath/img/calculus-dark.png similarity index 100% rename from img/calculus-dark.png rename to SwiftMath/img/calculus-dark.png diff --git a/img/calculus-light.png b/SwiftMath/img/calculus-light.png similarity index 100% rename from img/calculus-light.png rename to SwiftMath/img/calculus-light.png diff --git a/img/cases-dark.png b/SwiftMath/img/cases-dark.png similarity index 100% rename from img/cases-dark.png rename to SwiftMath/img/cases-dark.png diff --git a/img/cases-light.png b/SwiftMath/img/cases-light.png similarity index 100% rename from img/cases-light.png rename to SwiftMath/img/cases-light.png diff --git a/img/cauchyintegral-dark.png b/SwiftMath/img/cauchyintegral-dark.png similarity index 100% rename from img/cauchyintegral-dark.png rename to SwiftMath/img/cauchyintegral-dark.png diff --git a/img/cauchyintegral-light.png b/SwiftMath/img/cauchyintegral-light.png similarity index 100% rename from img/cauchyintegral-light.png rename to SwiftMath/img/cauchyintegral-light.png diff --git a/img/cauchyschwarz-dark.png b/SwiftMath/img/cauchyschwarz-dark.png similarity index 100% rename from img/cauchyschwarz-dark.png rename to SwiftMath/img/cauchyschwarz-dark.png diff --git a/img/cauchyschwarz-light.png b/SwiftMath/img/cauchyschwarz-light.png similarity index 100% rename from img/cauchyschwarz-light.png rename to SwiftMath/img/cauchyschwarz-light.png diff --git a/img/cross-dark.png b/SwiftMath/img/cross-dark.png similarity index 100% rename from img/cross-dark.png rename to SwiftMath/img/cross-dark.png diff --git a/img/cross-light.png b/SwiftMath/img/cross-light.png similarity index 100% rename from img/cross-light.png rename to SwiftMath/img/cross-light.png diff --git a/img/demorgan-dark.png b/SwiftMath/img/demorgan-dark.png similarity index 100% rename from img/demorgan-dark.png rename to SwiftMath/img/demorgan-dark.png diff --git a/img/demorgan-light.png b/SwiftMath/img/demorgan-light.png similarity index 100% rename from img/demorgan-light.png rename to SwiftMath/img/demorgan-light.png diff --git a/img/gaussintegral-dark.png b/SwiftMath/img/gaussintegral-dark.png similarity index 100% rename from img/gaussintegral-dark.png rename to SwiftMath/img/gaussintegral-dark.png diff --git a/img/gaussintegral-light.png b/SwiftMath/img/gaussintegral-light.png similarity index 100% rename from img/gaussintegral-light.png rename to SwiftMath/img/gaussintegral-light.png diff --git a/img/limit-dark.png b/SwiftMath/img/limit-dark.png similarity index 100% rename from img/limit-dark.png rename to SwiftMath/img/limit-dark.png diff --git a/img/limit-light.png b/SwiftMath/img/limit-light.png similarity index 100% rename from img/limit-light.png rename to SwiftMath/img/limit-light.png diff --git a/img/log-dark.png b/SwiftMath/img/log-dark.png similarity index 100% rename from img/log-dark.png rename to SwiftMath/img/log-dark.png diff --git a/img/log-light.png b/SwiftMath/img/log-light.png similarity index 100% rename from img/log-light.png rename to SwiftMath/img/log-light.png diff --git a/img/long-dark.png b/SwiftMath/img/long-dark.png similarity index 100% rename from img/long-dark.png rename to SwiftMath/img/long-dark.png diff --git a/img/long-light.png b/SwiftMath/img/long-light.png similarity index 100% rename from img/long-light.png rename to SwiftMath/img/long-light.png diff --git a/img/lorentz-dark.png b/SwiftMath/img/lorentz-dark.png similarity index 100% rename from img/lorentz-dark.png rename to SwiftMath/img/lorentz-dark.png diff --git a/img/lorentz-light.png b/SwiftMath/img/lorentz-light.png similarity index 100% rename from img/lorentz-light.png rename to SwiftMath/img/lorentz-light.png diff --git a/img/matrixmult-dark.png b/SwiftMath/img/matrixmult-dark.png similarity index 100% rename from img/matrixmult-dark.png rename to SwiftMath/img/matrixmult-dark.png diff --git a/img/matrixmult-light.png b/SwiftMath/img/matrixmult-light.png similarity index 100% rename from img/matrixmult-light.png rename to SwiftMath/img/matrixmult-light.png diff --git a/img/maxwell-dark.png b/SwiftMath/img/maxwell-dark.png similarity index 100% rename from img/maxwell-dark.png rename to SwiftMath/img/maxwell-dark.png diff --git a/img/maxwell-light.png b/SwiftMath/img/maxwell-light.png similarity index 100% rename from img/maxwell-light.png rename to SwiftMath/img/maxwell-light.png diff --git a/img/quadratic-dark.png b/SwiftMath/img/quadratic-dark.png similarity index 100% rename from img/quadratic-dark.png rename to SwiftMath/img/quadratic-dark.png diff --git a/img/quadratic-light.png b/SwiftMath/img/quadratic-light.png similarity index 100% rename from img/quadratic-light.png rename to SwiftMath/img/quadratic-light.png diff --git a/img/ramanujan-dark.png b/SwiftMath/img/ramanujan-dark.png similarity index 100% rename from img/ramanujan-dark.png rename to SwiftMath/img/ramanujan-dark.png diff --git a/img/ramanujan-light.png b/SwiftMath/img/ramanujan-light.png similarity index 100% rename from img/ramanujan-light.png rename to SwiftMath/img/ramanujan-light.png diff --git a/img/schroedinger-dark.png b/SwiftMath/img/schroedinger-dark.png similarity index 100% rename from img/schroedinger-dark.png rename to SwiftMath/img/schroedinger-dark.png diff --git a/img/schroedinger-light.png b/SwiftMath/img/schroedinger-light.png similarity index 100% rename from img/schroedinger-light.png rename to SwiftMath/img/schroedinger-light.png diff --git a/img/square-dark.png b/SwiftMath/img/square-dark.png similarity index 100% rename from img/square-dark.png rename to SwiftMath/img/square-dark.png diff --git a/img/square-light.png b/SwiftMath/img/square-light.png similarity index 100% rename from img/square-light.png rename to SwiftMath/img/square-light.png diff --git a/img/st-dark.png b/SwiftMath/img/st-dark.png similarity index 100% rename from img/st-dark.png rename to SwiftMath/img/st-dark.png diff --git a/img/st-light.png b/SwiftMath/img/st-light.png similarity index 100% rename from img/st-light.png rename to SwiftMath/img/st-light.png diff --git a/img/standard-dark.png b/SwiftMath/img/standard-dark.png similarity index 100% rename from img/standard-dark.png rename to SwiftMath/img/standard-dark.png diff --git a/img/standard-light.png b/SwiftMath/img/standard-light.png similarity index 100% rename from img/standard-light.png rename to SwiftMath/img/standard-light.png diff --git a/img/stirling-dark.png b/SwiftMath/img/stirling-dark.png similarity index 100% rename from img/stirling-dark.png rename to SwiftMath/img/stirling-dark.png diff --git a/img/stirling-light.png b/SwiftMath/img/stirling-light.png similarity index 100% rename from img/stirling-light.png rename to SwiftMath/img/stirling-light.png diff --git a/img/trig-dark.png b/SwiftMath/img/trig-dark.png similarity index 100% rename from img/trig-dark.png rename to SwiftMath/img/trig-dark.png diff --git a/img/trig-light.png b/SwiftMath/img/trig-light.png similarity index 100% rename from img/trig-light.png rename to SwiftMath/img/trig-light.png diff --git a/Tests/SwiftUIMathTests/Untitled.swift b/Tests/SwiftUIMathTests/Untitled.swift new file mode 100644 index 0000000..15fdc1e --- /dev/null +++ b/Tests/SwiftUIMathTests/Untitled.swift @@ -0,0 +1,6 @@ +// +// Untitled.swift +// swiftui-math +// +// Created by Guille Gonzalez on 31/12/25. +//