sugar-high 2.4.1 → 2.5.1

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
@@ -9,32 +9,6 @@ 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
-
38
12
  ## Install
39
13
 
40
14
  ```sh
@@ -57,28 +31,6 @@ import { highlight } from 'sugar-high'
57
31
  const html = highlight('const ready = true')
58
32
  ```
59
33
 
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
-
82
34
  JavaScript, including JSX, is the default. Pass a canonical name for another built-in language:
83
35
 
84
36
  ```js
@@ -112,11 +64,12 @@ behavior.
112
64
  ## Built-in languages
113
65
 
114
66
  `javascript`, `typescript`, `css`, `python`, `c`, `go`, `java`, `rust`, `json`, `diff`, `shell`,
115
- `cpp`, `csharp`, `sql`, `html`, `yaml`, `markdown`, `plaintext`, `ruby`, `kotlin`, `swift`, `php`,
67
+ `cpp`, `csharp`, `sql`, `html`, `vue`, `svelte`, `yaml`, `markdown`, `plaintext`, `ruby`, `kotlin`, `swift`, `php`,
116
68
  `toml`, `powershell`, `dockerfile`, `graphql`, `hcl`, `zig`, and `lua`.
117
69
 
118
70
  Related dialects share one implementation: JavaScript includes JSX, TypeScript includes TSX, JSON
119
- includes JSONC comments, Shell includes sh/Bash/Zsh, and HCL includes Terraform.
71
+ includes JSONC comments, Shell includes sh/Bash/Zsh, HTML currently provides the base highlighting
72
+ for Vue and Svelte, and HCL includes Terraform.
120
73
 
121
74
  ## Composable core
122
75
 
@@ -266,6 +219,54 @@ See [`docs/API.md`](https://github.com/huozhi/sugar-high/blob/main/docs/API.md)
266
219
  options, and lower-level functions. Upgrading from v1? Read the
267
220
  [v2 migration guide](https://github.com/huozhi/sugar-high/blob/main/docs/MIGRATION.md).
268
221
 
222
+ ## Experimental WebGPU highlighting
223
+
224
+ Use the opt-in `sugar-high/gpu` entry for asynchronous, language-agnostic highlighting powered by
225
+ [`gpu-lexer`](https://gpu-lexer.vercel.app). It requires WebGPU and rejects when WebGPU is
226
+ unavailable; importing any other Sugar High entry does not load the GPU model.
227
+
228
+ Install the experimental lexer separately when using this entry:
229
+
230
+ ```sh
231
+ npm install sugar-high gpu-lexer
232
+ ```
233
+
234
+ ```js
235
+ import { highlight, parse } from 'sugar-high/gpu'
236
+
237
+ const parsed = await parse(source)
238
+ const html = await highlight(source)
239
+ ```
240
+
241
+ GPU labels map onto Sugar High's existing token classes, themes, `cx`, `mark`, and `markLine`
242
+ hooks. The model infers syntax without a language option and may differ from grammar-based output.
243
+
244
+ ## Benchmarks
245
+
246
+ Sugar High, PrismJS, and highlight.js highlighting the same generated TypeScript files:
247
+
248
+ <!-- benchmark:start -->
249
+ Measured 2026-09-04 with Node v24.18.0, darwin arm64, Apple M4 Pro.
250
+
251
+ | TypeScript | Sugar High 2.2.2 | PrismJS 1.30.0 | highlight.js 11.12.0 |
252
+ | --- | ---: | ---: | ---: |
253
+ | Minified (KiB) | 9.90 | 14.63 | 29.54 |
254
+ | Gzip (KiB) | 4.35 | 5.47 | 11.12 |
255
+ | 11 KiB | 1.78 | 1.18 | 2.11 |
256
+ | 100 KiB | 18.02 | 15.25 | 23.05 |
257
+ | 500 KiB | 90.98 | 96.41 | 118.39 |
258
+
259
+ Median milliseconds per file; lower is better. 5 timed samples after warmup.
260
+ Sizes are TypeScript-only browser bundles, minified with Bun; gzip uses level 9. Theme CSS is excluded.
261
+ Loading and initialization are excluded. Each library highlights the same generated TypeScript
262
+ into HTML using an explicit language. Grammars and HTML output differ; this is not a measure
263
+ of highlighting quality or browser rendering speed. Results vary by machine and workload.
264
+ <!-- benchmark:end -->
265
+
266
+ Run `pnpm --filter sugar-high benchmark:large --write` to refresh this table and the
267
+ [website comparison](https://sugar-high.vercel.app/#benchmarks) from the same measurement.
268
+ See [benchmark methodology and options](../../docs/BENCHMARK.md).
269
+
269
270
  ## License
270
271
 
271
272
  MIT
package/lib/index.d.ts CHANGED
@@ -23,6 +23,8 @@ export type LanguageName =
23
23
  | 'csharp'
24
24
  | 'sql'
25
25
  | 'html'
26
+ | 'vue'
27
+ | 'svelte'
26
28
  | 'yaml'
27
29
  | 'markdown'
28
30
  | 'plaintext'
package/lib/lang/css.js CHANGED
@@ -36,17 +36,23 @@ const isHyphen = ([type, value]) => type === T_SIGN && value === '-'
36
36
  * @param {Array<[number, string]>} tokens
37
37
  */
38
38
  const mergeDashedNames = (tokens) => {
39
+ /** @type {Array<[number, string]>} */
40
+ const merged = []
39
41
  for (let index = 0; index < tokens.length; index++) {
40
42
  let firstWord = index
41
43
  let end = index
42
44
 
43
45
  if (isHyphen(tokens[end])) {
44
46
  while (tokens[end] && isHyphen(tokens[end])) end++
45
- if (!tokens[end] || !isNameStart(tokens[end])) continue
47
+ if (!tokens[end] || !isNameStart(tokens[end])) {
48
+ merged.push(tokens[index])
49
+ continue
50
+ }
46
51
  firstWord = end++
47
52
  } else if (isNameStart(tokens[end])) {
48
53
  end++
49
54
  } else {
55
+ merged.push(tokens[index])
50
56
  continue
51
57
  }
52
58
 
@@ -62,10 +68,15 @@ const mergeDashedNames = (tokens) => {
62
68
  end++
63
69
  }
64
70
 
65
- if (!dashed) continue
71
+ if (!dashed) {
72
+ merged.push(tokens[index])
73
+ continue
74
+ }
66
75
  const name = tokens.slice(index, end).map(([, value]) => value).join('')
67
- tokens.splice(index, end - index, [tokens[firstWord][0], name])
76
+ merged.push([tokens[firstWord][0], name])
77
+ index = end - 1
68
78
  }
79
+ return merged
69
80
  }
70
81
 
71
82
  /** Return true when a colon belongs to a nested selector instead of a declaration. */
@@ -91,29 +102,37 @@ const opensBlock = (tokens, start) => {
91
102
  * @param {import('../core.js').ParseOptions} options
92
103
  */
93
104
  export const tokenize = (code, options) => {
94
- const tokens = tokenizePlain(code, { ...options, tokenize: undefined })
95
- mergeDashedNames(tokens)
105
+ const tokens = mergeDashedNames(tokenizePlain(code, { ...options, tokenize: undefined }))
106
+ /** @type {Array<[number, string]>} */
107
+ const output = []
96
108
  let blockDepth = 0
97
109
  let declarationStart = false
98
110
 
99
111
  for (let index = 0; index < tokens.length; index++) {
100
- const [type, value] = tokens[index]
112
+ const token = tokens[index]
113
+ const [type, value] = token
101
114
 
102
115
  if (type === T_SIGN && value === '{') {
103
116
  blockDepth++
104
117
  declarationStart = true
118
+ output.push(token)
105
119
  continue
106
120
  }
107
121
  if (type === T_SIGN && value === '}') {
108
122
  blockDepth--
109
123
  declarationStart = false
124
+ output.push(token)
110
125
  continue
111
126
  }
112
127
  if (type === T_SIGN && value === ';') {
113
128
  declarationStart = blockDepth > 0
129
+ output.push(token)
130
+ continue
131
+ }
132
+ if (!declarationStart || isIgnored(type)) {
133
+ output.push(token)
114
134
  continue
115
135
  }
116
- if (!declarationStart || isIgnored(type)) continue
117
136
 
118
137
  const propertyStart = index
119
138
  let propertyEnd = index
@@ -133,10 +152,13 @@ export const tokenize = (code, options) => {
133
152
  .slice(propertyStart, propertyEnd)
134
153
  .map(([, part]) => part)
135
154
  .join('')
136
- tokens.splice(propertyStart, propertyEnd - propertyStart, [T_PROPERTY, property])
155
+ output.push([T_PROPERTY, property])
156
+ index = propertyEnd - 1
157
+ } else {
158
+ output.push(token)
137
159
  }
138
160
  declarationStart = false
139
161
  }
140
162
 
141
- return tokens
163
+ return output
142
164
  }
@@ -0,0 +1,112 @@
1
+ // @ts-check
2
+ import { tokenize as tokenizeCss } from './css.js'
3
+ import { tokenize as tokenizeJavaScript } from './javascript.js'
4
+
5
+ const htmlOptions = {
6
+ keywords: new Set(),
7
+ jsx: true,
8
+ regex: false,
9
+ templateStrings: false,
10
+ onCommentStart: (_currentChar, _nextChar, index, code) => code.startsWith('<!--', index) ? 2 : 0,
11
+ onCommentEnd: (_prevChar, _currChar, index, code) => code.slice(index - 2, index + 1) === '-->' ? 2 : 0,
12
+ }
13
+
14
+ const tokenizeHtml = (code) => tokenizeJavaScript(code, htmlOptions)
15
+
16
+ const embeddedOpeningTag = /^<\s*(script|style)\b/i
17
+ const embeddedClosingTag = (tag) => new RegExp(`</\\s*${tag}\\s*>`, 'ig')
18
+
19
+ /** Find the end of a markup tag without treating `>` inside quotes as its end. */
20
+ function findTagEnd(code, start) {
21
+ let quote = ''
22
+ for (let index = start; index < code.length; index++) {
23
+ const character = code[index]
24
+ if (quote) {
25
+ if (character === quote) quote = ''
26
+ } else if (character === '"' || character === "'") {
27
+ quote = character
28
+ } else if (character === '>') {
29
+ return index + 1
30
+ }
31
+ }
32
+ return -1
33
+ }
34
+
35
+ /** Find real script/style blocks while ignoring comments and quoted attributes. */
36
+ function findEmbeddedBlocks(code) {
37
+ const blocks = []
38
+ /** @type {Set<string>} */
39
+ const unclosed = new Set()
40
+ let index = 0
41
+ while (index < code.length) {
42
+ if (code.startsWith('<!--', index)) {
43
+ const commentEnd = code.indexOf('-->', index + 4)
44
+ index = commentEnd === -1 ? code.length : commentEnd + 3
45
+ continue
46
+ }
47
+ if (code[index] !== '<') {
48
+ index++
49
+ continue
50
+ }
51
+
52
+ const openEnd = findTagEnd(code, index)
53
+ if (openEnd === -1) break
54
+ const opening = code.slice(index, openEnd)
55
+ const match = opening.match(embeddedOpeningTag)
56
+ if (!match) {
57
+ index = openEnd
58
+ continue
59
+ }
60
+
61
+ const tag = match[1].toLowerCase()
62
+ if (unclosed.has(tag)) {
63
+ index = openEnd
64
+ continue
65
+ }
66
+ const closing = embeddedClosingTag(tag)
67
+ closing.lastIndex = openEnd
68
+ const closeMatch = closing.exec(code)
69
+ if (!closeMatch) {
70
+ unclosed.add(tag)
71
+ index = openEnd
72
+ continue
73
+ }
74
+ blocks.push({ start: index, openEnd, closeStart: closeMatch.index, end: closing.lastIndex, tag })
75
+ index = closing.lastIndex
76
+ }
77
+ return blocks
78
+ }
79
+
80
+ /**
81
+ * Tokenize HTML-like files while delegating script and style bodies to their
82
+ * existing language presets. Framework-specific template syntax remains HTML-like.
83
+ * @param {string} code
84
+ * @returns {Array<[number, string]>}
85
+ */
86
+ export function tokenizeEmbeddedHtml(code) {
87
+ const blocks = findEmbeddedBlocks(code)
88
+ if (!blocks.length) return tokenizeHtml(code)
89
+
90
+ /** @type {Array<[number, string]>} */
91
+ const tokens = []
92
+ /** @param {Array<[number, string]>} part */
93
+ const append = (part) => {
94
+ for (let index = 0; index < part.length; index++) tokens.push(part[index])
95
+ }
96
+ let cursor = 0
97
+
98
+ for (const block of blocks) {
99
+ const { start, openEnd, closeStart, end, tag } = block
100
+
101
+ append(tokenizeHtml(code.slice(cursor, start)))
102
+ append(tokenizeHtml(code.slice(start, openEnd)))
103
+ append(tag === 'style'
104
+ ? tokenizeCss(code.slice(openEnd, closeStart))
105
+ : tokenizeJavaScript(code.slice(openEnd, closeStart), { jsx: false }))
106
+ append(tokenizeHtml(code.slice(closeStart, end)))
107
+ cursor = end
108
+ }
109
+
110
+ append(tokenizeHtml(code.slice(cursor)))
111
+ return tokens
112
+ }
package/lib/lang/html.js CHANGED
@@ -1,11 +1,11 @@
1
1
  // @ts-check
2
- import { tokenize as tokenizeJavaScript } from '../presets/javascript-runtime.js'
2
+ import { tokenizeEmbeddedHtml } from './embedded-html.js'
3
3
 
4
4
  export const keywords = new Set([])
5
5
  export const jsx = true
6
6
  export const regex = false
7
7
  export const templateStrings = false
8
- export const tokenize = tokenizeJavaScript
8
+ export const tokenize = tokenizeEmbeddedHtml
9
9
 
10
10
  export const onCommentStart = (_currentChar, _nextChar, index, code) =>
11
11
  code.startsWith('<!--', index) ? 2 : 0
@@ -0,0 +1,2 @@
1
+ export * from './html.js'
2
+ export { tokenizeEmbeddedHtml as tokenize } from './embedded-html.js'
@@ -0,0 +1,2 @@
1
+ export * from './html.js'
2
+ export { tokenizeEmbeddedHtml as tokenize } from './embedded-html.js'
@@ -0,0 +1,2 @@
1
+ export * from './html.js'
2
+ export { tokenizeEmbeddedHtml as tokenize } from './embedded-html.js'
@@ -0,0 +1,2 @@
1
+ export * from './html.js'
2
+ export { tokenizeEmbeddedHtml as tokenize } from './embedded-html.js'
package/lib/lang.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import {
4
4
  c, cpp, csharp, css, diff, dockerfile, go, graphql, hcl, html, java, javascript, json,
5
5
  kotlin, lua, markdown, nonJavaScript, php, plaintext, powershell, python, ruby, rust, shell, sql,
6
- swift, toml, typescript, yaml, zig,
6
+ svelte, swift, toml, typescript, vue, yaml, zig,
7
7
  } from './presets/configs.js'
8
8
 
9
9
  /**
@@ -33,6 +33,8 @@ const languages = [
33
33
  { id: 'csharp', extension: 'cs', aliases: ['c#', 'cs', 'dotnet'], config: nonJavaScript(csharp) },
34
34
  { id: 'sql', extension: 'sql', aliases: [], config: nonJavaScript(sql) },
35
35
  { id: 'html', extension: 'html', aliases: ['htm', 'xml'], config: html },
36
+ { id: 'vue', extension: 'vue', aliases: [], config: vue },
37
+ { id: 'svelte', extension: 'svelte', aliases: [], config: svelte },
36
38
  { id: 'yaml', extension: 'yaml', aliases: ['yml'], config: nonJavaScript(yaml) },
37
39
  { id: 'markdown', extension: 'md', aliases: ['md', 'mdx'], config: nonJavaScript(markdown) },
38
40
  { id: 'plaintext', extension: 'txt', aliases: ['text', 'plain'], config: nonJavaScript(plaintext) },
@@ -27,6 +27,8 @@ import * as sql from '../lang/sql.js'
27
27
  import * as swift from '../lang/swift.js'
28
28
  import * as toml from '../lang/toml.js'
29
29
  import * as typescript from '../lang/typescript.js'
30
+ import * as svelte from '../lang/svelte.js'
31
+ import * as vue from '../lang/vue.js'
30
32
  import * as yaml from '../lang/yaml.js'
31
33
  import * as zig from '../lang/zig.js'
32
34
 
@@ -61,6 +63,8 @@ function createConfigs() {
61
63
  csharp: nonJavaScript(csharp),
62
64
  sql: nonJavaScript(sql),
63
65
  html,
66
+ vue,
67
+ svelte,
64
68
  yaml: nonJavaScript(yaml),
65
69
  markdown: nonJavaScript(markdown),
66
70
  plaintext: nonJavaScript(plaintext),
@@ -88,5 +92,5 @@ function configFor(name) {
88
92
  export {
89
93
  c, configFor, configs, cpp, csharp, css, diff, dockerfile, go, graphql, hcl, html, java,
90
94
  javascript, json, kotlin, lua, markdown, nonJavaScript, php, plaintext, powershell, python, ruby,
91
- rust, shell, sql, swift, toml, typescript, yaml, zig,
95
+ rust, shell, sql, svelte, swift, toml, typescript, vue, yaml, zig,
92
96
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sugar-high",
3
- "version": "2.4.1",
3
+ "version": "2.5.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/huozhi/sugar-high.git",