Markdown improvements

This commit is contained in:
Ilya Laktyushin 2026-04-27 18:45:30 +02:00
parent 7fdc1ea1d2
commit d574182374
79 changed files with 68205 additions and 158 deletions

18
third-party/SwiftMath/BUILD vendored Normal file
View file

@ -0,0 +1,18 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SwiftMath",
module_name = "SwiftMath",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AppBundle",
],
visibility = [
"//visibility:public",
],
)

21
third-party/SwiftMath/LICENSE vendored Executable file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Computer Inspirations
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

714
third-party/SwiftMath/README.md vendored Normal file
View file

@ -0,0 +1,714 @@
# 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").
See `MULTILINE_IMPLEMENTATION_NOTES.md` for implementation details and recent changes.
#### 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
// Breaks between complex mathematical elements
```
#### Limited Support Cases
These cases work but with some constraints:
**✅ Large operators (NEW!):**
```swift
label.latex = "a + \\sum_{i=1}^{n} x_i + \\int_{0}^{1} f(x)dx + b"
label.preferredMaxLayoutWidth = 200
// Large operators stay inline when they fit!
// Includes height checking for operators with limits
```
**✅ Delimited expressions (NEW!):**
```swift
label.latex = "(a+b) + \\left(\\frac{c}{d}\\right) + e"
label.preferredMaxLayoutWidth = 200
// Delimiters stay inline when they fit!
// Inner content respects width constraints and wraps naturally
```
**✅ Colored expressions (NEW!):**
```swift
label.latex = "a + \\color{red}{b + c + d} + e"
label.preferredMaxLayoutWidth = 200
// Colored sections stay inline when they fit!
// Inner content respects width constraints and wraps properly
```
**✅ Matrices/tables (NEW!):**
```swift
label.latex = "A = \\begin{pmatrix} 1 & 2 \\end{pmatrix} + B"
label.preferredMaxLayoutWidth = 200
// Small matrices stay inline when they fit!
```
**✅ Atoms with superscripts/subscripts (IMPROVED!):**
```swift
label.latex = "a^{2}+b^{2}+c^{2}+d^{2}+e^{2}"
label.preferredMaxLayoutWidth = 150
// Now works with width-based breaking!
// Scripted atoms participate in smart line breaking decisions
```
**✅ Math accents:**
```swift
label.latex = "\\hat{x} + \\tilde{y} + \\bar{z} + \\vec{w}"
label.preferredMaxLayoutWidth = 150
// Common accents (\hat, \tilde, \bar, \vec) work well
// Vector arrows (\overrightarrow, etc.) supported with stretching
```
**⚠️ 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)
```
**Note**: Breaks within the text atom rather than between atoms, which is expected behavior for very long continuous text.
#### 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: "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**: Dynamic line height based on actual content (tall fractions get more space, regular content stays compact)
- **Breaking algorithm**: Greedy with look-ahead and break quality scoring - prefers breaking after operators over other positions
- **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
- **Advanced features**: Tokenization infrastructure with phase-based processing (preprocessing → tokenization → line fitting → display generation)
For complete implementation details including recent improvements (dynamic line height, break quality scoring, early exit optimization), see [MULTILINE_IMPLEMENTATION_NOTES.md](MULTILINE_IMPLEMENTATION_NOTES.md).
### 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 (including inverse hyperbolic: `\arcsinh`, `\arccosh`, etc.)
* 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`)
* Manual delimiter sizing (`\big`, `\Big`, `\bigg`, `\Bigg` and variants)
* Greek alphabet
* Bold Greek symbols (`\boldsymbol`)
* Combinatorics (`\binom`, `\choose` etc.)
* Geometry symbols (e.g. angle, congruence etc.)
* Ratios, proportions, percentages
* Math spacing
* Overline and underline
* Math accents (including `\hat`, `\tilde`, `\bar`, `\vec`, `\dot`, `\ddot`, etc.)
* Vector arrows (`\vec`, `\overrightarrow`, `\overleftarrow`, `\overleftrightarrow` with automatic stretching)
* Wide accents (`\widehat`, `\widetilde`)
* Operator names (`\operatorname{name}`)
* 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`)
* Custom operators (`\operatorname`, `\operatorname*`)
* Dirac notation (`\bra`, `\ket`, `\braket`)
* Most commonly used math symbols
* Colors for both text and background
* **Inline and display math mode delimiters** (see below)
* **Automatic line wrapping** (see Automatic Line Wrapping section)
### 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
```
#### Programmatic API
For advanced use cases where you need to parse LaTeX and determine the detected style programmatically, use the `buildWithStyle` method:
```swift
// Parse LaTeX and get both the math list and detected style
let (mathList, style) = MTMathListBuilder.buildWithStyle(fromString: "\\[x^2 + y^2 = z^2\\]")
// style will be .display for \[...\] or $$...$$
// style will be .text for \(...\) or $...$
// Create a display with the detected style
if let mathList = mathList {
let display = MTTypesetter.createLineForMathList(mathList, font: myFont, style: style)
// Use the display for rendering
}
```
This is particularly useful when building custom renderers or when you need to respect the user's choice of delimiter style.
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 pieces that are missing and may be included in future updates:
* `\middle` delimiter for use between `\left` and `\right`
* Some fine spacing commands (`\:`, `\;`, `\!` - note that `\,` works)
For a complete list of 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)

View file

@ -0,0 +1,194 @@
//
// MTFontMathTableV2.swift
//
//
// Created by Peter Tang on 15/9/2023.
//
import Foundation
import CoreGraphics
import CoreText
internal class MTFontMathTableV2: MTFontMathTable {
private let mathFont: MathFont
private let fontSize: CGFloat
private let unitsPerEm: UInt
private let mTable: NSDictionary
init(mathFont: MathFont, size: CGFloat, unitsPerEm: UInt) {
self.mathFont = mathFont
self.fontSize = size
self.unitsPerEm = unitsPerEm
mTable = mathFont.rawMathTable()
super.init(withFont: mathFont.mtfont(size: fontSize), mathTable: mTable)
super._mathTable = nil
// disable all possible access to _mathTable in superclass!
}
override var _mathTable: NSDictionary? {
set { fatalError("\(#function) change to _mathTable \(mathFont.rawValue) not allowed.") }
get { mTable }
}
override var muUnit: CGFloat { fontSize/18 }
override func fontUnitsToPt(_ fontUnits:Int) -> CGFloat {
CGFloat(fontUnits) * fontSize / CGFloat(unitsPerEm)
}
override func constantFromTable(_ constName: String) -> CGFloat {
guard let consts = mTable[kConstants] as? NSDictionary, let val = consts[constName] as? NSNumber else {
return .zero
}
return fontUnitsToPt(val.intValue)
}
override func percentFromTable(_ percentName: String) -> CGFloat {
guard let consts = mTable[kConstants] as? NSDictionary, let val = consts[percentName] as? NSNumber else {
return .zero
}
return CGFloat(val.floatValue) / 100
}
/** Returns an Array of all the vertical variants of the glyph if any. If
there are no variants for the glyph, the array contains the given glyph. */
override func getVerticalVariantsForGlyph(_ glyph: CGGlyph) -> [NSNumber?] {
guard let variants = mTable[kVertVariants] as? NSDictionary else { return [] }
return self.getVariantsForGlyph(glyph, inDictionary: variants)
}
/** Returns an Array of all the horizontal variants of the glyph if any. If
there are no variants for the glyph, the array contains the given glyph. */
override func getHorizontalVariantsForGlyph(_ glyph: CGGlyph) -> [NSNumber?] {
guard let variants = mTable[kHorizVariants] as? NSDictionary else { return [] }
return self.getVariantsForGlyph(glyph, inDictionary:variants)
}
override func getVariantsForGlyph(_ glyph: CGGlyph, inDictionary variants: NSDictionary) -> [NSNumber?] {
let font = mathFont.mtfont(size: fontSize)
let glyphName = font.get(nameForGlyph: glyph)
var glyphArray = [NSNumber]()
let variantGlyphs = variants[glyphName] as? NSArray
guard let variantGlyphs = variantGlyphs, variantGlyphs.count != .zero else {
// There are no extra variants, so just add the current glyph to it.
let glyph = font.get(glyphWithName: glyphName)
glyphArray.append(NSNumber(value:glyph))
return glyphArray
}
for gvn in variantGlyphs {
if let glyphVariantName = gvn as? String {
let variantGlyph = font.get(glyphWithName: glyphVariantName)
glyphArray.append(NSNumber(value:variantGlyph))
}
}
return glyphArray
}
/** Returns a larger vertical variant of the given glyph if any.
If there is no larger version, this returns the current glyph.
- Parameter glyph: The glyph to find a larger variant for
- Parameter forDisplayStyle: If true, selects the largest appropriate variant for display style.
If false, selects the next larger variant (incremental sizing).
- Returns: A larger glyph variant, or the original glyph if no variants exist
*/
override func getLargerGlyph(_ glyph: CGGlyph, forDisplayStyle: Bool = false) -> CGGlyph {
let font = mathFont.mtfont(size: fontSize)
let glyphName = font.get(nameForGlyph: glyph)
guard let variants = mTable[kVertVariants] as? NSDictionary,
let variantGlyphs = variants[glyphName] as? NSArray, variantGlyphs.count != .zero else {
// There are no extra variants, so just returnt the current glyph.
return glyph
}
if forDisplayStyle {
// For display style, select a large variant suitable for mathematical display mode
// Display integrals should be significantly larger (~2.2em) for visual prominence
let count = variantGlyphs.count
// Strategy: Use the largest variant, but avoid extreme sizes for fonts with many variants
let targetIndex: Int
if count <= 2 {
// Small variant list: use the last one (e.g., integral.size1 at ~2.2em)
targetIndex = count - 1
} else if count <= 4 {
// Medium variant list: use second-to-last to avoid extremes
targetIndex = count - 2
} else {
// Large variant list (like texgyretermes with 6 variants):
// Use variant at ~60% position to get appropriate display size (~2.0em)
// For 7 variants (0-6), this gives index 4
targetIndex = min(count - 2, Int(Double(count) * 0.6))
}
if let glyphVariantName = variantGlyphs[targetIndex] as? String {
let variantGlyph = font.get(glyphWithName: glyphVariantName)
return variantGlyph
}
} else {
// Text/inline style: use incremental sizing for moderate enlargement
// Find the first variant with a different name
for gvn in variantGlyphs {
if let glyphVariantName = gvn as? String, glyphVariantName != glyphName {
let variantGlyph = font.get(glyphWithName: glyphVariantName)
return variantGlyph
}
}
}
// We did not find any variants of this glyph so return it.
return glyph
}
/** Returns the italic correction for the given glyph if any. If there
isn't any this returns 0. */
override func getItalicCorrection(_ glyph: CGGlyph) -> CGFloat {
let font = mathFont.mtfont(size: fontSize)
let glyphName = font.get(nameForGlyph: glyph)
guard let italics = mTable[kItalic] as? NSDictionary, let val = italics[glyphName] as? NSNumber else {
return .zero
}
// if val is nil, this returns 0.
return fontUnitsToPt(val.intValue)
}
override func getTopAccentAdjustment(_ glyph: CGGlyph) -> CGFloat {
let font = mathFont.mtfont(size: fontSize)
let glyphName = font.get(nameForGlyph: glyph)
guard let accents = mTable[kAccents] as? NSDictionary, let val = accents[glyphName] as? NSNumber else {
// If no top accent is defined then it is the center of the advance width.
var glyph = glyph
var advances = CGSize.zero
CTFontGetAdvancesForGlyphs(font.ctFont, .horizontal, &glyph, &advances, 1)
return advances.width/2
}
return fontUnitsToPt(val.intValue)
}
override func getVerticalGlyphAssembly(forGlyph glyph: CGGlyph) -> [GlyphPart] {
let font = mathFont.mtfont(size: fontSize)
let glyphName = font.get(nameForGlyph: glyph)
guard let assemblyTable = mTable[kVertAssembly] as? NSDictionary,
let assemblyInfo = assemblyTable[glyphName] as? NSDictionary,
let parts = assemblyInfo[kAssemblyParts] as? NSArray else {
// No vertical assembly defined for glyph
// parts should always have been defined, but if it isn't return nil
return []
}
var rv = [GlyphPart]()
for part in parts {
guard let partInfo = part as? NSDictionary,
let adv = partInfo["advance"] as? NSNumber,
let end = partInfo["endConnector"] as? NSNumber,
let start = partInfo["startConnector"] as? NSNumber,
let ext = partInfo["extender"] as? NSNumber,
let glyphName = partInfo["glyph"] as? String else { continue }
let fullAdvance = fontUnitsToPt(adv.intValue)
let endConnectorLength = fontUnitsToPt(end.intValue)
let startConnectorLength = fontUnitsToPt(start.intValue)
let isExtender = ext.boolValue
let glyph = font.get(glyphWithName: glyphName)
let part = GlyphPart(glyph: glyph, fullAdvance: fullAdvance,
startConnectorLength: startConnectorLength,
endConnectorLength: endConnectorLength,
isExtender: isExtender)
rv.append(part)
}
return rv
}
}

View file

@ -0,0 +1,68 @@
//
// MTFontV2.swift
//
//
// Created by Peter Tang on 15/9/2023.
//
import Foundation
import CoreGraphics
import CoreText
extension MathFont {
public func mtfont(size: CGFloat) -> MTFontV2 {
MTFontV2(font: self, size: size)
}
}
public final class MTFontV2: MTFont {
let font: MathFont
let size: CGFloat
private let _cgFont: CGFont
private let _ctFont: CTFont
private let unitsPerEm: UInt
private var _mathTab: MTFontMathTableV2?
init(font: MathFont = .latinModernFont, size: CGFloat) {
self.font = font
self.size = size
// MathFont cgfont and ctfont are fast & threadsafe, keep a local copy is cheaper than
// handling via NSLock
self._cgFont = font.cgFont()
self._ctFont = font.ctFont(withSize: size)
self.unitsPerEm = self._ctFont.unitsPerEm
super.init()
super.defaultCGFont = nil
super.ctFont = nil
super.mathTable = nil
super.rawMathTable = nil
}
override var defaultCGFont: CGFont! {
set { fatalError("\(#function): change to \(font.fontName) not allowed.") }
get { _cgFont }
}
override var ctFont: CTFont! {
set { fatalError("\(#function): change to \(font.fontName) not allowed.") }
get { _ctFont }
}
private let mtfontV2LockOnMathTable = NSLock()
override var mathTable: MTFontMathTable? {
set { fatalError("\(#function): change to \(font.rawValue) not allowed.") }
get {
guard _mathTab == nil else { return _mathTab }
//Note: lazy _mathTab initialization is now threadsafe.
mtfontV2LockOnMathTable.lock()
defer { mtfontV2LockOnMathTable.unlock() }
if _mathTab == nil {
_mathTab = MTFontMathTableV2(mathFont: font, size: size, unitsPerEm: unitsPerEm)
}
return _mathTab
}
}
override var rawMathTable: NSDictionary? {
set { fatalError("\(#function): change to \(font.rawValue) not allowed.") }
get { fatalError("\(#function): access to \(font.rawValue) not allowed.") }
}
public override func copy(withSize size: CGFloat) -> MTFont {
MTFontV2(font: font, size: size)
}
}

View file

@ -0,0 +1,225 @@
//
// MathFont.swift
//
//
// Created by Peter Tang on 10/9/2023.
//
#if os(iOS) || os(visionOS)
import UIKit
#elseif os(macOS)
import AppKit
#endif
/// Now available for everyone to use
public enum MathFont: String, CaseIterable, Identifiable {
public var id: Self { self } // Makes things simpler for SwiftUI
case latinModernFont = "latinmodern-math"
case kpMathLightFont = "KpMath-Light"
case kpMathSansFont = "KpMath-Sans"
case xitsFont = "xits-math"
case termesFont = "texgyretermes-math"
case asanaFont = "Asana-Math"
case eulerFont = "Euler-Math"
case firaFont = "FiraMath-Regular"
case notoSansFont = "NotoSansMath-Regular"
case libertinusFont = "LibertinusMath-Regular"
case garamondFont = "Garamond-Math"
case leteSansFont = "LeteSansMath"
var fontFamilyName: String {
switch self {
case .latinModernFont: "Latin Modern Math"
case .kpMathLightFont: "KpMath"
case .kpMathSansFont: "KpMath"
case .xitsFont: "XITS Math"
case .termesFont: "TeX Gyre Termes Math"
case .asanaFont: "Asana Math"
case .eulerFont: "Euler Math"
case .firaFont: "Fira Math"
case .notoSansFont: "Noto Sans Math"
case .libertinusFont: "Libertinus Math"
case .garamondFont: "Garamond-Math" // PostScript name is "Garamond-Math", not "Garamond Math"
case .leteSansFont: "Lete Sans Math"
}
}
var postScriptName: String {
switch self {
case .latinModernFont: "LatinModernMath-Regular"
case .kpMathLightFont: "KpMath-Light"
case .kpMathSansFont: "KpMath-Sans"
case .xitsFont: "XITSMath"
case .termesFont: "TeXGyreTermesMath-Regular"
case .asanaFont: "Asana-Math"
case .eulerFont: "Euler-Math"
case .firaFont: "FiraMath-Regular"
case .notoSansFont: "NotoSansMath-Regular"
case .libertinusFont: "LibertinusMath-Regular"
case .garamondFont: "Garamond-Math"
case .leteSansFont: "LeteSansMath"
}
}
var fontName: String { self.rawValue }
public func cgFont() -> CGFont {
BundleManager.manager.obtainCGFont(font: self)
}
public func ctFont(withSize size: CGFloat) -> CTFont {
BundleManager.manager.obtainCTFont(font: self, withSize: size)
}
internal func rawMathTable() -> NSDictionary {
BundleManager.manager.obtainRawMathTable(font: self)
}
//Note: Below code are no longer supported, unable to tell if UIFont/NSFont is threadsafe, not used in SwiftMath.
// #if os(iOS) || os(visionOS)
// public func uiFont(withSize size: CGFloat) -> UIFont? {
// UIFont(name: fontName, size: size)
// }
// #endif
// #if os(macOS)
// public func nsFont(withSize size: CGFloat) -> NSFont? {
// NSFont(name: fontName, size: size)
// }
// #endif
}
internal extension CTFont {
/** The size of this font in points. */
var fontSize: CGFloat {
CTFontGetSize(self)
}
var unitsPerEm: UInt {
return UInt(CTFontGetUnitsPerEm(self))
}
}
private class BundleManager {
//Note: below should be lightweight and without threadsafe problem.
static internal let manager = BundleManager()
private var cgFonts = [MathFont: CGFont]()
private var ctFonts = [CTFontSizePair: CTFont]()
private var rawMathTables = [MathFont: NSDictionary]()
private let threadSafeQueue = DispatchQueue(label: "com.smartmath.mathfont.threadsafequeue",
qos: .userInitiated,
attributes: .concurrent)
private func registerCGFont(mathFont: MathFont) throws {
guard let frameworkBundleURL = Bundle.main.url(forResource: "mathFonts", withExtension: "bundle"),
let resourceBundleURL = Bundle(url: frameworkBundleURL)?.path(forResource: mathFont.rawValue, ofType: "otf") else {
throw FontError.fontPathNotFound
}
guard let fontData = NSData(contentsOfFile: resourceBundleURL), let dataProvider = CGDataProvider(data: fontData) else {
throw FontError.invalidFontFile
}
guard let defaultCGFont = CGFont(dataProvider) else {
throw FontError.initFontError
}
cgFonts[mathFont] = defaultCGFont
/// This does not load the complete math font, it only has about half the glyphs of the full math font.
/// In particular it does not have the math italic characters which breaks our variable rendering.
/// So we first load a CGFont from the file and then convert it to a CTFont.
var errorRef: Unmanaged<CFError>? = nil
guard CTFontManagerRegisterGraphicsFont(defaultCGFont, &errorRef) else {
throw FontError.registerFailed
}
let postsript = (defaultCGFont.postScriptName as? String) ?? ""
let cgfontName = (defaultCGFont.fullName as? String) ?? ""
let threadName = Thread.isMainThread ? "main" : "global"
debugPrint("mathFonts bundle resource: \(mathFont.rawValue), font: \(cgfontName), ps: \(postsript) registered on \(threadName).")
}
private func registerMathTable(mathFont: MathFont) throws {
guard let frameworkBundleURL = Bundle.main.url(forResource: "mathFonts", withExtension: "bundle"),
let mathTablePlist = Bundle(url: frameworkBundleURL)?.url(forResource: mathFont.rawValue, withExtension:"plist") else {
throw FontError.fontPathNotFound
}
guard let rawMathTable = NSDictionary(contentsOf: mathTablePlist),
let version = rawMathTable["version"] as? String,
version == "1.3" else {
throw FontError.invalidMathTable
}
rawMathTables[mathFont] = rawMathTable
let threadName = Thread.isMainThread ? "main" : "global"
debugPrint("mathFonts bundle resource: \(mathFont.rawValue).plist registered on \(threadName).")
}
private func onDemandRegistration(mathFont: MathFont) {
guard threadSafeQueue.sync(execute: { cgFonts[mathFont] }) == nil else { return }
// Note: resourceLoading is now serialized.
threadSafeQueue.sync(flags: .barrier, execute: { [weak self] in
if self?.cgFonts[mathFont] == nil {
do {
try BundleManager.manager.registerCGFont(mathFont: mathFont)
try BundleManager.manager.registerMathTable(mathFont: mathFont)
} catch {
fatalError("MTMathFonts:\(#function) ondemand loading failed, mathFont \(mathFont.rawValue), reason \(error)")
}
}
})
}
fileprivate func obtainCGFont(font: MathFont) -> CGFont {
onDemandRegistration(mathFont: font)
guard let cgFont = threadSafeQueue.sync(execute: { cgFonts[font] }) else {
fatalError("\(#function) unable to locate CGFont \(font.fontName)")
}
return cgFont
}
fileprivate func obtainCTFont(font: MathFont, withSize size: CGFloat) -> CTFont {
onDemandRegistration(mathFont: font)
let fontSizePair = CTFontSizePair(font: font, size: size)
let ctFont = threadSafeQueue.sync(execute: { ctFonts[fontSizePair] })
guard ctFont == nil else { return ctFont! }
guard let cgFont = threadSafeQueue.sync(execute: { cgFonts[font] }) else {
fatalError("\(#function) unable to locate CGFont \(font.fontName) to create CTFont")
}
//Note: ctfont creation and caching is now threadsafe.
guard threadSafeQueue.sync(execute: { ctFonts[fontSizePair] }) == nil else { return ctFonts[fontSizePair]! }
return threadSafeQueue.sync(flags: .barrier, execute: {
if let ctfont = ctFonts[fontSizePair] {
return ctfont
} else {
let result = CTFontCreateWithGraphicsFont(cgFont, size, nil, nil)
ctFonts[fontSizePair] = result
return result
}
})
}
fileprivate func obtainRawMathTable(font: MathFont) -> NSDictionary {
onDemandRegistration(mathFont: font)
guard let mathTable = threadSafeQueue.sync(execute: { rawMathTables[font] } ) else {
fatalError("\(#function) unable to locate mathTable: \(font.rawValue).plist")
}
return mathTable
}
deinit {
ctFonts.removeAll()
var errorRef: Unmanaged<CFError>? = nil
cgFonts.values.forEach { cgFont in
CTFontManagerUnregisterGraphicsFont(cgFont, &errorRef)
}
cgFonts.removeAll()
}
public enum FontError: Error {
case invalidFontFile
case fontPathNotFound
case initFontError
case registerFailed
case invalidMathTable
}
private struct CTFontSizePair: Hashable {
let font: MathFont
let size: CGFloat
}
}

View file

@ -0,0 +1,123 @@
//
// MathImage.swift
//
//
// Created by Peter Tang on 15/9/2023.
//
import Foundation
#if os(iOS) || os(visionOS)
import UIKit
#elseif os(macOS)
import AppKit
#endif
public struct MathImage {
public var font: MathFont = .latinModernFont
public var fontSize: CGFloat
public var textColor: MTColor
public var labelMode: MTMathUILabelMode
public var textAlignment: MTTextAlignment
public var contentInsets: MTEdgeInsets = MTEdgeInsetsZero
public let latex: String
private(set) var intrinsicContentSize = CGSize.zero
public init(latex: String, fontSize: CGFloat, textColor: MTColor, labelMode: MTMathUILabelMode = .display, textAlignment: MTTextAlignment = .center) {
self.latex = latex
self.fontSize = fontSize
self.textColor = textColor
self.labelMode = labelMode
self.textAlignment = textAlignment
}
}
extension MathImage {
public var currentStyle: MTLineStyle {
switch labelMode {
case .display: return .display
case .text: return .text
}
}
private func intrinsicContentSize(_ displayList: MTMathListDisplay) -> CGSize {
CGSize(width: displayList.width + contentInsets.left + contentInsets.right,
height: displayList.ascent + displayList.descent + contentInsets.top + contentInsets.bottom)
}
public struct LayoutInfo {
public var ascent: CGFloat = 0
public var descent: CGFloat = 0
public init(ascent: CGFloat, descent: CGFloat) {
self.ascent = ascent
self.descent = descent
}
}
public mutating func asImage() -> (NSError?, MTImage?, LayoutInfo?) {
func layoutImage(size: CGSize, displayList: MTMathListDisplay) {
var textX = CGFloat(0)
switch self.textAlignment {
case .left: textX = contentInsets.left
case .center: textX = (size.width - contentInsets.left - contentInsets.right - displayList.width) / 2 + contentInsets.left
case .right: textX = size.width - displayList.width - contentInsets.right
}
let availableHeight = size.height - contentInsets.bottom - contentInsets.top
// center things vertically
var height = displayList.ascent + displayList.descent
if height < fontSize/2 {
height = fontSize/2 // set height to half the font size
}
let textY = (availableHeight - height) / 2 + displayList.descent + contentInsets.bottom
displayList.position = CGPoint(x: textX, y: textY)
}
var error: NSError?
let mtfont: MTFont? = font.mtfont(size: fontSize)
guard let mathList = MTMathListBuilder.build(fromString: latex, error: &error), error == nil,
let displayList = MTTypesetter.createLineForMathList(mathList, font: mtfont, style: currentStyle) else {
return (error, nil, nil)
}
intrinsicContentSize = intrinsicContentSize(displayList)
displayList.textColor = textColor
let size = intrinsicContentSize.regularized
layoutImage(size: size, displayList: displayList)
#if os(iOS) || os(visionOS)
let renderer = UIGraphicsImageRenderer(size: size)
let image = renderer.image { rendererContext in
rendererContext.cgContext.saveGState()
rendererContext.cgContext.concatenate(.flippedVertically(size.height))
displayList.draw(rendererContext.cgContext)
rendererContext.cgContext.restoreGState()
}
return (nil, image, LayoutInfo(ascent: displayList.ascent, descent: displayList.descent))
#endif
#if os(macOS)
let image = NSImage(size: size, flipped: false) { bounds in
guard let context = NSGraphicsContext.current?.cgContext else { return false }
context.saveGState()
displayList.draw(context)
context.restoreGState()
return true
}
return (nil, image, LayoutInfo(ascent: displayList.ascent, descent: displayList.descent))
#endif
}
}
private extension CGAffineTransform {
static func flippedVertically(_ height: CGFloat) -> CGAffineTransform {
var transform = CGAffineTransform(scaleX: 1, y: -1)
transform = transform.translatedBy(x: 0, y: -height)
return transform
}
}
extension CGSize {
fileprivate var regularized: CGSize {
CGSize(width: ceil(width), height: ceil(height))
}
}

View file

@ -0,0 +1,35 @@
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by .
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
#if os(macOS)
extension MTBezierPath {
func addLine(to point: CGPoint) {
self.line(to: point)
}
}
extension MTView {
var backgroundColor:MTColor? {
get {
MTColor(cgColor: self.layer?.backgroundColor ?? MTColor.clear.cgColor)
}
set {
self.layer?.backgroundColor = MTColor.clear.cgColor
self.wantsLayer = true
}
}
}
#endif

View file

@ -0,0 +1,28 @@
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by Markus Sähn.
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
extension MTColor {
public convenience init?(fromHexString hexString:String) {
if hexString.isEmpty { return nil }
if !hexString.hasPrefix("#") { return nil }
var rgbValue = UInt64(0)
let scanner = Scanner(string: hexString)
scanner.charactersToBeSkipped = CharacterSet(charactersIn: "#")
scanner.scanHexInt64(&rgbValue)
self.init(red: CGFloat((rgbValue & 0xFF0000) >> 16)/255.0,
green: CGFloat((rgbValue & 0xFF00) >> 8)/255.0,
blue: CGFloat((rgbValue & 0xFF))/255.0,
alpha: 1.0)
}
}

View file

@ -0,0 +1,40 @@
import Foundation
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by .
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
#if os(iOS) || os(visionOS)
import UIKit
public typealias MTView = UIView
public typealias MTColor = UIColor
public typealias MTBezierPath = UIBezierPath
public typealias MTLabel = UILabel
public typealias MTEdgeInsets = UIEdgeInsets
public typealias MTRect = CGRect
public typealias MTImage = UIImage
let MTEdgeInsetsZero = UIEdgeInsets.zero
func MTGraphicsGetCurrentContext() -> CGContext? { UIGraphicsGetCurrentContext() }
#else
import AppKit
public typealias MTView = NSView
public typealias MTColor = NSColor
public typealias MTBezierPath = NSBezierPath
public typealias MTEdgeInsets = NSEdgeInsets
public typealias MTRect = NSRect
public typealias MTImage = NSImage
let MTEdgeInsetsZero = NSEdgeInsets.init(top: 0, left: 0, bottom: 0, right: 0)
func MTGraphicsGetCurrentContext() -> CGContext? { NSGraphicsContext.current?.cgContext }
#endif

View file

@ -0,0 +1,81 @@
import Foundation
import CoreText
import AppBundle
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by Kostub Deshmukh.
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
public class MTFont {
var defaultCGFont: CGFont!
var ctFont: CTFont!
var mathTable: MTFontMathTable?
var rawMathTable: NSDictionary?
/// Fallback font for characters not supported by the main math font.
/// Defaults to the system font at the same size. This is particularly useful
/// for rendering text in \text{} commands with characters outside the math font's coverage
/// (e.g., Chinese, Japanese, Korean, emoji, etc.)
public var fallbackFont: CTFont?
init() {}
/// `MTFont(fontWithName:)` does not load the complete math font, it only has about half the glyphs of the full math font.
/// In particular it does not have the math italic characters which breaks our variable rendering.
/// So we first load a CGFont from the file and then convert it to a CTFont.
convenience init(fontWithName name: String, size:CGFloat) {
self.init()
//print("Loading font \(name)")
let bundle = getAppBundle()
let fontPath = bundle.path(forResource: name, ofType: "otf")
let fontDataProvider = CGDataProvider(filename: fontPath!)
self.defaultCGFont = CGFont(fontDataProvider!)!
//print("Num glyphs: \(self.defaultCGFont.numberOfGlyphs)")
self.ctFont = CTFontCreateWithGraphicsFont(self.defaultCGFont, size, nil, nil);
//print("Loading associated .plist")
let mathTablePlist = bundle.url(forResource:name, withExtension:"plist")
self.rawMathTable = NSDictionary(contentsOf: mathTablePlist!)
self.mathTable = MTFontMathTable(withFont:self, mathTable:rawMathTable!)
}
static var fontBundle:Bundle {
// Uses bundle for class so that this can be access by the unit tests.
Bundle(url: Bundle.main.url(forResource: "mathFonts", withExtension: "bundle")!)!
}
/** Returns a copy of this font but with a different size. */
public func copy(withSize size: CGFloat) -> MTFont {
let newFont = MTFont()
newFont.defaultCGFont = self.defaultCGFont
newFont.ctFont = CTFontCreateWithGraphicsFont(self.defaultCGFont, size, nil, nil)
newFont.rawMathTable = self.rawMathTable
newFont.mathTable = MTFontMathTable(withFont: newFont, mathTable: newFont.rawMathTable!)
return newFont
}
func get(nameForGlyph glyph:CGGlyph) -> String {
let name = defaultCGFont.name(for: glyph) as? String
return name ?? ""
}
func get(glyphWithName name:String) -> CGGlyph {
defaultCGFont.getGlyphWithGlyphName(name: name as CFString)
}
/** The size of this font in points. */
public var fontSize:CGFloat { CTFontGetSize(self.ctFont) }
deinit {
self.ctFont = nil
self.defaultCGFont = nil
}
}

View file

@ -0,0 +1,93 @@
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by Kostub Deshmukh.
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
public class MTFontManager {
static public private(set) var manager: MTFontManager = {
MTFontManager()
}()
let kDefaultFontSize = CGFloat(20)
static var fontManager : MTFontManager {
return manager
}
public init() { }
@RWLocked
var nameToFontMap = [String: MTFont]()
public func font(withName name:String, size:CGFloat) -> MTFont? {
var f = self.nameToFontMap[name]
if f == nil {
f = MTFont(fontWithName: name, size: size)
self.nameToFontMap[name] = f
}
if f!.fontSize == size { return f }
else { return f!.copy(withSize: size) }
}
public func latinModernFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "latinmodern-math", size: size)
}
public func kpMathLightFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "KpMath-Light", size: size)
}
public func kpMathSansFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "KpMath-Sans", size: size)
}
public func xitsFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "xits-math", size: size)
}
public func termesFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "texgyretermes-math", size: size)
}
public func asanaFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "Asana-Math", size: size)
}
public func eulerFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "Euler-Math", size: size)
}
public func firaRegularFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "FiraMath-Regular", size: size)
}
public func notoSansRegularFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "NotoSansMath-Regular", size: size)
}
public func libertinusRegularFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "LibertinusMath-Regular", size: size)
}
public func garamondMathFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "Garamond-Math", size: size)
}
public func leteSansFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "LeteSansMath", size: size)
}
public var defaultFont: MTFont? {
MTFontManager.fontManager.latinModernFont(withSize: kDefaultFontSize)
}
}

View file

@ -0,0 +1,355 @@
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by Kostub Deshmukh.
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
import CoreText
struct GlyphPart {
/// The glyph that represents this part
var glyph: CGGlyph!
/// Full advance width/height for this part, in the direction of the extension in points.
var fullAdvance: CGFloat = 0
/// Advance width/ height of the straight bar connector material at the beginning of the glyph in points.
var startConnectorLength: CGFloat = 0
/// Advance width/ height of the straight bar connector material at the end of the glyph in points.
var endConnectorLength: CGFloat = 0
/// If this part is an extender. If set, the part can be skipped or repeated.
var isExtender: Bool = false
}
/** This class represents the Math table of an open type font.
The math table is documented here: https://www.microsoft.com/typography/otspec/math.htm
How the constants in this class affect the display is documented here:
http://www.tug.org/TUGboat/tb30-1/tb94vieth.pdf
Note: We don't parse the math table from the open type font. Rather we parse it
in python and convert it to a .plist file which is easily consumed by this class.
This approach is preferable to spending an inordinate amount of time figuring out
how to parse the returned NSData object using the open type rules.
Remark: This class is not meant to be used outside of this library.
*/
class MTFontMathTable {
// The font for this math table.
public private(set) weak var font:MTFont? // @property (nonatomic, readonly, weak) MTFont* font;
var _unitsPerEm: UInt
var _fontSize: CGFloat
var _mathTable: NSDictionary!
let kConstants = "constants"
/** MU unit in points */
var muUnit:CGFloat { _fontSize/18 }
func fontUnitsToPt(_ fontUnits:Int) -> CGFloat {
CGFloat(fontUnits) * _fontSize / CGFloat(_unitsPerEm)
}
init(withFont font: MTFont?, mathTable:NSDictionary) {
assert(font != nil, "font has nil value")
assert(font!.ctFont != nil, "font.ctFont has nil value")
self.font = font
// do domething with font
_unitsPerEm = UInt(CTFontGetUnitsPerEm(font!.ctFont))
_fontSize = font!.fontSize;
_mathTable = mathTable
let version = _mathTable["version"] as! String
if version != "1.3" {
NSException(name: NSExceptionName.internalInconsistencyException, reason: "Invalid version of math table plist: \(version)").raise()
}
}
func constantFromTable(_ constName:String) -> CGFloat {
let consts = _mathTable[kConstants] as! NSDictionary?
let val = consts![constName] as! NSNumber?
return fontUnitsToPt(val!.intValue)
}
func percentFromTable(_ percentName:String) -> CGFloat {
let consts = _mathTable[kConstants] as! NSDictionary?
let val = consts![percentName] as! NSNumber?
return CGFloat(val!.floatValue) / 100
}
/// Math Font Metrics from the opentype specification
// MARK: - Fractions
var fractionNumeratorDisplayStyleShiftUp:CGFloat { constantFromTable("FractionNumeratorDisplayStyleShiftUp") } // \sigma_8 in TeX
var fractionNumeratorShiftUp:CGFloat { constantFromTable("FractionNumeratorShiftUp") } // \sigma_9 in TeX
var fractionDenominatorDisplayStyleShiftDown:CGFloat { constantFromTable("FractionDenominatorDisplayStyleShiftDown") } // \sigma_11 in TeX
var fractionDenominatorShiftDown:CGFloat { constantFromTable("FractionDenominatorShiftDown") } // \sigma_12 in TeX
var fractionNumeratorDisplayStyleGapMin:CGFloat { constantFromTable("FractionNumDisplayStyleGapMin") } // 3 * \xi_8 in TeX
var fractionNumeratorGapMin:CGFloat { constantFromTable("FractionNumeratorGapMin") } // \xi_8 in TeX
var fractionDenominatorDisplayStyleGapMin:CGFloat { constantFromTable("FractionDenomDisplayStyleGapMin") } // 3 * \xi_8 in TeX
var fractionDenominatorGapMin:CGFloat { constantFromTable("FractionDenominatorGapMin") } // \xi_8 in TeX
var fractionRuleThickness:CGFloat { constantFromTable("FractionRuleThickness") } // \xi_8 in TeX
var skewedFractionHorizonalGap:CGFloat { constantFromTable("SkewedFractionHorizontalGap") } // \sigma_20 in TeX
var skewedFractionVerticalGap:CGFloat { constantFromTable("SkewedFractionVerticalGap") } // \sigma_21 in TeX
// MARK: - Non-standard
/// FractionDelimiterSize and FractionDelimiterDisplayStyleSize are not constants
/// specified in the OpenType Math specification. Rather these are proposed LuaTeX extensions
/// for the TeX parameters \sigma_20 (delim1) and \sigma_21 (delim2). Since these do not
/// exist in the fonts that we have, we use the same approach as LuaTeX and use the fontSize
/// to determine these values. The constants used match the metrics values of the original TeX fonts.
/// Note: An alternative approach is to use DelimitedSubFormulaMinHeight for \sigma21 and use a factor
/// of 2 to get \sigma 20 as proposed in Vieth paper.
/// The XeTeX implementation sets \sigma21 = fontSize and \sigma20 = DelimitedSubFormulaMinHeight which
/// will produce smaller delimiters.
/// Of all the approaches we've implemented LuaTeX's approach since it mimics LaTeX most accurately.
var fractionDelimiterSize: CGFloat { 1.01 * _fontSize }
/// Modified constant from 2.4 to 2.39 for better visual appearance.
var fractionDelimiterDisplayStyleSize: CGFloat { 2.39 * _fontSize }
// MARK: - Stacks
var stackTopDisplayStyleShiftUp:CGFloat { constantFromTable("StackTopDisplayStyleShiftUp") } // \sigma_8 in TeX
var stackTopShiftUp:CGFloat { constantFromTable("StackTopShiftUp") } // \sigma_10 in TeX
var stackDisplayStyleGapMin:CGFloat { constantFromTable("StackDisplayStyleGapMin") } // 7 \xi_8 in TeX
var stackGapMin:CGFloat { constantFromTable("StackGapMin") } // 3 \xi_8 in TeX
var stackBottomDisplayStyleShiftDown:CGFloat { constantFromTable("StackBottomDisplayStyleShiftDown") } // \sigma_11 in TeX
var stackBottomShiftDown:CGFloat { constantFromTable("StackBottomShiftDown") } // \sigma_12 in TeX
var stretchStackBottomShiftDown:CGFloat { constantFromTable("StretchStackBottomShiftDown") }
var stretchStackGapAboveMin:CGFloat { constantFromTable("StretchStackGapAboveMin") }
var stretchStackGapBelowMin:CGFloat { constantFromTable("StretchStackGapBelowMin") }
var stretchStackTopShiftUp:CGFloat { constantFromTable("StretchStackTopShiftUp") }
// MARK: - super/sub scripts
var superscriptShiftUp:CGFloat { constantFromTable("SuperscriptShiftUp") } // \sigma_13, \sigma_14 in TeX
var superscriptShiftUpCramped:CGFloat { constantFromTable("SuperscriptShiftUpCramped") } // \sigma_15 in TeX
var subscriptShiftDown:CGFloat { constantFromTable("SubscriptShiftDown") } // \sigma_16, \sigma_17 in TeX
var superscriptBaselineDropMax:CGFloat { constantFromTable("SuperscriptBaselineDropMax") } // \sigma_18 in TeX
var subscriptBaselineDropMin:CGFloat { constantFromTable("SubscriptBaselineDropMin") } // \sigma_19 in TeX
var superscriptBottomMin:CGFloat { constantFromTable("SuperscriptBottomMin") } // 1/4 \sigma_5 in TeX
var subscriptTopMax:CGFloat { constantFromTable("SubscriptTopMax") } // 4/5 \sigma_5 in TeX
var subSuperscriptGapMin:CGFloat { constantFromTable("SubSuperscriptGapMin") } // 4 \xi_8 in TeX
var superscriptBottomMaxWithSubscript:CGFloat { constantFromTable("SuperscriptBottomMaxWithSubscript") } // 4/5 \sigma_5 in TeX
var spaceAfterScript:CGFloat { constantFromTable("SpaceAfterScript") }
// MARK: - radicals
var radicalExtraAscender:CGFloat { constantFromTable("RadicalExtraAscender") } // \xi_8 in Tex
var radicalRuleThickness:CGFloat { constantFromTable("RadicalRuleThickness") } // \xi_8 in Tex
var radicalDisplayStyleVerticalGap:CGFloat { constantFromTable("RadicalDisplayStyleVerticalGap") } // \xi_8 + 1/4 \sigma_5 in Tex
var radicalVerticalGap:CGFloat { constantFromTable("RadicalVerticalGap") } // 5/4 \xi_8 in Tex
var radicalKernBeforeDegree:CGFloat { constantFromTable("RadicalKernBeforeDegree") } // 5 mu in Tex
var radicalKernAfterDegree:CGFloat { constantFromTable("RadicalKernAfterDegree") } // -10 mu in Tex
var radicalDegreeBottomRaisePercent:CGFloat { percentFromTable("RadicalDegreeBottomRaisePercent") } // 60% in Tex
// MARK: - Limits
var upperLimitBaselineRiseMin:CGFloat { constantFromTable("UpperLimitBaselineRiseMin") } // \xi_11 in TeX
var upperLimitGapMin:CGFloat { constantFromTable("UpperLimitGapMin") } // \xi_9 in TeX
var lowerLimitGapMin:CGFloat { constantFromTable("LowerLimitGapMin") } // \xi_10 in TeX
var lowerLimitBaselineDropMin:CGFloat { constantFromTable("LowerLimitBaselineDropMin") } // \xi_12 in TeX
var limitExtraAscenderDescender:CGFloat { 0 } // \xi_13 in TeX, not present in OpenType so we always set it to 0.
// MARK: - Underline
var underbarVerticalGap:CGFloat { constantFromTable("UnderbarVerticalGap") } // 3 \xi_8 in TeX
var underbarRuleThickness:CGFloat { constantFromTable("UnderbarRuleThickness") } // \xi_8 in TeX
var underbarExtraDescender:CGFloat { constantFromTable("UnderbarExtraDescender") } // \xi_8 in TeX
// MARK: - Overline
var overbarVerticalGap:CGFloat { constantFromTable("OverbarVerticalGap") } // 3 \xi_8 in TeX
var overbarRuleThickness:CGFloat { constantFromTable("OverbarRuleThickness") } // \xi_8 in TeX
var overbarExtraAscender:CGFloat { constantFromTable("OverbarExtraAscender") } // \xi_8 in TeX
// MARK: - Constants
var axisHeight:CGFloat { constantFromTable("AxisHeight") } // \sigma_22 in TeX
var scriptScaleDown:CGFloat { percentFromTable("ScriptPercentScaleDown") }
var scriptScriptScaleDown:CGFloat { percentFromTable("ScriptScriptPercentScaleDown") }
var mathLeading:CGFloat { constantFromTable("MathLeading") }
var delimitedSubFormulaMinHeight:CGFloat { constantFromTable("DelimitedSubFormulaMinHeight") }
// MARK: - Accent
var accentBaseHeight:CGFloat { constantFromTable("AccentBaseHeight") } // \fontdimen5 in TeX (x-height)
var flattenedAccentBaseHeight:CGFloat { constantFromTable("FlattenedAccentBaseHeight") }
// MARK: - Variants
let kVertVariants = "v_variants"
let kHorizVariants = "h_variants"
/** Returns an Array of all the vertical variants of the glyph if any. If
there are no variants for the glyph, the array contains the given glyph. */
func getVerticalVariantsForGlyph( _ glyph:CGGlyph) -> [NSNumber?] {
let variants = _mathTable[kVertVariants] as! NSDictionary?
return self.getVariantsForGlyph(glyph, inDictionary: variants!)
}
/** Returns an Array of all the horizontal variants of the glyph if any. If
there are no variants for the glyph, the array contains the given glyph. */
func getHorizontalVariantsForGlyph( _ glyph:CGGlyph) -> [NSNumber?] {
let variants = _mathTable[kHorizVariants] as! NSDictionary
return self.getVariantsForGlyph(glyph, inDictionary:variants)
}
func getVariantsForGlyph(_ glyph: CGGlyph, inDictionary variants:NSDictionary) -> [NSNumber?] {
let glyphName = self.font!.get(nameForGlyph: glyph)
let variantGlyphs = variants[glyphName] as! NSArray?
var glyphArray = [NSNumber]()
if variantGlyphs == nil || variantGlyphs?.count == 0 {
// There are no extra variants, so just add the current glyph to it.
let glyph = self.font!.get(glyphWithName: glyphName)
glyphArray.append(NSNumber(value:glyph))
return glyphArray
}
for gvn in variantGlyphs! {
let glyphVariantName = gvn as! String?
let variantGlyph = self.font?.get(glyphWithName: glyphVariantName!)
glyphArray.append(NSNumber(value:variantGlyph!))
}
return glyphArray
}
/** Returns a larger vertical variant of the given glyph if any.
If there is no larger version, this returns the current glyph.
- Parameter glyph: The glyph to find a larger variant for
- Parameter forDisplayStyle: If true, selects the largest appropriate variant for display style.
If false, selects the next larger variant (incremental sizing).
- Returns: A larger glyph variant, or the original glyph if no variants exist
*/
func getLargerGlyph(_ glyph:CGGlyph, forDisplayStyle: Bool = false) -> CGGlyph {
let variants = _mathTable[kVertVariants] as! NSDictionary?
let glyphName = self.font?.get(nameForGlyph: glyph)
let variantGlyphs = variants![glyphName!] as! NSArray?
if variantGlyphs == nil || variantGlyphs?.count == 0 {
// There are no extra variants, so just returnt the current glyph.
return glyph
}
if forDisplayStyle {
// For display style, select a large variant suitable for mathematical display mode
// Display integrals should be significantly larger (~2.2em) for visual prominence
let count = variantGlyphs!.count
// Strategy: Use the largest variant, but avoid extreme sizes for fonts with many variants
let targetIndex: Int
if count <= 2 {
// Small variant list: use the last one (e.g., integral.size1 at ~2.2em)
targetIndex = count - 1
} else if count <= 4 {
// Medium variant list: use second-to-last to avoid extremes
targetIndex = count - 2
} else {
// Large variant list (like texgyretermes with 6 variants):
// Use variant at ~60% position to get appropriate display size (~2.0em)
// For 7 variants (0-6), this gives index 4
targetIndex = min(count - 2, Int(Double(count) * 0.6))
}
if let glyphVariantName = variantGlyphs![targetIndex] as? String {
let variantGlyph = self.font?.get(glyphWithName: glyphVariantName)
return variantGlyph!
}
} else {
// Text/inline style: use incremental sizing for moderate enlargement
// Find the first variant with a different name
for gvn in variantGlyphs! {
let glyphVariantName = gvn as! String?
if glyphVariantName != glyphName {
let variantGlyph = self.font?.get(glyphWithName: glyphVariantName!)
return variantGlyph!
}
}
}
// We did not find any variants of this glyph so return it.
return glyph;
}
// MARK: - Italic Correction
let kItalic = "italic"
/** Returns the italic correction for the given glyph if any. If there
isn't any this returns 0. */
func getItalicCorrection(_ glyph: CGGlyph) -> CGFloat {
let italics = _mathTable[kItalic] as! NSDictionary?
let glyphName = self.font?.get(nameForGlyph: glyph)
let val = italics![glyphName!] as! NSNumber?
// if val is nil, this returns 0.
return self.fontUnitsToPt(val?.intValue ?? 0)
}
// MARK: - Accents
let kAccents = "accents"
/** Returns the adjustment to the top accent for the given glyph if any.
If there isn't any this returns -1. */
func getTopAccentAdjustment(_ glyph: CGGlyph) -> CGFloat {
var glyph = glyph
let accents = _mathTable[kAccents] as! NSDictionary?
let glyphName = self.font?.get(nameForGlyph: glyph)
let val = accents![glyphName!] as! NSNumber?
if let val = val {
return self.fontUnitsToPt(val.intValue)
} else {
// If no top accent is defined then it is the center of the advance width.
var advances = CGSize.zero
CTFontGetAdvancesForGlyphs(self.font!.ctFont, .horizontal, &glyph, &advances, 1)
return advances.width/2
}
}
// MARK: - Glyph Construction
/** Minimum overlap of connecting glyphs during glyph construction */
var minConnectorOverlap:CGFloat { constantFromTable("MinConnectorOverlap") }
let kVertAssembly = "v_assembly"
let kAssemblyParts = "parts"
/** Returns an array of the glyph parts to be used for constructing vertical variants
of this glyph. If there is no glyph assembly defined, returns an empty array. */
func getVerticalGlyphAssembly(forGlyph glyph:CGGlyph) -> [GlyphPart] {
let assemblyTable = _mathTable[kVertAssembly] as! NSDictionary?
let glyphName = self.font?.get(nameForGlyph: glyph)
let assemblyInfo = assemblyTable![glyphName!] as! NSDictionary?
if assemblyInfo == nil {
// No vertical assembly defined for glyph
return []
}
let parts = assemblyInfo![kAssemblyParts] as! NSArray?
if parts == nil {
// parts should always have been defined, but if it isn't return nil
return []
}
var rv = [GlyphPart]()
for part in parts! {
let partInfo = part as! NSDictionary?
var part = GlyphPart()
let adv = partInfo!["advance"] as! NSNumber?
part.fullAdvance = self.fontUnitsToPt(adv!.intValue)
let end = partInfo!["endConnector"] as! NSNumber?
part.endConnectorLength = self.fontUnitsToPt(end!.intValue)
let start = partInfo!["startConnector"] as! NSNumber?
part.startConnectorLength = self.fontUnitsToPt(start!.intValue)
let ext = partInfo!["extender"] as! NSNumber?
part.isExtender = ext!.boolValue
let glyphName = partInfo!["glyph"] as! String?
part.glyph = self.font?.get(glyphWithName: glyphName!)
rv.append(part)
}
return rv
}
}

View file

@ -0,0 +1,37 @@
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by .
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
import SwiftUI
#if os(macOS)
public class MTLabel : NSTextField {
init() {
super.init(frame: .zero)
self.stringValue = ""
self.isBezeled = false
self.drawsBackground = false
self.isEditable = false
self.isSelectable = false
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
// MARK: - Customized getter and setter methods for property text.
var text:String? {
get { super.stringValue }
set { super.stringValue = newValue! }
}
}
#endif

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,117 @@
//
// File.swift
//
//
// Created by Peter Tang on 12/9/2023.
//
import Foundation
#if os(iOS) || os(visionOS)
import UIKit
#endif
#if os(macOS)
import AppKit
#endif
public class MTMathImage {
public var font: MTFont? = MTFontManager.fontManager.defaultFont
public var fontSize:CGFloat {
set {
_fontSize = newValue
let font = font?.copy(withSize: newValue)
self.font = font // also forces an update
}
get { _fontSize }
}
private var _fontSize:CGFloat = 0
public let textColor: MTColor
public let labelMode: MTMathUILabelMode
public let textAlignment: MTTextAlignment
public var contentInsets: MTEdgeInsets = MTEdgeInsetsZero
public let latex: String
private(set) var intrinsicContentSize = CGSize.zero
public init(latex: String, fontSize: CGFloat, textColor: MTColor, labelMode: MTMathUILabelMode = .display, textAlignment: MTTextAlignment = .center) {
self.latex = latex
self.textColor = textColor
self.labelMode = labelMode
self.textAlignment = textAlignment
self.fontSize = fontSize
}
}
extension MTMathImage {
public var currentStyle: MTLineStyle {
switch labelMode {
case .display: return .display
case .text: return .text
}
}
private func intrinsicContentSize(_ displayList: MTMathListDisplay) -> CGSize {
CGSize(width: displayList.width + contentInsets.left + contentInsets.right,
height: displayList.ascent + displayList.descent + contentInsets.top + contentInsets.bottom)
}
public func asImage() -> (NSError?, MTImage?) {
func layoutImage(size: CGSize, displayList: MTMathListDisplay) {
var textX = CGFloat(0)
switch self.textAlignment {
case .left: textX = contentInsets.left
case .center: textX = (size.width - contentInsets.left - contentInsets.right - displayList.width) / 2 + contentInsets.left
case .right: textX = size.width - displayList.width - contentInsets.right
}
let availableHeight = size.height - contentInsets.bottom - contentInsets.top
// center things vertically
var height = displayList.ascent + displayList.descent
if height < fontSize/2 {
height = fontSize/2 // set height to half the font size
}
let textY = (availableHeight - height) / 2 + displayList.descent + contentInsets.bottom
displayList.position = CGPoint(x: textX, y: textY)
}
var error: NSError?
guard let mathList = MTMathListBuilder.build(fromString: latex, error: &error), error == nil,
let displayList = MTTypesetter.createLineForMathList(mathList, font: font, style: currentStyle) else {
return (error, nil)
}
intrinsicContentSize = intrinsicContentSize(displayList)
displayList.textColor = textColor
let size = intrinsicContentSize
layoutImage(size: size, displayList: displayList)
#if os(iOS) || os(visionOS)
let renderer = UIGraphicsImageRenderer(size: size)
let image = renderer.image { rendererContext in
rendererContext.cgContext.saveGState()
rendererContext.cgContext.concatenate(.flippedVertically(size.height))
displayList.draw(rendererContext.cgContext)
rendererContext.cgContext.restoreGState()
}
return (nil, image)
#endif
#if os(macOS)
let image = NSImage(size: size, flipped: false) { bounds in
guard let context = NSGraphicsContext.current?.cgContext else { return false }
context.saveGState()
displayList.draw(context)
context.restoreGState()
return true
}
return (nil, image)
#endif
}
}
private extension CGAffineTransform {
static func flippedVertically(_ height: CGFloat) -> CGAffineTransform {
var transform = CGAffineTransform(scaleX: 1, y: -1)
transform = transform.translatedBy(x: 0, y: -height)
return transform
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,881 @@
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by Kostub Deshmukh.
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
import QuartzCore
import CoreText
import SwiftUI
func isIos6Supported() -> Bool {
if !MTDisplay.initialized {
#if os(iOS) || os(visionOS)
let reqSysVer = "6.0"
let currSysVer = UIDevice.current.systemVersion
if currSysVer.compare(reqSysVer, options: .numeric) != .orderedAscending {
MTDisplay.supported = true
}
#else
MTDisplay.supported = true
#endif
MTDisplay.initialized = true
}
return MTDisplay.supported
}
// The Downshift protocol allows an MTDisplay to be shifted down by a given amount.
protocol DownShift {
var shiftDown:CGFloat { set get }
}
// MARK: - MTDisplay
/// The base class for rendering a math equation.
public class MTDisplay:NSObject {
// needed for isIos6Supported() func above
static var initialized = false
static var supported = false
/// Draws itself in the given graphics context.
public func draw(_ context:CGContext) {
if self.localBackgroundColor != nil {
context.saveGState()
context.setBlendMode(.normal)
context.setFillColor(self.localBackgroundColor!.cgColor)
context.fill(self.displayBounds())
context.restoreGState()
}
}
/// Gets the bounding rectangle for the MTDisplay
func displayBounds() -> CGRect {
CGRectMake(self.position.x, self.position.y - self.descent, self.width, self.ascent + self.descent)
}
/// For debugging. Shows the object in quick look in Xcode.
#if os(iOS) || os(visionOS)
func debugQuickLookObject() -> Any {
let size = CGSizeMake(self.width, self.ascent + self.descent);
UIGraphicsBeginImageContext(size);
// get a reference to that context we created
let context = UIGraphicsGetCurrentContext()!
// translate/flip the graphics context (for transforming from CG* coords to UI* coords
context.translateBy(x: 0, y: size.height);
context.scaleBy(x: 1.0, y: -1.0);
// move the position to (0,0)
context.translateBy(x: -self.position.x, y: -self.position.y);
// Move the line up by self.descent
context.translateBy(x: 0, y: self.descent);
// Draw self on context
self.draw(context)
// generate a new UIImage from the graphics context we drew onto
let img = UIGraphicsGetImageFromCurrentImageContext()
return img as Any
}
#endif
/// The distance from the axis to the top of the display
public var ascent:CGFloat = 0
/// The distance from the axis to the bottom of the display
public var descent:CGFloat = 0
/// The width of the display
public var width:CGFloat = 0
/// Position of the display with respect to the parent view or display.
var position = CGPoint.zero
/// The range of characters supported by this item
public var range:NSRange=NSMakeRange(0, 0)
/// Whether the display has a subscript/superscript following it.
public var hasScript:Bool = false
/// The text color for this display
var textColor: MTColor?
/// The local color, if the color was mutated local with the color command
var localTextColor: MTColor?
/// The background color for this display
var localBackgroundColor: MTColor?
}
/// Special class to be inherited from that implements the DownShift protocol
class MTDisplayDS : MTDisplay, DownShift {
var shiftDown: CGFloat = 0
}
// MARK: - MTCTLineDisplay
/// A rendering of a single CTLine as an MTDisplay
public class MTCTLineDisplay : MTDisplay {
/// The CTLine being displayed
public var line:CTLine!
/// The attributed string used to generate the CTLineRef. Note setting this does not reset the dimensions of
/// the display. So set only when
var attributedString:NSAttributedString? {
didSet {
line = CTLineCreateWithAttributedString(attributedString!)
}
}
/// An array of MTMathAtoms that this CTLine displays. Used for indexing back into the MTMathList
public fileprivate(set) var atoms = [MTMathAtom]()
init(withString attrString:NSAttributedString?, position:CGPoint, range:NSRange, font:MTFont?, atoms:[MTMathAtom]) {
super.init()
self.position = position
self.attributedString = attrString
self.line = CTLineCreateWithAttributedString(attrString!)
self.range = range
self.atoms = atoms
// We can't use typographic bounds here as the ascent and descent returned are for the font and not for the line.
// CRITICAL FIX for accented character clipping:
// Use the MAXIMUM of typographic width and visual width to account for glyph overhang.
// - Typographic width = advance width (how far the cursor moves)
// - Visual width = actual glyph extent (CGRectGetMaxX of glyph path bounds)
// Some glyphs (especially italic/oblique accented characters) extend beyond their advance width.
// Using max() ensures we account for overhang while maintaining proper spacing for normal glyphs.
let typographicWidth = CGFloat(CTLineGetTypographicBounds(line, nil, nil, nil))
if isIos6Supported() {
let bounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
self.ascent = max(0, CGRectGetMaxY(bounds) - 0);
self.descent = max(0, 0 - CGRectGetMinY(bounds));
// Use the maximum of visual and typographic width to handle both:
// 1. Overhanging glyphs (visual > typographic) - prevents clipping
// 2. Normal glyphs (typographic >= visual) - maintains correct spacing
let visualWidth = CGRectGetMaxX(bounds)
self.width = max(typographicWidth, visualWidth);
} else {
// Our own implementation of the ios6 function to get glyph path bounds.
self.computeDimensions(font, typographicWidth: typographicWidth)
}
}
override var textColor: MTColor? {
set {
super.textColor = newValue
let attrStr = attributedString!.mutableCopy() as! NSMutableAttributedString
let foregroundColor = NSAttributedString.Key(kCTForegroundColorAttributeName as String)
attrStr.addAttribute(foregroundColor, value:self.textColor!.cgColor, range:NSMakeRange(0, attrStr.length))
self.attributedString = attrStr
}
get { super.textColor }
}
func computeDimensions(_ font:MTFont?, typographicWidth: CGFloat) {
let runs = CTLineGetGlyphRuns(line) as NSArray
var maxVisualWidth: CGFloat = 0
var currentX: CGFloat = 0
for obj in runs {
let run = obj as! CTRun?
let numGlyphs = CTRunGetGlyphCount(run!)
var glyphs = [CGGlyph]()
glyphs.reserveCapacity(numGlyphs)
CTRunGetGlyphs(run!, CFRangeMake(0, numGlyphs), &glyphs);
let bounds = CTFontGetBoundingRectsForGlyphs(font!.ctFont, .horizontal, glyphs, nil, numGlyphs);
let ascent = max(0, CGRectGetMaxY(bounds) - 0);
// Descent is how much the line goes below the origin. However if the line is all above the origin, then descent can't be negative.
let descent = max(0, 0 - CGRectGetMinY(bounds));
if (ascent > self.ascent) {
self.ascent = ascent;
}
if (descent > self.descent) {
self.descent = descent;
}
// Calculate visual width using glyph extent
// Get the rightmost edge of this run's glyphs
let runVisualWidth = CGRectGetMaxX(bounds)
let runRightEdge = currentX + runVisualWidth
// Get advances to know where next run starts
var advances = [CGSize](repeating: CGSize.zero, count: numGlyphs)
CTRunGetAdvances(run!, CFRangeMake(0, numGlyphs), &advances)
for advance in advances {
currentX += advance.width
}
if (runRightEdge > maxVisualWidth) {
maxVisualWidth = runRightEdge
}
}
// Use maximum of typographic and visual width
self.width = max(typographicWidth, maxVisualWidth)
}
override public func draw(_ context: CGContext) {
super.draw(context)
context.saveGState()
context.textPosition = self.position
CTLineDraw(line, context)
context.restoreGState()
}
}
// MARK: - MTMathListDisplay
/// An MTLine is a rendered form of MTMathList in one line.
/// It can render itself using the draw method.
public class MTMathListDisplay : MTDisplay {
/**
The type of position for a line, i.e. subscript/superscript or regular.
*/
public enum LinePosition : Int {
/// Regular
case regular
/// Positioned at a subscript
case ssubscript
/// Positioned at a superscript
case superscript
}
/// Where the line is positioned
public var type:LinePosition = .regular
/// An array of MTDisplays which are positioned relative to the position of the
/// the current display.
public fileprivate(set) var subDisplays = [MTDisplay]()
/// If a subscript or superscript this denotes the location in the parent MTList. For a
/// regular list this is NSNotFound
public var index: Int = 0
init(withDisplays displays:[MTDisplay], range:NSRange) {
super.init()
self.subDisplays = displays
self.position = CGPoint.zero
self.type = .regular
self.index = NSNotFound
self.range = range
self.recomputeDimensions()
}
override var textColor: MTColor? {
set {
super.textColor = newValue
for displayAtom in self.subDisplays {
if displayAtom.localTextColor == nil {
displayAtom.textColor = newValue
} else {
displayAtom.textColor = displayAtom.localTextColor
}
}
}
get { super.textColor }
}
override public func draw(_ context: CGContext) {
super.draw(context)
context.saveGState()
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
context.translateBy(x: self.position.x, y: self.position.y)
context.textPosition = CGPoint.zero
// draw each atom separately
for displayAtom in self.subDisplays {
displayAtom.draw(context)
}
context.restoreGState()
}
func recomputeDimensions() {
var max_ascent:CGFloat = 0
var max_descent:CGFloat = 0
var max_width:CGFloat = 0
for atom in self.subDisplays {
let ascent = max(0, atom.position.y + atom.ascent);
if (ascent > max_ascent) {
max_ascent = ascent;
}
let descent = max(0, 0 - (atom.position.y - atom.descent));
if (descent > max_descent) {
max_descent = descent;
}
let width = atom.width + atom.position.x;
if (width > max_width) {
max_width = width;
}
}
self.ascent = max_ascent;
self.descent = max_descent;
self.width = max_width;
}
}
// MARK: - MTFractionDisplay
/// Rendering of an MTFraction as an MTDisplay
public class MTFractionDisplay : MTDisplay {
/** A display representing the numerator of the fraction. Its position is relative
to the parent and is not treated as a sub-display.
*/
public fileprivate(set) var numerator:MTMathListDisplay?
/** A display representing the denominator of the fraction. Its position is relative
to the parent is not treated as a sub-display.
*/
public fileprivate(set) var denominator:MTMathListDisplay?
var numeratorUp:CGFloat=0 { didSet { self.updateNumeratorPosition() } }
var denominatorDown:CGFloat=0 { didSet { self.updateDenominatorPosition() } }
var linePosition:CGFloat=0
var lineThickness:CGFloat=0
init(withNumerator numerator:MTMathListDisplay?, denominator:MTMathListDisplay?, position:CGPoint, range:NSRange) {
super.init()
self.numerator = numerator;
self.denominator = denominator;
self.position = position;
// CRITICAL FIX: Handle invalid ranges gracefully
// When table cells are typeset independently with maxWidth, atoms may have
// ranges that are invalid in the cell's context (e.g., (0,0) or other issues)
// Instead of crashing with assertion, normalize the range
if range.length == 0 {
// Create a dummy range with length 1 at the given location
self.range = NSMakeRange(range.location, 1)
} else {
self.range = range;
// Still assert for debugging if range length is something unexpected
assert(self.range.length == 1, "Fraction range length not 1 - range (\(range.location), \(range.length)")
}
}
override public var ascent:CGFloat {
set { super.ascent = newValue }
get { (numerator?.ascent ?? 0) + self.numeratorUp }
}
override public var descent:CGFloat {
set { super.descent = newValue }
get { (denominator?.descent ?? 0) + self.denominatorDown }
}
override public var width:CGFloat {
set { super.width = newValue }
get { max(numerator?.width ?? 0, denominator?.width ?? 0) }
}
func updateDenominatorPosition() {
guard denominator != nil else { return }
denominator!.position = CGPointMake(self.position.x + (self.width - denominator!.width)/2, self.position.y - self.denominatorDown)
}
func updateNumeratorPosition() {
guard numerator != nil else { return }
numerator!.position = CGPointMake(self.position.x + (self.width - numerator!.width)/2, self.position.y + self.numeratorUp)
}
override var position: CGPoint {
set {
super.position = newValue
self.updateDenominatorPosition()
self.updateNumeratorPosition()
}
get { super.position }
}
override var textColor: MTColor? {
set {
super.textColor = newValue
numerator?.textColor = newValue
denominator?.textColor = newValue
}
get { super.textColor }
}
override public func draw(_ context:CGContext) {
super.draw(context)
numerator?.draw(context)
denominator?.draw(context)
context.saveGState()
self.textColor?.setStroke()
// draw the horizontal line
// Note: line thickness of 0 draws the thinnest possible line - we want no line so check for 0s
if self.lineThickness > 0 {
let path = MTBezierPath()
path.move(to: CGPointMake(self.position.x, self.position.y + self.linePosition))
path.addLine(to: CGPointMake(self.position.x + self.width, self.position.y + self.linePosition))
path.lineWidth = self.lineThickness
path.stroke()
}
context.restoreGState()
}
}
// MARK: - MTRadicalDisplay
/// Rendering of an MTRadical as an MTDisplay
class MTRadicalDisplay : MTDisplay {
/** A display representing the radicand of the radical. Its position is relative
to the parent is not treated as a sub-display.
*/
public fileprivate(set) var radicand:MTMathListDisplay?
/** A display representing the degree of the radical. Its position is relative
to the parent is not treated as a sub-display.
*/
public fileprivate(set) var degree:MTMathListDisplay?
override var position: CGPoint {
set {
super.position = newValue
self.updateRadicandPosition()
}
get { super.position }
}
override var textColor: MTColor? {
set {
super.textColor = newValue
self.radicand?.textColor = newValue
self.degree?.textColor = newValue
}
get { super.textColor }
}
private var _radicalGlyph:MTDisplay?
private var _radicalShift:CGFloat=0
var topKern:CGFloat=0
var lineThickness:CGFloat=0
init(withRadicand radicand:MTMathListDisplay?, glyph:MTDisplay, position:CGPoint, range:NSRange) {
super.init()
self.radicand = radicand
_radicalGlyph = glyph
_radicalShift = 0
self.position = position
self.range = range
}
func setDegree(_ degree:MTMathListDisplay?, fontMetrics:MTFontMathTable?) {
// sets up the degree of the radical
var kernBefore = fontMetrics!.radicalKernBeforeDegree;
let kernAfter = fontMetrics!.radicalKernAfterDegree;
let raise = fontMetrics!.radicalDegreeBottomRaisePercent * (self.ascent - self.descent);
// The layout is:
// kernBefore, raise, degree, kernAfter, radical
self.degree = degree;
// the radical is now shifted by kernBefore + degree.width + kernAfter
_radicalShift = kernBefore + degree!.width + kernAfter;
if _radicalShift < 0 {
// we can't have the radical shift backwards, so instead we increase the kernBefore such
// that _radicalShift will be 0.
kernBefore -= _radicalShift;
_radicalShift = 0;
}
// Note: position of degree is relative to parent.
self.degree!.position = CGPointMake(self.position.x + kernBefore, self.position.y + raise);
// Update the width by the _radicalShift
self.width = _radicalShift + _radicalGlyph!.width + self.radicand!.width;
// update the position of the radicand
self.updateRadicandPosition()
}
func updateRadicandPosition() {
// The position of the radicand includes the position of the MTRadicalDisplay
// This is to make the positioning of the radical consistent with fractions and
// have the cursor position finding algorithm work correctly.
// move the radicand by the width of the radical sign
self.radicand!.position = CGPointMake(self.position.x + _radicalShift + _radicalGlyph!.width, self.position.y);
}
override public func draw(_ context: CGContext) {
super.draw(context)
// draw the radicand & degree at its position
self.radicand?.draw(context)
self.degree?.draw(context)
context.saveGState();
self.textColor?.setStroke()
self.textColor?.setFill()
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
context.translateBy(x: self.position.x + _radicalShift, y: self.position.y);
context.textPosition = CGPoint.zero
// Draw the glyph.
_radicalGlyph?.draw(context)
// Draw the VBOX
// for the kern of, we don't need to draw anything.
let heightFromTop = topKern;
// draw the horizontal line with the given thickness
let path = MTBezierPath()
let lineStart = CGPointMake(_radicalGlyph!.width, self.ascent - heightFromTop - self.lineThickness / 2); // subtract half the line thickness to center the line
let lineEnd = CGPointMake(lineStart.x + self.radicand!.width, lineStart.y);
path.move(to: lineStart)
path.addLine(to: lineEnd)
path.lineWidth = lineThickness
path.lineCapStyle = .round
path.stroke()
context.restoreGState();
}
}
// MARK: - MTGlyphDisplay
/// Rendering a glyph as a display
class MTGlyphDisplay : MTDisplayDS {
var glyph:CGGlyph!
var font:MTFont?
/// Horizontal scale factor for stretching glyphs (1.0 = no scaling)
var scaleX: CGFloat = 1.0
init(withGlpyh glyph:CGGlyph, range:NSRange, font:MTFont?) {
super.init()
self.font = font
self.glyph = glyph
self.position = CGPoint.zero
self.range = range
}
override public func draw(_ context: CGContext) {
super.draw(context)
context.saveGState()
self.textColor?.setFill()
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
context.translateBy(x: self.position.x, y: self.position.y - self.shiftDown);
// Apply horizontal scaling if needed (for stretchy arrows)
if scaleX != 1.0 {
context.scaleBy(x: scaleX, y: 1.0)
}
context.textPosition = CGPoint.zero
var pos = CGPoint.zero
CTFontDrawGlyphs(font!.ctFont, &glyph, &pos, 1, context);
context.restoreGState();
}
override var ascent:CGFloat {
set { super.ascent = newValue }
get { super.ascent - self.shiftDown }
}
override var descent:CGFloat {
set { super.descent = newValue }
get { super.descent + self.shiftDown }
}
}
// MARK: - MTGlyphConstructionDisplay
class MTGlyphConstructionDisplay:MTDisplayDS {
var glyphs = [CGGlyph]()
var positions = [CGPoint]()
var font:MTFont?
var numGlyphs:Int=0
init(withGlyphs glyphs:[NSNumber?], offsets:[NSNumber?], font:MTFont?) {
super.init()
assert(glyphs.count == offsets.count, "Glyphs and offsets need to match")
self.numGlyphs = glyphs.count;
self.glyphs = [CGGlyph](repeating: CGGlyph(), count: self.numGlyphs) //malloc(sizeof(CGGlyph) * _numGlyphs);
self.positions = [CGPoint](repeating: CGPoint.zero, count: self.numGlyphs) //malloc(sizeof(CGPoint) * _numGlyphs);
for i in 0 ..< self.numGlyphs {
self.glyphs[i] = glyphs[i]!.uint16Value
self.positions[i] = CGPointMake(0, CGFloat(offsets[i]!.floatValue))
}
self.font = font
self.position = CGPoint.zero
}
override public func draw(_ context: CGContext) {
super.draw(context)
context.saveGState()
self.textColor?.setFill()
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
context.translateBy(x: self.position.x, y: self.position.y - self.shiftDown)
context.textPosition = CGPoint.zero
// Draw the glyphs.
CTFontDrawGlyphs(font!.ctFont, glyphs, positions, numGlyphs, context)
context.restoreGState()
}
override var ascent:CGFloat {
set { super.ascent = newValue }
get { super.ascent - self.shiftDown }
}
override var descent:CGFloat {
set { super.descent = newValue }
get { super.descent + self.shiftDown }
}
}
// MARK: - MTLargeOpLimitsDisplay
/// Rendering a large operator with limits as an MTDisplay
class MTLargeOpLimitsDisplay : MTDisplay {
/** A display representing the upper limit of the large operator. Its position is relative
to the parent is not treated as a sub-display.
*/
var upperLimit:MTMathListDisplay?
/** A display representing the lower limit of the large operator. Its position is relative
to the parent is not treated as a sub-display.
*/
var lowerLimit:MTMathListDisplay?
var limitShift:CGFloat=0
var upperLimitGap:CGFloat=0 { didSet { self.updateUpperLimitPosition() } }
var lowerLimitGap:CGFloat=0 { didSet { self.updateLowerLimitPosition() } }
var extraPadding:CGFloat=0
var nucleus:MTDisplay?
init(withNucleus nucleus:MTDisplay?, upperLimit:MTMathListDisplay?, lowerLimit:MTMathListDisplay?, limitShift:CGFloat, extraPadding:CGFloat) {
super.init()
self.upperLimit = upperLimit;
self.lowerLimit = lowerLimit;
self.nucleus = nucleus;
var maxWidth = max(nucleus!.width, upperLimit?.width ?? 0)
maxWidth = max(maxWidth, lowerLimit?.width ?? 0)
self.limitShift = limitShift;
self.upperLimitGap = 0;
self.lowerLimitGap = 0;
self.extraPadding = extraPadding; // corresponds to \xi_13 in TeX
self.width = maxWidth;
}
override var ascent:CGFloat {
set { super.ascent = newValue }
get {
if self.upperLimit != nil {
return nucleus!.ascent + extraPadding + self.upperLimit!.ascent + upperLimitGap + self.upperLimit!.descent
} else {
return nucleus!.ascent
}
}
}
override var descent:CGFloat {
set { super.descent = newValue }
get {
if self.lowerLimit != nil {
return nucleus!.descent + extraPadding + lowerLimitGap + self.lowerLimit!.descent + self.lowerLimit!.ascent;
} else {
return nucleus!.descent;
}
}
}
override var position: CGPoint {
set {
super.position = newValue
self.updateLowerLimitPosition()
self.updateUpperLimitPosition()
self.updateNucleusPosition()
}
get { super.position }
}
func updateLowerLimitPosition() {
if self.lowerLimit != nil {
// The position of the lower limit includes the position of the MTLargeOpLimitsDisplay
// This is to make the positioning of the radical consistent with fractions and radicals
// Move the starting point to below the nucleus leaving a gap of _lowerLimitGap and subtract
// the ascent to to get the baseline. Also center and shift it to the left by _limitShift.
self.lowerLimit!.position = CGPointMake(self.position.x - limitShift + (self.width - lowerLimit!.width)/2,
self.position.y - nucleus!.descent - lowerLimitGap - self.lowerLimit!.ascent);
}
}
func updateUpperLimitPosition() {
if self.upperLimit != nil {
// The position of the upper limit includes the position of the MTLargeOpLimitsDisplay
// This is to make the positioning of the radical consistent with fractions and radicals
// Move the starting point to above the nucleus leaving a gap of _upperLimitGap and add
// the descent to to get the baseline. Also center and shift it to the right by _limitShift.
self.upperLimit!.position = CGPointMake(self.position.x + limitShift + (self.width - self.upperLimit!.width)/2,
self.position.y + nucleus!.ascent + upperLimitGap + self.upperLimit!.descent);
}
}
func updateNucleusPosition() {
// Center the nucleus
nucleus?.position = CGPointMake(self.position.x + (self.width - nucleus!.width)/2, self.position.y);
}
override var textColor: MTColor? {
set {
super.textColor = newValue
self.upperLimit?.textColor = newValue
self.lowerLimit?.textColor = newValue
nucleus?.textColor = newValue
}
get { super.textColor }
}
override func draw(_ context:CGContext) {
super.draw(context)
// Draw the elements.
self.upperLimit?.draw(context)
self.lowerLimit?.draw(context)
nucleus?.draw(context)
}
}
// MARK: - MTLineDisplay
/// Rendering of an list with an overline or underline
class MTLineDisplay : MTDisplay {
/** A display representing the inner list that is underlined. Its position is relative
to the parent is not treated as a sub-display.
*/
var inner:MTMathListDisplay?
var lineShiftUp:CGFloat=0
var lineThickness:CGFloat=0
init(withInner inner:MTMathListDisplay?, position:CGPoint, range:NSRange) {
super.init()
self.inner = inner;
self.position = position;
self.range = range;
}
override var textColor: MTColor? {
set {
super.textColor = newValue
inner?.textColor = newValue
}
get { super.textColor }
}
override var position: CGPoint {
set {
super.position = newValue
self.updateInnerPosition()
}
get { super.position }
}
override func draw(_ context:CGContext) {
super.draw(context)
self.inner?.draw(context)
context.saveGState();
self.textColor?.setStroke()
// draw the horizontal line
let path = MTBezierPath()
let lineStart = CGPointMake(self.position.x, self.position.y + self.lineShiftUp);
let lineEnd = CGPointMake(lineStart.x + self.inner!.width, lineStart.y);
path.move(to:lineStart)
path.addLine(to: lineEnd)
path.lineWidth = self.lineThickness;
path.stroke()
context.restoreGState();
}
func updateInnerPosition() {
self.inner?.position = CGPointMake(self.position.x, self.position.y);
}
}
// MARK: - MTAccentDisplay
/// Rendering an accent as a display
class MTAccentDisplay : MTDisplay {
/** A display representing the inner list that is accented. Its position is relative
to the parent is not treated as a sub-display.
*/
var accentee:MTMathListDisplay?
/** A display representing the accent. Its position is relative to the current display.
*/
var accent:MTGlyphDisplay?
init(withAccent glyph:MTGlyphDisplay?, accentee:MTMathListDisplay?, range:NSRange) {
super.init()
self.accent = glyph
self.accentee = accentee
self.accentee?.position = CGPoint.zero
self.range = range
}
override var textColor: MTColor? {
set {
super.textColor = newValue
accentee?.textColor = newValue
accent?.textColor = newValue
}
get { super.textColor }
}
override var position: CGPoint {
set {
super.position = newValue
self.updateAccenteePosition()
}
get { super.position }
}
func updateAccenteePosition() {
self.accentee?.position = CGPointMake(self.position.x, self.position.y);
}
override func draw(_ context:CGContext) {
super.draw(context)
self.accentee?.draw(context)
context.saveGState();
context.translateBy(x: self.position.x, y: self.position.y);
context.textPosition = CGPoint.zero
self.accent?.draw(context)
context.restoreGState();
}
}

View file

@ -0,0 +1,197 @@
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by Kostub Deshmukh.
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
/**
* An index that points to a particular character in the MTMathList. The index is a LinkedList that represents
* a path from the beginning of the MTMathList to reach a particular atom in the list. The next node of the path
* is represented by the subIndex. The path terminates when the subIndex is nil.
*
* If there is a subIndex, the subIndexType denotes what branch the path takes (i.e. superscript, subscript,
* numerator, denominator etc.).
* e.g in the expression 25^{2/4} the index of the character 4 is represented as:
* (1, superscript) -> (0, denominator) -> (0, none)
* This can be interpreted as start at index 1 (i.e. the 5) go up to the superscript.
* Then look at index 0 (i.e. 2/4) and go to the denominator. Then look up index 0 (i.e. the 4) which this final
* index.
*
* The level of an index is the number of nodes in the LinkedList to get to the final path.
*/
public class MTMathListIndex {
/**
The type of the subindex.
The type of the subindex denotes what branch the path to the atom that this index points to takes.
*/
public enum MTMathListSubIndexType: Int {
/// The index denotes the whole atom, subIndex is nil.
case none = 0
/// The position in the subindex is an index into the nucleus
case nucleus
/// The subindex indexes into the superscript.
case superscript
/// The subindex indexes into the subscript
case ssubscript
/// The subindex indexes into the numerator (only valid for fractions)
case numerator
/// The subindex indexes into the denominator (only valid for fractions)
case denominator
/// The subindex indexes into the radicand (only valid for radicals)
case radicand
/// The subindex indexes into the degree (only valid for radicals)
case degree
}
/// The index of the associated atom.
var atomIndex: Int
/// The type of subindex, e.g. superscript, numerator etc.
var subIndexType: MTMathListSubIndexType = .none
/// The index into the sublist.
var subIndex: MTMathListIndex?
var finalIndex: Int {
if self.subIndexType == .none {
return self.atomIndex
} else {
return self.subIndex?.finalIndex ?? 0
}
}
/// Returns the previous index if present. Returns `nil` if there is no previous index.
func prevIndex() -> MTMathListIndex? {
if self.subIndexType == .none {
if self.atomIndex > 0 {
return MTMathListIndex(level0Index: self.atomIndex - 1)
}
} else {
if let prevSubIndex = self.subIndex?.prevIndex() {
return MTMathListIndex(at: self.atomIndex, with: prevSubIndex, type: self.subIndexType)
}
}
return nil
}
/// Returns the next index.
func nextIndex() -> MTMathListIndex {
if self.subIndexType == .none {
return MTMathListIndex(level0Index: self.atomIndex + 1)
} else if self.subIndexType == .nucleus {
return MTMathListIndex(at: self.atomIndex + 1, with: self.subIndex, type: self.subIndexType)
} else {
return MTMathListIndex(at: self.atomIndex, with: self.subIndex?.nextIndex(), type: self.subIndexType)
}
}
/**
* Returns true if this index represents the beginning of a line. Note there may be multiple lines in a MTMathList,
* e.g. a superscript or a fraction numerator. This returns true if the innermost subindex points to the beginning of a
* line.
*/
func isBeginningOfLine() -> Bool { self.finalIndex == 0 }
func isAtSameLevel(with index: MTMathListIndex?) -> Bool {
if self.subIndexType != index?.subIndexType {
return false
} else if self.subIndexType == .none {
// No subindexes, they are at the same level.
return true
} else if (self.atomIndex != index?.atomIndex) {
return false
} else {
return self.subIndex?.isAtSameLevel(with: index?.subIndex) ?? false
}
}
/** Returns the type of the innermost sub index. */
func finalSubIndexType() -> MTMathListSubIndexType {
if self.subIndex?.subIndex != nil {
return self.subIndex!.finalSubIndexType()
} else {
return self.subIndexType
}
}
/** Returns true if any of the subIndexes of this index have the given type. */
func hasSubIndex(ofType type: MTMathListSubIndexType) -> Bool {
if self.subIndexType == type {
return true
} else {
return self.subIndex?.hasSubIndex(ofType: type) ?? false
}
}
func levelUp(with subIndex: MTMathListIndex?, type: MTMathListSubIndexType) -> MTMathListIndex {
if self.subIndexType == .none {
return MTMathListIndex(at: self.atomIndex, with: subIndex, type: type)
}
return MTMathListIndex(at: self.atomIndex, with: self.subIndex?.levelUp(with: subIndex, type: type), type: self.subIndexType)
}
func levelDown() -> MTMathListIndex? {
if self.subIndexType == .none {
return nil
}
if let subIndexDown = self.subIndex?.levelDown() {
return MTMathListIndex(at: self.atomIndex, with: subIndexDown, type: self.subIndexType)
} else {
return MTMathListIndex(level0Index: self.atomIndex)
}
}
/** Factory function to create a `MTMathListIndex` with no subindexes.
@param index The index of the atom that the `MTMathListIndex` points at.
*/
public init(level0Index: Int) {
self.atomIndex = level0Index
}
public convenience init(at location: Int, with subIndex: MTMathListIndex?, type: MTMathListSubIndexType) {
self.init(level0Index: location)
self.subIndexType = type
self.subIndex = subIndex
}
}
extension MTMathListIndex: CustomStringConvertible {
public var description: String {
if self.subIndex != nil {
return "[\(self.atomIndex), \(self.subIndexType.rawValue):\(self.subIndex!)]"
}
return "[\(self.atomIndex)]"
}
}
extension MTMathListIndex: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(self.atomIndex)
hasher.combine(self.subIndexType)
hasher.combine(self.subIndex)
}
}
extension MTMathListIndex: Equatable {
public static func ==(lhs: MTMathListIndex, rhs: MTMathListIndex) -> Bool {
if lhs.atomIndex != rhs.atomIndex || lhs.subIndexType != rhs.subIndexType {
return false
}
if rhs.subIndex != nil {
return rhs.subIndex == lhs.subIndex
} else {
return lhs.subIndex == nil
}
}
}

View file

@ -0,0 +1,83 @@
import Foundation
#if os(iOS) || os(visionOS)
import UIKit
#endif
#if os(macOS)
import AppKit
#endif
public struct MTMathRenderedFormula {
public let image: MTImage
public let size: CGSize
public let width: CGFloat
public let ascent: CGFloat
public let descent: CGFloat
public init(image: MTImage, size: CGSize, width: CGFloat, ascent: CGFloat, descent: CGFloat) {
self.image = image
self.size = size
self.width = width
self.ascent = ascent
self.descent = descent
}
}
public enum MTMathRenderer {
public static func render(latex: String, fontSize: CGFloat, textColor: MTColor, mode: MTMathUILabelMode = .display) -> MTMathRenderedFormula? {
guard let font = MTFontManager.fontManager.defaultFont?.copy(withSize: fontSize) else {
return nil
}
var error: NSError?
guard let mathList = MTMathListBuilder.build(fromString: latex, error: &error), error == nil else {
return nil
}
let style: MTLineStyle
switch mode {
case .display:
style = .display
case .text:
style = .text
}
guard let displayList = MTTypesetter.createLineForMathList(mathList, font: font, style: style) else {
return nil
}
displayList.textColor = textColor
let width = max(1.0, ceil(displayList.width))
let height = max(1.0, ceil(displayList.ascent + displayList.descent))
let size = CGSize(width: width, height: height)
displayList.position = CGPoint(x: 0.0, y: displayList.descent)
#if os(iOS) || os(visionOS)
let renderer = UIGraphicsImageRenderer(size: size)
let image = renderer.image { rendererContext in
rendererContext.cgContext.saveGState()
var transform = CGAffineTransform(scaleX: 1.0, y: -1.0)
transform = transform.translatedBy(x: 0.0, y: -size.height)
rendererContext.cgContext.concatenate(transform)
displayList.draw(rendererContext.cgContext)
rendererContext.cgContext.restoreGState()
}
return MTMathRenderedFormula(image: image, size: size, width: displayList.width, ascent: displayList.ascent, descent: displayList.descent)
#endif
#if os(macOS)
let image = NSImage(size: size, flipped: false) { _ in
guard let context = NSGraphicsContext.current?.cgContext else {
return false
}
context.saveGState()
displayList.draw(context)
context.restoreGState()
return true
}
return MTMathRenderedFormula(image: image, size: size, width: displayList.width, ascent: displayList.ascent, descent: displayList.descent)
#endif
}
}

View file

@ -0,0 +1,587 @@
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by Kostub Deshmukh.
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
import CoreText
/**
Different display styles supported by the `MTMathUILabel`.
The only significant difference between the two modes is how fractions
and limits on large operators are displayed.
*/
public enum MTMathUILabelMode {
/// Display mode. Equivalent to $$ in TeX
case display
/// Text mode. Equivalent to $ in TeX.
case text
}
/**
Horizontal text alignment for `MTMathUILabel`.
*/
public enum MTTextAlignment : UInt {
/// Align left.
case left
/// Align center.
case center
/// Align right.
case right
}
/** The main view for rendering math.
`MTMathLabel` accepts either a string in LaTeX or an `MTMathList` to display. Use
`MTMathList` directly only if you are building it programmatically (e.g. using an
editor), otherwise using LaTeX is the preferable method.
The math display is centered vertically in the label. The default horizontal alignment is
is left. This can be changed by setting `textAlignment`. The math is default displayed in
*Display* mode. This can be changed using `labelMode`.
When created it uses `[MTFontManager defaultFont]` as its font. This can be changed using
the `font` parameter.
*/
@IBDesignable
public class MTMathUILabel : MTView {
/** The `MTMathList` to render. Setting this will remove any
`latex` that has already been set. If `latex` has been set, this will
return the parsed `MTMathList` if the `latex` parses successfully. Use this
setting if the `MTMathList` has been programmatically constructed, otherwise it
is preferred to use `latex`.
*/
public var mathList:MTMathList? {
set {
_mathList = newValue
_error = nil
_latex = MTMathListBuilder.mathListToString(newValue)
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
get { _mathList }
}
private var _mathList:MTMathList?
/** The latex string to be displayed. Setting this will remove any `mathList` that
has been set. If latex has not been set, this will return the latex output for the
`mathList` that is set.
@see error */
@IBInspectable
public var latex:String {
set {
_latex = newValue
_error = nil
var error : NSError? = nil
_mathList = MTMathListBuilder.build(fromString: newValue, error: &error)
if error != nil {
_mathList = nil
_error = error
self.errorLabel?.text = error!.localizedDescription
self.errorLabel?.frame = self.bounds
self.errorLabel?.isHidden = !self.displayErrorInline
} else {
self.errorLabel?.isHidden = true
}
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
get { _latex }
}
private var _latex = ""
/** This contains any error that occurred when parsing the latex. */
public var error:NSError? { _error }
private var _error:NSError?
/** If true, if there is an error it displays the error message inline. Default true. */
public var displayErrorInline = true
/** If true, enables detailed debug logging for intrinsicContentSize calculations.
This helps diagnose rendering issues like missing row breaks in aligned environments.
Default false. */
public var debugLogging = false
/** The MTFont to use for rendering. */
public var font:MTFont? {
set {
guard newValue != nil else { return }
_font = newValue
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
get { _font }
}
private var _font:MTFont?
/** Convenience method to just set the size of the font without changing the fontface. */
@IBInspectable
public var fontSize:CGFloat {
set {
_fontSize = newValue
let font = font?.copy(withSize: newValue)
self.font = font // also forces an update
}
get { _fontSize }
}
private var _fontSize:CGFloat=0
/** This sets the text color of the rendered math formula. The default color is black. */
@IBInspectable
public var textColor:MTColor? {
set {
guard newValue != nil else { return }
_textColor = newValue
self.displayList?.textColor = newValue
self.setNeedsDisplay()
}
get { _textColor }
}
private var _textColor:MTColor?
/** The minimum distance from the margin of the view to the rendered math. This value is
`UIEdgeInsetsZero` by default. This is useful if you need some padding between the math and
the border/background color. sizeThatFits: will have its returned size increased by these insets.
*/
@IBInspectable
public var contentInsets:MTEdgeInsets {
set {
_contentInsets = newValue
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
get { _contentInsets }
}
private var _contentInsets = MTEdgeInsetsZero
/** The Label mode for the label. The default mode is Display */
public var labelMode:MTMathUILabelMode {
set {
_labelMode = newValue
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
get { _labelMode }
}
private var _labelMode = MTMathUILabelMode.display
/** Horizontal alignment for the text. The default is align left. */
public var textAlignment:MTTextAlignment {
set {
_textAlignment = newValue
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
get { _textAlignment }
}
private var _textAlignment = MTTextAlignment.left
/** The internal display of the MTMathUILabel. This is for advanced use only. */
public var displayList: MTMathListDisplay? { _displayList }
private var _displayList:MTMathListDisplay?
/** The preferred maximum width (in points) for a multiline label.
Set this property to enable line wrapping based on available width. */
public var preferredMaxLayoutWidth: CGFloat {
set {
_preferredMaxLayoutWidth = newValue
_displayList = nil // Clear cached display list when width constraint changes
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
get { _preferredMaxLayoutWidth }
}
private var _preferredMaxLayoutWidth: CGFloat = 0
public var currentStyle:MTLineStyle {
switch _labelMode {
case .display: return .display
case .text: return .text
}
}
public var errorLabel: MTLabel?
public override init(frame: CGRect) {
super.init(frame: frame)
self.initCommon()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
self.initCommon()
}
func initCommon() {
#if os(macOS)
self.layer?.isGeometryFlipped = true
#else
self.layer.isGeometryFlipped = true
self.clipsToBounds = true
#endif
_fontSize = 20
_contentInsets = MTEdgeInsetsZero
_labelMode = .display
let font = MTFontManager.fontManager.defaultFont
self.font = font
_textAlignment = .left
_displayList = nil
displayErrorInline = true
self.backgroundColor = MTColor.clear
_textColor = MTColor.black
let label = MTLabel()
self.errorLabel = label
#if os(macOS)
label.layer?.isGeometryFlipped = true
#else
label.layer.isGeometryFlipped = true
#endif
label.isHidden = true
label.textColor = MTColor.red
self.addSubview(label)
}
override public func draw(_ dirtyRect: MTRect) {
super.draw(dirtyRect)
if self.mathList == nil { return }
if self.font == nil { return }
// Ensure display list is created before drawing
if _displayList == nil {
_layoutSubviews()
}
guard let displayList = _displayList else { return }
// drawing code
let context = MTGraphicsGetCurrentContext()!
context.saveGState()
// CRITICAL FIX for clipping: If the displayList is wider than our bounds,
// expand the clipping rect to prevent content clipping.
// This handles cases where preferredMaxLayoutWidth is a hint but the content
// cannot fit within it even with line breaking.
let contentWidth = displayList.width + contentInsets.left + contentInsets.right
if contentWidth > bounds.size.width {
// Content is wider than bounds - expand clip rect
let expandedRect = CGRect(
x: bounds.origin.x,
y: bounds.origin.y,
width: contentWidth,
height: bounds.size.height
)
context.clip(to: expandedRect)
}
displayList.draw(context)
context.restoreGState()
}
func _layoutSubviews() {
guard _mathList != nil && self.font != nil else {
_displayList = nil
errorLabel?.frame = self.bounds
self.setNeedsDisplay()
return
}
// Ensure we have a valid font before attempting to typeset
if self.font == nil {
// No valid font - try to get default font
if let defaultFont = MTFontManager.fontManager.defaultFont {
self._font = defaultFont
} else {
// Cannot typeset without a font, clear display list
_displayList = nil
errorLabel?.frame = self.bounds
self.setNeedsDisplay()
return
}
}
// Use the effective width for layout
let effectiveWidth = _preferredMaxLayoutWidth > 0 ? _preferredMaxLayoutWidth : bounds.size.width
var availableWidth = effectiveWidth - contentInsets.left - contentInsets.right
// CRITICAL FIX: Ensure availableWidth is never negative
// Negative maxWidth passed to MTTypesetter can cause "Negative value is not representable" crashes
availableWidth = max(0, availableWidth)
_displayList = MTTypesetter.createLineForMathList(_mathList, font: self.font, style: currentStyle, maxWidth: availableWidth)
guard let displayList = _displayList else {
// Empty or invalid input - nothing to display
return
}
displayList.textColor = textColor
var textX = CGFloat(0)
switch self.textAlignment {
case .left: textX = contentInsets.left
case .center: textX = (bounds.size.width - contentInsets.left - contentInsets.right - displayList.width) / 2 + contentInsets.left
case .right: textX = bounds.size.width - displayList.width - contentInsets.right
}
let availableHeight = bounds.size.height - contentInsets.bottom - contentInsets.top
// center things vertically
var height = displayList.ascent + displayList.descent
if height < fontSize/2 {
height = fontSize/2 // set height to half the font size
}
let textY = (availableHeight - height) / 2 + displayList.descent + contentInsets.bottom
displayList.position = CGPointMake(textX, textY)
errorLabel?.frame = self.bounds
self.setNeedsDisplay()
}
func _sizeThatFits(_ size:CGSize) -> CGSize {
// Check if we have empty latex (empty string case)
if _latex.isEmpty {
// Empty latex - return zero size
return CGSize(width: 0, height: 0)
}
guard _mathList != nil else {
// No content - return no-intrinsic-size marker
return CGSize(width: -1, height: -1)
}
// Ensure we have a valid font before attempting to typeset
if self.font == nil {
// No valid font - try to get default font
if let defaultFont = MTFontManager.fontManager.defaultFont {
self._font = defaultFont
} else {
// Cannot typeset without a font
return CGSize(width: -1, height: -1)
}
}
// Determine the maximum width to use
var maxWidth: CGFloat = 0
if _preferredMaxLayoutWidth > 0 {
maxWidth = _preferredMaxLayoutWidth - contentInsets.left - contentInsets.right
// CRITICAL FIX: Ensure maxWidth is never negative
// If contentInsets exceed available width, clamp to 0
maxWidth = max(0, maxWidth)
} else if size.width > 0 {
maxWidth = size.width - contentInsets.left - contentInsets.right
// CRITICAL FIX: Ensure maxWidth is never negative
maxWidth = max(0, maxWidth)
}
var displayList:MTMathListDisplay? = nil
displayList = MTTypesetter.createLineForMathList(_mathList, font: self.font, style: currentStyle, maxWidth: maxWidth)
guard displayList != nil else {
// Failed to create display list
return CGSize(width: -1, height: -1)
}
var resultWidth = displayList!.width + contentInsets.left + contentInsets.right
var resultHeight = displayList!.ascent + displayList!.descent + contentInsets.top + contentInsets.bottom
// DEBUG LOGGING for width calculation
if debugLogging {
print("\n=== MTMathUILabel intrinsicContentSize DEBUG ===")
print("LaTeX: \(self.latex)")
// Show raw bytes to help debug escaping issues
let latexBytes = Array(self.latex.utf8.prefix(100))
print("LaTeX bytes (first 100): \(latexBytes)")
print("preferredMaxLayoutWidth: \(_preferredMaxLayoutWidth)")
print("size constraint: \(size)")
print("maxWidth passed to typesetter: \(maxWidth)")
print("displayList.width: \(displayList!.width)")
print("displayList.ascent: \(displayList!.ascent)")
print("displayList.descent: \(displayList!.descent)")
print("Number of subDisplays: \(displayList!.subDisplays.count)")
// Count lines by unique Y positions
let yPositions = Set(displayList!.subDisplays.map { $0.position.y })
print("Number of lines (unique Y positions): \(yPositions.count)")
print("contentInsets: \(contentInsets)")
print("resultWidth (before clamping): \(resultWidth)")
print("resultHeight (before clamping): \(resultHeight)")
// HELPFUL WARNING: Detect common escaping mistake
// Check for environments that typically have multiple rows but only 1 row is rendered
let multiRowEnvs = ["aligned", "align", "eqnarray", "gather", "split"]
let latexLower = self.latex.lowercased()
for env in multiRowEnvs {
if latexLower.contains("\\begin{\(env)}") {
// Check if there's a table with only 1 row
if let ml = _mathList {
for atom in ml.atoms {
if let table = atom as? MTMathTable, table.numRows == 1 {
// Check if latex contains single backslash (spacing) where double might be intended
// Pattern: \\ followed by non-backslash (like space or letter)
if self.latex.contains("\\ ") || self.latex.range(of: #"\\[a-zA-Z]"#, options: .regularExpression) != nil {
print("⚠️ WARNING: '\(env)' environment has only 1 row.")
print(" If you intended multiple rows, use '\\\\\\\\' (4 backslashes in Swift strings)")
print(" or use raw strings: #\"...42\\\\ \\\\dfrac...\"#")
print(" Current input has '\\\\ ' which is a SPACING command, not a row break.")
}
}
}
}
}
}
}
// CRITICAL FIX: Ensure dimensions are never negative
// Negative values cause crashes in NSRange calculations and SwiftUI layout
resultWidth = max(0, resultWidth)
resultHeight = max(0, resultHeight)
// CRITICAL FIX for accented character clipping:
// The preferredMaxLayoutWidth is a HINT for line breaking, NOT a hard constraint.
// If the typesetter cannot fit content within that width (even with line breaking),
// we MUST return the actual content width to prevent clipping.
//
// ONLY clamp in extreme cases to prevent layout explosion (>50% over or >100pt over)
if _preferredMaxLayoutWidth > 0 && resultWidth > _preferredMaxLayoutWidth {
let overflow = resultWidth - _preferredMaxLayoutWidth
let overflowPercent = (overflow / _preferredMaxLayoutWidth) * 100
if debugLogging {
print(" Content exceeds preferredMaxLayoutWidth:")
print(" preferredMaxLayoutWidth: \(_preferredMaxLayoutWidth)")
print(" resultWidth: \(resultWidth)")
print(" overflow: \(overflow) (\(String(format: "%.1f", overflowPercent))%)")
// Check line breaking
let yPositions = Set(displayList!.subDisplays.map { $0.position.y })
print(" hasMultipleLines: \(yPositions.count > 1) (yPositions: \(yPositions.count))")
// Check if any content would be clipped at different widths
print(" SubDisplay analysis:")
for (i, sub) in displayList!.subDisplays.enumerated() {
let rightEdge = sub.position.x + sub.width
let clippedAtPreferred = rightEdge > _preferredMaxLayoutWidth
let clippedAtResult = rightEdge > resultWidth
print(" Sub[\(i)]: rightEdge=\(rightEdge) clippedAt\(_preferredMaxLayoutWidth)=\(clippedAtPreferred) clippedAt\(resultWidth)=\(clippedAtResult)")
}
}
// ONLY clamp for truly excessive overflow (>50% or >100pt)
// This prevents layout explosion while allowing normal overflow
let extremeOverflowThreshold: CGFloat = max(_preferredMaxLayoutWidth * 0.5, 100.0)
if overflow > extremeOverflowThreshold {
// Extreme overflow - clamp to prevent layout issues
let clampedWidth = _preferredMaxLayoutWidth + extremeOverflowThreshold
if debugLogging {
print(" ⚠️ EXTREME OVERFLOW - clamping from \(resultWidth) to \(clampedWidth)")
print(" ⚠️ WARNING: This will cause content clipping!")
}
resultWidth = clampedWidth
} else {
// Normal overflow - keep actual content width to prevent clipping
if debugLogging {
print(" ✓ Normal overflow - keeping actual width \(resultWidth) to prevent clipping")
}
// resultWidth stays as is - NO CLAMPING
}
} else if _preferredMaxLayoutWidth == 0 && size.width > 0 && resultWidth > size.width {
// Similar tolerance for size.width constraint
let tolerance = max(size.width * 0.05, 10.0)
let maxAllowedWidth = size.width + tolerance
if debugLogging {
print(" Exceeds size.width constraint:")
print(" tolerance: \(tolerance)")
print(" maxAllowedWidth: \(maxAllowedWidth)")
print(" overflow amount: \(resultWidth - size.width)")
}
if resultWidth <= maxAllowedWidth {
// Within tolerance - use actual content width
// resultWidth stays as is
if debugLogging {
print(" ✓ Within tolerance - keeping actual width")
}
} else {
if debugLogging {
print(" ⚠️ CLAMPING to maxAllowedWidth (may clip content!)")
}
resultWidth = maxAllowedWidth
}
}
if debugLogging {
print(" Final resultWidth: \(resultWidth)")
print(" Final resultHeight: \(resultHeight)")
// Check if any display elements would be clipped
if let display = displayList {
print(" Display subdisplays: \(display.subDisplays.count)")
let yPositions = Set(display.subDisplays.map { $0.position.y }).sorted()
print(" Unique Y positions (lines): \(yPositions.count) -> \(yPositions)")
for (i, sub) in display.subDisplays.enumerated() {
let rightEdge = sub.position.x + sub.width
let clipped = rightEdge > resultWidth
// Extract text content if this is a CTLineDisplay
var textContent = ""
if let ctLineDisplay = sub as? MTCTLineDisplay,
let attrString = ctLineDisplay.attributedString {
textContent = " text=\"\(attrString.string)\""
}
print(" Sub[\(i)]: type=\(type(of: sub)), y=\(sub.position.y), x=\(sub.position.x), width=\(sub.width), rightEdge=\(rightEdge)\(textContent)\(clipped ? " ⚠️ CLIPPED" : "")")
// Show internal structure for MTMathListDisplay
if let mathListDisplay = sub as? MTMathListDisplay, !mathListDisplay.subDisplays.isEmpty {
print(" → Contains \(mathListDisplay.subDisplays.count) sub-displays:")
for (j, innerSub) in mathListDisplay.subDisplays.enumerated() {
var innerTextContent = ""
if let innerCTLineDisplay = innerSub as? MTCTLineDisplay,
let innerAttrString = innerCTLineDisplay.attributedString {
innerTextContent = " text=\"\(innerAttrString.string)\""
}
print(" [\(j)]: type=\(type(of: innerSub)), y=\(innerSub.position.y), x=\(innerSub.position.x), width=\(innerSub.width)\(innerTextContent)")
}
}
if clipped {
print(" ⚠️ CLIPPING: rightEdge \(rightEdge) > resultWidth \(resultWidth)")
print(" Clipped amount: \(rightEdge - resultWidth)")
}
}
}
print("=== END DEBUG ===\n")
}
return CGSize(width: resultWidth, height: resultHeight)
}
#if os(macOS)
public func sizeThatFits(_ size: CGSize) -> CGSize {
return _sizeThatFits(size)
}
#else
public override func sizeThatFits(_ size: CGSize) -> CGSize {
return _sizeThatFits(size)
}
#endif
#if os(macOS)
func setNeedsDisplay() { self.needsDisplay = true }
func setNeedsLayout() { self.needsLayout = true }
public override var fittingSize: CGSize { _sizeThatFits(CGSizeZero) }
public override var intrinsicContentSize: CGSize { _sizeThatFits(CGSizeZero) }
override public func layout() {
self._layoutSubviews()
super.layout()
}
#else
public override var intrinsicContentSize: CGSize { _sizeThatFits(CGSizeZero) }
override public func layoutSubviews() { _layoutSubviews() }
#endif
}

View file

@ -0,0 +1,68 @@
//
// MTTypesetter+Tokenization.swift
// SwiftMath
//
// Created by Claude Code on 2025-12-16.
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
import CoreGraphics
extension MTTypesetter {
/// Create a line for a math list using the new tokenization approach
/// This is an alternative to the existing createLineForMathList that uses
/// pre-tokenization and greedy line fitting
static func createLineForMathListWithTokenization(
_ mathList: MTMathList?,
font: MTFont?,
style: MTLineStyle,
cramped: Bool,
spaced: Bool,
maxWidth: CGFloat
) -> MTMathListDisplay? {
guard let mathList = mathList else { return nil }
guard let font = font else { return nil }
guard !mathList.atoms.isEmpty else {
// Return empty display instead of nil (matches KaTeX behavior)
return MTMathListDisplay(withDisplays: [], range: NSMakeRange(0, 0))
}
// Phase 0: Preprocess atoms to fuse ordinary characters
// This is critical for accents and other structures where multi-character
// text like "xyzw" should stay together as a single atom
let preprocessedAtoms = MTTypesetter.preprocessMathList(mathList)
// Phase 1: Tokenize atoms into breakable elements
let tokenizer = MTAtomTokenizer(font: font, style: style, cramped: cramped, maxWidth: maxWidth)
let elements = tokenizer.tokenize(preprocessedAtoms)
guard !elements.isEmpty else { return nil }
// Phase 2: Fit elements into lines
let margin = spaced ? font.mathTable?.muUnit ?? 0 : 0
let fitter = MTLineFitter(maxWidth: maxWidth, margin: margin)
let fittedLines = fitter.fitLines(elements)
// Phase 3: Generate displays from fitted lines
let generator = MTDisplayGenerator(font: font, style: style)
let displays = generator.generateDisplays(from: fittedLines, startPosition: CGPoint.zero)
// Determine range from atoms
let range: NSRange
if let firstAtom = mathList.atoms.first, let lastAtom = mathList.atoms.last {
let start = firstAtom.indexRange.location
let end = NSMaxRange(lastAtom.indexRange)
range = NSMakeRange(start, end - start)
} else {
range = NSMakeRange(0, 0)
}
// Create and return the math list display
let mathListDisplay = MTMathListDisplay(withDisplays: displays, range: range)
return mathListDisplay
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,89 @@
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by Kostub Deshmukh.
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
public struct UnicodeSymbol {
static let multiplication = "\u{00D7}"
static let division = "\u{00F7}"
static let fractionSlash = "\u{2044}"
static let whiteSquare = "\u{25A1}"
static let blackSquare = "\u{25A0}"
static let lessEqual = "\u{2264}"
static let greaterEqual = "\u{2265}"
static let notEqual = "\u{2260}"
static let squareRoot = "\u{221A}" // \sqrt
static let cubeRoot = "\u{221B}"
static let infinity = "\u{221E}" // \infty
static let angle = "\u{2220}" // \angle
static let degree = "\u{00B0}" // \circ
static let capitalGreekStart = UInt32(0x0391)
static let capitalGreekEnd = UInt32(0x03A9)
static let lowerGreekStart = UInt32(0x03B1)
static let lowerGreekEnd = UInt32(0x03C9)
static let planksConstant = UInt32(0x210e)
static let lowerItalicStart = UInt32(0x1D44E)
static let capitalItalicStart = UInt32(0x1D434)
static let greekLowerItalicStart = UInt32(0x1D6FC)
static let greekCapitalItalicStart = UInt32(0x1D6E2)
static let greekSymbolItalicStart = UInt32(0x1D716)
static let mathCapitalBoldStart = UInt32(0x1D400)
static let mathLowerBoldStart = UInt32(0x1D41A)
static let greekCapitalBoldStart = UInt32(0x1D6A8)
static let greekLowerBoldStart = UInt32(0x1D6C2)
static let greekSymbolBoldStart = UInt32(0x1D6DC)
static let numberBoldStart = UInt32(0x1D7CE)
static let mathCapitalBoldItalicStart = UInt32(0x1D468)
static let mathLowerBoldItalicStart = UInt32(0x1D482)
static let greekCapitalBoldItalicStart = UInt32(0x1D71C)
static let greekLowerBoldItalicStart = UInt32(0x1D736)
static let greekSymbolBoldItalicStart = UInt32(0x1D750)
static let mathCapitalScriptStart = UInt32(0x1D49C)
static let mathCapitalTTStart = UInt32(0x1D670)
static let mathLowerTTStart = UInt32(0x1D68A)
static let numberTTStart = UInt32(0x1D7F6)
static let mathCapitalSansSerifStart = UInt32(0x1D5A0)
static let mathLowerSansSerifStart = UInt32(0x1D5BA)
static let numberSansSerifStart = UInt32(0x1D7E2)
static let mathCapitalFrakturStart = UInt32(0x1D504)
static let mathLowerFrakturStart = UInt32(0x1D51E)
static let mathCapitalBlackboardStart = UInt32(0x1D538)
static let mathLowerBlackboardStart = UInt32(0x1D552)
static let numberBlackboardStart = UInt32(0x1D7D8)
}
extension Character {
var utf32Char: UTF32Char { self.unicodeScalars.map { $0.value }.reduce(0, +) }
var isLowerEnglish : Bool { self >= "a" && self <= "z" }
var isUpperEnglish : Bool { self >= "A" && self <= "Z" }
var isNumber : Bool { self >= "0" && self <= "9" }
var isLowerGreek : Bool {
let uch = self.utf32Char
return uch >= UnicodeSymbol.lowerGreekStart && uch <= UnicodeSymbol.lowerGreekEnd
}
var isCapitalGreek : Bool {
let uch = self.utf32Char
return uch >= UnicodeSymbol.capitalGreekStart && uch <= UnicodeSymbol.capitalGreekEnd
}
var greekSymbolOrder : UInt32? {
let greekSymbols : [UTF32Char] = [0x03F5, 0x03D1, 0x03F0, 0x03D5, 0x03F1, 0x03D6]
let index = greekSymbols.firstIndex(of: self.utf32Char)
if let pos = index { return UInt32(pos) }
return nil
}
var isGreekSymbol : Bool { self.greekSymbolOrder != nil }
}

View file

@ -0,0 +1,58 @@
import Foundation
final class RWLock {
init() {
pthread_rwlock_init(&lock, nil)
}
deinit {
pthread_rwlock_destroy(&lock)
}
func read<T>(_ block: () -> T) -> T {
pthread_rwlock_rdlock(&lock)
defer { pthread_rwlock_unlock(&lock) }
return block()
}
func readWrite<T>(_ block: () -> T) -> T {
pthread_rwlock_wrlock(&lock)
defer { pthread_rwlock_unlock(&lock) }
return block()
}
private var lock = pthread_rwlock_t()
}
@propertyWrapper
struct RWLocked<T> {
init(wrappedValue: T) {
value = wrappedValue
}
var wrappedValue: T {
get {
lock.read {
value
}
}
set {
lock.readWrite {
value = newValue
}
}
}
@discardableResult
mutating func readWrite(_ block: (inout T) -> Void) -> (oldValue: T, newValue: T) {
lock.readWrite {
let old = value
block(&value)
return (old, value)
}
}
private var value: T
private let lock = RWLock()
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,115 @@
//
// MTBreakableElement.swift
// SwiftMath
//
// Created by Claude Code on 2025-12-16.
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
import CoreGraphics
// MARK: - MTElementContent
/// Represents the content type of a breakable element
enum MTElementContent {
/// Simple text content
case text(String)
/// Pre-rendered display (fraction, radical, etc.)
case display(MTDisplay)
/// Math operator with spacing
case `operator`(String, type: MTMathAtomType)
/// Explicit spacing
case space(CGFloat)
/// Superscript or subscript display
case script(MTDisplay, isSuper: Bool)
}
// MARK: - MTBreakableElement
/// Represents a breakable element with pre-calculated width and break rules
struct MTBreakableElement {
// MARK: Display properties
/// The content of this element
let content: MTElementContent
/// Pre-calculated width (cached)
let width: CGFloat
/// Height of the element
let height: CGFloat
/// Distance from baseline to top
let ascent: CGFloat
/// Distance from baseline to bottom
let descent: CGFloat
// MARK: Breaking rules
/// Can break BEFORE this element?
let isBreakBefore: Bool
/// Can break AFTER this element?
let isBreakAfter: Bool
/// Penalty for breaking before (0=good, 100=bad, 150=never)
let penaltyBefore: Int
/// Penalty for breaking after (0=good, 100=bad, 150=never)
let penaltyAfter: Int
// MARK: Relationship tracking
/// Elements with same groupId must stay together
let groupId: UUID?
/// Parent element ID (for scripts)
let parentId: UUID?
// MARK: Source tracking
/// Original atom this element was created from
let originalAtom: MTMathAtom
/// Index range in the original math list
let indexRange: NSRange
// MARK: Optional attributes
/// Text color for this element
let color: MTColor?
/// Background color for this element
let backgroundColor: MTColor?
// MARK: Atomicity flag
/// If true, NEVER break this element internally
let indivisible: Bool
}
// MARK: - Penalty Constants
/// Penalty values for line breaking decisions
enum MTBreakPenalty {
/// Best break points (operators, relations)
static let best = 0
/// Good break points (ordinary atoms, after scripts)
static let good = 10
/// Moderate penalty (before fractions, radicals)
static let moderate = 15
/// Acceptable break points
static let acceptable = 50
/// Bad break points (avoid if possible)
static let bad = 100
/// Never break here (grouped elements)
static let never = 150
}

View file

@ -0,0 +1,389 @@
//
// MTDisplayGenerator.swift
// SwiftMath
//
// Created by Claude Code on 2025-12-16.
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
import CoreGraphics
import CoreText
/// Generates MTDisplay objects from fitted lines of breakable elements
class MTDisplayGenerator {
// MARK: - Properties
let font: MTFont
let style: MTLineStyle
let widthCalculator: MTElementWidthCalculator
// MARK: - Initialization
init(font: MTFont, style: MTLineStyle) {
self.font = font
self.style = style
self.widthCalculator = MTElementWidthCalculator(font: font, style: style)
}
// MARK: - Display Generation
/// Generate displays from fitted lines
func generateDisplays(from lines: [[MTBreakableElement]], startPosition: CGPoint) -> [MTDisplay] {
var allDisplays: [MTDisplay] = []
var currentY = startPosition.y
// Minimum spacing between lines (20% of font size for breathing room)
let minimumLineSpacing = font.fontSize * 0.2
for (index, line) in lines.enumerated() {
let (lineDisplays, currentLineMetrics) = generateLine(line, at: CGPoint(x: startPosition.x, y: currentY))
allDisplays.append(contentsOf: lineDisplays)
// Calculate spacing for next line based on actual content heights
if index < lines.count - 1 {
let nextLine = lines[index + 1]
let nextLineAscent = nextLine.map { $0.ascent }.max() ?? 0
// Space needed = current line's descent + minimum spacing + next line's ascent
let spaceNeeded = currentLineMetrics.descent + minimumLineSpacing + nextLineAscent
// Ensure minimum spacing of 1.2x font size for readability
let minSpacing = font.fontSize * 1.2
currentY -= max(spaceNeeded, minSpacing)
}
}
return allDisplays
}
/// Line metrics for spacing calculation
struct LineMetrics {
let ascent: CGFloat
let descent: CGFloat
var height: CGFloat { ascent + descent }
}
/// Generate displays for a single line
private func generateLine(_ elements: [MTBreakableElement], at position: CGPoint) -> ([MTDisplay], LineMetrics) {
var displays: [MTDisplay] = []
var xOffset: CGFloat = 0
// Calculate line metrics
let lineAscent = elements.map { $0.ascent }.max() ?? 0
let lineDescent = elements.map { $0.descent }.max() ?? 0
// Baseline y position
let baseline = position.y
var i = 0
while i < elements.count {
let element = elements[i]
// Check if this is part of a group (base + scripts)
if let groupId = element.groupId {
// Collect all elements in this group
var groupElements: [MTBreakableElement] = []
var j = i
while j < elements.count && elements[j].groupId == groupId {
groupElements.append(elements[j])
j += 1
}
// Render the group
let groupAdvance = renderGroup(groupElements, at: CGPoint(x: position.x + xOffset, y: baseline), displays: &displays)
xOffset += groupAdvance
i = j
} else {
// Regular element (not part of a group)
// CRITICAL: For operators, spacing should be split evenly before and after
// The element.width includes both spacing, but we need to position the operator
// with half spacing before it
var spacingBefore: CGFloat = 0
if case .operator(let op, _) = element.content {
// Get the actual text width vs element width to calculate spacing
let textWidth = widthCalculator.measureText(op)
let totalSpacing = element.width - textWidth
spacingBefore = totalSpacing / 2
}
let elementPosition = CGPoint(x: position.x + xOffset + spacingBefore, y: baseline)
switch element.content {
case .text(let text):
let display = createTextDisplay(text, at: elementPosition, element: element)
displays.append(display)
case .display(let preRenderedDisplay):
// Use pre-rendered display (fraction, radical, etc.)
let mutableDisplay = preRenderedDisplay
mutableDisplay.position = elementPosition
displays.append(mutableDisplay)
case .operator(let op, _):
let display = createTextDisplay(op, at: elementPosition, element: element)
displays.append(display)
case .script:
// Standalone script (shouldn't happen, but handle gracefully)
break
case .space:
// No display for space, just advance position
break
}
xOffset += element.width
i += 1
}
}
return (displays, LineMetrics(ascent: lineAscent, descent: lineDescent))
}
/// Render a group of elements (base + scripts) and return the horizontal advance
private func renderGroup(_ groupElements: [MTBreakableElement], at position: CGPoint, displays: inout [MTDisplay]) -> CGFloat {
var baseWidth: CGFloat = 0
var superscriptWidth: CGFloat = 0
var subscriptWidth: CGFloat = 0
var baseXOffset: CGFloat = 0
// Check if this group has any scripts
let hasScripts = groupElements.contains { element in
if case .script = element.content {
return true
}
return false
}
// Track the start index of base displays for dimension adjustment
let baseDisplayStartIndex = displays.count
// First pass: render base elements and collect script widths
for element in groupElements {
switch element.content {
case .script:
// Skip scripts in first pass
break
default:
// Render base element
let basePosition = CGPoint(x: position.x + baseXOffset, y: position.y)
switch element.content {
case .text(let text):
let display = createTextDisplay(text, at: basePosition, element: element, hasScript: hasScripts)
displays.append(display)
case .display(let preRenderedDisplay):
let mutableDisplay = preRenderedDisplay
mutableDisplay.position = basePosition
displays.append(mutableDisplay)
case .operator(let op, _):
let display = createTextDisplay(op, at: basePosition, element: element, hasScript: hasScripts)
displays.append(display)
default:
break
}
baseWidth += element.width
baseXOffset += element.width
}
}
// Second pass: collect script information for joint positioning
var superscriptDisplay: MTDisplay? = nil
var subscriptDisplay: MTDisplay? = nil
var hasBothScripts = false
for element in groupElements {
if case .script(let scriptDisplay, let isSuper) = element.content {
if isSuper {
superscriptDisplay = scriptDisplay
} else {
subscriptDisplay = scriptDisplay
}
}
}
hasBothScripts = superscriptDisplay != nil && subscriptDisplay != nil
// Third pass: render scripts with proper positioning
var superScriptShiftUp: CGFloat = 0
var subscriptShiftDown: CGFloat = 0
// Check if base is a glyph (not CTLineDisplay) for special positioning
// For glyphs (like large operators), position scripts relative to glyph edges
var isGlyphBase = false
for disp in displays[baseDisplayStartIndex..<displays.count] {
if disp is MTGlyphDisplay {
isGlyphBase = true
// Get script font metrics for display-based positioning
let scriptStyle: MTLineStyle = (style == .display || style == .text) ? .script : .scriptOfScript
let scriptFontSize = MTTypesetter.getStyleSize(scriptStyle, font: font)
let scriptFont = font.copy(withSize: scriptFontSize)
if let scriptFontMetrics = scriptFont.mathTable {
// Position scripts relative to the glyph's edges (matches MTTypesetter line 571-572)
superScriptShiftUp = disp.ascent - scriptFontMetrics.superscriptBaselineDropMax
subscriptShiftDown = disp.descent + scriptFontMetrics.subscriptBaselineDropMin
}
break
}
}
for element in groupElements {
if case .script(let scriptDisplay, let isSuper) = element.content {
guard let mathTable = font.mathTable else { continue }
if !isGlyphBase {
// Standard positioning for text (CTLineDisplay)
if isSuper {
superScriptShiftUp = mathTable.superscriptShiftUp
superScriptShiftUp = max(superScriptShiftUp, scriptDisplay.descent + mathTable.superscriptBottomMin)
} else {
subscriptShiftDown = mathTable.subscriptShiftDown
subscriptShiftDown = max(subscriptShiftDown, scriptDisplay.ascent - mathTable.subscriptTopMax)
}
} else {
// For glyphs, apply the minimum constraints (matches MTTypesetter line 594-595, 581-582)
if isSuper {
superScriptShiftUp = max(superScriptShiftUp, mathTable.superscriptShiftUp)
superScriptShiftUp = max(superScriptShiftUp, scriptDisplay.descent + mathTable.superscriptBottomMin)
} else {
subscriptShiftDown = max(subscriptShiftDown, mathTable.subscriptShiftDown)
subscriptShiftDown = max(subscriptShiftDown, scriptDisplay.ascent - mathTable.subscriptTopMax)
}
}
if isSuper {
superscriptWidth = element.width
} else {
subscriptWidth = element.width
}
}
}
// If both scripts present, apply joint positioning adjustments
if hasBothScripts, let superDisp = superscriptDisplay, let subDisp = subscriptDisplay,
let mathTable = font.mathTable {
let subSuperScriptGap = (superScriptShiftUp - superDisp.descent) + (subscriptShiftDown - subDisp.ascent)
if subSuperScriptGap < mathTable.subSuperscriptGapMin {
// Set the gap to at least the minimum
subscriptShiftDown += mathTable.subSuperscriptGapMin - subSuperScriptGap
let superscriptBottomDelta = mathTable.superscriptBottomMaxWithSubscript - (superScriptShiftUp - superDisp.descent)
if superscriptBottomDelta > 0 {
// Superscript is lower than the max allowed by the font with a subscript
superScriptShiftUp += superscriptBottomDelta
subscriptShiftDown -= superscriptBottomDelta
}
}
}
// Calculate italic correction (delta) for superscript positioning
// Superscripts are positioned at baseWidth + delta, subscripts at baseWidth
var delta: CGFloat = 0
if superscriptDisplay != nil {
// Get italic correction from the base display if it's a glyph
for disp in displays[baseDisplayStartIndex..<displays.count] {
if let glyphDisplay = disp as? MTGlyphDisplay,
let mathTable = font.mathTable {
delta = mathTable.getItalicCorrection(glyphDisplay.glyph)
break
}
}
}
// Fourth pass: create wrapped script displays with final positions
for element in groupElements {
if case .script(let scriptDisplay, let isSuper) = element.content {
let scriptShift = isSuper ? superScriptShiftUp : -subscriptShiftDown
let scriptType: MTMathListDisplay.LinePosition = isSuper ? .superscript : .ssubscript
// Superscripts get delta added, subscripts don't (matches MTTypesetter line 622, 624)
let deltaOffset = isSuper ? delta : 0
let scriptPosition = CGPoint(x: position.x + baseWidth + deltaOffset, y: position.y + scriptShift)
// Reset the scriptDisplay's position to (0, 0) since it will be positioned by the wrapper
let mutableScript = scriptDisplay
mutableScript.position = CGPoint.zero
let wrappedScript = MTMathListDisplay(
withDisplays: [mutableScript],
range: scriptDisplay.range
)
wrappedScript.type = scriptType
wrappedScript.position = scriptPosition
wrappedScript.index = 0 // Index of the base atom this script belongs to
displays.append(wrappedScript)
}
}
// Calculate horizontal advance: base width + max(script widths) + spacing
// This matches MTTypesetter.makeScripts() logic (line 626)
// Superscript width includes delta, subscript doesn't
let scriptWidth = max(superscriptWidth + delta, subscriptWidth)
let spaceAfterScript = font.mathTable?.spaceAfterScript ?? 0
let totalWidth = baseWidth + scriptWidth + spaceAfterScript
// If this group has scripts, adjust the base display's dimensions to include scripts
// This matches the legacy typesetter behavior where display.ascent == line.ascent
if hasScripts && displays.count > baseDisplayStartIndex {
// Calculate the full extent of the group including scripts
var maxAscent: CGFloat = 0
var maxDescent: CGFloat = 0
for i in baseDisplayStartIndex..<displays.count {
let disp = displays[i]
let topY = disp.position.y + disp.ascent
let bottomY = disp.position.y - disp.descent
maxAscent = max(maxAscent, topY - position.y)
maxDescent = max(maxDescent, position.y - bottomY)
}
// Update the first base display's dimensions to reflect the full extent
// Width should be base + script, without the spaceAfterScript (that's for cursor advancement)
let baseDisplay = displays[baseDisplayStartIndex]
baseDisplay.ascent = maxAscent
baseDisplay.descent = maxDescent
baseDisplay.width = baseWidth + scriptWidth
displays[baseDisplayStartIndex] = baseDisplay
}
return totalWidth
}
/// Create a text display for a character or string
private func createTextDisplay(_ text: String, at position: CGPoint, element: MTBreakableElement, hasScript: Bool = false) -> MTDisplay {
let attrString = NSMutableAttributedString(string: text)
attrString.addAttribute(
NSAttributedString.Key(kCTFontAttributeName as String),
value: font.ctFont as Any,
range: NSMakeRange(0, attrString.length)
)
// If the atom was fused (multiple ordinary chars combined), use fusedAtoms
// Otherwise, use the original atom
let atoms: [MTMathAtom]
if !element.originalAtom.fusedAtoms.isEmpty {
atoms = element.originalAtom.fusedAtoms
} else {
atoms = [element.originalAtom]
}
let display = MTCTLineDisplay(
withString: attrString,
position: position,
range: element.indexRange,
font: font,
atoms: atoms
)
// Mark if this base element has associated scripts
display.hasScript = hasScript
return display
}
}

View file

@ -0,0 +1,88 @@
//
// MTDisplayPreRenderer.swift
// SwiftMath
//
// Created by Claude Code on 2025-12-16.
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
import CoreGraphics
/// Pre-renders complex atoms (fractions, radicals, etc.) as MTDisplay objects during tokenization
class MTDisplayPreRenderer {
// MARK: - Properties
let font: MTFont
let style: MTLineStyle
let cramped: Bool
// MARK: - Initialization
init(font: MTFont, style: MTLineStyle, cramped: Bool) {
self.font = font
self.style = style
self.cramped = cramped
}
// MARK: - Script Rendering
/// Render a script (superscript or subscript) as a display
func renderScript(_ mathList: MTMathList, isSuper: Bool) -> MTDisplay? {
let scriptStyle = getScriptStyle()
let scriptCramped = isSuper ? cramped : true // Subscripts are always cramped
// Scale the font for the script style
let scriptFontSize = MTTypesetter.getStyleSize(scriptStyle, font: font)
let scriptFont = font.copy(withSize: scriptFontSize)
guard let display = MTTypesetter.createLineForMathList(
mathList,
font: scriptFont,
style: scriptStyle,
cramped: scriptCramped,
spaced: false
) else {
return nil
}
// If the result is a MTMathListDisplay with a single subdisplay, unwrap it
// This matches the behavior of the legacy typesetter
if display.subDisplays.count == 1 {
return display.subDisplays[0]
}
return display
}
/// Get the appropriate style for scripts
private func getScriptStyle() -> MTLineStyle {
switch style {
case .display, .text:
return .script
case .script, .scriptOfScript:
return .scriptOfScript
}
}
// MARK: - Helper Methods
/// Pre-render a simple math list without width constraints
/// Used for rendering content inside fractions, radicals, etc.
func renderMathList(_ mathList: MTMathList?, style renderStyle: MTLineStyle? = nil, cramped renderCramped: Bool? = nil) -> MTDisplay? {
guard let mathList = mathList else { return nil }
let actualStyle = renderStyle ?? style
let actualCramped = renderCramped ?? cramped
return MTTypesetter.createLineForMathList(
mathList,
font: font,
style: actualStyle,
cramped: actualCramped,
spaced: false
)
}
}

View file

@ -0,0 +1,165 @@
//
// MTElementWidthCalculator.swift
// SwiftMath
//
// Created by Claude Code on 2025-12-16.
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
import CoreText
import CoreGraphics
/// Calculates widths for breakable elements with appropriate spacing
class MTElementWidthCalculator {
// MARK: - Properties
let font: MTFont
let style: MTLineStyle
// MARK: - Initialization
init(font: MTFont, style: MTLineStyle) {
self.font = font
self.style = style
}
// MARK: - Text Width Measurement
/// Measure width of simple text
func measureText(_ text: String) -> CGFloat {
guard !text.isEmpty else { return 0 }
let attrString = NSAttributedString(string: text, attributes: [
kCTFontAttributeName as NSAttributedString.Key: font.ctFont as Any
])
let line = CTLineCreateWithAttributedString(attrString as CFAttributedString)
return CGFloat(CTLineGetTypographicBounds(line, nil, nil, nil))
}
// MARK: - Operator Width Measurement
/// Measure width of operator with appropriate spacing
func measureOperator(_ op: String, type: MTMathAtomType) -> CGFloat {
let baseWidth = measureText(op)
let spacing = getOperatorSpacing(type)
return baseWidth + spacing
}
/// Get spacing for an operator (both sides)
private func getOperatorSpacing(_ type: MTMathAtomType) -> CGFloat {
guard let mathTable = font.mathTable else { return 0 }
let muUnit = mathTable.muUnit
switch type {
case .binaryOperator:
// Binary operators: 4mu on each side = 8mu total
return 2 * muUnit * 4
case .relation:
// Relations: 5mu on each side = 10mu total
return 2 * muUnit * 5
case .largeOperator:
// Large operators in inline mode: 1mu on each side
if style == .display || style == .text {
return 0 // In display mode, handled by MTLargeOpLimitsDisplay
}
return 2 * muUnit * 1
default:
return 0
}
}
// MARK: - Display Width Measurement
/// Measure width of a pre-rendered display
func measureDisplay(_ display: MTDisplay) -> CGFloat {
return display.width
}
// MARK: - Space Width Measurement
/// Get width of explicit spacing command
func measureSpace(_ spaceType: MTMathAtomType) -> CGFloat {
guard let mathTable = font.mathTable else { return 0 }
let muUnit = mathTable.muUnit
// Note: These are the explicit spacing commands in LaTeX
// \, = thin space (3mu)
// \: = medium space (4mu)
// \; = thick space (5mu)
// \quad = 1em
// \qquad = 2em
switch spaceType {
case .space:
// Default space - context dependent
// For now, use thin space
return muUnit * 3
default:
return 0
}
}
/// Measure explicit space value
func measureExplicitSpace(_ width: CGFloat) -> CGFloat {
return width
}
// MARK: - Inter-element Spacing
/// Get inter-element spacing between two atom types
func getInterElementSpacing(left: MTMathAtomType, right: MTMathAtomType) -> CGFloat {
let leftIndex = getInterElementSpaceArrayIndexForType(left, row: true)
let rightIndex = getInterElementSpaceArrayIndexForType(right, row: false)
let spaceArray = getInterElementSpaces()[Int(leftIndex)]
let spaceType = spaceArray[Int(rightIndex)]
guard spaceType != .invalid else {
// Should not happen in well-formed math
return 0
}
let spaceMultiplier = getSpacingInMu(spaceType)
if spaceMultiplier > 0, let mathTable = font.mathTable {
return CGFloat(spaceMultiplier) * mathTable.muUnit
}
return 0
}
/// Get spacing multiplier in mu units
private func getSpacingInMu(_ spaceType: InterElementSpaceType) -> Int {
switch style {
case .display, .text:
switch spaceType {
case .none, .invalid:
return 0
case .thin:
return 3
case .nsThin, .nsMedium, .nsThick:
// ns = non-script, same as regular in display/text mode
switch spaceType {
case .nsThin: return 3
case .nsMedium: return 4
case .nsThick: return 5
default: return 0
}
}
case .script, .scriptOfScript:
switch spaceType {
case .none, .invalid:
return 0
case .thin:
return 3
case .nsThin, .nsMedium, .nsThick:
// In script mode, ns types don't add space
return 0
}
}
}
}

View file

@ -0,0 +1,266 @@
//
// MTLineFitter.swift
// SwiftMath
//
// Created by Claude Code on 2025-12-16.
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
import CoreGraphics
/// Fits breakable elements into lines respecting width constraints and break rules
class MTLineFitter {
// MARK: - Properties
let maxWidth: CGFloat
let margin: CGFloat
// MARK: - Initialization
init(maxWidth: CGFloat, margin: CGFloat = 0) {
self.maxWidth = maxWidth
self.margin = margin
}
// MARK: - Line Fitting
/// Fit elements into lines using greedy algorithm with backtracking
func fitLines(_ elements: [MTBreakableElement]) -> [[MTBreakableElement]] {
guard !elements.isEmpty else { return [] }
guard maxWidth > 0 else { return [elements] } // No width constraint
let debugPunctuation = !"".isEmpty // Enable to debug line breaking issues
if debugPunctuation {
print("\n=== MTLineFitter: fitting \(elements.count) elements, maxWidth=\(maxWidth) ===")
for (idx, elem) in elements.enumerated() {
if case .text(let t) = elem.content {
print("[\(idx)] '\(t)' breakBefore=\(elem.isBreakBefore) breakAfter=\(elem.isBreakAfter) width=\(elem.width)")
}
}
}
var lines: [[MTBreakableElement]] = [[]]
var currentWidth: CGFloat = 0
var i = 0
while i < elements.count {
let element = elements[i]
if debugPunctuation, case .text(let t) = element.content {
print("\n Processing element[\(i)]: '\(t)' breakBefore=\(element.isBreakBefore)")
}
// Handle grouped elements (base + scripts)
if let groupId = element.groupId {
let (groupElements, nextIndex) = collectGroup(elements, startIndex: i, groupId: groupId)
// Calculate group width correctly for scripts
// Scripts overlap vertically, so width = max(script widths), not sum
let groupWidth = calculateGroupWidth(groupElements)
// Check if group fits on current line
if !lines.last!.isEmpty && currentWidth + groupWidth > maxWidth - margin {
// Group doesn't fit - check if first element of group can start a new line
if groupElements.first?.isBreakBefore ?? true {
// Can start new line
lines.append([])
currentWidth = 0
} else {
// Cannot start new line - keep with previous line (allow overflow)
// This handles cases like punctuation after base+script groups
}
}
// Add entire group to current line
lines[lines.count - 1].append(contentsOf: groupElements)
currentWidth += groupWidth
i = nextIndex
continue
}
// Check if element fits on current line
if !lines.last!.isEmpty && currentWidth + element.width > maxWidth - margin {
if debugPunctuation, case .text = element.content {
print(" Doesn't fit (width=\(currentWidth) + \(element.width) > \(maxWidth)), current line has \(lines.last!.count) elements")
}
// Element doesn't fit - find best break point in current line
if let breakIndex = findBestBreak(in: lines[lines.count - 1]) {
if debugPunctuation {
print(" Found break at index \(breakIndex) out of \(lines.last!.count) elements")
if breakIndex < lines.last!.count {
if case .text(let t) = lines.last![breakIndex].content {
print(" Break at element: '\(t)'")
}
}
}
// Found a break point - move elements from breakIndex onward to next line
let moveElements = Array(lines[lines.count - 1][breakIndex...])
let oldLine = Array(lines[lines.count - 1][..<breakIndex])
// Verify the first element being moved can start a line
// (findBestBreak already ensures this, but double-check)
if moveElements.first?.isBreakBefore ?? true {
// Move elements to new line
lines[lines.count - 1] = oldLine
lines.append(moveElements)
currentWidth = moveElements.reduce(0) { $0 + $1.width }
// Now check if current element should go on new line or stay with old line
if debugPunctuation, case .text(let t) = element.content {
print(" Checking element '\(t)' (i=\(i)): breakBefore=\(element.isBreakBefore)")
}
if !element.isBreakBefore {
// CRITICAL FIX: Current element cannot start a line, so it's part of the
// unbreakable sequence that was just moved to the new line.
// Add it to the NEW line, not the old line!
// Example: "matrices" breaks before 'm', so 'm','a','t','r','i' move to new line.
// When we process 'c', it can't start a line, so it must join the new line.
if debugPunctuation, case .text(let t) = element.content {
print(" -> Adding '\(t)' to new line (part of unbreakable sequence)")
}
lines[lines.count - 1].append(element)
currentWidth += element.width
i += 1
continue
} else {
if debugPunctuation, case .text(let t) = element.content {
print(" -> Adding '\(t)' to new line (can start line)")
}
}
// Current element can start a line, will be added to new line below
} else {
// Should not happen if findBestBreak is correct, but handle gracefully
// Keep elements on current line (allow overflow)
lines[lines.count - 1].append(contentsOf: moveElements)
currentWidth += moveElements.reduce(0) { $0 + $1.width }
}
} else {
// No good break point found
// Check if current element can start a new line
if element.isBreakBefore {
// Element can start new line
lines.append([])
currentWidth = 0
} else {
// Element cannot start a new line (e.g., closing punctuation)
// Keep it on current line even if it causes overflow
// This respects punctuation rules over width constraints
lines[lines.count - 1].append(element)
currentWidth += element.width
i += 1
continue
}
}
}
// Add element to current line (may overflow if indivisible and too wide)
lines[lines.count - 1].append(element)
currentWidth += element.width
i += 1
}
let finalLines = lines.filter { !$0.isEmpty }
if debugPunctuation {
print("\n=== Final lines: ===")
for (lineIdx, line) in finalLines.enumerated() {
print("Line \(lineIdx):")
for elem in line {
if case .text(let t) = elem.content {
print(" '\(t)'", terminator: "")
}
}
print()
}
}
return finalLines
}
// MARK: - Helper Methods
/// Calculate the correct width for a group of elements (e.g., base + scripts)
/// Scripts overlap vertically, so the group width is not the sum of all widths
private func calculateGroupWidth(_ groupElements: [MTBreakableElement]) -> CGFloat {
// For grouped elements (base + scripts), just sum all widths
// The display generator will handle the actual positioning and overlap
// This is just for line fitting purposes
return groupElements.reduce(0) { $0 + $1.width }
}
/// Collect all elements that share the same groupId
private func collectGroup(_ elements: [MTBreakableElement], startIndex: Int, groupId: UUID) -> ([MTBreakableElement], Int) {
var groupElements: [MTBreakableElement] = []
var index = startIndex
while index < elements.count && elements[index].groupId == groupId {
groupElements.append(elements[index])
index += 1
}
return (groupElements, index)
}
/// Find the best break point in a line
/// Returns the index where the break should occur (elements from this index move to next line)
private func findBestBreak(in line: [MTBreakableElement]) -> Int? {
var bestIndex: Int? = nil
var lowestPenalty = Int.max
let debugBreak = !"".isEmpty // Enable to debug break point selection
// Scan from right to left to prefer breaking later in the line
// Note: Skip the last element (idx == line.count - 1) because breaking after it
// would move 0 elements to the next line, which is pointless
for (idx, element) in line.enumerated().reversed() {
// Skip the last element - we need to move at least 1 element to the next line
if idx >= line.count - 1 {
continue
}
// Can we break after this element?
let canBreakAfter = element.isBreakAfter
let penaltyAfter = element.penaltyAfter
// Check if next element (which would move to new line) allows breaking before it
let canBreakBeforeNext = line[idx + 1].isBreakBefore
let penaltyBeforeNext = line[idx + 1].penaltyBefore
// We can break here only if BOTH:
// 1. Current element allows breaking after it
// 2. Next element allows breaking before it
if canBreakAfter && canBreakBeforeNext {
let totalPenalty = max(penaltyAfter, penaltyBeforeNext)
if totalPenalty < lowestPenalty {
if debugBreak && idx < line.count - 1 {
let currText = if case .text(let t) = element.content { t } else { "?" }
let nextText = if case .text(let t) = line[idx + 1].content { t } else { "?" }
print(" Considering break: '\(currText)' | '\(nextText)' at idx=\(idx), penalty=\(totalPenalty)")
}
bestIndex = idx + 1
lowestPenalty = totalPenalty
}
}
}
if debugBreak {
print(" Best break: index=\(bestIndex ?? -1), penalty=\(lowestPenalty)")
}
// Only return if we found an acceptable break point
if let index = bestIndex, lowestPenalty <= MTBreakPenalty.bad {
return index
}
return nil
}
/// Check if a line width exceeds the maximum
private func exceedsMaxWidth(_ width: CGFloat) -> Bool {
return width > maxWidth - margin
}
}

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,30 @@
% This is a preliminary version (2006-09-30), barring acceptance from
% the LaTeX Project Team and other feedback, of the GUST Font License.
% (GUST is the Polish TeX Users Group, http://www.gust.org.pl)
%
% For the most recent version of this license see
% http://www.gust.org.pl/fonts/licenses/GUST-FONT-LICENSE.txt
% or
% http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt
%
% This work may be distributed and/or modified under the conditions
% of the LaTeX Project Public License, either version 1.3c of this
% license or (at your option) any later version.
%
% Please also observe the following clause:
% 1) it is requested, but not legally required, that derived works be
% distributed only after changing the names of the fonts comprising this
% work and given in an accompanying "manifest", and that the
% files comprising the Work, as listed in the manifest, also be given
% new names. Any exceptions to this request are also given in the
% manifest.
%
% We recommend the manifest be given in a separate file named
% MANIFEST-<fontid>.txt, where <fontid> is some unique identification
% of the font family. If a separate "readme" file accompanies the Work,
% we recommend a name of the form README-<fontid>.txt.
%
% The latest version of the LaTeX Project Public License is in
% http://www.latex-project.org/lppl.txt and version 1.3c or later
% is part of all distributions of LaTeX version 2006/05/20 or later.

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2013 MathChat
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,103 @@
STIX Font License
24 May 2010
Copyright (c) 2001-2010 by the STI Pub Companies, consisting of the American
Institute of Physics, the American Chemical Society, the American Mathematical
Society, the American Physical Society, Elsevier, Inc., and The Institute of
Electrical and Electronic Engineers, Inc. (www.stixfonts.org), with Reserved
Font Name STIX Fonts, STIX Fonts (TM) is a trademark of The Institute of
Electrical and Electronics Engineers, Inc.
Portions copyright (c) 1998-2003 by MicroPress, Inc. (www.micropress-inc.com),
with Reserved Font Name TM Math. To obtain additional mathematical fonts, please
contact MicroPress, Inc., 68-30 Harrow Street, Forest Hills, NY 11375, USA,
Phone: (718) 575-1816.
Portions copyright (c) 1990 by Elsevier, Inc.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
---------------------------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
---------------------------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide development
of collaborative font projects, to support the font creation efforts of academic
and linguistic communities, and to provide a free and open framework in which
fonts may be shared and improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and redistributed
freely as long as they are not sold by themselves. The fonts, including any
derivative works, can be bundled, embedded, redistributed and/or sold with any
software provided that any reserved names are not used by derivative works. The
fonts and derivatives, however, cannot be released under any other type of license.
The requirement for fonts to remain under this license does not apply to any
document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright Holder(s) under
this license and clearly marked as such. This may include source files, build
scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the copyright
statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting, or
substituting -- in part or in whole -- any of the components of the Original Version,
by changing formats or by porting the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical writer or other
person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining a copy of the
Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell
modified and unmodified copies of the Font Software, subject to the following
conditions:
1) Neither the Font Software nor any of its individual components, in Original or
Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled, redistributed
and/or sold with any software, provided that each copy contains the above copyright
notice and this license. These can be included either as stand-alone text files,
human-readable headers or in the appropriate machine-readable metadata fields within
text or binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless
explicit written permission is granted by the corresponding Copyright Holder. This
restriction only applies to the primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall
not be used to promote, endorse or advertise any Modified Version, except to
acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with
their explicit written permission.
5) The Font Software, modified or unmodified, in part or in whole, must be distributed
entirely under this license, and must not be distributed under any other license. The
requirement for fonts to remain under this license does not apply to any document
created using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER
RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR
INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,205 @@
#!/usr/local/bin/python3
import plistlib
import sys
from fontTools.ttLib import TTFont
def usage(code):
print('Usage math_table_to_plist.py <fontfile> <plistfile>')
sys.exit(code)
def process_font(font_file, out_file):
font = TTFont(font_file)
math_table = font['MATH'].table
constants = get_constants(math_table)
italic_c = get_italic_correction(math_table)
v_variants = get_v_variants(math_table)
h_variants = get_h_variants(math_table)
assembly = get_v_assembly(math_table)
accents = get_accent_attachments(math_table)
pl = {
"version" : "1.3",
"constants": constants,
"v_variants" : v_variants,
"h_variants" : h_variants,
"italic" : italic_c,
"accents" : accents,
"v_assembly" : assembly }
ofile = open(out_file, 'w+b')
plistlib.dump(pl, ofile)
ofile.close()
def get_constants(math_table):
constants = math_table.MathConstants
if constants is None:
raise 'Cannot find MathConstants in MATH table'
int_consts = [ 'ScriptPercentScaleDown',
'ScriptScriptPercentScaleDown',
'DelimitedSubFormulaMinHeight',
'DisplayOperatorMinHeight',
'RadicalDegreeBottomRaisePercent']
consts = { c : getattr(constants, c) for c in int_consts }
record_consts = [ 'MathLeading',
'AxisHeight',
'AccentBaseHeight',
'FlattenedAccentBaseHeight',
'SubscriptShiftDown',
'SubscriptTopMax',
'SubscriptBaselineDropMin',
'SuperscriptShiftUp',
'SuperscriptShiftUpCramped',
'SuperscriptBottomMin',
'SuperscriptBaselineDropMax',
'SubSuperscriptGapMin',
'SuperscriptBottomMaxWithSubscript',
'SpaceAfterScript',
'UpperLimitGapMin',
'UpperLimitBaselineRiseMin',
'LowerLimitGapMin',
'LowerLimitBaselineDropMin',
'StackTopShiftUp',
'StackTopDisplayStyleShiftUp',
'StackBottomShiftDown',
'StackBottomDisplayStyleShiftDown',
'StackGapMin',
'StackDisplayStyleGapMin',
'StretchStackTopShiftUp',
'StretchStackBottomShiftDown',
'StretchStackGapAboveMin',
'StretchStackGapBelowMin',
'FractionNumeratorShiftUp',
'FractionNumeratorDisplayStyleShiftUp',
'FractionDenominatorShiftDown',
'FractionDenominatorDisplayStyleShiftDown',
'FractionNumeratorGapMin',
'FractionNumDisplayStyleGapMin',
'FractionRuleThickness',
'FractionDenominatorGapMin',
'FractionDenomDisplayStyleGapMin',
'SkewedFractionHorizontalGap',
'SkewedFractionVerticalGap',
'OverbarVerticalGap',
'OverbarRuleThickness',
'OverbarExtraAscender',
'UnderbarVerticalGap',
'UnderbarRuleThickness',
'UnderbarExtraDescender',
'RadicalVerticalGap',
'RadicalDisplayStyleVerticalGap',
'RadicalRuleThickness',
'RadicalExtraAscender',
'RadicalKernBeforeDegree',
'RadicalKernAfterDegree',
]
consts_2 = { c : getattr(constants, c).Value for c in record_consts }
consts.update(consts_2)
variants = math_table.MathVariants
consts['MinConnectorOverlap'] = variants.MinConnectorOverlap
return consts
def get_italic_correction(math_table):
glyph_info = math_table.MathGlyphInfo
if glyph_info is None:
raise "Cannot find MathGlyphInfo in MATH table."
italic = glyph_info.MathItalicsCorrectionInfo
if italic is None:
raise "Cannot find Italic Correction in GlyphInfo"
glyphs = italic.Coverage.glyphs
count = italic.ItalicsCorrectionCount
records = italic.ItalicsCorrection
italic_dict = {}
for i in range(count):
name = glyphs[i]
record = records[i]
if record.DeviceTable is not None:
raise "Don't know how to process device table for italic correction."
italic_dict[name] = record.Value
return italic_dict
def get_accent_attachments(math_table):
glyph_info = math_table.MathGlyphInfo
if glyph_info is None:
raise "Cannot find MathGlyphInfo in MATH table."
attach = glyph_info.MathTopAccentAttachment
if attach is None:
raise "Cannot find Top Accent Attachment in GlyphInfo"
glyphs = attach.TopAccentCoverage.glyphs
count = attach.TopAccentAttachmentCount
records = attach.TopAccentAttachment
attach_dict = {}
for i in range(count):
name = glyphs[i]
record = records[i]
if record.DeviceTable is not None:
raise "Don't know how to process device table for accent attachment."
attach_dict[name] = record.Value
return attach_dict
def get_v_variants(math_table):
variants = math_table.MathVariants
vglyphs = variants.VertGlyphCoverage.glyphs
vconstruction = variants.VertGlyphConstruction
count = variants.VertGlyphCount
variant_dict = {}
for i in range(count):
name = vglyphs[i]
record = vconstruction[i]
glyph_variants = [x.VariantGlyph for x in
record.MathGlyphVariantRecord]
variant_dict[name] = glyph_variants
return variant_dict
def get_h_variants(math_table):
variants = math_table.MathVariants
hglyphs = variants.HorizGlyphCoverage.glyphs
hconstruction = variants.HorizGlyphConstruction
count = variants.HorizGlyphCount
variant_dict = {}
for i in range(count):
name = hglyphs[i]
record = hconstruction[i]
glyph_variants = [x.VariantGlyph for x in
record.MathGlyphVariantRecord]
variant_dict[name] = glyph_variants
return variant_dict
def get_v_assembly(math_table):
variants = math_table.MathVariants
vglyphs = variants.VertGlyphCoverage.glyphs
vconstruction = variants.VertGlyphConstruction
count = variants.VertGlyphCount
assembly_dict = {}
for i in range(count):
name = vglyphs[i]
record = vconstruction[i]
assembly = record.GlyphAssembly
if assembly is not None:
# There is an assembly for this glyph
italic = assembly.ItalicsCorrection.Value
parts = [part_dict(part) for part in assembly.PartRecords]
assembly_dict[name] = {
"italic" : assembly.ItalicsCorrection.Value,
"parts" : parts }
return assembly_dict
def part_dict(part):
return {
"glyph": part.glyph,
"startConnector" : part.StartConnectorLength,
"endConnector" : part.EndConnectorLength,
"advance" : part.FullAdvance,
"extender" : (part.PartFlags == 1) }
def main():
if len(sys.argv) != 3:
usage(1)
font_file = sys.argv[1]
plist_file = sys.argv[2]
process_font(font_file, plist_file)
if __name__ == '__main__':
main()

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff