sugar-high 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,18 +3,52 @@
3
3
  [![version][npm-version-badge]][npm]
4
4
  [![downloads][npm-downloads-badge]][npm]
5
5
 
6
- Lightweight, zero-dependency syntax highlighting for JavaScript, popular programming languages,
7
- and formats commonly generated by coding agents. It runs in browsers and JavaScript runtimes and
8
- returns HTML without requiring a DOM.
6
+ Lightweight syntax highlighting for JavaScript, popular programming languages, and formats
7
+ commonly generated by coding agents. It runs in browsers and JavaScript runtimes and returns HTML
8
+ without requiring a DOM.
9
9
 
10
10
  ![Sugar High preview](https://repository-images.githubusercontent.com/453236442/9aa2144a-3a4c-4a93-a87f-92ca6a37ded6)
11
11
 
12
+ ## Benchmarks
13
+
14
+ Sugar High, PrismJS, and highlight.js highlighting the same generated TypeScript files:
15
+
16
+ <!-- benchmark:start -->
17
+ Measured 2026-09-04 with Node v24.18.0, darwin arm64, Apple M4 Pro.
18
+
19
+ | TypeScript | Sugar High 2.2.2 | PrismJS 1.30.0 | highlight.js 11.12.0 |
20
+ | --- | ---: | ---: | ---: |
21
+ | Minified (KiB) | 9.90 | 14.63 | 29.54 |
22
+ | Gzip (KiB) | 4.35 | 5.47 | 11.12 |
23
+ | 11 KiB | 1.78 | 1.18 | 2.11 |
24
+ | 100 KiB | 18.02 | 15.25 | 23.05 |
25
+ | 500 KiB | 90.98 | 96.41 | 118.39 |
26
+
27
+ Median milliseconds per file; lower is better. 5 timed samples after warmup.
28
+ Sizes are TypeScript-only browser bundles, minified with Bun; gzip uses level 9. Theme CSS is excluded.
29
+ Loading and initialization are excluded. Each library highlights the same generated TypeScript
30
+ into HTML using an explicit language. Grammars and HTML output differ; this is not a measure
31
+ of highlighting quality or browser rendering speed. Results vary by machine and workload.
32
+ <!-- benchmark:end -->
33
+
34
+ Run `pnpm --filter sugar-high benchmark:large --write` to refresh this table and the
35
+ [website comparison](https://sugar-high.vercel.app/#benchmarks) from the same measurement.
36
+ See [benchmark methodology and options](../../docs/BENCHMARK.md).
37
+
12
38
  ## Install
13
39
 
14
40
  ```sh
15
41
  npm install sugar-high
16
42
  ```
17
43
 
44
+ ## Agent skill
45
+
46
+ Install Sugar High guidance for an AI coding agent with the Skills CLI:
47
+
48
+ ```sh
49
+ npx skills add huozhi/sugar-high --skill sugar-high
50
+ ```
51
+
18
52
  ## Highlight code
19
53
 
20
54
  ```js
@@ -23,6 +57,28 @@ import { highlight } from 'sugar-high'
23
57
  const html = highlight('const ready = true')
24
58
  ```
25
59
 
60
+ ## Experimental WebGPU highlighting
61
+
62
+ Use the opt-in `sugar-high/gpu` entry for asynchronous, language-agnostic highlighting powered by
63
+ [`gpu-lexer`](https://gpu-lexer.vercel.app). It requires WebGPU and rejects when WebGPU is
64
+ unavailable; importing any other Sugar High entry does not load the GPU model.
65
+
66
+ Install the experimental lexer separately when using this entry:
67
+
68
+ ```sh
69
+ npm install sugar-high gpu-lexer
70
+ ```
71
+
72
+ ```js
73
+ import { highlight, parse } from 'sugar-high/gpu'
74
+
75
+ const parsed = await parse(source)
76
+ const html = await highlight(source)
77
+ ```
78
+
79
+ GPU labels map onto Sugar High's existing token classes, themes, `cx`, `mark`, and `markLine`
80
+ hooks. The model infers syntax without a language option and may differ from grammar-based output.
81
+
26
82
  JavaScript, including JSX, is the default. Pass a canonical name for another built-in language:
27
83
 
28
84
  ```js
package/lib/core.js CHANGED
@@ -110,6 +110,10 @@ function tokenize(code, options) {
110
110
  if (isWord(curr)) {
111
111
  const start = i++
112
112
  while (i < code.length && isWord(code[i])) i++
113
+ if (/^\d/.test(curr) && code[i] === '.' && /\d/.test(code[i + 1] || '')) {
114
+ i++
115
+ while (i < code.length && isWord(code[i])) i++
116
+ }
113
117
  const value = code.slice(start, i)
114
118
  const normalized = normalize(value)
115
119
  const type = typeKeywords.has(normalized)
package/lib/gpu.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { DisplayOptions, ParsedCode } from './core.js'
2
+
3
+ /** Parse source code asynchronously with gpu-lexer and return Sugar High's structured format. */
4
+ export function parse(code: string): Promise<ParsedCode>
5
+
6
+ /** Highlight source code asynchronously with WebGPU. */
7
+ export function highlight(code: string, options?: DisplayOptions): Promise<string>
package/lib/gpu.js ADDED
@@ -0,0 +1,85 @@
1
+ // @ts-check
2
+
3
+ import { parse as parseWithGpu } from 'gpu-lexer'
4
+ import {
5
+ assemble,
6
+ render,
7
+ T_CLASS,
8
+ T_COMMENT,
9
+ T_ENTITY,
10
+ T_IDENTIFIER,
11
+ T_KEYWORD,
12
+ T_SIGN,
13
+ T_SPACE,
14
+ T_STRING,
15
+ T_BREAK,
16
+ } from './shared.js'
17
+
18
+ const tokenTypes = Object.freeze({
19
+ plain: T_IDENTIFIER,
20
+ comment: T_COMMENT,
21
+ string: T_STRING,
22
+ number: T_CLASS,
23
+ keyword: T_KEYWORD,
24
+ type: T_CLASS,
25
+ function: T_ENTITY,
26
+ constant: T_CLASS,
27
+ operator: T_SIGN,
28
+ })
29
+
30
+ /**
31
+ * Preserve plain whitespace as Sugar High space and break tokens while mapping
32
+ * the GPU lexer's semantic labels onto the existing theme vocabulary.
33
+ * @param {Array<[number, string]>} tokens
34
+ * @param {string} value
35
+ */
36
+ function appendPlain(tokens, value) {
37
+ for (const part of value.match(/\r\n|\r|\n|[^\S\r\n]+|[^\s\r\n]+/g) || []) {
38
+ const type = part === '\n' || part === '\r' || part === '\r\n'
39
+ ? T_BREAK
40
+ : /^\s/.test(part)
41
+ ? T_SPACE
42
+ : T_IDENTIFIER
43
+ tokens.push([type, part])
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Parse source code with gpu-lexer and return Sugar High's structured format.
49
+ * GPU labels are adapted to the existing token and theme types.
50
+ * @param {string} code
51
+ * @returns {Promise<import('./core.js').ParsedCode>}
52
+ */
53
+ async function parse(code) {
54
+ const spans = await parseWithGpu(code)
55
+ /** @type {Array<[number, string]>} */
56
+ const tokens = []
57
+ let cursor = 0
58
+
59
+ for (const span of spans) {
60
+ const start = Math.max(cursor, Math.min(code.length, span.start))
61
+ const end = Math.max(start, Math.min(code.length, span.end))
62
+ if (start > cursor) appendPlain(tokens, code.slice(cursor, start))
63
+ cursor = start
64
+ if (end === start) continue
65
+
66
+ const value = code.slice(start, end)
67
+ if (span.type === 'plain') appendPlain(tokens, value)
68
+ else tokens.push([tokenTypes[span.type] ?? T_IDENTIFIER, value])
69
+ cursor = end
70
+ }
71
+
72
+ if (cursor < code.length) appendPlain(tokens, code.slice(cursor))
73
+ return assemble(code, tokens)
74
+ }
75
+
76
+ /**
77
+ * Highlight source code asynchronously with WebGPU.
78
+ * @param {string} code
79
+ * @param {import('./core.js').DisplayOptions | undefined} options
80
+ */
81
+ async function highlight(code, options) {
82
+ return render(await parse(code), options)
83
+ }
84
+
85
+ export { highlight, parse }
@@ -283,6 +283,7 @@ function tokenize(code, options) {
283
283
  /** @type {0 | 1 | 2} 0 = none; 1 = inside `<open`; 2 = inside `</close` */
284
284
  let __jsxTag = 0
285
285
  let __jsxExpr = false
286
+ let __jsxTagExpr = 0
286
287
 
287
288
  /** Nested `<open>…</open>` depth (content between tags, including nested elements). */
288
289
  let __jsxStack = 0
@@ -323,6 +324,8 @@ function tokenize(code, options) {
323
324
  // classify jsx open tag
324
325
  if ((lastToken === '<' || lastToken === '</'))
325
326
  return T_ENTITY
327
+ if (!__jsxTagExpr && /^\s+$/.test(tokens[tokens.length - 1]?.[1] || ''))
328
+ return T_PROPERTY
326
329
  }
327
330
  }
328
331
  // Then determine if they're jsx literals
@@ -343,7 +346,7 @@ function tokenize(code, options) {
343
346
  } else if (token.split('').every(isSign)) {
344
347
  return T_SIGN
345
348
  } else if (isCls(token)) {
346
- return inJsxTag() ? T_IDENTIFIER : T_CLS_NUMBER
349
+ return inJsxTag() && !__jsxTagExpr ? T_IDENTIFIER : T_CLS_NUMBER
347
350
  } else {
348
351
  if (isIdentifier(token)) {
349
352
  const isLastPropDot = last[1] === '.' && isIdentifier(beforeLast[1])
@@ -372,6 +375,10 @@ function tokenize(code, options) {
372
375
  beforeLast = last
373
376
  last = pair
374
377
  }
378
+ if (inJsxTag() && type === T_SIGN) {
379
+ if (current === '{') __jsxTagExpr++
380
+ if (current === '}') __jsxTagExpr--
381
+ }
375
382
  tokens.push(pair)
376
383
  }
377
384
  current = ''
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sugar-high",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/huozhi/sugar-high.git",
@@ -18,6 +18,10 @@
18
18
  "types": "./lib/core.d.ts",
19
19
  "default": "./lib/core.js"
20
20
  },
21
+ "./gpu": {
22
+ "types": "./lib/gpu.d.ts",
23
+ "default": "./lib/gpu.js"
24
+ },
21
25
  "./lang": {
22
26
  "types": "./lib/lang.d.ts",
23
27
  "default": "./lib/lang.js"
@@ -34,9 +38,18 @@
34
38
  "license": "MIT",
35
39
  "devDependencies": {
36
40
  "@types/node": "22.12.0",
41
+ "gpu-lexer": "0.0.2",
37
42
  "typescript": "6.0.2",
38
43
  "vitest": "^3.0.2"
39
44
  },
45
+ "peerDependencies": {
46
+ "gpu-lexer": ">=0.0.2"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "gpu-lexer": {
50
+ "optional": true
51
+ }
52
+ },
40
53
  "scripts": {
41
54
  "test": "vitest",
42
55
  "build": "echo 'package requires no build'",