From 43695a10179a31f0640847a82e8f40be64e27cd1 Mon Sep 17 00:00:00 2001 From: ugi-dev Date: Thu, 22 Oct 2026 16:10:45 +1100 Subject: [PATCH 1/2] fix(kirari): keep private class names as one token Scan `#name` in JavaScript-like languages as a single name token. Private names are always class members, so they are highlighted as properties, or as functions where a public member would be. The documented bundle sizes are refreshed for the top of the stack: core 3,774, JavaScript 4,294, all languages 23,273, highlight() wrapper 4,197 bytes gzip. Closes #4 --- .changeset/fix-private-class-names.md | 5 +++++ docs/comparison.md | 6 +++--- packages/kirari/README.md | 6 +++--- packages/kirari/src/tokenize.ts | 19 ++++++++++++------- packages/kirari/test/corpus/fixtures.ts | 8 ++++++++ packages/kirari/test/script-accuracy.test.ts | 11 +++++++++++ packages/kirari/test/stream-fuzz.test.ts | 1 + sites/docs/changelog/kirari/1.1.3.md | 1 + sites/docs/content/limitations.md | 2 +- sites/docs/src/lib/site.ts | 2 +- 10 files changed, 46 insertions(+), 15 deletions(-) create mode 100644 .changeset/fix-private-class-names.md diff --git a/.changeset/fix-private-class-names.md b/.changeset/fix-private-class-names.md new file mode 100644 index 0000000..f3419ac --- /dev/null +++ b/.changeset/fix-private-class-names.md @@ -0,0 +1,5 @@ +--- +'@vortexlabs/kirari': patch +--- + +Fix JavaScript and TypeScript private class names such as `#count` being split into a `#` text token and a separate name. They are now one token, highlighted as a property, or as a function for private methods. diff --git a/docs/comparison.md b/docs/comparison.md index 66a9870..ef37139 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -18,7 +18,7 @@ Competitors measured on 2026-10-21; Kirari working tree measured on 2026-10-22: | Setup | Gzip bytes | | --- | ---: | -| Kirari working tree, `highlight()` wrapper | 4,153 | +| Kirari working tree, `highlight()` wrapper | 4,197 | | Shiki 4.5.0, synchronous core, JavaScript regex engine, JavaScript grammar, GitHub Dark | 73,530 | | Prism 1.30.0, core, clike dependency, JavaScript grammar | 5,278 | | highlight.js 11.12.0, core, JavaScript grammar | 11,056 | @@ -26,8 +26,8 @@ Competitors measured on 2026-10-21; Kirari working tree measured on 2026-10-22: The script validates that each bundled function produces highlighted HTML and preserves an escaped source sample. It does not measure speed or grammar accuracy. -Kirari's full JavaScript entry is 4,252 bytes gzip; its full set of 48 language -definitions with core is 23,171 bytes. These come from `bun run size`. The wrapper +Kirari's full JavaScript entry is 4,294 bytes gzip; its full set of 48 language +definitions with core is 23,273 bytes. These come from `bun run size`. The wrapper above is smaller because unused exports can be removed. Kirari measurements use the current source, including unreleased changes; competitors use the named npm versions. Rerun both commands when the source or bundler changes. diff --git a/packages/kirari/README.md b/packages/kirari/README.md index cbd4b6c..7c2a453 100644 --- a/packages/kirari/README.md +++ b/packages/kirari/README.md @@ -333,9 +333,9 @@ Measured on 2026-10-22 using Bun 1.3.4: | Entry | Minified bytes | Gzip bytes | | --- | ---: | ---: | -| Core API | 8,527 | 3,735 | -| JavaScript default | 9,700 | 4,252 | -| All 48 languages | 69,243 | 23,171 | +| Core API | 8,607 | 3,774 | +| JavaScript default | 9,780 | 4,294 | +| All 48 languages | 69,380 | 23,273 | ## Development diff --git a/packages/kirari/src/tokenize.ts b/packages/kirari/src/tokenize.ts index f56dbc5..b84c822 100644 --- a/packages/kirari/src/tokenize.ts +++ b/packages/kirari/src/tokenize.ts @@ -155,13 +155,13 @@ export function* scan(source: string, language: Language, start = 0, previous?: position = end } - const match = (pattern: RegExp): string | undefined => { - pattern.lastIndex = position + const match = (pattern: RegExp, at = position): string | undefined => { + pattern.lastIndex = at const result = pattern.exec(source) // Unicode regexes can rewind into a surrogate pair in V8. - if (!result || result.index !== position) return undefined + if (!result || result.index !== at) return undefined if (result[0].length === 0) { - throw new Error(`Kirari: empty match in ${language.name} at offset ${position}`) + throw new Error(`Kirari: empty match in ${language.name} at offset ${at}`) } return result[0] } @@ -216,12 +216,16 @@ export function* scan(source: string, language: Language, start = 0, previous?: frames.push({ kind: 'template', opening: true }) continue } - let name = identifier && match(identifier) + // `#name` is a private class member: one name token, and never a variable. + const privateName = script && char === '#' + const start = privateName ? position + 1 : position + let name = identifier && match(identifier, start) if (script && identifier) { - const end = escapedIdentifierEnd(source, position, position + (name?.length ?? 0)) - if (end > position) name = source.slice(position, end) + const end = escapedIdentifierEnd(source, start, start + (name?.length ?? 0)) + if (end > start) name = source.slice(start, end) } if (name !== undefined) { + if (privateName) name = `#${name}` // Classify the name; earlier branches win. let type = 'variable' const end = position + name.length @@ -251,6 +255,7 @@ export function* scan(source: string, language: Language, start = 0, previous?: if (found.function) type = 'function' if (found.pending) context.functionPending = true } + if (privateName && type === 'variable') type = 'property' context.annotation = false emit(type, end) continue diff --git a/packages/kirari/test/corpus/fixtures.ts b/packages/kirari/test/corpus/fixtures.ts index f618999..f8e6494 100644 --- a/packages/kirari/test/corpus/fixtures.ts +++ b/packages/kirari/test/corpus/fixtures.ts @@ -107,6 +107,14 @@ export const corpus: readonly CorpusFixture[] = [ { text: 'view', type: 'variable' }, { text: 'Box', type: 'tag' }, ], }, + { + id: 'javascript-private-members', language: 'javascript', + source: 'class Counter {\n #count = 0;\n #step() { return this.#count++ }\n has(other) { return #count in other }\n}', + assertions: [ + { text: '#count', type: 'property', occurrence: 0 }, { text: '#count', type: 'property', occurrence: 1 }, + { text: '#count', type: 'property', occurrence: 2 }, { text: '#step', type: 'function' }, + ], + }, { id: 'typescript-class-and-operators', language: 'typescript', source: 'class Meter {\n private reading: number = 1_024;\n sample(): unknown { return this.reading ?? undefined; }\n}', diff --git a/packages/kirari/test/script-accuracy.test.ts b/packages/kirari/test/script-accuracy.test.ts index 1dfa378..a08f992 100644 --- a/packages/kirari/test/script-accuracy.test.ts +++ b/packages/kirari/test/script-accuracy.test.ts @@ -55,6 +55,16 @@ for (const language of [javascript, typescript, jsx, tsx]) { const validPrefix = 'parcel'.repeat(500) expect(pairs(`${validPrefix}\\u0020`, language)).toContainEqual(['variable', validPrefix]) }) + + test(`${language.name} keeps private class names as one member token`, () => { + const names = (source: string) => pairs(source, language).filter(token => token[1]!.startsWith('#')) + expect(names('class C { #p = 1; m() { this.#p } }')).toEqual([['property', '#p'], ['property', '#p']]) + expect(names('class C { static #s; get #a() {} #m() {} n() { this.#m(); return #s in C } }')) + .toEqual([['property', '#s'], ['function', '#a'], ['function', '#m'], ['property', '#m'], ['property', '#s']]) + expect(names('class C { #handler = () => null }')).toEqual([['function', '#handler']]) + expect(names('class C { #\\u0070 = 1; m() { this?.#\\u0070 } }')).toEqual([['property', '#\\u0070'], ['property', '#\\u0070']]) + expect(pairs('class C { # = 1 }', language)).toContainEqual(['text', ' # ']) + }) } for (const language of [typescript, tsx]) { @@ -126,6 +136,7 @@ test('script fixes preserve every unfinished UTF-16 prefix and streaming token p 'type\nParcel\n=\n{ label: string };\nconst cargo\n:\nParcel\n= item;\nfunction finish()\n:\nvoid {}', 'const result = active\n ? other\n : Parcel;\nconst n = void execute()', 'class Box { v: T;\n /* member */ w: string; x: number }\ninterface I { v: T; w: Box }', + 'class Counter {\n #count = 0;\n #step() {}\n add() { this.#step(); return #count in this }\n}', 'const node = >\n rows={items} />;\nnext()', '>>= text', '= C>', diff --git a/packages/kirari/test/stream-fuzz.test.ts b/packages/kirari/test/stream-fuzz.test.ts index 46290d1..28b41cf 100644 --- a/packages/kirari/test/stream-fuzz.test.ts +++ b/packages/kirari/test/stream-fuzz.test.ts @@ -60,6 +60,7 @@ const scriptFragments = [ const scriptSources = [ 'class Box {\n v: T;\n w: string;\n target: Handler = () => null;\n}\n', 'interface I { v: T; /* member */\n w: Box; }\n', + 'class Counter {\n #count = 0;\n #step() {}\n add() { this.#step(); return #count in this }\n}\n', 'name\n\t(\nname\n)', '{name\n\t: true, "key"\r\n: null}', 'return /[a\\/]*/g\nname()', diff --git a/sites/docs/changelog/kirari/1.1.3.md b/sites/docs/changelog/kirari/1.1.3.md index cb14fc4..6bb3bc5 100644 --- a/sites/docs/changelog/kirari/1.1.3.md +++ b/sites/docs/changelog/kirari/1.1.3.md @@ -7,3 +7,4 @@ date: 2026-10-22 ### Bug fixes - Markdown heading tokens no longer include the trailing line break, so heading styles and line-based renderers stop at the end of the line. +- JavaScript and TypeScript private class names such as `#count` are now one token, highlighted as properties at both the declaration and `this.#count`, and as functions for private methods. diff --git a/sites/docs/content/limitations.md b/sites/docs/content/limitations.md index 6250490..4a03d0e 100644 --- a/sites/docs/content/limitations.md +++ b/sites/docs/content/limitations.md @@ -16,7 +16,7 @@ Modern ESM runtimes with ES2022 features are required. The library has no DOM, N ## Bundle measurements -The JavaScript entry measures 4,252 bytes gzip; all 48 definitions together measure 23,171 bytes gzip. Measured on 2026-10-22 with Bun 1.3.4 minification and gzip level 9, excluding CSS. These are library bundle sizes, not website sizes or throughput benchmarks. +The JavaScript entry measures 4,294 bytes gzip; all 48 definitions together measure 23,273 bytes gzip. Measured on 2026-10-22 with Bun 1.3.4 minification and gzip level 9, excluding CSS. These are library bundle sizes, not website sizes or throughput benchmarks. To reproduce, run `bun run size` in the Kirari repository. diff --git a/sites/docs/src/lib/site.ts b/sites/docs/src/lib/site.ts index 2a5e617..75b2bf2 100644 --- a/sites/docs/src/lib/site.ts +++ b/sites/docs/src/lib/site.ts @@ -12,6 +12,6 @@ export const site = { x: 'https://x.com/intent/follow?screen_name=ugi_dev', version: kirariPackage.version, // Tree-shaken highlight() setup; measured by packages/benchmarks/compare-size.ts. - javascriptHighlightGzipBytes: 4153, + javascriptHighlightGzipBytes: 4197, install: 'npm install @vortexlabs/kirari', } as const From dddbe67d597f5b7ae2f26dc30d333ed25a10b2fd Mon Sep 17 00:00:00 2001 From: ugi-dev Date: Thu, 22 Oct 2026 16:20:33 +1100 Subject: [PATCH 2/2] docs: note type alias declarations in the 1.1.3 changelog List the switch from interfaces to type aliases under Breaking changes, with migration steps for code that relied on declaration merging. --- sites/docs/changelog/kirari/1.1.3.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/sites/docs/changelog/kirari/1.1.3.md b/sites/docs/changelog/kirari/1.1.3.md index 6bb3bc5..215d4b2 100644 --- a/sites/docs/changelog/kirari/1.1.3.md +++ b/sites/docs/changelog/kirari/1.1.3.md @@ -4,6 +4,17 @@ version: 1.1.3 date: 2026-10-22 --- +### Breaking changes + +#### Public object types are type aliases + +`Token`, `Rule`, `Language`, `RenderOptions`, `Decoration`, `TokenPatch`, and `TokenStream` are now type aliases instead of interfaces. Their members are unchanged, but code that extends them through declaration merging (module augmentation) no longer compiles. + +**How to migrate** + +1. Replace `declare module` augmentations of these types with your own type that intersects them, such as `type AppToken = Token & { line: number }`. +2. No change is needed if you only import and use the types. + ### Bug fixes - Markdown heading tokens no longer include the trailing line break, so heading styles and line-based renderers stop at the end of the line.