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
5 changes: 5 additions & 0 deletions .changeset/swift.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tanstack/highlight": minor
---

Add an isolated Swift language definition. Core and existing selective bundles are unchanged.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ Available themes: Aurora X, Dracula, GitHub Dark, GitHub Light, Gruvbox Dark, Gr

## Languages

`apache`, `cmake`, `cpp`, `csharp`, `css`, `dart`, `diff`, `dockerfile`, `ejs`, `env`, `go`, `html`, `http`, `java`, `js`, `json`, `jsx`, `kotlin`, `lua`, `markdown`, `mermaid`, `nginx`, `perl`, `php`, `plaintext`, `python`, `ruby`, `rust`, `scheme`, `shell`, `sql`, `svelte`, `toml`, `ts`, `tsrx`, `tsx`, `vue`, and `yaml`.
`apache`, `cmake`, `cpp`, `csharp`, `css`, `dart`, `diff`, `dockerfile`, `ejs`, `env`, `go`, `html`, `http`, `java`, `js`, `json`, `jsx`, `kotlin`, `lua`, `markdown`, `mermaid`, `nginx`, `perl`, `php`, `plaintext`, `python`, `ruby`, `rust`, `scheme`, `shell`, `sql`, `svelte`, `swift`, `toml`, `ts`, `tsrx`, `tsx`, `vue`, and `yaml`.

Each language is available from `@tanstack/highlight/languages/<name>`. The aggregate `@tanstack/highlight/languages` entry can tree-shake, while direct subpaths make isolation explicit. Importing only core helpers from the root entry also removes unused language registrations in a compatible bundler.

Expand All @@ -218,7 +218,7 @@ Local browser bundles, minified with esbuild and compressed independently. KB us
| Core + TSX | 10.11 KB | 4.26 KB | 3.90 KB |
| Octane MDX + TypeScript | 13.86 KB | 5.59 KB | 5.17 KB |
| Nine-language docs set | 16.04 KB | 6.20 KB | 5.66 KB |
| All 38 languages | 49.99 KB | 16.82 KB | 15.19 KB |
| All 39 languages | 52.74 KB | 17.73 KB | 15.93 KB |

The following comparison was measured before the 1.0 property-context correction. Re-run the comparison commands below for current timings.

Expand Down
6 changes: 3 additions & 3 deletions docs/guides/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ CI measures selective browser bundles and highlighting performance on real docum

## Bundle profiles

`pnpm run size` builds twenty-five browser profiles with esbuild and measures minified, gzip, and Brotli bytes independently. It also checks that helper, adapter, and selective language imports retain only the requested modules.
`pnpm run size` builds twenty-six browser profiles with esbuild and measures minified, gzip, and Brotli bytes independently. It also checks that helper, adapter, and selective language imports retain only the requested modules.

| Profile | Registered languages | Current gzip | CI budget |
| --- | --- | ---: | ---: |
| Core | None | 1.82 KB | 2.0 KB |
| TSX | TSX | 4.26 KB | 4.35 KB |
| Octane | TypeScript plus Octane MDX adapter | 5.59 KB | 5.7 KB |
| Docs | CSS, HTML, JS, JSON, JSX, Markdown, Shell, TS, TSX | 6.20 KB | 6.3 KB |
| All | All 38 definitions | 16.82 KB | 17.1 KB |
| All | All 39 definitions | 17.73 KB | 18.0 KB |

KB uses 1,000 bytes. Core helpers imported from the root tree-shake to the same engine size. The standalone theme helper is 695 gzip bytes.

Expand All @@ -26,7 +26,7 @@ Selective profiles are the primary metric. The all-language profile exists to pr

The committed corpus contains 334 real code fences sampled from TanStack documentation, with up to twenty samples per normalized language.

`pnpm run bench` measures tokenization, HTML, Markdown, HAST, line numbers, long decorated blocks, and dedicated C#, C++, CMake, Dart, Java, Kotlin, Lua, Perl, PHP, Ruby, and Rust samples. Each profile reports the median of three samples after two warmup passes, with a 1.2 second CI budget. The main highlighting profile processes at least 10,000 blocks.
`pnpm run bench` measures tokenization, HTML, Markdown, HAST, line numbers, long decorated blocks, and dedicated C#, C++, CMake, Dart, Java, Kotlin, Lua, Perl, PHP, Ruby, Rust, and Swift samples. Each profile reports the median of three samples after two warmup passes, with a 1.2 second CI budget. The main highlighting profile processes at least 10,000 blocks.

A local before-and-after review used the same minified bundle settings, fixtures, and benchmark harness on macOS arm64 with Node 24.15.0:

Expand Down
2 changes: 1 addition & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const highlighter = createHighlighter({
})
```

The root entry is useful for prototypes, server-only scripts, or sites where the roughly 17 KB gzip all-language build is acceptable:
The root entry is useful for prototypes, server-only scripts, or sites where the roughly 18 KB gzip all-language build is acceptable:

```ts
import { highlight } from '@tanstack/highlight'
Expand Down
1 change: 1 addition & 0 deletions docs/language-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Every language is an isolated definition imported from `@tanstack/highlight/lang
| Shell | `shell` | `bash`, `sh`, `zsh`, `cmd`, `console` | Heredocs, parameter expansion, comment boundaries |
| SQL | `sql` | - | Strings, comments, common SQL clauses |
| Svelte | `svelte` | - | Markup plus optional script/style and expression delegation |
| Swift | `swift` | - | Interpolated, multi-line, and raw strings with nested quotes, regex literals vs division, nested block comments, contextual keywords, attributes and directives |
| TOML | `toml` | - | Strings, comments, tables, properties |
| TypeScript | `ts` | `typescript`, `angular-ts` | JavaScript scanner plus TypeScript keywords/types |
| TSRX | `tsrx` | `octane` | TypeScript, contextual JSX, Octane component shorthand and template directives |
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/default-entry.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Returns the canonical registered language name for a name or alias. Names are tr
function listLanguages(): Array<HighlightLanguage>
```

Returns the 38 canonical language names registered in `defaultHighlighter`.
Returns the 39 canonical language names registered in `defaultHighlighter`.

### `tokenize`

Expand Down
3 changes: 2 additions & 1 deletion docs/reference/languages.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,14 @@ const highlighter = createHighlighter({
| `shell` | `@tanstack/highlight/languages/shell` | `bash`, `sh`, `zsh`, `cmd`, `console` |
| `sql` | `@tanstack/highlight/languages/sql` | None |
| `svelte` | `@tanstack/highlight/languages/svelte` | None |
| `swift` | `@tanstack/highlight/languages/swift` | None |
| `toml` | `@tanstack/highlight/languages/toml` | None |
| `ts` | `@tanstack/highlight/languages/ts` | `typescript`, `angular-ts` |
| `tsrx` | `@tanstack/highlight/languages/tsrx` | `octane` |
| `tsx` | `@tanstack/highlight/languages/tsx` | None |
| `vue` | `@tanstack/highlight/languages/vue` | None |
| `yaml` | `@tanstack/highlight/languages/yaml` | `yml` |

`@tanstack/highlight/languages` re-exports `apache`, `cmake`, `cpp`, `csharp`, `css`, `dart`, `diff`, `dockerfile`, `ejs`, `env`, `go`, `html`, `http`, `java`, `js`, `json`, `jsx`, `kotlin`, `lua`, `markdown`, `mermaid`, `nginx`, `perl`, `php`, `plaintext`, `python`, `ruby`, `rust`, `scheme`, `shell`, `sql`, `svelte`, `toml`, `ts`, `tsrx`, `tsx`, `vue`, and `yaml`. The barrel is convenient but individual subpaths make bundle intent explicit.
`@tanstack/highlight/languages` re-exports `apache`, `cmake`, `cpp`, `csharp`, `css`, `dart`, `diff`, `dockerfile`, `ejs`, `env`, `go`, `html`, `http`, `java`, `js`, `json`, `jsx`, `kotlin`, `lua`, `markdown`, `mermaid`, `nginx`, `perl`, `php`, `plaintext`, `python`, `ruby`, `rust`, `scheme`, `shell`, `sql`, `svelte`, `swift`, `toml`, `ts`, `tsrx`, `tsx`, `vue`, and `yaml`. The barrel is convenient but individual subpaths make bundle intent explicit.

See the [language support matrix](../language-support) for the context-aware behavior and current scope of each registration.
6 changes: 3 additions & 3 deletions docs/test-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,21 @@ The suite protects the package's actual product boundary: valid code commonly pu

## Size Profiles

`pnpm run size` checks twenty-five independent browser profiles, including root helpers, language barrel imports, adapters, and themes. Each has minified, gzip, and Brotli budgets. The main highlighter profiles are:
`pnpm run size` checks twenty-six independent browser profiles, including root helpers, language barrel imports, adapters, and themes. Each has minified, gzip, and Brotli budgets. The main highlighter profiles are:

| Profile | Languages | Gzip budget |
| --- | --- | ---: |
| Core | None | 2.0 KB |
| TSX | TSX | 4.35 KB |
| Octane | TypeScript plus Octane MDX adapter | 5.7 KB |
| Docs | CSS, HTML, JS, JSON, JSX, Markdown, Shell, TS, TSX | 6.3 KB |
| All | All 38 definitions | 17.1 KB |
| All | All 39 definitions | 18.0 KB |

The selective profiles are the primary product metric. The all-language profile protects the convenience entry from unbounded growth. Bundle graphs reject unexpected language or theme code. Package tests repeat isolation checks through public exports after building.

## Throughput

`pnpm run bench` measures highlighting, tokenization, Markdown, HAST, line numbers, long numbered blocks, long decorated blocks, and dedicated C#, C++, CMake, Dart, Java, Kotlin, Lua, Perl, PHP, Ruby, and Rust samples. Timings use the median of three samples after two warmup passes. Each profile has a 1.2 second CI budget; the main highlighting profile processes at least 10,000 blocks.
`pnpm run bench` measures highlighting, tokenization, Markdown, HAST, line numbers, long numbered blocks, long decorated blocks, and dedicated C#, C++, CMake, Dart, Java, Kotlin, Lua, Perl, PHP, Ruby, Rust, and Swift samples. Timings use the median of three samples after two warmup passes. Each profile has a 1.2 second CI budget; the main highlighting profile processes at least 10,000 blocks.

`pnpm run compare:sugar-high` compares the overlapping JS/TS/JSX/TSX use case. `pnpm run compare:shiki` compares all supported fixtures. These are directional measurements, not claims of equivalent grammar depth.

Expand Down
6 changes: 6 additions & 0 deletions scripts/bench.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ try {
observe: (result) => result.html.length,
targetBlocks: 10_000,
},
swift: {
fixtures: [{ rawLang: 'swift', code: '@available(iOS 15, *)\nlet greeting = "Hi \\(user.name) \\(dict["key"] ?? "none")"\nlet raw = #"C:\\path "quoted" \\#(name)"#\n/* outer /* nested */ comment */\n#if DEBUG\nlet n = 0x1F + 1_000 + 1.5e3\n#endif' }],
run: (fixture) => highlight(fixture.code, { lang: fixture.rawLang }),
observe: (result) => result.html.length,
targetBlocks: 10_000,
},
tokenize: {
fixtures,
run: (fixture) => tokenize(fixture.code, { lang: fixture.rawLang }),
Expand Down
1 change: 1 addition & 0 deletions scripts/language-utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const supportedLanguages = [
'shell',
'sql',
'svelte',
'swift',
'toml',
'ts',
'tsrx',
Expand Down
11 changes: 10 additions & 1 deletion scripts/measure-size.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,22 @@ const profiles = {
languages: ['rust'],
limits: { minified: 6_800, gzip: 3_200, brotli: 2_950 },
},
swift: {
source: `
import { createHighlighter } from './src/core.ts'
import { swift } from './src/languages/swift.ts'
globalThis.highlighter = createHighlighter({ languages: [swift] })
`,
languages: ['swift'],
limits: { minified: 7_450, gzip: 3_550, brotli: 3_250 },
},
all: {
source: `
import { defaultHighlighter } from './src/index.ts'
globalThis.highlighter = defaultHighlighter
`,
languages: 'all',
limits: { minified: 50_400, gzip: 17_100, brotli: 15_500 },
limits: { minified: 53_150, gzip: 18_000, brotli: 16_200 },
},
reactAdapter: {
source: `export * from './src/react.ts'`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Import only the definitions the application registers.
| Shell | `shell` | `@tanstack/highlight/languages/shell` | `bash`, `sh`, `zsh`, `cmd`, `console` |
| SQL | `sql` | `@tanstack/highlight/languages/sql` | - |
| Svelte | `svelte` | `@tanstack/highlight/languages/svelte` | - |
| Swift | `swift` | `@tanstack/highlight/languages/swift` | - |
| TOML | `toml` | `@tanstack/highlight/languages/toml` | - |
| TypeScript | `ts` | `@tanstack/highlight/languages/ts` | `typescript`, `angular-ts` |
| TSRX | `tsrx` | `@tanstack/highlight/languages/tsrx` | `octane` |
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { scheme } from './languages/scheme.js'
import { shell } from './languages/shell.js'
import { sql } from './languages/sql.js'
import { svelte } from './languages/svelte.js'
import { swift } from './languages/swift.js'
import { toml } from './languages/toml.js'
import { ts } from './languages/ts.js'
import { tsrx } from './languages/tsrx.js'
Expand Down Expand Up @@ -77,6 +78,7 @@ export type HighlightLanguage =
| 'shell'
| 'sql'
| 'svelte'
| 'swift'
| 'toml'
| 'ts'
| 'tsrx'
Expand Down Expand Up @@ -157,6 +159,7 @@ export const allLanguages = [
shell,
sql,
svelte,
swift,
toml,
ts,
tsrx,
Expand Down
1 change: 1 addition & 0 deletions src/languages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export { scheme } from './scheme.js'
export { shell } from './shell.js'
export { sql } from './sql.js'
export { svelte } from './svelte.js'
export { swift } from './swift.js'
export { toml } from './toml.js'
export { ts } from './ts.js'
export { tsrx } from './tsrx.js'
Expand Down
88 changes: 88 additions & 0 deletions src/languages/swift.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { defineLanguage, type TokenRange } from '../core.js'
import { collectPatternRanges } from '../internal/patterns.js'

const patterns = [
{ className: 'meta', regex: /^#!.*|#\w+/gm },
{ className: 'attr', regex: /@\w+/g },
{ className: 'variable', regex: /\$\w+/g },
// Argument and parameter labels; the lookbehind keeps ternaries and switch cases plain.
{ className: 'property', regex: /\b(?=[a-z]\w*:)(?<=[(,]\s*(?:\w+[ \t]+)?)\w+/g },
// Contextual words are keywords only before declarations, types, or as accessors.
{
className: 'keyword',
regex: /(?<!`)\b(?:(?<!\.)(?:associatedtype|as|async|await|break|case|catch|class|continue|convenience|default|defer|deinit|didSet|distributed|do|else|enum|extension|fallthrough|fileprivate|for|func|guard|if|import|in|indirect|inout|internal|is|let|mutating|nonisolated|nonmutating|operator|override|precedencegroup|private|protocol|public|repeat|rethrows|return|static|struct|subscript|switch|throw|throws|try|typealias|unowned|var|where|while|willSet)|init|self|Self|super)\b|(?<![\w.`])(?:(?:open|package|optional|dynamic|final|lazy|weak|required|prefix|infix|postfix|actor|macro|some|any|each|isolated|consuming|borrowing|sending)(?=[ \t]+(?:[a-z]{3}|[A-Z([]))|[gs]et(?=[ \t]*[{}]|[ \t]+[a-z]|\(\w+\)[ \t]*\{)|(?<=\()set(?=\)))/g,
},
{ className: 'literal', regex: /(?<![.`])\b(?:true|false|nil)\b/g },
{ className: 'type', regex: /\b(?:class|struct|enum|protocol|extension|actor|typealias|associatedtype)\s+(\w+)/g, group: 1 },
{ className: 'type', regex: /\b[A-Z][A-Z\d_]*[a-z]\w*/g },
{ className: 'function', regex: /\b(?:func|macro)\s+(\w+)/g, group: 1 },
{ className: 'function', regex: /\b[a-z_]\w*(?=\()|(?<=^[ \t]*\.)[a-z_]\w*(?=[ \t]*\{)|(?<=\.)[a-z_]\w*(?=[ \t]*\{[ \t]*(?:!?[$[]|[\w, ]+ in\b))/gm },
{ className: 'number', regex: /(?<!\w|[^.]\.)(?:0[xob][\da-f_]+(?:(?:\.[\da-f_]+)?p[+-]?\d+)?|\d[\d_]*(?:\.\d[\d_]*)?(?:e[+-]?\d[\d_]*)?)(?!\w|\.\d)/gi },
{ className: 'property', regex: /(?<!\.)\.([A-Za-z_]\w*)/g, group: 1 },
{ className: 'operator', regex: /\.\.[.<]|[-+*/%&|^~!<>=?]+/g },
] satisfies Parameters<typeof collectPatternRanges>[1]

// `(?<!#)` keeps runs of `#` from being retried at every position.
const quote = /(?<!#)(#*)("(?:"")?)/y

export const swift = defineLanguage({
name: 'swift',
tokenize(code) {
const ranges: Array<TokenRange> = []
// Comments; string openers (closed by stringEnd); `#/…/#` regexes, multi-line only when `#/` ends
// the line and running to the line (or input) end when unterminated so retries stay linear;
// bare `/re/` only where an operand is expected, never across lines.
const lexical = /(\/\/.*|\/\*)|(?<!#)(?:(#*")|(#+)\/(?:\n[^]*?(?:\/\3|(?![^]))|.*?(?:\/\3|$)))|\/(?<=(?:[=(,:[{]|\b(?:return|case))[ \t]*\/)(?![\s/*])(?:\\.|[^\\\n/])+(?<!\s)\//gm
let match: RegExpExecArray | null
while ((match = lexical.exec(code))) {
const comment = match[1]
let end = lexical.lastIndex
if (comment === '/*') {
for (let depth = 1; depth && end < code.length;) {
const step = code.startsWith('/*', end) ? 1 : code.startsWith('*/', end) ? -1 : 0
depth += step
end += step ? 2 : 1
}
} else if (match[2]) end = stringEnd(code, match.index)
ranges.push({ start: match.index, end, className: comment ? 'comment' : 'string' })
lexical.lastIndex = end
}
return collectPatternRanges(code, patterns, ranges)
},
})

// Each frame is the text that closes it: an open string's delimiter, or the `)`s that still close
// a `\( )` hole, so quotes and parens inside holes nest.
function stringEnd(code: string, index: number) {
const stack: Array<string> = []
let open: RegExpExecArray | null
do {
const top = stack.at(-1) ?? ')'
const hole = top[0] === ')'
const char = code[index]
quote.lastIndex = index
if (hole && (open = quote.exec(code))) {
stack.push(open[2] + open[1])
index = quote.lastIndex
} else if (char === '\n' && !(hole ? stack.at(-2)! : top).startsWith('"""')) {
// A line break ends every single-line string, and the holes inside it, down to the nearest `"""`.
while (stack.length && !stack.at(-1)!.startsWith('"""')) stack.pop()
if (stack.length) index++
} else if (hole) {
if (char === '(') stack.push(stack.pop() + ')')
else if (char === ')' && stack.pop()!.length > 1) stack.push(top.slice(1))
index++
} else if (code.startsWith(top, index)) {
stack.pop()
index += top.length
} else {
const hashes = top.replace(/"+/, '')
if (char === '\\' && code.startsWith(hashes, index + 1)) {
index += hashes.length + 1
if (code[index] === '(') stack.push(')')
if (code[index] !== '\n') index++
} else index++
}
} while (stack.length && index < code.length)
return Math.min(index, code.length)
}
6 changes: 6 additions & 0 deletions test/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ export const languageFixtures: Array<LanguageFixture> = [
code: 'use std::fmt;\n#[derive(Debug)]\nstruct Point { x: i32 }\nfn main() {\n let count = 42; // total\n println!("Hello {}", count);\n}',
expectedClasses: ['th-keyword', 'th-type', 'th-function', 'th-number', 'th-operator', 'th-comment', 'th-string'],
},
{
lang: 'swift',
normalized: 'swift',
code: 'import Foundation\n@MainActor\nfunc greet(name: String) -> String {\n let count = 42 // total\n return "Hello \\(name)"\n}',
expectedClasses: ['th-keyword', 'th-type', 'th-attr', 'th-function', 'th-number', 'th-operator', 'th-comment', 'th-string'],
},
{
lang: 'octane',
normalized: 'tsrx',
Expand Down
2 changes: 1 addition & 1 deletion test/real-doc-fixtures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type DocFixture = {
}

const fixtures = fixtureData.fixtures as Array<DocFixture>
const languagesWithoutRealDocFixtures = new Set(['go', 'cpp', 'cmake', 'php', 'csharp', 'dart', 'java', 'kotlin', 'lua', 'perl', 'ruby', 'rust'])
const languagesWithoutRealDocFixtures = new Set(['go', 'cpp', 'cmake', 'php', 'csharp', 'dart', 'java', 'kotlin', 'lua', 'perl', 'ruby', 'rust', 'swift'])

describe('real TanStack docs fixtures', () => {
it('covers normalized language targets available in TanStack docs', () => {
Expand Down
Loading
Loading