sugar-high 0.9.5 → 1.0.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 +5 -0
- package/lib/index.js +202 -33
- package/lib/presets/index.d.ts +3 -2
- package/lib/presets/lang/rust.js +35 -0
- package/package.json +6 -7
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
|
@@ -2,6 +2,11 @@ type HighlightOptions = {
|
|
|
2
2
|
keywords?: Set<string>
|
|
3
3
|
onCommentStart?: (curr: string, next: string) => number | boolean
|
|
4
4
|
onCommentEnd?: (curr: string, prev: string) => number | boolean
|
|
5
|
+
/**
|
|
6
|
+
* At `code[i] === "'"`: return how many code units to consume from `i` as one token,
|
|
7
|
+
* or null/undefined or a number below 1 for default JS single-quoted string rules.
|
|
8
|
+
*/
|
|
9
|
+
onQuote?: (curr: string, i: number, code: string) => number | null | undefined
|
|
5
10
|
}
|
|
6
11
|
|
|
7
12
|
export function highlight(code: string, options?: HighlightOptions): string
|
package/lib/index.js
CHANGED
|
@@ -45,6 +45,32 @@ 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
|
+
])
|
|
73
|
+
|
|
48
74
|
const Signs = new Set([
|
|
49
75
|
'+',
|
|
50
76
|
'-',
|
|
@@ -82,6 +108,76 @@ const DefaultOptions = {
|
|
|
82
108
|
onCommentEnd: isCommentEnd_Js,
|
|
83
109
|
}
|
|
84
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Fast, heuristic TS detection. It intentionally prefers speed over full parsing.
|
|
113
|
+
* @param {string} code
|
|
114
|
+
* @returns {boolean}
|
|
115
|
+
*/
|
|
116
|
+
function isLikelyTypeScript(code) {
|
|
117
|
+
let tsScore = 0
|
|
118
|
+
|
|
119
|
+
// TS-only declarations and operators.
|
|
120
|
+
if (/\binterface\s+[A-Za-z_$][\w$]*/.test(code)) tsScore += 2
|
|
121
|
+
if (/\btype\s+[A-Za-z_$][\w$]*\s*=/.test(code)) tsScore += 2
|
|
122
|
+
if (/\benum\s+[A-Za-z_$][\w$]*/.test(code)) tsScore += 2
|
|
123
|
+
if (/\b(?:implements|readonly|declare|namespace|satisfies|infer|keyof|asserts)\b/.test(code)) tsScore += 2
|
|
124
|
+
|
|
125
|
+
// Common TS annotations/signatures.
|
|
126
|
+
if (/:\s*[A-Za-z_$][\w$]*(?:<[^>\n]+>)?(?:\[\])?(?=\s*[,)=;{])/m.test(code)) tsScore += 1
|
|
127
|
+
if (/\b(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*:\s*/.test(code)) tsScore += 1
|
|
128
|
+
if (/\)\s*:\s*[A-Za-z_$][\w$]*(?:<[^>\n]+>)?(?:\[\])?\s*(?:=>|\{)/.test(code)) tsScore += 1
|
|
129
|
+
|
|
130
|
+
return tsScore >= 2
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Detects `<T, U = ...>(` style generic parameter lists so they are not
|
|
135
|
+
* treated as JSX tags.
|
|
136
|
+
* @param {string} code
|
|
137
|
+
* @param {number} startIndex
|
|
138
|
+
* @returns {boolean}
|
|
139
|
+
*/
|
|
140
|
+
function isTypeParameterListStart(code, startIndex) {
|
|
141
|
+
if (code[startIndex] !== '<') return false
|
|
142
|
+
|
|
143
|
+
let depth = 0
|
|
144
|
+
let sawIdentifierStart = false
|
|
145
|
+
|
|
146
|
+
for (let i = startIndex; i < code.length; i++) {
|
|
147
|
+
const ch = code[i]
|
|
148
|
+
|
|
149
|
+
if (ch === '<') {
|
|
150
|
+
depth++
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
if (ch === '>') {
|
|
154
|
+
depth--
|
|
155
|
+
if (depth === 0) {
|
|
156
|
+
let next = i + 1
|
|
157
|
+
while (next < code.length && /\s/.test(code[next])) next++
|
|
158
|
+
if (!(sawIdentifierStart && code[next] === '(')) return false
|
|
159
|
+
|
|
160
|
+
// Focus this heuristic on generic arrow functions:
|
|
161
|
+
// const fn = <T>(arg) => ...
|
|
162
|
+
const tail = code.slice(next, next + 320)
|
|
163
|
+
return /\)\s*(?::[\s\S]{0,120}?)?=>/.test(tail)
|
|
164
|
+
}
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
if (depth === 0) continue
|
|
168
|
+
|
|
169
|
+
if (/[$A-Za-z_]/.test(ch)) {
|
|
170
|
+
sawIdentifierStart = true
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
if (/[\s,\.\=\?\:\|\&\[\]]/.test(ch)) continue
|
|
174
|
+
|
|
175
|
+
return false
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return false
|
|
179
|
+
}
|
|
180
|
+
|
|
85
181
|
/**
|
|
86
182
|
*
|
|
87
183
|
* 0 - identifier
|
|
@@ -200,16 +296,29 @@ function isRegexStart(str) {
|
|
|
200
296
|
|
|
201
297
|
/**
|
|
202
298
|
* @param {string} code
|
|
203
|
-
* @param {{
|
|
299
|
+
* @param {{
|
|
300
|
+
* keywords?: Set<string>
|
|
301
|
+
* onCommentStart?: (curr: string, next: string) => number | boolean
|
|
302
|
+
* onCommentEnd?: (prev: string, curr: string) => number | boolean
|
|
303
|
+
* onQuote?: (curr: string, i: number, code: string) => number | null | undefined
|
|
304
|
+
* } | undefined} options
|
|
305
|
+
* Optional `onQuote(curr, i, code)` at `code[i] === "'"`: return length to consume from `i` (>= 1),
|
|
306
|
+
* or null/undefined/below 1 for default JS single-quoted strings. No substring allocation.
|
|
204
307
|
* @return {Array<[number, string]>}
|
|
205
308
|
*/
|
|
206
309
|
function tokenize(code, options) {
|
|
310
|
+
const mergedOptions = { ...DefaultOptions, ...options }
|
|
311
|
+
const hasCustomKeywords = !!(options && options.keywords instanceof Set)
|
|
312
|
+
const isTs = isLikelyTypeScript(code)
|
|
313
|
+
const resolvedKeywords = hasCustomKeywords
|
|
314
|
+
? mergedOptions.keywords
|
|
315
|
+
: (isTs ? Keywords_Ts : Keywords_Js)
|
|
316
|
+
|
|
207
317
|
const {
|
|
208
|
-
keywords,
|
|
209
318
|
onCommentStart,
|
|
210
319
|
onCommentEnd,
|
|
211
|
-
} =
|
|
212
|
-
|
|
320
|
+
} = mergedOptions
|
|
321
|
+
|
|
213
322
|
let current = ''
|
|
214
323
|
let type = -1
|
|
215
324
|
/** @type {[number, string]} */
|
|
@@ -219,20 +328,22 @@ function tokenize(code, options) {
|
|
|
219
328
|
/** @type {Array<[number, string]>} */
|
|
220
329
|
const tokens = []
|
|
221
330
|
|
|
222
|
-
/** @type boolean if entered jsx tag, inside <open tag> or </close tag> */
|
|
223
|
-
let __jsxEnter = false
|
|
224
331
|
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
|
|
332
|
+
* TS generics (`Map<string>`) and JSX (`<div>`) share the same `<Name …>` lexical shape. We use
|
|
333
|
+
* one tag-lexer mode (__jsxTag + __jsxStack) for both when we enter it; isTsTypeArgStart and
|
|
334
|
+
* isTypeParameterListStart only decide when *not* to enter (e.g. `foo<T>`, `<T>(x)=>`).
|
|
335
|
+
* __jsxEnter gates that mode so a latched __jsxTag (from `<` in `"a<b"`) does not run the tag
|
|
336
|
+
* lexer until we are in real JSX/TSX surface (not inside strings).
|
|
337
|
+
* @type {boolean}
|
|
338
|
+
*/
|
|
339
|
+
let __jsxEnter = false
|
|
340
|
+
/** @type {0 | 1 | 2} 0 = none; 1 = inside `<open`; 2 = inside `</close` */
|
|
231
341
|
let __jsxTag = 0
|
|
232
342
|
let __jsxExpr = false
|
|
233
343
|
|
|
234
|
-
|
|
344
|
+
/** Nested `<open>…</open>` depth (content between tags, including nested elements). */
|
|
235
345
|
let __jsxStack = 0
|
|
346
|
+
|
|
236
347
|
const __jsxChild = () => __jsxEnter && !__jsxExpr && !__jsxTag
|
|
237
348
|
// < __content__ >
|
|
238
349
|
const inJsxTag = () => __jsxTag && !__jsxChild()
|
|
@@ -266,7 +377,7 @@ function tokenize(code, options) {
|
|
|
266
377
|
const [, lastToken] = last
|
|
267
378
|
if (isIdentifier(token)) {
|
|
268
379
|
// classify jsx open tag
|
|
269
|
-
if ((lastToken === '<' || lastToken === '</'))
|
|
380
|
+
if ((lastToken === '<' || lastToken === '</'))
|
|
270
381
|
return T_ENTITY
|
|
271
382
|
}
|
|
272
383
|
}
|
|
@@ -277,7 +388,7 @@ function tokenize(code, options) {
|
|
|
277
388
|
// Determine strings first before other types
|
|
278
389
|
if (inStringQuotes() || inStrTemplateLiterals()) {
|
|
279
390
|
return T_STRING
|
|
280
|
-
} else if (
|
|
391
|
+
} else if (resolvedKeywords.has(token)) {
|
|
281
392
|
return last[1] === '.' ? T_IDENTIFIER : T_KEYWORD
|
|
282
393
|
} else if (isLineBreak) {
|
|
283
394
|
return T_BREAK
|
|
@@ -299,16 +410,16 @@ function tokenize(code, options) {
|
|
|
299
410
|
}
|
|
300
411
|
|
|
301
412
|
/**
|
|
302
|
-
*
|
|
303
|
-
* @param {number} type_
|
|
304
|
-
* @param {string} token_
|
|
413
|
+
*
|
|
414
|
+
* @param {number | undefined} [type_]
|
|
415
|
+
* @param {string | undefined} [token_]
|
|
305
416
|
*/
|
|
306
417
|
const append = (type_, token_) => {
|
|
307
418
|
if (token_) {
|
|
308
419
|
current = token_
|
|
309
420
|
}
|
|
310
421
|
if (current) {
|
|
311
|
-
type = type_
|
|
422
|
+
type = typeof type_ === 'number' ? type_ : classify(current)
|
|
312
423
|
/** @type [number, string] */
|
|
313
424
|
const pair = [type, current]
|
|
314
425
|
if (type !== T_SPACE && type !== T_BREAK) {
|
|
@@ -326,6 +437,30 @@ function tokenize(code, options) {
|
|
|
326
437
|
const p_c = prev + curr // previous and current
|
|
327
438
|
const c_n = curr + next // current and next
|
|
328
439
|
|
|
440
|
+
// onQuote(curr, i, code): length from i; end = i + len (capped).
|
|
441
|
+
if (
|
|
442
|
+
typeof mergedOptions.onQuote === 'function' &&
|
|
443
|
+
curr === "'" &&
|
|
444
|
+
!inStringQuotes() &&
|
|
445
|
+
!inJsxLiterals() &&
|
|
446
|
+
!inStrTemplateLiterals()
|
|
447
|
+
) {
|
|
448
|
+
const rawLen = mergedOptions.onQuote(curr, i, code)
|
|
449
|
+
if (
|
|
450
|
+
typeof rawLen === 'number' &&
|
|
451
|
+
rawLen >= 1 &&
|
|
452
|
+
!Number.isNaN(rawLen)
|
|
453
|
+
) {
|
|
454
|
+
const len = Math.min(rawLen, code.length - i)
|
|
455
|
+
const end = i + len
|
|
456
|
+
append()
|
|
457
|
+
current = code.slice(i, end)
|
|
458
|
+
append(T_IDENTIFIER)
|
|
459
|
+
i = end - 1
|
|
460
|
+
continue
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
329
464
|
// Determine string quotation outside of jsx literals and template literals.
|
|
330
465
|
// Inside jsx literals or template literals, string quotation is still part of it.
|
|
331
466
|
if (isSingleQuotes(curr) && !inJsxLiterals() && !inStrTemplateLiterals()) {
|
|
@@ -469,7 +604,7 @@ function tokenize(code, options) {
|
|
|
469
604
|
if (isSpaces(current)) {
|
|
470
605
|
append()
|
|
471
606
|
}
|
|
472
|
-
|
|
607
|
+
|
|
473
608
|
// Now check if the accumulated token is a property
|
|
474
609
|
const prop = current + curr
|
|
475
610
|
if (isIdentifier(prop)) {
|
|
@@ -483,12 +618,45 @@ function tokenize(code, options) {
|
|
|
483
618
|
|
|
484
619
|
// if it's not in a jsx tag declaration or a string, close child if next is jsx close tag
|
|
485
620
|
if (!__jsxTag && (curr === '<' && isIdentifierChar(next) || c_n === '</')) {
|
|
486
|
-
|
|
621
|
+
let prevNonSpace = i - 1
|
|
622
|
+
while (prevNonSpace >= 0 && /\s/.test(code[prevNonSpace])) prevNonSpace--
|
|
623
|
+
const prevChar = prevNonSpace >= 0 ? code[prevNonSpace] : ''
|
|
624
|
+
|
|
625
|
+
const [lastType, lastTok] = last
|
|
626
|
+
// Without a space before `<`, the LHS is often still in `current` (not flushed), so `last` is stale.
|
|
627
|
+
let typeArgFromPending = false
|
|
628
|
+
let jsxFromPending = false
|
|
629
|
+
if (current && !isSpaces(current)) {
|
|
630
|
+
const w = current
|
|
631
|
+
// Unflushed LHS before `<`: numbers/null/Upper (isCls), and booleans, behave like `foo<` (type
|
|
632
|
+
// args / `<`). Any other keyword (`return`, `void`, …) is JSX-friendly — no keyword allowlist.
|
|
633
|
+
if (isCls(w) || w === 'true' || w === 'false') {
|
|
634
|
+
typeArgFromPending = true
|
|
635
|
+
} else if (resolvedKeywords.has(w) && isIdentifier(w)) {
|
|
636
|
+
jsxFromPending = true
|
|
637
|
+
} else if (isIdentifier(w)) {
|
|
638
|
+
typeArgFromPending = true
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const isTsTypeArgStart =
|
|
643
|
+
curr === '<' &&
|
|
644
|
+
/[$\w\]\)]/.test(prevChar) &&
|
|
645
|
+
(typeArgFromPending ||
|
|
646
|
+
(!jsxFromPending &&
|
|
647
|
+
(lastType === T_IDENTIFIER ||
|
|
648
|
+
lastType === T_CLS_NUMBER ||
|
|
649
|
+
(lastType === T_SIGN && (lastTok === ')' || lastTok === ']')))))
|
|
650
|
+
const isTsGenericStart = curr === '<' && isTypeParameterListStart(code, i)
|
|
651
|
+
if (!isTsTypeArgStart && !isTsGenericStart) {
|
|
652
|
+
__jsxTag = next === '/' ? 2 : 1
|
|
653
|
+
}
|
|
487
654
|
|
|
488
|
-
// current and next char can form a jsx open or close tag
|
|
489
655
|
if (curr === '<' && (next === '/' || isAlpha(next))) {
|
|
490
656
|
if (
|
|
491
|
-
!
|
|
657
|
+
!isTsTypeArgStart &&
|
|
658
|
+
!isTsGenericStart &&
|
|
659
|
+
!inStringContent() &&
|
|
492
660
|
!inJsxLiterals() &&
|
|
493
661
|
!inRegexQuotes()
|
|
494
662
|
) {
|
|
@@ -532,7 +700,7 @@ function tokenize(code, options) {
|
|
|
532
700
|
const isEol = () => isEof() || code[i] === '\n'
|
|
533
701
|
|
|
534
702
|
let foundClose = false
|
|
535
|
-
|
|
703
|
+
|
|
536
704
|
// traverse to find closing regex slash
|
|
537
705
|
for (; !isEol(); i++) {
|
|
538
706
|
if (code[i] === '/' && code[i - 1] !== '\\') {
|
|
@@ -640,7 +808,7 @@ function generate(tokens) {
|
|
|
640
808
|
* @param {any} children
|
|
641
809
|
* @return {{type: string, tagName: string, children: any[], properties: Record<string, string>}}
|
|
642
810
|
*/
|
|
643
|
-
const createLine = (children) =>
|
|
811
|
+
const createLine = (children) =>
|
|
644
812
|
({
|
|
645
813
|
type: 'element',
|
|
646
814
|
tagName: 'span',
|
|
@@ -679,12 +847,12 @@ function generate(tokens) {
|
|
|
679
847
|
/** @type {Array<[number, string]>} */
|
|
680
848
|
const lineTokens = []
|
|
681
849
|
let lastWasBreak = false
|
|
682
|
-
|
|
850
|
+
|
|
683
851
|
for (let i = 0; i < tokens.length; i++) {
|
|
684
852
|
const token = tokens[i]
|
|
685
853
|
const [type, value] = token
|
|
686
854
|
const isLastToken = i === tokens.length - 1
|
|
687
|
-
|
|
855
|
+
|
|
688
856
|
if (type !== T_BREAK) {
|
|
689
857
|
// Divide multi-line token into multi-line code
|
|
690
858
|
if (value.includes('\n')) {
|
|
@@ -709,12 +877,12 @@ function generate(tokens) {
|
|
|
709
877
|
flushLine(lineTokens)
|
|
710
878
|
lineTokens.length = 0
|
|
711
879
|
}
|
|
712
|
-
|
|
880
|
+
|
|
713
881
|
// If this is the last token and it's a break, create an empty line
|
|
714
882
|
if (isLastToken) {
|
|
715
883
|
flushLine([])
|
|
716
884
|
}
|
|
717
|
-
|
|
885
|
+
|
|
718
886
|
lastWasBreak = true
|
|
719
887
|
}
|
|
720
888
|
}
|
|
@@ -727,7 +895,7 @@ function generate(tokens) {
|
|
|
727
895
|
return lines
|
|
728
896
|
}
|
|
729
897
|
|
|
730
|
-
/** @param { className: string, style?: Record<string, string> } */
|
|
898
|
+
/** @param {{ className: string, style?: Record<string, string> }} props */
|
|
731
899
|
const propsToString = (props) => {
|
|
732
900
|
let str = `class="${props.className}"`
|
|
733
901
|
|
|
@@ -758,12 +926,13 @@ function toHtml(lines) {
|
|
|
758
926
|
/**
|
|
759
927
|
*
|
|
760
928
|
* @param {string} code
|
|
761
|
-
* @param {
|
|
762
|
-
*
|
|
763
|
-
* keywords: Set<string>
|
|
929
|
+
* @param {{
|
|
930
|
+
* keywords?: Set<string>
|
|
764
931
|
* onCommentStart?: (curr: string, next: string) => number | boolean
|
|
765
932
|
* onCommentEnd?: (curr: string, prev: string) => number | boolean
|
|
933
|
+
* onQuote?: (curr: string, i: number, code: string) => number | null | undefined
|
|
766
934
|
* } | undefined} options
|
|
935
|
+
* `onQuote` same as `tokenize`.
|
|
767
936
|
* @returns {string}
|
|
768
937
|
*/
|
|
769
938
|
function highlight(code, options) {
|
package/lib/presets/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
type LanguageConfig = {
|
|
2
2
|
keywords: Set<string>
|
|
3
|
-
onCommentStart(curr: string, next: string): 0 | 1 | 2
|
|
4
|
-
onCommentEnd(prev: string, curr: string): 0 | 1 | 2
|
|
3
|
+
onCommentStart?(curr: string, next: string): 0 | 1 | 2
|
|
4
|
+
onCommentEnd?(prev: string, curr: string): 0 | 1 | 2
|
|
5
|
+
onQuote?(curr: string, i: number, code: string): number | null | undefined
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
export const css: LanguageConfig
|
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,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sugar-high",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"types": "./lib/index.d.ts",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -19,14 +19,13 @@
|
|
|
19
19
|
"lib"
|
|
20
20
|
],
|
|
21
21
|
"license": "MIT",
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "vitest",
|
|
24
|
+
"build": "echo 'package requires no build'"
|
|
25
|
+
},
|
|
22
26
|
"devDependencies": {
|
|
23
27
|
"@types/node": "22.12.0",
|
|
24
28
|
"typescript": "5.7.3",
|
|
25
29
|
"vitest": "^3.0.2"
|
|
26
|
-
},
|
|
27
|
-
"packageManager": "pnpm@8.7.1",
|
|
28
|
-
"scripts": {
|
|
29
|
-
"test": "vitest",
|
|
30
|
-
"build": "echo 'package requires no build'"
|
|
31
30
|
}
|
|
32
|
-
}
|
|
31
|
+
}
|