Skip to content
Open
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/c.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tanstack/highlight": minor
---

Add an isolated C language definition with an `h` alias. 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`, `swift`, `toml`, `ts`, `tsrx`, `tsx`, `vue`, and `yaml`.
`apache`, `c`, `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 39 languages | 52.74 KB | 17.73 KB | 15.93 KB |
| All 40 languages | 54.19 KB | 17.88 KB | 16.15 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-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.
`pnpm run size` builds twenty-seven 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 39 definitions | 17.73 KB | 18.0 KB |
| All | All 40 definitions | 17.88 KB | 18.15 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, 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.
`pnpm run bench` measures tokenization, HTML, Markdown, HAST, line numbers, long decorated blocks, and dedicated C, 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
1 change: 1 addition & 0 deletions docs/language-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Every language is an isolated definition imported from `@tanstack/highlight/lang
| Language | Export | Aliases | Context-aware behavior |
| --- | --- | --- | --- |
| Apache | `apache` | - | Directives, tags, comments |
| C | `c` | `h` | Prefixed strings, character literals, preprocessor directives, C11 and C23 keywords, digit separators; C++-only words stay plain |
| CMake | `cmake` | - | Bracket strings/comments, nested variables, generator expressions |
| C++ | `cpp` | `c++`, `cc`, `cxx`, `hpp`, `hxx` | Raw and prefixed strings, character literals, preprocessor directives, digit separators |
| C# | `csharp` | `c#`, `cs` | Verbatim, interpolated, and raw strings with nested quotes, character literals, preprocessor directives, line-leading and parameter attributes, contextual and LINQ keywords |
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 39 canonical language names registered in `defaultHighlighter`.
Returns the 40 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 @@ -22,6 +22,7 @@ const highlighter = createHighlighter({
| Export | Module | Aliases |
| --- | --- | --- |
| `apache` | `@tanstack/highlight/languages/apache` | None |
| `c` | `@tanstack/highlight/languages/c` | `h` |
| `cmake` | `@tanstack/highlight/languages/cmake` | None |
| `cpp` | `@tanstack/highlight/languages/cpp` | `c++`, `cc`, `cxx`, `hpp`, `hxx` |
| `csharp` | `@tanstack/highlight/languages/csharp` | `c#`, `cs` |
Expand Down Expand Up @@ -61,6 +62,6 @@ const highlighter = createHighlighter({
| `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`, `swift`, `toml`, `ts`, `tsrx`, `tsx`, `vue`, and `yaml`. The barrel is convenient but individual subpaths make bundle intent explicit.
`@tanstack/highlight/languages` re-exports `apache`, `c`, `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-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:
`pnpm run size` checks twenty-seven 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 39 definitions | 18.0 KB |
| All | All 40 definitions | 18.15 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, 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 bench` measures highlighting, tokenization, Markdown, HAST, line numbers, long numbered blocks, long decorated blocks, and dedicated C, 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 @@ -56,6 +56,12 @@ try {
outputBytes: htmlBytes,
targetBlocks: 10_000,
},
c: {
fixtures: [{ rawLang: 'c', code: '#include <stdio.h>\n_Static_assert(sizeof(int) == 4, "int");\nstatic void copy(char *restrict dst, const char *restrict src) { while ((*dst++ = *src++)) {} }\nint main(void) { long n = 1\'000 + 0x1Fu; printf("%ld\\n", n); return 0; } // done' }],
run: (fixture) => highlight(fixture.code, { lang: fixture.rawLang }),
observe: (result) => result.html.length,
targetBlocks: 10_000,
},
cpp: {
fixtures: [{ rawLang: 'cpp', code: '#include <vector>\nconstexpr auto text = R"tag(// raw text)tag";\nint main() { std::vector<int> values{1, 2, 3}; return values.size(); }' }],
run: (fixture) => highlight(fixture.code, { lang: fixture.rawLang }),
Expand Down
2 changes: 2 additions & 0 deletions scripts/language-utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from 'node:path'

export const supportedLanguages = [
'apache',
'c',
'cmake',
'cpp',
'csharp',
Expand Down Expand Up @@ -44,6 +45,7 @@ export const supportedLanguages = [
]

const aliases = {
h: 'c',
'c++': 'cpp',
cc: 'cpp',
cxx: 'cpp',
Expand Down
11 changes: 10 additions & 1 deletion scripts/measure-size.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ const profiles = {
languages: ['php'],
limits: { minified: 8_000, gzip: 3_700, brotli: 3_400 },
},
c: {
source: `
import { createHighlighter } from './src/core.ts'
import { c } from './src/languages/c.ts'
globalThis.highlighter = createHighlighter({ languages: [c] })
`,
languages: ['c'],
limits: { minified: 6_250, gzip: 2_950, brotli: 2_750 },
},
cpp: {
source: `
import { createHighlighter } from './src/core.ts'
Expand Down Expand Up @@ -185,7 +194,7 @@ const profiles = {
globalThis.highlighter = defaultHighlighter
`,
languages: 'all',
limits: { minified: 53_150, gzip: 18_000, brotli: 16_200 },
limits: { minified: 54_600, gzip: 18_150, brotli: 16_450 },
},
reactAdapter: {
source: `export * from './src/react.ts'`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Import only the definitions the application registers.
| Language | Export | Direct import | Aliases |
| --- | --- | --- | --- |
| Apache | `apache` | `@tanstack/highlight/languages/apache` | - |
| C | `c` | `@tanstack/highlight/languages/c` | `h` |
| CMake | `cmake` | `@tanstack/highlight/languages/cmake` | - |
| C++ | `cpp` | `@tanstack/highlight/languages/cpp` | `c++`, `cc`, `cxx`, `hpp`, `hxx` |
| C# | `csharp` | `@tanstack/highlight/languages/csharp` | `c#`, `cs` |
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type RenderedCodeBlockData as CoreRenderedCodeBlockData,
} from './core.js'
import { apache } from './languages/apache.js'
import { c } from './languages/c.js'
import { cmake } from './languages/cmake.js'
import { cpp } from './languages/cpp.js'
import { csharp } from './languages/csharp.js'
Expand Down Expand Up @@ -47,6 +48,7 @@ import { yaml } from './languages/yaml.js'

export type HighlightLanguage =
| 'apache'
| 'c'
| 'cmake'
| 'cpp'
| 'csharp'
Expand Down Expand Up @@ -128,6 +130,7 @@ export {

export const allLanguages = [
apache,
c,
cmake,
cpp,
csharp,
Expand Down
30 changes: 30 additions & 0 deletions src/languages/c.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { defineLanguage } from '../core.js'
import { patternTokenizer } from '../internal/patterns.js'

// C++-only words such as `class`, `new`, `this`, and `template` stay plain because they are valid C identifiers.
export const c = defineLanguage({
name: 'c',
aliases: ['h'],
tokenize: patternTokenizer([
{
className: (match) => match[0].startsWith('/') ? 'comment' : 'string',
regex: /\/\/(?:\\\r?\n|[^\n])*|\/\*[\s\S]*?\*\/|(?:u8|u|U|L)?"(?:\\[\s\S]|[^"\\\r\n])*"?|(?<![\w'])(?:u8|u|U|L)?'(?:\\.|[^'\\\r\n])*'/g,
},
{ className: 'string', regex: /^[ \t]*#[ \t]*(?:include|embed)[ \t]*(<[^>\r\n]+>)/gm, group: 1 },
{ className: 'meta', regex: /^[ \t]*(#[ \t]*[A-Za-z_]\w*)/gm, group: 1 },
{
className: 'keyword',
regex: /\b(?:alignas|alignof|auto|break|case|const|constexpr|continue|default|do|else|enum|extern|for|goto|if|inline|register|restrict|return|sizeof|static|static_assert|struct|switch|thread_local|typedef|typeof|typeof_unqual|union|volatile|while|_Alignas|_Alignof|_Atomic|_Generic|_Noreturn|_Static_assert|_Thread_local)\b/g,
},
{ className: 'literal', regex: /\b(?:true|false|nullptr|NULL)\b/g },
{ className: 'type', regex: /\b(?:bool|char|double|float|int|long|short|signed|unsigned|void|_BitInt|_Bool|_Complex|_Decimal32|_Decimal64|_Decimal128|_Imaginary)\b/g },
{ className: 'type', regex: /\b(?:struct|enum|union)\s+([A-Za-z_]\w*)/g, group: 1 },
{
className: 'number',
regex: /(?:^|[^\w.])((?:0[xX][\da-fA-F](?:'?[\da-fA-F])*(?:\.(?:[\da-fA-F]'?)*)?(?:[pP][+-]?\d(?:'?\d)*)?|0[bB][01](?:'?[01])*|(?:\d(?:'?\d)*(?:\.(?:\d(?:'?\d)*)?)?|\.\d(?:'?\d)*)(?:[eE][+-]?\d(?:'?\d)*)?)(?:[uUlLfF]|wb|WB)*)(?![\w.])/g,
group: 1,
},
{ className: 'function', regex: /\b[A-Za-z_]\w*(?=\s*\()/g },
{ className: 'operator', regex: /->|\.\.\.|<<=?|>>=?|\+\+|--|&&|\|\||[+\-*/%&|^!<>=]=?|[~?:]/g },
]),
})
1 change: 1 addition & 0 deletions src/languages/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { apache } from './apache.js'
export { c } from './c.js'
export { cmake } from './cmake.js'
export { cpp } from './cpp.js'
export { csharp } from './csharp.js'
Expand Down
85 changes: 85 additions & 0 deletions test/c.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, expect, it } from 'vitest'
import { createHighlighter } from '../src/core'
import { c } from '../src/languages/c'
import { cpp } from '../src/languages/cpp'

const highlighter = createHighlighter({ languages: [c] })

function classes(code: string, text: string, lang = 'c') {
const result = highlighter.tokenize(code, { lang })
expect(result.tokens.map((token) => token.value).join('')).toBe(code)
const start = code.indexOf(text)
expect(start).toBeGreaterThanOrEqual(0)
let offset = 0
return result.tokens.flatMap((token) => {
const from = offset
offset += token.value.length
return from < start + text.length && offset > start ? [token.className] : []
})
}

describe('C documentation syntax', () => {
it('isolates comments, prefixed strings, and character literals', () => {
const code = 'const char *url = "https://example.com /* text */";\nwchar_t ch = L\'\\n\'; // "comment"\nchar8_t *s = u8"utf";'
expect(classes(code, '"https://example.com /* text */"')).toEqual(['string'])
expect(classes(code, "L'\\n'")).toEqual(['string'])
expect(classes(code, '// "comment"')).toEqual(['comment'])
expect(classes(code, 'u8"utf"')).toEqual(['string'])
})

it('ends unterminated strings at the line end and keeps escaped line continuations', () => {
const code = 'const char *open = "unterminated;\nint count = 1;\nconst char *joined = "first \\\nsecond";'
expect(classes(code, '"unterminated;')).toEqual(['string'])
expect(classes(code, 'int')).toEqual(['type'])
expect(classes(code, '1')).toEqual(['number'])
expect(classes(code, '"first \\\nsecond"')).toEqual(['string'])
})

it('keeps C++-only words plain because C allows them as identifiers', () => {
const code = 'int new = 1;\nstruct node *class = this_node(template, namespace);\nint this = new + 1;'
for (const word of ['new', 'namespace', 'template']) {
expect(classes(code, word), word).toEqual([undefined])
}
expect(classes('int this = 0;', 'this')).toEqual([undefined])
expect(classes('int x = 0; void *p = class;', 'class')).toEqual([undefined])
})

it('recognizes C11 and C23 keywords, types, and literals', () => {
const code = '_Static_assert(sizeof(int) == 4, "int");\nstatic_assert(true);\nvoid copy(char *restrict dst, const char *restrict src);\n_Atomic _Bool ready = false;\nauto p = nullptr;\ntypeof(p) q = _Generic(p, default: 0);\nconstexpr _BitInt(7) small = 3wb;'
for (const keyword of ['_Static_assert', 'sizeof', 'static_assert', 'restrict', '_Atomic', 'auto', 'typeof', '_Generic', 'constexpr']) {
expect(classes(code, keyword), keyword).toEqual(['keyword'])
}
for (const type of ['_Bool', '_BitInt']) {
expect(classes(code, type), type).toEqual(['type'])
}
expect(classes(code, 'nullptr')).toEqual(['literal'])
expect(classes(code, 'false')).toEqual(['literal'])
expect(classes(code, '3wb')).toEqual(['number'])
})

it('recognizes numbers with suffixes and digit separators', () => {
for (const number of ["1'000", '42ULL', '0b1010u', "0xAB'CD", '0x1.fp+2', '.5f', '1.e2', '10uwb']) {
const code = `long value = ${number};`
expect(classes(code, number), number).toEqual(['number'])
}
})

it('recognizes include paths, directives, tagged types, and calls', () => {
const code = '#include <stdio.h>\n#define MAX 10\n// continued \\\n"still a comment"\nstruct point { int x; };\nint main(void) { printf("%d", MAX); return 0; }'
expect(classes(code, '#include')).toEqual(['meta'])
expect(classes(code, '<stdio.h>')).toEqual(['string'])
expect(classes(code, '#define')).toEqual(['meta'])
expect(classes(code, '"still a comment"')).toEqual(['comment'])
expect(classes(code, 'point')).toEqual(['type'])
expect(classes(code, 'printf')).toEqual(['function'])
expect(classes('node->next = ptr;', '->')).toEqual(['operator'])
})

it('registers only the requested aliases', () => {
for (const name of ['c', 'h', 'C', 'H']) {
expect(highlighter.normalizeLanguage(name)).toBe('c')
}
expect(createHighlighter({ languages: [cpp] }).normalizeLanguage('c')).toBe('plaintext')
expect(highlighter.normalizeLanguage('cpp')).toBe('plaintext')
})
})
6 changes: 6 additions & 0 deletions test/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ export const languageFixtures: Array<LanguageFixture> = [
code: '<?php\n#[Route("/hello")]\nfunction greet(string $name): string { return "Hello " . $name; }\n$count = 42; // count\n$ready = true;',
expectedClasses: ['th-meta', 'th-attr', 'th-keyword', 'th-function', 'th-type', 'th-variable', 'th-string', 'th-operator', 'th-number', 'th-comment', 'th-literal'],
},
{
lang: 'h',
normalized: 'c',
code: '#include <stdio.h>\nstruct point { int x; };\nint main(void) { const char *s = "hello"; return s[0] == 0x41; } // done',
expectedClasses: ['th-meta', 'th-string', 'th-keyword', 'th-type', 'th-function', 'th-number', 'th-operator', 'th-comment'],
},
{
lang: 'c++',
normalized: 'cpp',
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', 'swift'])
const languagesWithoutRealDocFixtures = new Set(['go', 'c', '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