mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Markdown improvements
This commit is contained in:
parent
7fdc1ea1d2
commit
d574182374
79 changed files with 68205 additions and 158 deletions
18
third-party/SwiftMath/BUILD
vendored
Normal file
18
third-party/SwiftMath/BUILD
vendored
Normal 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
21
third-party/SwiftMath/LICENSE
vendored
Executable 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
714
third-party/SwiftMath/README.md
vendored
Normal 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}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
```LaTeX
|
||||
f(x) = \int\limits_{-\infty}^\infty\!\hat f(\xi)\,e^{2 \pi i \xi x}\,\mathrm{d}\xi
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
```LaTeX
|
||||
\frac{1}{n}\sum_{i=1}^{n}x_i \geq \sqrt[n]{\prod_{i=1}^{n}x_i}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
```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} } } }
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
More examples are included in [EXAMPLES](EXAMPLES.md)
|
||||
|
||||
## Fonts
|
||||
Here are previews of the included fonts:
|
||||
|
||||

|
||||

|
||||
|
||||
## 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)
|
||||
194
third-party/SwiftMath/Sources/MathBundle/MTFontMathTableV2.swift
vendored
Executable file
194
third-party/SwiftMath/Sources/MathBundle/MTFontMathTableV2.swift
vendored
Executable 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
|
||||
}
|
||||
}
|
||||
68
third-party/SwiftMath/Sources/MathBundle/MTFontV2.swift
vendored
Executable file
68
third-party/SwiftMath/Sources/MathBundle/MTFontV2.swift
vendored
Executable 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)
|
||||
}
|
||||
}
|
||||
225
third-party/SwiftMath/Sources/MathBundle/MathFont.swift
vendored
Normal file
225
third-party/SwiftMath/Sources/MathBundle/MathFont.swift
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
123
third-party/SwiftMath/Sources/MathBundle/MathImage.swift
vendored
Executable file
123
third-party/SwiftMath/Sources/MathBundle/MathImage.swift
vendored
Executable 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))
|
||||
}
|
||||
}
|
||||
35
third-party/SwiftMath/Sources/MathRender/MTBezierPath.swift
vendored
Executable file
35
third-party/SwiftMath/Sources/MathRender/MTBezierPath.swift
vendored
Executable 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
|
||||
|
||||
28
third-party/SwiftMath/Sources/MathRender/MTColor.swift
vendored
Executable file
28
third-party/SwiftMath/Sources/MathRender/MTColor.swift
vendored
Executable 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)
|
||||
}
|
||||
|
||||
}
|
||||
40
third-party/SwiftMath/Sources/MathRender/MTConfig.swift
vendored
Executable file
40
third-party/SwiftMath/Sources/MathRender/MTConfig.swift
vendored
Executable 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
|
||||
81
third-party/SwiftMath/Sources/MathRender/MTFont.swift
vendored
Normal file
81
third-party/SwiftMath/Sources/MathRender/MTFont.swift
vendored
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
93
third-party/SwiftMath/Sources/MathRender/MTFontManager.swift
vendored
Executable file
93
third-party/SwiftMath/Sources/MathRender/MTFontManager.swift
vendored
Executable 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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
355
third-party/SwiftMath/Sources/MathRender/MTFontMathTable.swift
vendored
Executable file
355
third-party/SwiftMath/Sources/MathRender/MTFontMathTable.swift
vendored
Executable 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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
37
third-party/SwiftMath/Sources/MathRender/MTLabel.swift
vendored
Executable file
37
third-party/SwiftMath/Sources/MathRender/MTLabel.swift
vendored
Executable 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
|
||||
1141
third-party/SwiftMath/Sources/MathRender/MTMathAtomFactory.swift
vendored
Normal file
1141
third-party/SwiftMath/Sources/MathRender/MTMathAtomFactory.swift
vendored
Normal file
File diff suppressed because it is too large
Load diff
117
third-party/SwiftMath/Sources/MathRender/MTMathImage.swift
vendored
Executable file
117
third-party/SwiftMath/Sources/MathRender/MTMathImage.swift
vendored
Executable 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
|
||||
}
|
||||
}
|
||||
1058
third-party/SwiftMath/Sources/MathRender/MTMathList.swift
vendored
Normal file
1058
third-party/SwiftMath/Sources/MathRender/MTMathList.swift
vendored
Normal file
File diff suppressed because it is too large
Load diff
1613
third-party/SwiftMath/Sources/MathRender/MTMathListBuilder.swift
vendored
Normal file
1613
third-party/SwiftMath/Sources/MathRender/MTMathListBuilder.swift
vendored
Normal file
File diff suppressed because it is too large
Load diff
881
third-party/SwiftMath/Sources/MathRender/MTMathListDisplay.swift
vendored
Executable file
881
third-party/SwiftMath/Sources/MathRender/MTMathListDisplay.swift
vendored
Executable 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();
|
||||
}
|
||||
|
||||
}
|
||||
197
third-party/SwiftMath/Sources/MathRender/MTMathListIndex.swift
vendored
Executable file
197
third-party/SwiftMath/Sources/MathRender/MTMathListIndex.swift
vendored
Executable 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
|
||||
}
|
||||
}
|
||||
}
|
||||
83
third-party/SwiftMath/Sources/MathRender/MTMathRenderer.swift
vendored
Normal file
83
third-party/SwiftMath/Sources/MathRender/MTMathRenderer.swift
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
587
third-party/SwiftMath/Sources/MathRender/MTMathUILabel.swift
vendored
Normal file
587
third-party/SwiftMath/Sources/MathRender/MTMathUILabel.swift
vendored
Normal 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
|
||||
|
||||
}
|
||||
68
third-party/SwiftMath/Sources/MathRender/MTTypesetter+Tokenization.swift
vendored
Normal file
68
third-party/SwiftMath/Sources/MathRender/MTTypesetter+Tokenization.swift
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
1927
third-party/SwiftMath/Sources/MathRender/MTTypesetter.swift
vendored
Normal file
1927
third-party/SwiftMath/Sources/MathRender/MTTypesetter.swift
vendored
Normal file
File diff suppressed because it is too large
Load diff
89
third-party/SwiftMath/Sources/MathRender/MTUnicode.swift
vendored
Executable file
89
third-party/SwiftMath/Sources/MathRender/MTUnicode.swift
vendored
Executable 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 }
|
||||
}
|
||||
58
third-party/SwiftMath/Sources/MathRender/RWLock.swift
vendored
Normal file
58
third-party/SwiftMath/Sources/MathRender/RWLock.swift
vendored
Normal 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()
|
||||
}
|
||||
|
||||
1145
third-party/SwiftMath/Sources/MathRender/Tokenization/MTAtomTokenizer.swift
vendored
Normal file
1145
third-party/SwiftMath/Sources/MathRender/Tokenization/MTAtomTokenizer.swift
vendored
Normal file
File diff suppressed because it is too large
Load diff
115
third-party/SwiftMath/Sources/MathRender/Tokenization/MTBreakableElement.swift
vendored
Normal file
115
third-party/SwiftMath/Sources/MathRender/Tokenization/MTBreakableElement.swift
vendored
Normal 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
|
||||
}
|
||||
389
third-party/SwiftMath/Sources/MathRender/Tokenization/MTDisplayGenerator.swift
vendored
Normal file
389
third-party/SwiftMath/Sources/MathRender/Tokenization/MTDisplayGenerator.swift
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
88
third-party/SwiftMath/Sources/MathRender/Tokenization/MTDisplayPreRenderer.swift
vendored
Normal file
88
third-party/SwiftMath/Sources/MathRender/Tokenization/MTDisplayPreRenderer.swift
vendored
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
165
third-party/SwiftMath/Sources/MathRender/Tokenization/MTElementWidthCalculator.swift
vendored
Normal file
165
third-party/SwiftMath/Sources/MathRender/Tokenization/MTElementWidthCalculator.swift
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
266
third-party/SwiftMath/Sources/MathRender/Tokenization/MTLineFitter.swift
vendored
Normal file
266
third-party/SwiftMath/Sources/MathRender/Tokenization/MTLineFitter.swift
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/Asana-Math.otf
vendored
Executable file
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/Asana-Math.otf
vendored
Executable file
Binary file not shown.
4343
third-party/SwiftMath/Sources/mathFonts.bundle/Asana-Math.plist
vendored
Executable file
4343
third-party/SwiftMath/Sources/mathFonts.bundle/Asana-Math.plist
vendored
Executable file
File diff suppressed because it is too large
Load diff
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/Euler-Math.otf
vendored
Executable file
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/Euler-Math.otf
vendored
Executable file
Binary file not shown.
2165
third-party/SwiftMath/Sources/mathFonts.bundle/Euler-Math.plist
vendored
Executable file
2165
third-party/SwiftMath/Sources/mathFonts.bundle/Euler-Math.plist
vendored
Executable file
File diff suppressed because it is too large
Load diff
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/FiraMath-Regular.otf
vendored
Executable file
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/FiraMath-Regular.otf
vendored
Executable file
Binary file not shown.
3064
third-party/SwiftMath/Sources/mathFonts.bundle/FiraMath-Regular.plist
vendored
Executable file
3064
third-party/SwiftMath/Sources/mathFonts.bundle/FiraMath-Regular.plist
vendored
Executable file
File diff suppressed because it is too large
Load diff
30
third-party/SwiftMath/Sources/mathFonts.bundle/GUST-FONT-LICENSE.txt
vendored
Executable file
30
third-party/SwiftMath/Sources/mathFonts.bundle/GUST-FONT-LICENSE.txt
vendored
Executable 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.
|
||||
|
||||
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/Garamond-Math.otf
vendored
Executable file
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/Garamond-Math.otf
vendored
Executable file
Binary file not shown.
7910
third-party/SwiftMath/Sources/mathFonts.bundle/Garamond-Math.plist
vendored
Executable file
7910
third-party/SwiftMath/Sources/mathFonts.bundle/Garamond-Math.plist
vendored
Executable file
File diff suppressed because it is too large
Load diff
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/KpMath-Light.otf
vendored
Executable file
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/KpMath-Light.otf
vendored
Executable file
Binary file not shown.
6501
third-party/SwiftMath/Sources/mathFonts.bundle/KpMath-Light.plist
vendored
Executable file
6501
third-party/SwiftMath/Sources/mathFonts.bundle/KpMath-Light.plist
vendored
Executable file
File diff suppressed because it is too large
Load diff
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/KpMath-Sans.otf
vendored
Executable file
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/KpMath-Sans.otf
vendored
Executable file
Binary file not shown.
4539
third-party/SwiftMath/Sources/mathFonts.bundle/KpMath-Sans.plist
vendored
Executable file
4539
third-party/SwiftMath/Sources/mathFonts.bundle/KpMath-Sans.plist
vendored
Executable file
File diff suppressed because it is too large
Load diff
21
third-party/SwiftMath/Sources/mathFonts.bundle/LICENSE
vendored
Executable file
21
third-party/SwiftMath/Sources/mathFonts.bundle/LICENSE
vendored
Executable 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.
|
||||
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/LeteSansMath.otf
vendored
Executable file
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/LeteSansMath.otf
vendored
Executable file
Binary file not shown.
5393
third-party/SwiftMath/Sources/mathFonts.bundle/LeteSansMath.plist
vendored
Executable file
5393
third-party/SwiftMath/Sources/mathFonts.bundle/LeteSansMath.plist
vendored
Executable file
File diff suppressed because it is too large
Load diff
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/LibertinusMath-Regular.otf
vendored
Executable file
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/LibertinusMath-Regular.otf
vendored
Executable file
Binary file not shown.
4276
third-party/SwiftMath/Sources/mathFonts.bundle/LibertinusMath-Regular.plist
vendored
Executable file
4276
third-party/SwiftMath/Sources/mathFonts.bundle/LibertinusMath-Regular.plist
vendored
Executable file
File diff suppressed because it is too large
Load diff
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/NotoSansMath-Regular.otf
vendored
Executable file
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/NotoSansMath-Regular.otf
vendored
Executable file
Binary file not shown.
9356
third-party/SwiftMath/Sources/mathFonts.bundle/NotoSansMath-Regular.plist
vendored
Executable file
9356
third-party/SwiftMath/Sources/mathFonts.bundle/NotoSansMath-Regular.plist
vendored
Executable file
File diff suppressed because it is too large
Load diff
103
third-party/SwiftMath/Sources/mathFonts.bundle/OFL.txt
vendored
Executable file
103
third-party/SwiftMath/Sources/mathFonts.bundle/OFL.txt
vendored
Executable 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.
|
||||
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/latinmodern-math.otf
vendored
Executable file
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/latinmodern-math.otf
vendored
Executable file
Binary file not shown.
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/latinmodern-math.plist
vendored
Executable file
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/latinmodern-math.plist
vendored
Executable file
Binary file not shown.
205
third-party/SwiftMath/Sources/mathFonts.bundle/math_table_to_plist.py
vendored
Executable file
205
third-party/SwiftMath/Sources/mathFonts.bundle/math_table_to_plist.py
vendored
Executable 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()
|
||||
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/texgyretermes-math.otf
vendored
Executable file
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/texgyretermes-math.otf
vendored
Executable file
Binary file not shown.
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/texgyretermes-math.plist
vendored
Executable file
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/texgyretermes-math.plist
vendored
Executable file
Binary file not shown.
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/xits-math.otf
vendored
Executable file
BIN
third-party/SwiftMath/Sources/mathFonts.bundle/xits-math.otf
vendored
Executable file
Binary file not shown.
6599
third-party/SwiftMath/Sources/mathFonts.bundle/xits-math.plist
vendored
Executable file
6599
third-party/SwiftMath/Sources/mathFonts.bundle/xits-math.plist
vendored
Executable file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue