Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ Breaks equations **between atoms** (mathematical elements) when content exceeds
##### 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:
Expand Down Expand Up @@ -365,10 +367,12 @@ label.latex = "a + \\color{red}{b} + c"
// Colored portion causes line break
```

**⚠️ Math accents:**
**⚠️ Math accents (partial support):**
```swift
label.latex = "\\hat{x} + \\tilde{y} + \\bar{z}"
// Accents may cause line breaks
// Common accents (\hat, \tilde, \bar) are positioned correctly in most cases.
// Some complex grapheme clusters or font-specific metrics may still need additional polishing.
// See MULTILINE_IMPLEMENTATION_NOTES.md for details and known edge cases.
```

#### Best Practices
Expand Down Expand Up @@ -507,6 +511,25 @@ Equations without explicit delimiters continue to work as before, defaulting to
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:
Expand Down
21 changes: 19 additions & 2 deletions Sources/SwiftMath/MathRender/MTMathAtomFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,10 @@ public class MTMathAtomFactory {
"check" : "\u{030C}",
"vec" : "\u{20D7}",
"widehat" : "\u{0302}",
"widetilde" : "\u{0303}"
"widetilde" : "\u{0303}",
"overleftarrow" : "\u{20D6}", // Combining left arrow above
"overrightarrow" : "\u{20D7}", // Combining right arrow above (same as vec)
"overleftrightarrow" : "\u{20E1}" // Combining left right arrow above
]

private static let accentValueLock = NSLock()
Expand Down Expand Up @@ -525,6 +528,7 @@ public class MTMathAtomFactory {
"mathbfit": .boldItalic,
"bm": .boldItalic,
"text": .roman,
"operatorname": .roman,
]

public static func fontStyleWithName(_ fontName:String) -> MTFontStyle? {
Expand Down Expand Up @@ -725,7 +729,20 @@ public class MTMathAtomFactory {
*/
public static func accent(withName name: String) -> MTAccent? {
if let accentValue = accents[name] {
return MTAccent(value: accentValue)
let accent = MTAccent(value: accentValue)
// Mark stretchy arrow accents (\overleftarrow, \overrightarrow, \overleftrightarrow)
// These should stretch to match content width
// \vec is NOT stretchy - it should use a small fixed-size arrow
let stretchyAccents: Set<String> = ["overleftarrow", "overrightarrow", "overleftrightarrow"]
accent.isStretchy = stretchyAccents.contains(name)

// Mark wide accents (\widehat, \widetilde, \widecheck)
// These should stretch horizontally to cover content width
// \hat, \tilde, \check are NOT wide - they use fixed-size accents
let wideAccents: Set<String> = ["widehat", "widetilde", "widecheck"]
accent.isWide = wideAccents.contains(name)

return accent
}
return nil
}
Expand Down
16 changes: 13 additions & 3 deletions Sources/SwiftMath/MathRender/MTMathList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -565,19 +565,29 @@ public class MTUnderLine: MTMathAtom {

public class MTAccent: MTMathAtom {
public var innerList: MTMathList?

/// Indicates if this accent should use stretchy arrow behavior (for \overrightarrow, etc.)
/// vs short accent behavior (for \vec). Only applies to arrow accents.
public var isStretchy: Bool = false
/// Indicates if this accent should use wide stretching behavior (for \widehat, \widetilde)
/// vs regular fixed-size accent behavior (for \hat, \tilde).
public var isWide: Bool = false

override public var finalized: MTMathAtom {
let newAccent = super.finalized as! MTAccent
newAccent.innerList = newAccent.innerList?.finalized
newAccent.isStretchy = self.isStretchy
newAccent.isWide = self.isWide
return newAccent
}

init(_ accent: MTAccent?) {
super.init(accent)
self.type = .accent
self.innerList = MTMathList(accent?.innerList)
self.isStretchy = accent?.isStretchy ?? false
self.isWide = accent?.isWide ?? false
}

init(value: String) {
super.init()
self.type = .accent
Expand Down
49 changes: 49 additions & 0 deletions Sources/SwiftMath/MathRender/MTMathListBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ public struct MTMathListBuilder {
case display
/// Inline/text style - compact operators, limits to the side (e.g., $...$, \(...\))
case inline

/// Convert MathMode to MTLineStyle for rendering
func toLineStyle() -> MTLineStyle {
switch self {
case .display:
return .display
case .inline:
return .text
}
}
}

var string: String
Expand Down Expand Up @@ -306,6 +316,45 @@ public struct MTMathListBuilder {
}
return output
}

/** Construct a math list from a given string and return the detected style.
This method detects LaTeX delimiters like \[...\], $$...$$, $...$, \(...\)
and returns the appropriate rendering style (.display or .text).

If there is a parse error, returns nil for the MathList.

- Parameter string: The LaTeX string to parse
- Returns: A tuple containing the parsed MathList and the detected MTLineStyle
*/
public static func buildWithStyle(fromString string: String) -> (mathList: MTMathList?, style: MTLineStyle) {
var builder = MTMathListBuilder(string: string)
let mathList = builder.build()
let style = builder.mathMode.toLineStyle()
return (mathList, style)
}

/** Construct a math list from a given string and return the detected style.
This method detects LaTeX delimiters like \[...\], $$...$$, $...$, \(...\)
and returns the appropriate rendering style (.display or .text).

If there is an error while constructing the string, this returns nil for the MathList.
The error is returned in the `error` parameter.

- Parameters:
- string: The LaTeX string to parse
- error: An inout parameter that will contain any parse error
- Returns: A tuple containing the parsed MathList and the detected MTLineStyle
*/
public static func buildWithStyle(fromString string: String, error: inout NSError?) -> (mathList: MTMathList?, style: MTLineStyle) {
var builder = MTMathListBuilder(string: string)
let output = builder.build()
let style = builder.mathMode.toLineStyle()
if builder.error != nil {
error = builder.error
return (nil, style)
}
return (output, style)
}

public mutating func buildInternal(_ oneCharOnly: Bool) -> MTMathList? {
self.buildInternal(oneCharOnly, stopChar: nil)
Expand Down
16 changes: 12 additions & 4 deletions Sources/SwiftMath/MathRender/MTMathListDisplay.swift
Original file line number Diff line number Diff line change
Expand Up @@ -503,10 +503,12 @@ class MTRadicalDisplay : MTDisplay {

/// 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
Expand All @@ -521,10 +523,16 @@ class MTGlyphDisplay : MTDisplayDS {
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
Expand Down
Loading