Profile
Back to NewsBack
GitHub Trending 8 min
Reader Mode
PhraseHQ/SwaTex: 99.9% KaTeX syntax coverage. LaTeX math renderer in pure Swift. No JavaScript, no WebView, no DOM. One Swift core → iOS, macOS, tvOS, watchOS, visionOS. SwiftUI · UIKit/AppKit · SVG · PNG. 3.3× the Rust reference engine. Zero re-rast

PhraseHQ/SwaTex: 99.9% KaTeX syntax coverage. LaTeX math renderer in pure Swift. No JavaScript, no WebView, no DOM. One Swift core → iOS, macOS, tvOS, watchOS, visionOS. SwiftUI · UIKit/AppKit · SVG · PNG. 3.3× the Rust reference engine. Zero re-rast

20 hours ago

SwaTex

KaTeX-compatible math rendering engine in pure Swift — no JavaScript, no WebView, no DOM.

CI status Swift 6.1+ iOS, macOS, tvOS, watchOS, visionOS MIT license Made by Phrase

SwaTex is a ground-up Swift port of RaTeX, the Rust KaTeX-compatible math renderer, built and maintained by the team behind Phrase — it renders every formula in Phrase's AI notes on iOS and macOS. One Swift core, one display list, native rendering on every Apple platform:

\frac{-b \pm \sqrt{b^2-4ac}}{2a}   →   iOS · macOS · tvOS · watchOS · visionOS · SVG · PNG
Light Dark — adaptive colors built in
SwaTex rendering inline math with baseline alignment, a Fourier transform, a 3D rotation matrix, the Schrödinger equation, and mhchem chemistry — light mode The same SwaTex showcase rendered in dark mode via dynamic colors

Generated straight from the library (SwiftUI MathView, dynamic .mathColor(.primary)) by Tests/SwaTexRenderTests/ScreenshotGenerator.swift — regenerate with SWATEX_SCREENSHOTS=.github/assets swift test --filter Screenshot.

Why

Every major cross-platform math renderer runs LaTeX through a browser or JavaScript engine — a hidden WebView eating 50–150 MB RAM, startup latency before the first formula, no offline guarantee. SwaTex is the same KaTeX-compatible pipeline as a native Swift library:

  • Pure Swift 6 — value types, Sendable throughout, typed throws, Synchronization.Mutex. No Objective-C, no C dependencies.
  • KaTeX-metric-exact layout — the layout engine drives TeX's box-and-glue rules from the bundled KaTeX font metrics, not from platform text APIs, so output matches KaTeX (and RaTeX) glyph-for-glyph.
  • Flat display list — layout produces a renderer-agnostic DisplayList (glyphs, lines, rects, paths) whose JSON wire format is byte-compatible with RaTeX's, enabling cross-engine differential testing.
  • Native backends — CoreText/CoreGraphics rendering with the bundled KaTeX fonts (no font registration side effects), SwiftUI MathView, PNG export via ImageIO, and an SVG string backend.

What it renders

Math — 1007 of 1008 entries in the official KaTeX support table; the only exception is \includegraphics, which KaTeX itself gates behind trust. (Four \html* commands render their content but drop DOM-only attributes — there is no DOM in a native renderer.) Fractions, radicals, integrals, matrices, environments (pmatrix, align, cases, CD, …), stretchy delimiters, accents, macros (\def, \newcommand, \expandafter), and more.

Chemistry — full mhchem support via \ce and \pu (state-machine port verified byte-identical to RaTeX on a 253-case corpus):

\ce{H2SO4 + 2NaOH -> Na2SO4 + 2H2O}

Proof trees — bussproofs-style prooftree for inference rules and sequent calculi.

Installation

Swift Package Manager. In Package.swift:

dependencies: [
    .package(url: "https://github.com/PhraseHQ/SwaTex.git", from: "0.5.0")
]

Then add the product you need to your target — SwaTexRender for apps (SwiftUI/UIKit/AppKit views, PNG; pulls in the core), or SwaTex alone for the engine + SVG with zero platform dependencies:

.target(
    name: "YourApp",
    dependencies: [
        .product(name: "SwaTexRender", package: "SwaTex")
    ]
)

Or in Xcode: File → Add Package Dependencies… and paste https://github.com/PhraseHQ/SwaTex.git.

Requires Xcode 16.3+ to build; deploys to iOS 18 / macOS 15 / tvOS 18 / watchOS 11 / visionOS 2.

Usage

import SwaTexRender  // SwiftUI + UIKit/AppKit + CoreGraphics (Apple platforms)

// SwiftUI MathView(#"\sum_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6}"#) .font(size: 24)

// UIKit / AppKit — built for block editors let view = SwaTexView() view.latex = #"\frac{a}{b}"# view.fontSize = 18 view.mathStyle = .text // inline let baseline = view.baselineFromTop // align with surrounding text runs

// PNG let png = try ImageRenderer.png(latex: #"\frac{a}{b}"#)

SwaTexView is editor-grade by construction: parse+layout is memoized through a shared formula cache (~100 ns for repeated formulas), property sets are lazily coalesced (configuring a recycled cell costs one layout), and on macOS the view renders via updateLayer — it owns its rasterized bitmap, so scrolling and window redisplays never re-rasterize glyphs; rasterization happens only when content, backing scale, or orientation actually change (CI-asserted through real display cycles).

import SwaTex  // platform-independent core

// Parse → layout → display list (em units) let list = try SwaTexEngine.displayList(for: #"x^2 + y^2 = z^2"#)

// SVG string let svg = SVGRenderer().render(list)

Architecture

LaTeX string
   │  Lexer            (KaTeX-compatible tokens, catcodes, \verb)
   │  MacroExpander    (builtin macro table, \def/\newcommand, mhchem \ce/\pu)
   │  Parser           (KaTeX ParseNode AST; functions + environments registries)
   │  Layout engine    (TeX box/glue rules from KaTeX font metrics → LayoutBox)
   ▼  toDisplayList    (flat, absolute-coordinate DisplayList in em units)
DisplayList ──► CoreGraphics/CoreText (SwaTexRender) ──► CGImage / PNG / SwiftUI
            └─► SVG string (core)

| Target | Contents | Dependencies | |---|---|---| | SwaTex | lexer, parser, mhchem, layout engine, SVG backend | none | | SwaTexRender | CoreText/CG renderer, bundled KaTeX fonts, MathView, SwaTexView | SwaTex | | LayoutDump | differential-testing CLI (mirrors RaTeX's ratex-layout binary) | SwaTex |

Performance

Measured against the Rust reference engine on identical corpora, same machine, median of 3 runs. Every one of the 2 541 golden formulas is bit-exact between the engines except the deliberate KaTeX-correct divergences listed under Known limitations, so both engines are doing the same work. Every number is reproducible with the commands in BENCHMARKS.md:

| Workload | RaTeX (Rust) | SwaTex | |---|---|---| | parse → layout → display list | 41.4 µs/formula | 12.5 µs/formula (3.3×) | | PNG batch, 100 formulas @40px 2× | ~200 µs/formula | 34 µs effective (parallel, 5.9×) | | repeated formula (editor cache hit) | 41.4 µs | 105 ns (~390×) | | editor scroll | — | 0 glyph rasterization (updateLayer) |

Requirements

  • Builds with Swift 6.1+ (Xcode 16.3+); consuming apps can use any Swift language mode
  • Deploys to iOS 18 / macOS 15 / tvOS 18 / watchOS 11 / visionOS 2
(floor set by Synchronization.Mutex)

Known limitations

  • \includegraphics is not supported (the one missing entry of KaTeX's
1008; KaTeX itself gates it behind trust). The four \html* commands render their content but drop DOM-only attributes.
  • Deployment floors are iOS 18 / macOS 15 / tvOS 18 / watchOS 11 /
visionOS 2 (Synchronization.Mutex). Apps targeting older OS versions cannot adopt SwaTex yet.
  • Deliberate, documented divergences favor KaTeX over other engines:
size units accept ASCII digits only (KaTeX's JS \d; Rust/Swift regex \d also accept Unicode digits), and tokenization steps by Unicode scalar, so combining marks on syntax characters behave like the JS/Rust references rather than Swift's grapheme clusters. Additional KaTeX-correct divergences from RaTeX (each pinned by a regression test): unterminated \verb and x\limits^2 are parse errors instead of being silently accepted; \vcenter physically centers ink on the math axis; \gdef/\xdef survive group exit; zero-arg delimited \def macros validate and consume their parameter text; \middle nested deeper than 8 \left…\right stretch scopes renders at natural (unstretched) size; align-family environments space relations with the ordinary thickmuskip, put no gap inside a column pair and one \quad between aligned pairs (RaTeX caps relation spacing at 3 mu and, outside alignat, adds a 3 mu column gap, so a &= b came out 6 mu / 3 mu); column gaps come from each column's own pregap/postgap falling back to \arraycolsep, so a cell beyond the environment's column spec — a third cell in a cases row — is one \arraycolsep away rather than a full environment gap; consecutive |/: in a column spec each render a \doublerulesep apart (\begin{array}{l||l} drew one rule), and a trailing rule past a short row renders instead of being dropped; and \begin{alignat}{N} rejects an N that is not a decimal integer instead of silently laying the rows out as aligned ({x} and {-1} are parse errors in KaTeX too, while {2.5}, which KaTeX reads as five columns, is rejected rather than given a meaning; {} and {0} keep KaTeX's own fallback to aligned); and a \tag renders upright, in text mode, the way KaTeX's \tag@paren expansion does, while an automatic equation number past 9 draws every digit. The pinned reference revision (see “Provenance” in BENCHMARKS.md) renders a tag in math italic and draws only a number's first digit, wraps an overflowing \char literal onto a valid code point where SwaTex rejects it, frames \fbox in black rather than the formula color, and lends a multi-glyph base's last italic correction to its subscript (where SwaTex takes one only from a symbol base, as KaTeX does, keeping it for \oiint / \oiiint); RaTeX fixed all six upstream after our fixtures were cut.
  • \begin{alignat}{N} bounds N at 256 (a deliberate divergence: KaTeX
and RaTeX leave it unbounded, where a hostile count hangs the process). Deeply nested input is guarded the same way (recursion count + stack headroom → parse error, never a crash).
  • Zero-redraw scrolling on macOS requires a layer-backed host hierarchy
(wantsLayer — the default in modern AppKit apps).
  • Native Apple platforms only: no React Native / Flutter / Web wrappers.

Demo app

swift run SwaTexDemo   # interactive gallery: formulas, styles, sizes (macOS)

Testing

swift test

The suite ports RaTeX's Rust tests: lexer/token, symbol/metrics tables, parser spec (KaTeX katex-spec.ts–derived), environments, mhchem, layout primitives, KaTeX SVG geometry, and renderer smoke tests. Cross-engine layout parity is checked with scripts/diff_layout.py, which diffs LayoutDump against RaTeX's ratex-layout binary over the 1000+-formula golden corpus. It reads Tests/SwaTexTests/Resources/golden/ratex_divergences.json — the deliberate KaTeX-correct divergences, with a reason each — so it reports a regression apart from an expected difference, and exits non-zero only on the former.

Who's behind SwaTex

Phrase — Turn Recordings & PDFs into AI Notes (app icon)

Phrase — Turn Recordings & PDFs into AI Notes

Capture anything. Turn it into notes you can trust.
Phrase turns recordings, PDFs, images, and videos into source-backed AI notes,
slide decks, study tools, and dashboards — Apple-native, for notes that need proof.
SwaTex is the engine that renders every math formula in Phrase.

Download Phrase on the App Store   Phrase website   Follow Phrase on X

Contributing

See CONTRIBUTING.md — the short version: keep the 2 541 golden fixtures green, pin parser fast paths with differential tests, run the formatter, and bring numbers for performance claims. Security reports: SECURITY.md.

Fonts & licenses

The bundled KaTeX fonts (Sources/SwaTexRender/Resources/Fonts) are from the KaTeX project, licensed under the SIL Open Font License 1.1 (see OFL.txt / FONT_NOTICE.txt alongside the fonts).

Chat with me