Contents

colinc86/LaTeXSwiftUI

A SwiftUI view that renders LaTeX.

πŸ†• What's New in v2

  • β™Ώ Accessibility β€” VoiceOver support via the Speech Rule Engine (SRE)
  • 🌐 Script Scaling β€” CJK and non-Latin script support with .script()
  • πŸ“ Line Spacing β€” Automatic normalization on iOS 18+ / macOS 15+
  • πŸ“Š Arrays & Tables β€” array, matrix, cases, and more with borders and rules
  • πŸ”² Redacted Placeholder β€” New redactedOriginal async rendering style
  • πŸ“ Dynamic Type β€” Equations scale with system text size settings
  • πŸ–ΌοΈ Render to Image β€” Render LaTeX to UIImage/NSImage without a SwiftUI view
  • 🧩 Generic Environments β€” \begin{align}, \begin{cases}, etc. work standalone

πŸ“– Contents

- πŸ”€ Fonts - πŸ”§ Parsing & Input - 🎨 Visual & Layout - β™Ώ Accessibility - πŸ–ΌοΈ Rendering to Images - ⚑ Performance & Caching

ℹ️ About

LaTeXSwiftUI is a package that exposes a LaTeX view capable of parsing and rendering TeX and LaTeX equations containing math-mode macros. It uses the MathJaxSwift package to render equations with MathJax, so the view's capabilities are influenced by MathJax's supported features.

The view renders math-mode equations (inline and block), \text{} within equations, numbered block equations, any \begin{...}...\end{...} environment (including align, gather, cases, array, and more), and provides VoiceOver accessibility via the Speech Rule Engine. It scales equations for non-Latin scripts and normalizes line spacing on iOS 18+ / macOS 15+. You can also render equations directly to UIImage/NSImage without a SwiftUI view. Equations automatically scale with Dynamic Type accessibility settings. It does not render full LaTeX documents or text-mode macros.

Requires Swift 6.0. Supports iOS 15+, macOS 12+, and visionOS 1+. All rendering is performed off the main thread.

πŸ“¦ Installation

Add the dependency to your package manifest file.

.package(url: "https://github.com/colinc86/LaTeXSwiftUI", from: "2.0.0")

πŸš€ Quick Start

import LaTeXSwiftUI

struct MyView: View {

  var body: some View {
    LaTeX("Hello, $\\LaTeX$!")
  }

}

<img src="./assets/images/hello.png" width="85" height="21.5">

That's it! The LaTeX view's body is built from Text views, so standard SwiftUI modifiers work out of the box.

LaTeX("Hello, $\\LaTeX$!")
  .fontDesign(.serif)
  .foregroundColor(.blue)

<img src="./assets/images/hello_blue.png" width="87" height="21.5">

⌨️ Usage

### πŸ”€ Fonts

The view measures the current font's x-height to correctly size rendered equations. It converts SwiftUI's `Font` to `UIFont`/`NSFont` internally, which currently works with SwiftUI's preferred fonts (`largeTitle`, `title`, `headline`, `caption`, etc.) and platform font types passed directly.

```swift
// SwiftUI preferred fonts
LaTeX("Hello, $\\LaTeX$!")
  .font(.title)

// UIFont/NSFont passed directly
LaTeX("Hello, $\\LaTeX$!")
  .font(UIFont.systemFont(ofSize: 30))

LaTeX("Hello, $\\LaTeX$!")
  .font(UIFont(name: "Avenir", size: 25)!)
```

> ⚠️ Custom SwiftUI fonts (`.custom(name:size:)`, `.system(size:)`) and `UIFont`/`NSFont` wrapped in `Font()` will not size equations correctly. Use preferred fonts or pass platform font types directly.

### πŸ”§ Parsing & Input

#### Parsing Mode

The view can search for top-level equations delimited by the following terminators, or render the entire input as math.

| Terminators |
|-------------|
| `$...$` |
| `$$...$$` |
| `\(...\)` |
| `\[...\]` |
| `\begin{equation}...\end{equation}` |
| `\begin{equation*}...\end{equation*}` |
| `\begin{...}...\end{...}` |

##### Generic Environments

Any `\begin{name}...\end{name}` environment is automatically recognized as a block equation β€” including `align`, `gather`, `cases`, `array`, `matrix`, `pmatrix`, and more. There's no need to wrap them in `$$...$$`.

```swift
LaTeX("The function is \\begin{cases} x & \\text{if } x \\geq 0 \\\\ -x & \\text{if } x < 0 \\end{cases}")
```

```swift
// Only parse equations (default)
LaTeX("Euler's identity is $e^{i\\pi}+1=0$.")
  .parsingMode(.onlyEquations)

// Parse the entire input
LaTeX("\\text{Euler's identity is } e^{i\\pi}+1=0\\text{.}")
  .parsingMode(.all)
```

> <img src="./assets/images/euler.png" width="293" height="80">

#### Unencode HTML

Input may contain HTML entities such as `&lt;` which LaTeX won't parse. Use the `unencoded` modifier to decode them.

```swift
LaTeX("$x^2&lt;1$")
  .errorMode(.error)

// Replace "&lt;" with "<"
LaTeX("$x^2&lt;1$")
  .unencoded()
```

> <img src="./assets/images/unencoded.png" width="72.5" height="34">

#### String Formatting

The view renders the following markdown syntax by default.

| Syntax | Description |
|-------------|-------------|
| `*...*` | Italic |
| `**...**` | Bold |
| `***...***` | Bold & Italic |
| `~~...~~` | Strikethrough |
| `` `...` `` | Monospaced |
| `[...](...)` | Links |

The reserved LaTeX characters `&`, `%`, `$`, `#`, `_`, `{`, `}`, `~`, `^`, and `\` are also unescaped when preceded by a backslash. Use `ignoreStringFormatting()` to disable both markdown rendering and escape replacement.

```swift
LaTeX(input)
  .ignoreStringFormatting()
```

Use `processEscapes()` to allow `\$` for literal dollar signs and `\\` for literal backslashes within your input.

### 🎨 Visual & Layout

#### Image Rendering Mode

Equations can match the surrounding text style or display the original MathJax-rendered colors.

```swift
// Match surrounding text (default)
LaTeX("Hello, $\\color{red}\\LaTeX$!")
  .imageRenderingMode(.template)

// Display original rendered colors
LaTeX("Hello, ${\\color{red} \\LaTeX}$!")
  .imageRenderingMode(.original)
```

> <img src="./assets/images/rendering_mode.png" width="84.5" height="43">

#### Error Mode

Control how the view handles rendering errors.

> When `rendered` mode is used, MathJax loads the `noerrors` and `noundefined` packages. In the other modes, errors are either displayed or replaced with the original text.

```swift
LaTeX("$\\asdf$")
  .errorMode(.original)  // Show original text

LaTeX("$\\asdf$")
  .errorMode(.error)     // Show error message

LaTeX("$\\asdf$")
  .errorMode(.rendered)  // Show rendered image if available
```

> <img src="./assets/images/errors.png" width="199.5" height="55">

#### Block Rendering Mode

Block equations can be rendered centered on their own line (`blockViews`, the default), forced inline (`alwaysInline`), or as text with newlines (`blockText`). Block equations are placed in horizontal scroll views when they exceed the view width.

```swift
LaTeX("The quadratic formula is $$x=\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a}$$ and it has zeros at the roots of $f(x)=ax^2+bx+c$.")
  .blockMode(.blockViews)

Divider()

LaTeX("The quadratic formula is $$x=\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a}$$ and it has zeros at the roots of $f(x)=ax^2+bx+c$.")
  .blockMode(.alwaysInline)

Divider()

LaTeX("The quadratic formula is $$x=\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a}$$ and it has zeros at the roots of $f(x)=ax^2+bx+c$.")
  .blockMode(.blockText)
```

> <img src="./assets/images/blocks.png" width="430" height="350">

#### Numbered Block Equations

The view supports simple numbering of block equations when using `blockViews` mode.

| Modifier | Description |
|:---------|:------------|
| `.equationNumberMode(_:)` | Position: `.left`, `.right`, or `.none` (default) |
| `.equationNumberStart(_:)` | Starting number (default: `1`) |
| `.equationNumberOffset(_:)` | Left or right offset in points |
| `.formatEquationNumber(_:)` | Custom formatting closure `(Int) -> String` |

```swift
LaTeX("$$E = mc^2$$")
  .equationNumberMode(.right)
  .equationNumberOffset(10)
  .padding([.bottom])

LaTeX("$$E = mc^2$$ $$E = mc^2$$")
  .equationNumberMode(.right)
  .equationNumberOffset(10)
  .equationNumberStart(2)
  .formatEquationNumber { n in
    return "~[\(n)]~"
  }
```

> <img src="./assets/images/numbers.png" width="433" height="153">

#### Rendering Style

All rendering (MathJax conversion and SVG rasterization) is performed off the main thread. Choose a rendering style to control loading behavior.

| Style      | Async | Description                                                              |
|:-----------|:------|:-------------------------------------------------------------------------|
| `empty`    | Yes   | The view remains empty until rendering completes.                         |
| `original` | Yes   | The view displays the input text until rendering completes.               |
| `redactedOriginal` | Yes | The view displays a redacted placeholder until rendering completes. πŸ†• |
| `progress` | Yes   | The view displays a progress indicator until rendering completes.         |
| `wait`     | No    | *(default)* The view blocks until rendering completes.                    |

When using an asynchronous style, use `renderingAnimation` to animate the transition.

```swift
LaTeX(input)
  .renderingStyle(.original)
  .renderingAnimation(.easeIn)
```

> **Note:** The `LaTeX` view automatically re-renders when its input string changes, so you can bind it to `@State` variables without needing `.id()`:
> ```swift
> @State var text: String = ""
>
> var body: some View {
>     VStack {
>         TextField("LaTeX", text: $text)
>         LaTeX("$\(text)$")
>     }
> }
> ```

#### Script Mode

πŸ†• When displaying equations inline with non-Latin scripts such as Korean, Japanese, or Chinese, equations may appear undersized or misaligned. The `script` modifier adjusts equation scaling to match the surrounding text.

```swift
// Korean
LaTeX("방정식 $x^2 + y^2 = z^2$ 은 잘 μ•Œλ €μ Έ μžˆμŠ΅λ‹ˆλ‹€.")
  .script(.cjk)

// Japanese
LaTeX("方程式 $E = mc^2$ γ―ζœ‰εγ§γ™γ€‚")
  .script(.cjk)

// Custom scale factor
LaTeX("Scaled equation: $\\int_0^1 x^2 dx$")
  .script(.custom(1.3))
```

| Script | Description |
|:-------|:------------|
| `.latin` | *(default)* Uses the font's x-height. Suitable for Latin, Cyrillic, and similar scripts. |
| `.cjk` | Uses the font's cap-height. Suitable for Korean, Japanese, and Chinese. |
| `.custom(CGFloat)` | Multiplies the font's x-height by the given factor. |

#### Line Spacing

πŸ†• On iOS 18+ / macOS 15+, the view automatically normalizes line spacing when inline equations cause uneven line gaps. This uses a custom `TextRenderer` and requires no configuration.

#### Arrays & Tables

πŸ†• The view renders LaTeX array and table environments including `array`, `matrix`, `pmatrix`, `vmatrix`, and `cases`. Horizontal rules (`\hline`) and vertical column borders are supported.

```swift
LaTeX("$$\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}$$")

LaTeX("$$\\begin{cases} x & \\text{if } x \\geq 0 \\\\ -x & \\text{if } x < 0 \\end{cases}$$")
```

### β™Ώ Accessibility

πŸ†• Rendered equations are images that need accessibility labels for VoiceOver. By default, LaTeXSwiftUI uses MathJax's Speech Rule Engine (SRE) to generate natural language descriptions automatically.

```swift
// Default (.sre) β€” VoiceOver reads "x squared plus y squared equals z squared"
LaTeX("$x^2 + y^2 = z^2$")

// Use the raw TeX input as the label
LaTeX("$x^2 + y^2 = z^2$")
  .imageAccessibility(.input)

// No accessibility label
LaTeX("$x^2 + y^2 = z^2$")
  .imageAccessibility(.none)

// Custom label
LaTeX("$E = mc^2$")
  .imageAccessibility(.custom("Einstein's mass-energy equivalence"))
```

| Mode | Description |
|:-----|:------------|
| `.sre` | *(default)* Uses the Speech Rule Engine to generate natural language. Falls back to raw TeX on failure. |
| `.input` | Uses the raw TeX input as the accessibility label. |
| `.none` | No accessibility label (default SwiftUI behavior). |
| `.custom(String)` | Uses a custom string as the accessibility label. |

### πŸ–ΌοΈ Rendering to Images

πŸ†• You can render LaTeX equations directly to `UIImage` (iOS/visionOS) or `NSImage` (macOS) without using the `LaTeX` SwiftUI view. This is useful for UIKit integration, image export, or custom rendering pipelines.

```swift
// Render all equations to images
let images = LaTeX.renderToImages("$x^2 + y^2 = z^2$")

// With custom options
let images = LaTeX.renderToImages(
  "Euler's identity: $e^{i\\pi}+1=0$ and $\\int_0^1 x\\,dx$",
  displayScale: 3.0,
  processEscapes: true
)

// Each equation produces one image
for image in images {
  imageView.image = image
}
```

### ⚑ Performance & Caching

All rendering is performed off the main thread. The package caches both SVG data from MathJax and the rasterized images. You can control the caches directly.

```swift
// Clear the SVG data cache
LaTeX.dataCache.removeAllObjects()

// Clear the rendered image cache
LaTeX.imageCache.removeAllObjects()
```

#### Preloading

SVGs and images are rendered on demand, but you can preload them to minimize lag when the view appears. Call `preload` **last** in the modifier chain.

```swift
VStack {
  ForEach(expressions, id: \.self) { expression in
    LaTeX(expression)
      .font(.caption2)
      .foregroundColor(.green)
      .unencoded()
      .errorMode(.error)
      .processEscapes()
      .preload()
  }
}
```

Package Metadata

Repository: colinc86/LaTeXSwiftUI

Stars: 365

Forks: 67

Open issues: 5

Default branch: main

Primary language: swift

License: MIT

Topics: latex, mathematics, mathjax, swift, swiftui, tex

README: README.md