highlight.js-compatible syntax highlighting in pure Swift — no JavaScript engine, no WebView, no HTML round-trip.
HighlightKit is a ground-up Swift port of highlight.js 11.11.1, built and maintained by the team behind Phrase — it highlights every code block in Phrase's AI notes on iOS and macOS. 65 languages, token-exact output verified against the reference fixture corpus on every test run, and native NSAttributedString rendering on every Apple platform. See fidelity and differential-testing details.
| Light | Dark — adaptive themes built in |
|---|---|
![]() |
![]() |
Generated straight from the library by
Tests/HighlightKitTests/ScreenshotGenerator.swift — regenerate with
HIGHLIGHTKIT_SCREENSHOTS=.github/assets swift test --filter Screenshot.
The popular syntax highlighters for Apple platforms wrap highlight.js in JavaScriptCore, render HTML, and parse that HTML back into an attributed string — a JS context per highlighter, string round-trips on every call, and a failable initializer for when the bundle doesn't load. HighlightKit is the same battle-tested grammar semantics as a native library:
- Pure Swift 6.1 — strict concurrency on the runtime API,
Synchronizationprimitives, and zero package dependencies.Highlighter()cannot fail. - Tokens, not HTML — every token is an
NSRange(UTF-16,NSTextStorage-aligned) plus a scope name, so you can drive your own renderer or editor, or use the built-inNSAttributedStringpath. - Token-exact — 375 upstream-derived and adversarial fixtures, with expectations generated by the actual pinned highlight.js source, pin every release token for token.
- Typed themes — GitHub and Xcode styles in light, dark, and adaptive variants: one attributed string follows the system appearance without re-highlighting.
- Built for editors — continuation-threaded incremental highlighting (~40 µs per line on an M-series), and processor-bounded parallel automatic language detection across all 65 grammars.
Swift Package Manager. In Package.swift:
dependencies: [
.package(url: "https://github.com/PhraseHQ/HighlightKit.git", from: "0.1.0")
]Platforms: iOS 18+, macOS 15+, tvOS 18+, watchOS 11+, visionOS 2+. The core engine (parsing, tokens) uses Foundation and Synchronization; the rendering layer uses UIKit/AppKit colors and fonts.
One call for the common case:
import HighlightKit
label.attributedText = Highlighter.shared.attributedString(
for: code, language: "swift", theme: .xcode
)Pass no language to auto-detect. Themes: .github / .xcode (adaptive),
their Light/Dark variants, by-name lookup with
HighlightTheme.named("github-dark"), or build your own from typed
scope styles and a font:
let theme = HighlightTheme(
foregroundColor: .black,
backgroundColor: .white,
font: .init(name: "SF Mono", size: 13),
styles: [
.keyword: ScopeStyle(color: .systemPink, bold: true),
.string: ScopeStyle(color: .systemRed),
.comment: ScopeStyle(color: .gray, italic: true),
"my-custom-scope": ScopeStyle(color: .cyan), // open set
]
)HighlightScope is typed like Notification.Name — autocomplete and
typo safety for the ~50 standard scopes, string literals for custom ones,
and dot-path scopes (.titleFunction) resolve through structural
fallback.
let result = Highlighter.shared.highlight(code, as: "swift")
for token in result.tokens {
print(token.range, token.scope) // NSRange + "keyword", "title.function", …
}let result = await Highlighter.shared.highlightAuto(code) // processor-bounded parallel scan
print(result.language ?? "plain", result.relevance, result.secondBest?.language ?? "-")The async overload runs candidates through a processor-bounded child-task
window. When not cancelled, it returns the same ranked result as the
synchronous overload; after cancellation it ranks only candidates that
completed.
Highlight line by line, threading parser state, so edits only re-highlight what changed — multi-line strings and block comments carry across lines:
var continuation: Continuation?
for chunk in linesIncludingTerminators {
let result = Highlighter.shared.highlight(chunk, as: "swift", continuation: continuation)
render(result.tokens, for: chunk)
continuation = result.continuation
}After an edit, re-highlight forward until the new continuation equals the stored end state. A line-isolated regex cannot observe lookaround or boundaries across a chunk edge, so whole-block highlighting remains the exact path for constructs that depend on cross-line regex context. See the documented incremental limits.
let highlighter = Highlighter(languages: [.swift, .javascript]) // just what you need
highlighter.register(myLanguage) // or your own grammarGrammars compile lazily on first use and are cached per instance; the shared instance's cost scales with the languages you actually highlight.
Measured in a production release build on an Apple M3 Max (see BENCHMARKS.md for the dated environment, workloads, and reproduction protocol):
| Workload | Result |
|---|---|
| JavaScript throughput (62 KB, grammar-heavy) | 1.12 MU/s (≈ MB/s for ASCII) |
| Swift throughput (real Swift source) | 1.84 MU/s |
| SQL / JSON throughput | ~2 MU/s |
| Incremental JavaScript, per line (the editor path) | ~36 µs |
| Auto-detect across all 65 languages, warm | 3.3 ms |
NSAttributedString rendering |
~7.6 MU/s |
| Cold start (first highlight, incl. grammar compile) | ~4 ms |
The engine replaces highlight.js's combined-alternation scan with per-rule windowed match caching plus shape-specialized prefilters — roughly 10× the naive port. Every optimization was adopted or rejected on measurements, and differential test suites keep each hand-written fast path pinned to ICU semantics.
All 65 grammars ship in the package; Highlighter.shared.languageNames
lists them. Ada, Apache, AppleScript, ARM assembly, Bash, BASIC, C,
Clojure, CMake, CoffeeScript, C++, C#, CSS, Dart, Delphi, Diff,
Dockerfile, DOS, Elm, Erlang, Fortran, F#, Go, Groovy, Haskell, HTTP,
INI/TOML, Java, JavaScript, JSON, Kotlin, Leaf, Less, Lisp, LLVM IR, Lua,
Makefile, Markdown, MATLAB, Nginx, Nim, Nix, Objective-C, OCaml, Perl,
PHP, plaintext, PowerShell, Prolog, Properties, Python, R, Ruby, Rust,
Scala, Scheme, SCSS, Shell session, SQL, Stylus, Swift, TypeScript, Vim
script, XML/HTML, YAML.
swift test runs the unit, integration, stress, and 375-case token-exact
fidelity suites, including
differential and adversarial checks for specialized matcher paths,
incremental-state regressions, hostile-input handling, and memory-stability
stress. Run concurrency instrumentation separately after registry,
continuation, callback, or other shared-state changes:
swift test --sanitize=thread \
--filter 'LanguageRegistryTests|IncrementalTests|StressTests|CoreCoverageCompletionTests'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.
HighlightKit is the engine that highlights every code block in Phrase.
See CONTRIBUTING.md — the short version: keep the 375 token-exact fixtures green, pin matcher fast paths with differential tests, and bring numbers for performance claims. Security reports: SECURITY.md.
MIT — see LICENSE. HighlightKit derives from highlight.js (BSD 3-Clause); see THIRD_PARTY_NOTICES.md.
Built by Phrase.

