sugar-high 0.9.5 → 1.1.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 +33 -55
- package/lib/index.d.ts +9 -0
- package/lib/index.js +216 -33
- package/lib/presets/index.d.ts +7 -2
- package/lib/presets/index.js +3 -0
- package/lib/presets/lang/c.js +17 -0
- package/lib/presets/lang/clike-base.js +12 -0
- package/lib/presets/lang/go.js +16 -0
- package/lib/presets/lang/java.js +17 -0
- package/lib/presets/lang/rust.js +35 -0
- package/package.json +12 -8
package/README.md
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# Sugar High
|
|
2
2
|
|
|
3
|
-
[![
|
|
3
|
+
[![npm][npm-badge]][npm]
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Super lightweight syntax highlighter for JavaScript and JSX—about **1 kB** minified and gzipped. Works in the browser or any JS runtime that can set HTML strings.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+

|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
### Usage
|
|
9
|
+
## Install
|
|
12
10
|
|
|
13
11
|
```sh
|
|
14
|
-
npm install
|
|
12
|
+
npm install sugar-high
|
|
15
13
|
```
|
|
16
14
|
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
17
|
```js
|
|
18
18
|
import { highlight } from 'sugar-high'
|
|
19
19
|
|
|
@@ -22,26 +22,26 @@ const codeHTML = highlight(code)
|
|
|
22
22
|
document.querySelector('pre > code').innerHTML = codeHTML
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
###
|
|
25
|
+
### Language presets
|
|
26
|
+
|
|
27
|
+
The core highlighter targets JavaScript and JSX. For CSS, Rust, Python, and similar, import a preset from [`sugar-high/presets`](https://github.com/huozhi/sugar-high/tree/main/packages/sugar-high/lib/presets) and pass it into `highlight`:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
import { highlight } from 'sugar-high'
|
|
31
|
+
import { rust } from 'sugar-high/presets'
|
|
32
|
+
|
|
33
|
+
const html = highlight(source, { ...rust })
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
For more language presets and syntax color themes, see **[sugar-high.vercel.app](https://sugar-high.vercel.app/)**.
|
|
26
37
|
|
|
27
|
-
|
|
38
|
+
## Styling
|
|
39
|
+
|
|
40
|
+
Each line is wrapped in `sh__line`. Set **CSS custom properties** `--sh-*` on an ancestor (for example `:root`) to pick colors—inspect the output or the example below for the variable names you need.
|
|
41
|
+
|
|
42
|
+
Example theme:
|
|
28
43
|
|
|
29
44
|
```css
|
|
30
|
-
/**
|
|
31
|
-
* Types that sugar-high have:
|
|
32
|
-
*
|
|
33
|
-
* identifier
|
|
34
|
-
* keyword
|
|
35
|
-
* string
|
|
36
|
-
* Class, number and null
|
|
37
|
-
* property
|
|
38
|
-
* entity
|
|
39
|
-
* jsx literals
|
|
40
|
-
* sign
|
|
41
|
-
* comment
|
|
42
|
-
* break
|
|
43
|
-
* space
|
|
44
|
-
*/
|
|
45
45
|
:root {
|
|
46
46
|
--sh-class: #2d5e9d;
|
|
47
47
|
--sh-identifier: #354150;
|
|
@@ -55,11 +55,9 @@ Then make your own theme with customized colors by token type and put in global
|
|
|
55
55
|
}
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
###
|
|
59
|
-
|
|
60
|
-
#### Line number
|
|
58
|
+
### Line numbers
|
|
61
59
|
|
|
62
|
-
|
|
60
|
+
Use a `::before` counter on `.sh__line` for gutter numbers:
|
|
63
61
|
|
|
64
62
|
```css
|
|
65
63
|
pre code {
|
|
@@ -75,9 +73,9 @@ pre code {
|
|
|
75
73
|
}
|
|
76
74
|
```
|
|
77
75
|
|
|
78
|
-
###
|
|
76
|
+
### Highlighting a line
|
|
79
77
|
|
|
80
|
-
|
|
78
|
+
Target a line with `.sh__line:nth-child(<n>)` (1-based):
|
|
81
79
|
|
|
82
80
|
```css
|
|
83
81
|
.sh__line:nth-child(5) {
|
|
@@ -85,36 +83,16 @@ Use `.sh__line:nth-child(<line number>)` to highlight specific line.
|
|
|
85
83
|
}
|
|
86
84
|
```
|
|
87
85
|
|
|
88
|
-
|
|
86
|
+
## Remark
|
|
89
87
|
|
|
90
|
-
|
|
88
|
+
Use the [remark plugin](https://sugar-high.vercel.app/remark) to highlight fenced code blocks when processing Markdown with [remark](https://remark.js.org/). Details: [`packages/remark-sugar-high`](https://github.com/huozhi/sugar-high/tree/main/packages/remark-sugar-high).
|
|
91
89
|
|
|
92
|
-
|
|
93
|
-
.sh__token--keyword {
|
|
94
|
-
background: #f47067;
|
|
95
|
-
}
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
### Use With Remark.js
|
|
99
|
-
|
|
100
|
-
[Remark.js](https://remark.js.org/) is a powerful markdown processor, you can use the [sugar-high remark plugin](https://sugar-high.vercel.app/remark) with remark.js to highlight code blocks in markdown.
|
|
101
|
-
|
|
102
|
-
Check out the [readme](https://github.com/huozhi/sugar-high/tree/main/packages/remark-sugar-high) for more details.
|
|
103
|
-
|
|
104
|
-
### LICENSE
|
|
90
|
+
## License
|
|
105
91
|
|
|
106
92
|
MIT
|
|
107
93
|
|
|
108
94
|
<!-- Definitions -->
|
|
109
95
|
|
|
110
|
-
[
|
|
111
|
-
|
|
112
|
-
[build]: https://github.com/huozhi/sugar-high/actions
|
|
113
|
-
|
|
114
|
-
[coverage-badge]: https://badge.fury.io/js/sugar-high.svg
|
|
115
|
-
|
|
116
|
-
[coverage]: https://codecov.io/github/huozhi/sugar-high
|
|
117
|
-
|
|
118
|
-
[downloads-badge]: https://img.shields.io/npm/dm/sugar-high.svg
|
|
96
|
+
[npm-badge]: https://img.shields.io/npm/v/sugar-high.svg
|
|
119
97
|
|
|
120
|
-
[
|
|
98
|
+
[npm]: https://www.npmjs.com/package/sugar-high
|
package/lib/index.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
type HighlightOptions = {
|
|
2
2
|
keywords?: Set<string>
|
|
3
|
+
/**
|
|
4
|
+
* Highlighted as the `class` token type (e.g. built-in types). Checked before `keywords`.
|
|
5
|
+
*/
|
|
6
|
+
typeKeywords?: Set<string>
|
|
3
7
|
onCommentStart?: (curr: string, next: string) => number | boolean
|
|
4
8
|
onCommentEnd?: (curr: string, prev: string) => number | boolean
|
|
9
|
+
/**
|
|
10
|
+
* At `code[i] === "'"`: return how many code units to consume from `i` as one token,
|
|
11
|
+
* or null/undefined or a number below 1 for default JS single-quoted string rules.
|
|
12
|
+
*/
|
|
13
|
+
onQuote?: (curr: string, i: number, code: string) => number | null | undefined
|
|
5
14
|
}
|
|
6
15
|
|
|
7
16
|
export function highlight(code: string, options?: HighlightOptions): string
|
package/lib/index.js
CHANGED
|
@@ -45,6 +45,39 @@ const Keywords_Js = new Set([
|
|
|
45
45
|
'static',
|
|
46
46
|
])
|
|
47
47
|
|
|
48
|
+
const Keywords_Ts = new Set([
|
|
49
|
+
...Keywords_Js,
|
|
50
|
+
'type',
|
|
51
|
+
'interface',
|
|
52
|
+
'enum',
|
|
53
|
+
'implements',
|
|
54
|
+
'readonly',
|
|
55
|
+
'abstract',
|
|
56
|
+
'declare',
|
|
57
|
+
'namespace',
|
|
58
|
+
'module',
|
|
59
|
+
'private',
|
|
60
|
+
'protected',
|
|
61
|
+
'public',
|
|
62
|
+
'override',
|
|
63
|
+
'keyof',
|
|
64
|
+
'infer',
|
|
65
|
+
'is',
|
|
66
|
+
'asserts',
|
|
67
|
+
'satisfies',
|
|
68
|
+
'as',
|
|
69
|
+
'unknown',
|
|
70
|
+
'never',
|
|
71
|
+
'any',
|
|
72
|
+
// Built-in type names (annotations / type positions)
|
|
73
|
+
'number',
|
|
74
|
+
'string',
|
|
75
|
+
'boolean',
|
|
76
|
+
'bigint',
|
|
77
|
+
'symbol',
|
|
78
|
+
'object',
|
|
79
|
+
])
|
|
80
|
+
|
|
48
81
|
const Signs = new Set([
|
|
49
82
|
'+',
|
|
50
83
|
'-',
|
|
@@ -82,6 +115,76 @@ const DefaultOptions = {
|
|
|
82
115
|
onCommentEnd: isCommentEnd_Js,
|
|
83
116
|
}
|
|
84
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Fast, heuristic TS detection. It intentionally prefers speed over full parsing.
|
|
120
|
+
* @param {string} code
|
|
121
|
+
* @returns {boolean}
|
|
122
|
+
*/
|
|
123
|
+
function isLikelyTypeScript(code) {
|
|
124
|
+
let tsScore = 0
|
|
125
|
+
|
|
126
|
+
// TS-only declarations and operators.
|
|
127
|
+
if (/\binterface\s+[A-Za-z_$][\w$]*/.test(code)) tsScore += 2
|
|
128
|
+
if (/\btype\s+[A-Za-z_$][\w$]*\s*=/.test(code)) tsScore += 2
|
|
129
|
+
if (/\benum\s+[A-Za-z_$][\w$]*/.test(code)) tsScore += 2
|
|
130
|
+
if (/\b(?:implements|readonly|declare|namespace|satisfies|infer|keyof|asserts)\b/.test(code)) tsScore += 2
|
|
131
|
+
|
|
132
|
+
// Common TS annotations/signatures.
|
|
133
|
+
if (/:\s*[A-Za-z_$][\w$]*(?:<[^>\n]+>)?(?:\[\])?(?=\s*[,)=;{])/m.test(code)) tsScore += 1
|
|
134
|
+
if (/\b(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*:\s*/.test(code)) tsScore += 1
|
|
135
|
+
if (/\)\s*:\s*[A-Za-z_$][\w$]*(?:<[^>\n]+>)?(?:\[\])?\s*(?:=>|\{)/.test(code)) tsScore += 1
|
|
136
|
+
|
|
137
|
+
return tsScore >= 2
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Detects `<T, U = ...>(` style generic parameter lists so they are not
|
|
142
|
+
* treated as JSX tags.
|
|
143
|
+
* @param {string} code
|
|
144
|
+
* @param {number} startIndex
|
|
145
|
+
* @returns {boolean}
|
|
146
|
+
*/
|
|
147
|
+
function isTypeParameterListStart(code, startIndex) {
|
|
148
|
+
if (code[startIndex] !== '<') return false
|
|
149
|
+
|
|
150
|
+
let depth = 0
|
|
151
|
+
let sawIdentifierStart = false
|
|
152
|
+
|
|
153
|
+
for (let i = startIndex; i < code.length; i++) {
|
|
154
|
+
const ch = code[i]
|
|
155
|
+
|
|
156
|
+
if (ch === '<') {
|
|
157
|
+
depth++
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
if (ch === '>') {
|
|
161
|
+
depth--
|
|
162
|
+
if (depth === 0) {
|
|
163
|
+
let next = i + 1
|
|
164
|
+
while (next < code.length && /\s/.test(code[next])) next++
|
|
165
|
+
if (!(sawIdentifierStart && code[next] === '(')) return false
|
|
166
|
+
|
|
167
|
+
// Focus this heuristic on generic arrow functions:
|
|
168
|
+
// const fn = <T>(arg) => ...
|
|
169
|
+
const tail = code.slice(next, next + 320)
|
|
170
|
+
return /\)\s*(?::[\s\S]{0,120}?)?=>/.test(tail)
|
|
171
|
+
}
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
if (depth === 0) continue
|
|
175
|
+
|
|
176
|
+
if (/[$A-Za-z_]/.test(ch)) {
|
|
177
|
+
sawIdentifierStart = true
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
if (/[\s,\.\=\?\:\|\&\[\]]/.test(ch)) continue
|
|
181
|
+
|
|
182
|
+
return false
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return false
|
|
186
|
+
}
|
|
187
|
+
|
|
85
188
|
/**
|
|
86
189
|
*
|
|
87
190
|
* 0 - identifier
|
|
@@ -200,16 +303,33 @@ function isRegexStart(str) {
|
|
|
200
303
|
|
|
201
304
|
/**
|
|
202
305
|
* @param {string} code
|
|
203
|
-
* @param {{
|
|
306
|
+
* @param {{
|
|
307
|
+
* keywords?: Set<string>
|
|
308
|
+
* typeKeywords?: Set<string>
|
|
309
|
+
* onCommentStart?: (curr: string, next: string) => number | boolean
|
|
310
|
+
* onCommentEnd?: (prev: string, curr: string) => number | boolean
|
|
311
|
+
* onQuote?: (curr: string, i: number, code: string) => number | null | undefined
|
|
312
|
+
* } | undefined} options
|
|
313
|
+
* Optional `onQuote(curr, i, code)` at `code[i] === "'"`: return length to consume from `i` (>= 1),
|
|
314
|
+
* or null/undefined/below 1 for default JS single-quoted strings. No substring allocation.
|
|
204
315
|
* @return {Array<[number, string]>}
|
|
205
316
|
*/
|
|
206
317
|
function tokenize(code, options) {
|
|
318
|
+
const mergedOptions = { ...DefaultOptions, ...options }
|
|
319
|
+
const hasCustomKeywords = !!(options && options.keywords instanceof Set)
|
|
320
|
+
const isTs = isLikelyTypeScript(code)
|
|
321
|
+
const resolvedKeywords = hasCustomKeywords
|
|
322
|
+
? mergedOptions.keywords
|
|
323
|
+
: (isTs ? Keywords_Ts : Keywords_Js)
|
|
324
|
+
|
|
207
325
|
const {
|
|
208
|
-
keywords,
|
|
209
326
|
onCommentStart,
|
|
210
327
|
onCommentEnd,
|
|
211
|
-
} =
|
|
212
|
-
|
|
328
|
+
} = mergedOptions
|
|
329
|
+
|
|
330
|
+
const resolvedTypeKeywords =
|
|
331
|
+
mergedOptions.typeKeywords instanceof Set ? mergedOptions.typeKeywords : null
|
|
332
|
+
|
|
213
333
|
let current = ''
|
|
214
334
|
let type = -1
|
|
215
335
|
/** @type {[number, string]} */
|
|
@@ -219,20 +339,22 @@ function tokenize(code, options) {
|
|
|
219
339
|
/** @type {Array<[number, string]>} */
|
|
220
340
|
const tokens = []
|
|
221
341
|
|
|
222
|
-
/** @type boolean if entered jsx tag, inside <open tag> or </close tag> */
|
|
223
|
-
let __jsxEnter = false
|
|
224
342
|
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
|
|
343
|
+
* TS generics (`Map<string>`) and JSX (`<div>`) share the same `<Name …>` lexical shape. We use
|
|
344
|
+
* one tag-lexer mode (__jsxTag + __jsxStack) for both when we enter it; isTsTypeArgStart and
|
|
345
|
+
* isTypeParameterListStart only decide when *not* to enter (e.g. `foo<T>`, `<T>(x)=>`).
|
|
346
|
+
* __jsxEnter gates that mode so a latched __jsxTag (from `<` in `"a<b"`) does not run the tag
|
|
347
|
+
* lexer until we are in real JSX/TSX surface (not inside strings).
|
|
348
|
+
* @type {boolean}
|
|
349
|
+
*/
|
|
350
|
+
let __jsxEnter = false
|
|
351
|
+
/** @type {0 | 1 | 2} 0 = none; 1 = inside `<open`; 2 = inside `</close` */
|
|
231
352
|
let __jsxTag = 0
|
|
232
353
|
let __jsxExpr = false
|
|
233
354
|
|
|
234
|
-
|
|
355
|
+
/** Nested `<open>…</open>` depth (content between tags, including nested elements). */
|
|
235
356
|
let __jsxStack = 0
|
|
357
|
+
|
|
236
358
|
const __jsxChild = () => __jsxEnter && !__jsxExpr && !__jsxTag
|
|
237
359
|
// < __content__ >
|
|
238
360
|
const inJsxTag = () => __jsxTag && !__jsxChild()
|
|
@@ -266,7 +388,7 @@ function tokenize(code, options) {
|
|
|
266
388
|
const [, lastToken] = last
|
|
267
389
|
if (isIdentifier(token)) {
|
|
268
390
|
// classify jsx open tag
|
|
269
|
-
if ((lastToken === '<' || lastToken === '</'))
|
|
391
|
+
if ((lastToken === '<' || lastToken === '</'))
|
|
270
392
|
return T_ENTITY
|
|
271
393
|
}
|
|
272
394
|
}
|
|
@@ -277,7 +399,9 @@ function tokenize(code, options) {
|
|
|
277
399
|
// Determine strings first before other types
|
|
278
400
|
if (inStringQuotes() || inStrTemplateLiterals()) {
|
|
279
401
|
return T_STRING
|
|
280
|
-
} else if (
|
|
402
|
+
} else if (resolvedTypeKeywords && resolvedTypeKeywords.has(token)) {
|
|
403
|
+
return last[1] === '.' ? T_IDENTIFIER : T_CLS_NUMBER
|
|
404
|
+
} else if (resolvedKeywords.has(token)) {
|
|
281
405
|
return last[1] === '.' ? T_IDENTIFIER : T_KEYWORD
|
|
282
406
|
} else if (isLineBreak) {
|
|
283
407
|
return T_BREAK
|
|
@@ -299,16 +423,16 @@ function tokenize(code, options) {
|
|
|
299
423
|
}
|
|
300
424
|
|
|
301
425
|
/**
|
|
302
|
-
*
|
|
303
|
-
* @param {number} type_
|
|
304
|
-
* @param {string} token_
|
|
426
|
+
*
|
|
427
|
+
* @param {number | undefined} [type_]
|
|
428
|
+
* @param {string | undefined} [token_]
|
|
305
429
|
*/
|
|
306
430
|
const append = (type_, token_) => {
|
|
307
431
|
if (token_) {
|
|
308
432
|
current = token_
|
|
309
433
|
}
|
|
310
434
|
if (current) {
|
|
311
|
-
type = type_
|
|
435
|
+
type = typeof type_ === 'number' ? type_ : classify(current)
|
|
312
436
|
/** @type [number, string] */
|
|
313
437
|
const pair = [type, current]
|
|
314
438
|
if (type !== T_SPACE && type !== T_BREAK) {
|
|
@@ -326,6 +450,30 @@ function tokenize(code, options) {
|
|
|
326
450
|
const p_c = prev + curr // previous and current
|
|
327
451
|
const c_n = curr + next // current and next
|
|
328
452
|
|
|
453
|
+
// onQuote(curr, i, code): length from i; end = i + len (capped).
|
|
454
|
+
if (
|
|
455
|
+
typeof mergedOptions.onQuote === 'function' &&
|
|
456
|
+
curr === "'" &&
|
|
457
|
+
!inStringQuotes() &&
|
|
458
|
+
!inJsxLiterals() &&
|
|
459
|
+
!inStrTemplateLiterals()
|
|
460
|
+
) {
|
|
461
|
+
const rawLen = mergedOptions.onQuote(curr, i, code)
|
|
462
|
+
if (
|
|
463
|
+
typeof rawLen === 'number' &&
|
|
464
|
+
rawLen >= 1 &&
|
|
465
|
+
!Number.isNaN(rawLen)
|
|
466
|
+
) {
|
|
467
|
+
const len = Math.min(rawLen, code.length - i)
|
|
468
|
+
const end = i + len
|
|
469
|
+
append()
|
|
470
|
+
current = code.slice(i, end)
|
|
471
|
+
append(T_IDENTIFIER)
|
|
472
|
+
i = end - 1
|
|
473
|
+
continue
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
329
477
|
// Determine string quotation outside of jsx literals and template literals.
|
|
330
478
|
// Inside jsx literals or template literals, string quotation is still part of it.
|
|
331
479
|
if (isSingleQuotes(curr) && !inJsxLiterals() && !inStrTemplateLiterals()) {
|
|
@@ -469,7 +617,7 @@ function tokenize(code, options) {
|
|
|
469
617
|
if (isSpaces(current)) {
|
|
470
618
|
append()
|
|
471
619
|
}
|
|
472
|
-
|
|
620
|
+
|
|
473
621
|
// Now check if the accumulated token is a property
|
|
474
622
|
const prop = current + curr
|
|
475
623
|
if (isIdentifier(prop)) {
|
|
@@ -483,12 +631,45 @@ function tokenize(code, options) {
|
|
|
483
631
|
|
|
484
632
|
// if it's not in a jsx tag declaration or a string, close child if next is jsx close tag
|
|
485
633
|
if (!__jsxTag && (curr === '<' && isIdentifierChar(next) || c_n === '</')) {
|
|
486
|
-
|
|
634
|
+
let prevNonSpace = i - 1
|
|
635
|
+
while (prevNonSpace >= 0 && /\s/.test(code[prevNonSpace])) prevNonSpace--
|
|
636
|
+
const prevChar = prevNonSpace >= 0 ? code[prevNonSpace] : ''
|
|
637
|
+
|
|
638
|
+
const [lastType, lastTok] = last
|
|
639
|
+
// Without a space before `<`, the LHS is often still in `current` (not flushed), so `last` is stale.
|
|
640
|
+
let typeArgFromPending = false
|
|
641
|
+
let jsxFromPending = false
|
|
642
|
+
if (current && !isSpaces(current)) {
|
|
643
|
+
const w = current
|
|
644
|
+
// Unflushed LHS before `<`: numbers/null/Upper (isCls), and booleans, behave like `foo<` (type
|
|
645
|
+
// args / `<`). Any other keyword (`return`, `void`, …) is JSX-friendly — no keyword allowlist.
|
|
646
|
+
if (isCls(w) || w === 'true' || w === 'false') {
|
|
647
|
+
typeArgFromPending = true
|
|
648
|
+
} else if (resolvedKeywords.has(w) && isIdentifier(w)) {
|
|
649
|
+
jsxFromPending = true
|
|
650
|
+
} else if (isIdentifier(w)) {
|
|
651
|
+
typeArgFromPending = true
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const isTsTypeArgStart =
|
|
656
|
+
curr === '<' &&
|
|
657
|
+
/[$\w\]\)]/.test(prevChar) &&
|
|
658
|
+
(typeArgFromPending ||
|
|
659
|
+
(!jsxFromPending &&
|
|
660
|
+
(lastType === T_IDENTIFIER ||
|
|
661
|
+
lastType === T_CLS_NUMBER ||
|
|
662
|
+
(lastType === T_SIGN && (lastTok === ')' || lastTok === ']')))))
|
|
663
|
+
const isTsGenericStart = curr === '<' && isTypeParameterListStart(code, i)
|
|
664
|
+
if (!isTsTypeArgStart && !isTsGenericStart) {
|
|
665
|
+
__jsxTag = next === '/' ? 2 : 1
|
|
666
|
+
}
|
|
487
667
|
|
|
488
|
-
// current and next char can form a jsx open or close tag
|
|
489
668
|
if (curr === '<' && (next === '/' || isAlpha(next))) {
|
|
490
669
|
if (
|
|
491
|
-
!
|
|
670
|
+
!isTsTypeArgStart &&
|
|
671
|
+
!isTsGenericStart &&
|
|
672
|
+
!inStringContent() &&
|
|
492
673
|
!inJsxLiterals() &&
|
|
493
674
|
!inRegexQuotes()
|
|
494
675
|
) {
|
|
@@ -532,7 +713,7 @@ function tokenize(code, options) {
|
|
|
532
713
|
const isEol = () => isEof() || code[i] === '\n'
|
|
533
714
|
|
|
534
715
|
let foundClose = false
|
|
535
|
-
|
|
716
|
+
|
|
536
717
|
// traverse to find closing regex slash
|
|
537
718
|
for (; !isEol(); i++) {
|
|
538
719
|
if (code[i] === '/' && code[i - 1] !== '\\') {
|
|
@@ -640,7 +821,7 @@ function generate(tokens) {
|
|
|
640
821
|
* @param {any} children
|
|
641
822
|
* @return {{type: string, tagName: string, children: any[], properties: Record<string, string>}}
|
|
642
823
|
*/
|
|
643
|
-
const createLine = (children) =>
|
|
824
|
+
const createLine = (children) =>
|
|
644
825
|
({
|
|
645
826
|
type: 'element',
|
|
646
827
|
tagName: 'span',
|
|
@@ -679,12 +860,12 @@ function generate(tokens) {
|
|
|
679
860
|
/** @type {Array<[number, string]>} */
|
|
680
861
|
const lineTokens = []
|
|
681
862
|
let lastWasBreak = false
|
|
682
|
-
|
|
863
|
+
|
|
683
864
|
for (let i = 0; i < tokens.length; i++) {
|
|
684
865
|
const token = tokens[i]
|
|
685
866
|
const [type, value] = token
|
|
686
867
|
const isLastToken = i === tokens.length - 1
|
|
687
|
-
|
|
868
|
+
|
|
688
869
|
if (type !== T_BREAK) {
|
|
689
870
|
// Divide multi-line token into multi-line code
|
|
690
871
|
if (value.includes('\n')) {
|
|
@@ -709,12 +890,12 @@ function generate(tokens) {
|
|
|
709
890
|
flushLine(lineTokens)
|
|
710
891
|
lineTokens.length = 0
|
|
711
892
|
}
|
|
712
|
-
|
|
893
|
+
|
|
713
894
|
// If this is the last token and it's a break, create an empty line
|
|
714
895
|
if (isLastToken) {
|
|
715
896
|
flushLine([])
|
|
716
897
|
}
|
|
717
|
-
|
|
898
|
+
|
|
718
899
|
lastWasBreak = true
|
|
719
900
|
}
|
|
720
901
|
}
|
|
@@ -727,7 +908,7 @@ function generate(tokens) {
|
|
|
727
908
|
return lines
|
|
728
909
|
}
|
|
729
910
|
|
|
730
|
-
/** @param { className: string, style?: Record<string, string> } */
|
|
911
|
+
/** @param {{ className: string, style?: Record<string, string> }} props */
|
|
731
912
|
const propsToString = (props) => {
|
|
732
913
|
let str = `class="${props.className}"`
|
|
733
914
|
|
|
@@ -758,12 +939,14 @@ function toHtml(lines) {
|
|
|
758
939
|
/**
|
|
759
940
|
*
|
|
760
941
|
* @param {string} code
|
|
761
|
-
* @param {
|
|
762
|
-
*
|
|
763
|
-
*
|
|
942
|
+
* @param {{
|
|
943
|
+
* keywords?: Set<string>
|
|
944
|
+
* typeKeywords?: Set<string>
|
|
764
945
|
* onCommentStart?: (curr: string, next: string) => number | boolean
|
|
765
946
|
* onCommentEnd?: (curr: string, prev: string) => number | boolean
|
|
947
|
+
* onQuote?: (curr: string, i: number, code: string) => number | null | undefined
|
|
766
948
|
* } | undefined} options
|
|
949
|
+
* `onQuote` same as `tokenize`.
|
|
767
950
|
* @returns {string}
|
|
768
951
|
*/
|
|
769
952
|
function highlight(code, options) {
|
package/lib/presets/index.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
type LanguageConfig = {
|
|
2
2
|
keywords: Set<string>
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
typeKeywords?: Set<string>
|
|
4
|
+
onCommentStart?(curr: string, next: string): 0 | 1 | 2
|
|
5
|
+
onCommentEnd?(prev: string, curr: string): 0 | 1 | 2
|
|
6
|
+
onQuote?(curr: string, i: number, code: string): number | null | undefined
|
|
5
7
|
}
|
|
6
8
|
|
|
7
9
|
export const css: LanguageConfig
|
|
8
10
|
export const rust: LanguageConfig
|
|
9
11
|
export const python: LanguageConfig
|
|
12
|
+
export const c: LanguageConfig
|
|
13
|
+
export const go: LanguageConfig
|
|
14
|
+
export const java: LanguageConfig
|
package/lib/presets/index.js
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { onCommentStart, onCommentEnd } from './clike-base.js'
|
|
3
|
+
|
|
4
|
+
export const typeKeywords = new Set([
|
|
5
|
+
'void', 'char', 'short', 'int', 'long', 'float', 'double', 'signed', 'unsigned',
|
|
6
|
+
'_Bool', '_Complex', '_Imaginary',
|
|
7
|
+
])
|
|
8
|
+
|
|
9
|
+
export const keywords = new Set([
|
|
10
|
+
'auto', 'break', 'case', 'const', 'continue', 'default', 'do', 'else', 'enum',
|
|
11
|
+
'extern', 'for', 'goto', 'if', 'inline', 'register', 'restrict', 'return',
|
|
12
|
+
'sizeof', 'static', 'struct', 'switch', 'typedef', 'union', 'volatile', 'while',
|
|
13
|
+
'_Alignas', '_Alignof', '_Atomic', '_Generic', '_Noreturn', '_Static_assert',
|
|
14
|
+
'_Thread_local',
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
export { onCommentStart, onCommentEnd }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
export const onCommentStart = (currentChar, nextChar) => {
|
|
3
|
+
const pair = currentChar + nextChar
|
|
4
|
+
if (pair === '//') return 1
|
|
5
|
+
if (pair === '/*') return 1
|
|
6
|
+
return 0
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const onCommentEnd = (prevChar, currChar) => {
|
|
10
|
+
if (currChar === '\n') return 1
|
|
11
|
+
return prevChar + currChar === '*/' ? 1 : 0
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { onCommentStart, onCommentEnd } from './clike-base.js'
|
|
3
|
+
|
|
4
|
+
export const typeKeywords = new Set([
|
|
5
|
+
'bool', 'byte', 'complex64', 'complex128', 'error', 'float32', 'float64', 'int',
|
|
6
|
+
'int8', 'int16', 'int32', 'int64', 'rune', 'string', 'uint', 'uint8', 'uint16',
|
|
7
|
+
'uint32', 'uint64', 'uintptr',
|
|
8
|
+
])
|
|
9
|
+
|
|
10
|
+
export const keywords = new Set([
|
|
11
|
+
'break', 'case', 'chan', 'const', 'continue', 'default', 'defer', 'else',
|
|
12
|
+
'fallthrough', 'for', 'func', 'go', 'goto', 'if', 'import', 'interface', 'map',
|
|
13
|
+
'package', 'range', 'return', 'select', 'struct', 'switch', 'type', 'var',
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
export { onCommentStart, onCommentEnd }
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { onCommentStart, onCommentEnd } from './clike-base.js'
|
|
3
|
+
|
|
4
|
+
export const typeKeywords = new Set([
|
|
5
|
+
'boolean', 'byte', 'char', 'double', 'float', 'int', 'long', 'short', 'void',
|
|
6
|
+
])
|
|
7
|
+
|
|
8
|
+
export const keywords = new Set([
|
|
9
|
+
'abstract', 'assert', 'break', 'case', 'catch', 'class', 'const', 'continue',
|
|
10
|
+
'default', 'do', 'else', 'enum', 'extends', 'final', 'finally', 'for', 'goto',
|
|
11
|
+
'if', 'implements', 'import', 'instanceof', 'interface', 'native', 'new',
|
|
12
|
+
'package', 'private', 'protected', 'public', 'return', 'static', 'strictfp',
|
|
13
|
+
'super', 'switch', 'synchronized', 'this', 'throw', 'throws', 'transient',
|
|
14
|
+
'try', 'volatile', 'while',
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
export { onCommentStart, onCommentEnd }
|
package/lib/presets/lang/rust.js
CHANGED
|
@@ -6,3 +6,38 @@ export const keywords = new Set([
|
|
|
6
6
|
'async', 'await', 'dyn', 'abstract', 'become', 'box', 'do', 'final', 'macro', 'override',
|
|
7
7
|
'priv', 'typeof', 'unsized', 'virtual', 'yield', 'try',
|
|
8
8
|
])
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} curr `code[i]` (apostrophe)
|
|
12
|
+
* @param {number} i
|
|
13
|
+
* @param {string} code
|
|
14
|
+
* @returns {number} code units to consume from `i`
|
|
15
|
+
*/
|
|
16
|
+
export function onQuote(curr, i, code) {
|
|
17
|
+
if (curr !== "'" || i + 1 >= code.length) return 1
|
|
18
|
+
const n1 = code[i + 1]
|
|
19
|
+
if (n1 === '\\') {
|
|
20
|
+
let j = i + 2
|
|
21
|
+
while (j < code.length) {
|
|
22
|
+
if (code[j] === '\\') {
|
|
23
|
+
j += 2
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
if (code[j] === "'") return j - i + 1
|
|
27
|
+
j++
|
|
28
|
+
}
|
|
29
|
+
return code.length - i
|
|
30
|
+
}
|
|
31
|
+
if (n1 === '_') {
|
|
32
|
+
return 2
|
|
33
|
+
}
|
|
34
|
+
if (/[a-zA-Z]/.test(n1)) {
|
|
35
|
+
let j = i + 2
|
|
36
|
+
while (j < code.length && /[a-zA-Z0-9_]/.test(code[j])) j++
|
|
37
|
+
if (j < code.length && code[j] === "'") {
|
|
38
|
+
return j - i + 1
|
|
39
|
+
}
|
|
40
|
+
return j - i
|
|
41
|
+
}
|
|
42
|
+
return 1
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sugar-high",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/huozhi/sugar-high.git",
|
|
7
|
+
"directory": "packages/sugar-high"
|
|
8
|
+
},
|
|
4
9
|
"type": "module",
|
|
5
10
|
"types": "./lib/index.d.ts",
|
|
6
11
|
"main": "./lib/index.js",
|
|
@@ -19,14 +24,13 @@
|
|
|
19
24
|
"lib"
|
|
20
25
|
],
|
|
21
26
|
"license": "MIT",
|
|
22
|
-
"devDependencies": {
|
|
23
|
-
"@types/node": "22.12.0",
|
|
24
|
-
"typescript": "5.7.3",
|
|
25
|
-
"vitest": "^3.0.2"
|
|
26
|
-
},
|
|
27
|
-
"packageManager": "pnpm@8.7.1",
|
|
28
27
|
"scripts": {
|
|
29
28
|
"test": "vitest",
|
|
30
29
|
"build": "echo 'package requires no build'"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "22.12.0",
|
|
33
|
+
"typescript": "6.0.2",
|
|
34
|
+
"vitest": "^3.0.2"
|
|
31
35
|
}
|
|
32
|
-
}
|
|
36
|
+
}
|