From eb4dc42a9c1cd60d393761f0a092021c7cd234e9 Mon Sep 17 00:00:00 2001 From: Lukas Bindreiter Date: Tue, 11 Aug 2026 11:52:59 +0200 Subject: [PATCH 1/2] Support Go --- README.md | 4 ++-- docs/guides/performance.md | 2 +- docs/language-support.md | 1 + docs/reference/default-entry.md | 2 +- docs/reference/languages.md | 3 ++- docs/test-strategy.md | 2 +- scripts/language-utils.mjs | 2 ++ scripts/measure-size.mjs | 2 +- src/index.ts | 3 +++ src/languages/go.ts | 39 +++++++++++++++++++++++++++++++++ src/languages/index.ts | 1 + test/fixtures.ts | 29 ++++++++++++++++++++++++ test/highlight.contract.test.ts | 7 ++++++ test/real-doc-fixtures.test.ts | 7 ++++-- test/regressions.test.ts | 23 +++++++++++++++++++ 15 files changed, 118 insertions(+), 9 deletions(-) create mode 100644 src/languages/go.ts diff --git a/README.md b/README.md index 3339ce0..9fdbf8e 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ Available themes: Aurora X, Dracula, GitHub Dark, GitHub Light, Monokai, Nord, O ## Languages -`apache`, `css`, `diff`, `dockerfile`, `ejs`, `env`, `html`, `http`, `js`, `json`, `jsx`, `markdown`, `mermaid`, `nginx`, `plaintext`, `python`, `scheme`, `shell`, `sql`, `svelte`, `toml`, `ts`, `tsrx`, `tsx`, `vue`, and `yaml`. +`apache`, `css`, `diff`, `dockerfile`, `ejs`, `env`, `go`, `html`, `http`, `js`, `json`, `jsx`, `markdown`, `mermaid`, `nginx`, `plaintext`, `python`, `scheme`, `shell`, `sql`, `svelte`, `toml`, `ts`, `tsrx`, `tsx`, `vue`, and `yaml`. Each language is available from `@tanstack/highlight/languages/`. The aggregate `@tanstack/highlight/languages` entry can tree-shake, while direct subpaths make isolation explicit. @@ -199,7 +199,7 @@ Local browser bundles, minified with esbuild and compressed independently: | Core + TSX | 9.3 KB | 3.9 KB | 3.5 KB | | Octane MDX + TypeScript | 12.5 KB | 4.9 KB | 4.5 KB | | Nine-language docs set | 15.3 KB | 5.8 KB | 5.3 KB | -| All 25 languages | 22.2 KB | 8.0 KB | 7.2 KB | +| All 26 languages | 23.8 KB | 8.6 KB | 7.7 KB | On 80 real JavaScript/TypeScript/JSX/TSX TanStack docs fixtures repeated across 5,040 blocks: diff --git a/docs/guides/performance.md b/docs/guides/performance.md index 6fbc544..9da7be9 100644 --- a/docs/guides/performance.md +++ b/docs/guides/performance.md @@ -16,7 +16,7 @@ Small and fast are product constraints, not README adjectives. The repository me | TSX | TSX | 3.86 KB | 4.1 KB | | Octane | TypeScript plus Octane MDX adapter | 4.93 KB | 5.2 KB | | Docs | CSS, HTML, JS, JSON, JSX, Markdown, Shell, TS, TSX | 5.83 KB | 6.1 KB | -| All | All 25 definitions | 7.96 KB | 8.3 KB | +| All | All 26 definitions | 8.55 KB | 8.9 KB | Selective profiles are the primary metric. The all-language profile exists to prevent convenience-entry growth from becoming invisible. diff --git a/docs/language-support.md b/docs/language-support.md index f7de9bc..35b2b29 100644 --- a/docs/language-support.md +++ b/docs/language-support.md @@ -16,6 +16,7 @@ Every language is an isolated definition imported from `@tanstack/highlight/lang | Dockerfile | `dockerfile` | `docker` | Common directives, variables, commands | | EJS | `ejs` | - | HTML plus optional JavaScript delegation | | Env | `env` | `dotenv` | Properties, values, comments | +| Go | `go` | `golang` | Raw strings, runes, comments, declarations | | HTML | `html` | `htm`, `xml`, `angular-html` | Optional JavaScript/TypeScript and CSS delegation | | HTTP | `http` | - | Methods, headers, protocol, paths | | JavaScript | `js` | `javascript`, `mjs`, `cjs`, `js-vue` | Templates, interpolation, regex literals | diff --git a/docs/reference/default-entry.md b/docs/reference/default-entry.md index 80a0f45..e893fa4 100644 --- a/docs/reference/default-entry.md +++ b/docs/reference/default-entry.md @@ -43,7 +43,7 @@ Returns the canonical registered language name for a name or alias. Names are tr function listLanguages(): Array ``` -Returns the 25 canonical language names registered in `defaultHighlighter`. +Returns the 26 canonical language names registered in `defaultHighlighter`. ### `tokenize` diff --git a/docs/reference/languages.md b/docs/reference/languages.md index 2de8bee..3089e6b 100644 --- a/docs/reference/languages.md +++ b/docs/reference/languages.md @@ -27,6 +27,7 @@ const highlighter = createHighlighter({ | `dockerfile` | `@tanstack/highlight/languages/dockerfile` | `docker` | | `ejs` | `@tanstack/highlight/languages/ejs` | None | | `env` | `@tanstack/highlight/languages/env` | `dotenv` | +| `go` | `@tanstack/highlight/languages/go` | `golang` | | `html` | `@tanstack/highlight/languages/html` | `htm`, `xml`, `angular-html` | | `http` | `@tanstack/highlight/languages/http` | None | | `js` | `@tanstack/highlight/languages/js` | `javascript`, `mjs`, `cjs`, `js-vue` | @@ -48,6 +49,6 @@ const highlighter = createHighlighter({ | `vue` | `@tanstack/highlight/languages/vue` | None | | `yaml` | `@tanstack/highlight/languages/yaml` | `yml` | -`@tanstack/highlight/languages` re-exports `apache`, `css`, `diff`, `dockerfile`, `ejs`, `env`, `html`, `http`, `js`, `json`, `jsx`, `markdown`, `mermaid`, `nginx`, `plaintext`, `python`, `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`, `css`, `diff`, `dockerfile`, `ejs`, `env`, `go`, `html`, `http`, `js`, `json`, `jsx`, `markdown`, `mermaid`, `nginx`, `plaintext`, `python`, `scheme`, `shell`, `sql`, `svelte`, `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. diff --git a/docs/test-strategy.md b/docs/test-strategy.md index 1cc255a..5aa42d9 100644 --- a/docs/test-strategy.md +++ b/docs/test-strategy.md @@ -27,7 +27,7 @@ The suite protects the package's actual product boundary: valid code commonly pu | TSX | TSX | 4.1 KB | | Octane | TypeScript plus Octane MDX adapter | 5.2 KB | | Docs | CSS, HTML, JS, JSON, JSX, Markdown, Shell, TS, TSX | 6.1 KB | -| All | All 25 definitions | 8.3 KB | +| All | All 26 definitions | 8.9 KB | The selective profiles are the primary product metric. The all-language profile protects the convenience entry from unbounded growth. diff --git a/scripts/language-utils.mjs b/scripts/language-utils.mjs index c31105c..451eb1e 100644 --- a/scripts/language-utils.mjs +++ b/scripts/language-utils.mjs @@ -8,6 +8,7 @@ export const supportedLanguages = [ 'dockerfile', 'ejs', 'env', + 'go', 'html', 'http', 'js', @@ -39,6 +40,7 @@ const aliases = { cmd: 'shell', console: 'shell', dotenv: 'env', + golang: 'go', htm: 'html', javascript: 'js', 'js-vue': 'js', diff --git a/scripts/measure-size.mjs b/scripts/measure-size.mjs index 927b5ad..71c7291 100644 --- a/scripts/measure-size.mjs +++ b/scripts/measure-size.mjs @@ -51,7 +51,7 @@ const profiles = { import { defaultHighlighter } from './src/index.ts' globalThis.highlighter = defaultHighlighter `, - limits: { minified: 23_000, gzip: 8_300, brotli: 7_500 }, + limits: { minified: 24_500, gzip: 8_900, brotli: 8_000 }, }, } diff --git a/src/index.ts b/src/index.ts index 5a231f5..b061000 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ import { diff } from './languages/diff.js' import { dockerfile } from './languages/dockerfile.js' import { ejs } from './languages/ejs.js' import { env } from './languages/env.js' +import { go } from './languages/go.js' import { html } from './languages/html.js' import { http } from './languages/http.js' import { js } from './languages/js.js' @@ -39,6 +40,7 @@ export type HighlightLanguage = | 'dockerfile' | 'ejs' | 'env' + | 'go' | 'html' | 'http' | 'js' @@ -107,6 +109,7 @@ export const allLanguages = [ dockerfile, ejs, env, + go, html, http, js, diff --git a/src/languages/go.ts b/src/languages/go.ts new file mode 100644 index 0000000..0b86e52 --- /dev/null +++ b/src/languages/go.ts @@ -0,0 +1,39 @@ +import { defineLanguage } from '../core.js' +import { patternTokenizer } from '../internal/patterns.js' + +export const go = defineLanguage({ + name: 'go', + aliases: ['golang'], + tokenize: patternTokenizer([ + { + className: (match) => + match[0].startsWith('/') ? 'comment' : 'string', + regex: + /\/\/[^\n]*|\/\*[\s\S]*?\*\/|`[\s\S]*?`|"(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*'/g, + }, + { + className: 'keyword', + regex: + /\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go|goto|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/g, + }, + { className: 'literal', regex: /\b(?:false|iota|nil|true)\b/g }, + { className: 'type', regex: /\btype\s+([A-Za-z_]\w*)/g, group: 1 }, + { + className: 'type', + regex: + /\b(?:any|bool|byte|comparable|complex64|complex128|error|float32|float64|int|int8|int16|int32|int64|rune|string|uint|uint8|uint16|uint32|uint64|uintptr)\b/g, + }, + { className: 'function', regex: /\b[A-Za-z_]\w*(?=\s*\()/g }, + { + className: 'number', + regex: + /\b(?:0[bB][01](?:_?[01])*|0[oO][0-7](?:_?[0-7])*|0[xX][\da-fA-F](?:_?[\da-fA-F])*(?:\.[\da-fA-F](?:_?[\da-fA-F])*)?(?:[pP][+-]?\d(?:_?\d)*)?|\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?)(?:i)?\b/g, + }, + { className: 'property', regex: /\.\s*([A-Za-z_]\w*)/g, group: 1 }, + { + className: 'operator', + regex: + /\.\.\.|<<=?|>>=?|&\^=?|:=|<-|\+\+|--|==|!=|<=|>=|&&|\|\||[+\-*/%&|^!<>=~]=?/g, + }, + ]), +}) diff --git a/src/languages/index.ts b/src/languages/index.ts index ef279c9..50c2791 100644 --- a/src/languages/index.ts +++ b/src/languages/index.ts @@ -4,6 +4,7 @@ export { diff } from './diff.js' export { dockerfile } from './dockerfile.js' export { ejs } from './ejs.js' export { env } from './env.js' +export { go } from './go.js' export { html } from './html.js' export { http } from './http.js' export { js } from './js.js' diff --git a/test/fixtures.ts b/test/fixtures.ts index f2ebe9b..a3ead0d 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -29,6 +29,35 @@ export function Counter() @{ 'th-number', ], }, + { + lang: 'golang', + normalized: 'go', + code: `package main + +import "fmt" + +type Greeter struct { + Name string +} + +func (g Greeter) Greet() { + count := 2 + url := \`https://go.dev\` + initial := 'G' + fmt.Println(url, initial, g.Name, count, true) // greet +}`, + expectedClasses: [ + 'th-keyword', + 'th-string', + 'th-type', + 'th-function', + 'th-property', + 'th-number', + 'th-literal', + 'th-comment', + 'th-operator', + ], + }, { lang: 'tsx', normalized: 'tsx', diff --git a/test/highlight.contract.test.ts b/test/highlight.contract.test.ts index 49137b9..a8bd809 100644 --- a/test/highlight.contract.test.ts +++ b/test/highlight.contract.test.ts @@ -28,11 +28,17 @@ describe('language inventory', () => { it('lists the normalized support targets once', () => { const languages = listLanguages() + const fixtureLanguages = new Set( + languageFixtures.map((fixture) => fixture.normalized), + ) expect(new Set(languages).size).toBe(languages.length) for (const fixture of languageFixtures) { expect(languages).toContain(fixture.normalized) } + for (const language of languages) { + expect(fixtureLanguages, `missing representative fixture for ${language}`).toContain(language) + } }) }) @@ -82,6 +88,7 @@ describe('output contract', () => { it('keeps comment markers inside strings and quotes inside comments', () => { const cases = [ ['ts', `const url = "https://example.com" // "comment"`], + ['go', `url := "https://go.dev" // "comment"`], ['python', `value = "# not a comment" # "comment"`], ['shell', `echo "# not a comment" # "comment"`], ['yaml', `url: "https://example.com/#hash" # "comment"`], diff --git a/test/real-doc-fixtures.test.ts b/test/real-doc-fixtures.test.ts index f8480de..74242e4 100644 --- a/test/real-doc-fixtures.test.ts +++ b/test/real-doc-fixtures.test.ts @@ -11,12 +11,14 @@ type DocFixture = { } const fixtures = fixtureData.fixtures as Array +const languagesWithoutRealDocFixtures = new Set(['go']) describe('real TanStack docs fixtures', () => { - it('covers every normalized language target with sampled docs code', () => { + it('covers normalized language targets available in TanStack docs', () => { const fixtureLanguages = new Set(fixtures.map((fixture) => fixture.lang)) for (const language of listLanguages()) { + if (languagesWithoutRealDocFixtures.has(language)) continue expect(fixtureLanguages, `missing real docs fixture for ${language}`).toContain(language) } }) @@ -53,8 +55,9 @@ describe('real TanStack docs fixtures', () => { } } + const fixtureLanguages = new Set(fixtures.map((fixture) => fixture.lang)) for (const language of listLanguages()) { - if (language === 'plaintext') continue + if (language === 'plaintext' || !fixtureLanguages.has(language)) continue expect(highlightedLanguages, `${language} should highlight at least one real docs fixture`).toContain(language) } }) diff --git a/test/regressions.test.ts b/test/regressions.test.ts index 6671856..bea5364 100644 --- a/test/regressions.test.ts +++ b/test/regressions.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import { createHighlighter } from '../src/core' import { css } from '../src/languages/css' import { dockerfile } from '../src/languages/dockerfile' +import { go } from '../src/languages/go' import { html } from '../src/languages/html' import { http } from '../src/languages/http' import { js } from '../src/languages/js' @@ -19,6 +20,7 @@ const highlighter = createHighlighter({ languages: [ css, dockerfile, + go, html, http, js, @@ -305,6 +307,27 @@ export function View() @{

Ready

}`, }) describe('docs language regressions', () => { + it('handles Go raw strings, runes, and numeric forms', () => { + const result = highlighter.tokenize( + [ + 'raw := `https://go.dev/a//b`', + "runeValue := '\\n'", + 'values := []complex128{0b1010, 0o755, 0x1.fp2, 1_000.5e-2i}', + '// done', + ].join('\n'), + { lang: 'go' }, + ) + + expect(exactClassesFor(result, '`https://go.dev/a//b`')).toEqual(['string']) + expect(exactClassesFor(result, "'\\n'")).toEqual(['string']) + expect(exactClassesFor(result, 'complex128')).toEqual(['type']) + for (const number of ['0b1010', '0o755', '0x1.fp2', '1_000.5e-2i']) { + expect(exactClassesFor(result, number), number).toEqual(['number']) + } + expect(exactClassesFor(result, '// done')).toEqual(['comment']) + expect(reconstruct(result)).toBe(result.code) + }) + it('keeps Python triple strings together and highlights literals and numbers', () => { const result = highlighter.tokenize( 'text = """hello\nworld"""\nempty = None\nenabled = True\ncount = 42', From 35f40906c07fcbcfd4f89f0bbfff0067dcd5aa9a Mon Sep 17 00:00:00 2001 From: Lukas Bindreiter Date: Tue, 11 Aug 2026 13:39:28 +0200 Subject: [PATCH 2/2] Improve go number regex --- README.md | 2 +- docs/guides/performance.md | 2 +- docs/reference/default-entry.md | 2 +- docs/test-strategy.md | 2 +- src/languages/go.ts | 3 ++- test/regressions.test.ts | 23 +++++++++++++++++++++-- 6 files changed, 27 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9fdbf8e..28e0227 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,7 @@ Local browser bundles, minified with esbuild and compressed independently: | Core + TSX | 9.3 KB | 3.9 KB | 3.5 KB | | Octane MDX + TypeScript | 12.5 KB | 4.9 KB | 4.5 KB | | Nine-language docs set | 15.3 KB | 5.8 KB | 5.3 KB | -| All 26 languages | 23.8 KB | 8.6 KB | 7.7 KB | +| All 27 languages | 24.0 KB | 8.6 KB | 7.7 KB | On 80 real JavaScript/TypeScript/JSX/TSX TanStack docs fixtures repeated across 5,040 blocks: diff --git a/docs/guides/performance.md b/docs/guides/performance.md index 9da7be9..aaace82 100644 --- a/docs/guides/performance.md +++ b/docs/guides/performance.md @@ -16,7 +16,7 @@ Small and fast are product constraints, not README adjectives. The repository me | TSX | TSX | 3.86 KB | 4.1 KB | | Octane | TypeScript plus Octane MDX adapter | 4.93 KB | 5.2 KB | | Docs | CSS, HTML, JS, JSON, JSX, Markdown, Shell, TS, TSX | 5.83 KB | 6.1 KB | -| All | All 26 definitions | 8.55 KB | 8.9 KB | +| All | All 27 definitions | 8.60 KB | 8.9 KB | Selective profiles are the primary metric. The all-language profile exists to prevent convenience-entry growth from becoming invisible. diff --git a/docs/reference/default-entry.md b/docs/reference/default-entry.md index e893fa4..b42d226 100644 --- a/docs/reference/default-entry.md +++ b/docs/reference/default-entry.md @@ -43,7 +43,7 @@ Returns the canonical registered language name for a name or alias. Names are tr function listLanguages(): Array ``` -Returns the 26 canonical language names registered in `defaultHighlighter`. +Returns the 27 canonical language names registered in `defaultHighlighter`. ### `tokenize` diff --git a/docs/test-strategy.md b/docs/test-strategy.md index 5aa42d9..a7fdf43 100644 --- a/docs/test-strategy.md +++ b/docs/test-strategy.md @@ -27,7 +27,7 @@ The suite protects the package's actual product boundary: valid code commonly pu | TSX | TSX | 4.1 KB | | Octane | TypeScript plus Octane MDX adapter | 5.2 KB | | Docs | CSS, HTML, JS, JSON, JSX, Markdown, Shell, TS, TSX | 6.1 KB | -| All | All 26 definitions | 8.9 KB | +| All | All 27 definitions | 8.9 KB | The selective profiles are the primary product metric. The all-language profile protects the convenience entry from unbounded growth. diff --git a/src/languages/go.ts b/src/languages/go.ts index 0b86e52..c899748 100644 --- a/src/languages/go.ts +++ b/src/languages/go.ts @@ -27,7 +27,8 @@ export const go = defineLanguage({ { className: 'number', regex: - /\b(?:0[bB][01](?:_?[01])*|0[oO][0-7](?:_?[0-7])*|0[xX][\da-fA-F](?:_?[\da-fA-F])*(?:\.[\da-fA-F](?:_?[\da-fA-F])*)?(?:[pP][+-]?\d(?:_?\d)*)?|\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?)(?:i)?\b/g, + /(?:^|[^\w.])((?:0[bB]_?[01](?:_?[01])*|0[oO]_?[0-7](?:_?[0-7])*|0[xX](?:(?:_?[\da-fA-F](?:_?[\da-fA-F])*(?:\.(?:[\da-fA-F](?:_?[\da-fA-F])*)?)?|\.[\da-fA-F](?:_?[\da-fA-F])*)[pP][+-]?\d(?:_?\d)*|_?[\da-fA-F](?:_?[\da-fA-F])*)|\d(?:_?\d)*\.(?:\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?|\d(?:_?\d)*[eE][+-]?\d(?:_?\d)*|\.\d(?:_?\d)*(?:[eE][+-]?\d(?:_?\d)*)?|\d(?:_?\d)*)(?:i)?)(?![\w.])/g, + group: 1, }, { className: 'property', regex: /\.\s*([A-Za-z_]\w*)/g, group: 1 }, { diff --git a/test/regressions.test.ts b/test/regressions.test.ts index bea5364..a8549a6 100644 --- a/test/regressions.test.ts +++ b/test/regressions.test.ts @@ -308,11 +308,30 @@ export function View() @{

Ready

}`, describe('docs language regressions', () => { it('handles Go raw strings, runes, and numeric forms', () => { + const numbers = [ + '0b1010', + '0o755', + '0x1.fp2', + '1.', + '1.e2', + '1.e2i', + '.5', + '.5i', + '.5e2', + '.5e2i', + '1e2', + '1e2i', + '0x1.p2', + '0x.8p2', + '0x_1p2', + '0x1.p2i', + '1_000.5e-2i', + ] const result = highlighter.tokenize( [ 'raw := `https://go.dev/a//b`', "runeValue := '\\n'", - 'values := []complex128{0b1010, 0o755, 0x1.fp2, 1_000.5e-2i}', + `values := []complex128{${numbers.join(', ')}}`, '// done', ].join('\n'), { lang: 'go' }, @@ -321,7 +340,7 @@ describe('docs language regressions', () => { expect(exactClassesFor(result, '`https://go.dev/a//b`')).toEqual(['string']) expect(exactClassesFor(result, "'\\n'")).toEqual(['string']) expect(exactClassesFor(result, 'complex128')).toEqual(['type']) - for (const number of ['0b1010', '0o755', '0x1.fp2', '1_000.5e-2i']) { + for (const number of numbers) { expect(exactClassesFor(result, number), number).toEqual(['number']) } expect(exactClassesFor(result, '// done')).toEqual(['comment'])