sugar-high 1.3.0 → 2.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/lib/index.js CHANGED
@@ -1,1017 +1,25 @@
1
1
  // @ts-check
2
2
 
3
- const JSXBrackets = new Set(['<', '>', '{', '}', '[', ']'])
4
- const Keywords_Js = new Set([
5
- 'for',
6
- 'do',
7
- 'while',
8
- 'if',
9
- 'else',
10
- 'return',
11
- 'function',
12
- 'var',
13
- 'let',
14
- 'const',
15
- 'true',
16
- 'false',
17
- 'undefined',
18
- 'this',
19
- 'new',
20
- 'delete',
21
- 'typeof',
22
- 'in',
23
- 'instanceof',
24
- 'void',
25
- 'break',
26
- 'continue',
27
- 'switch',
28
- 'case',
29
- 'default',
30
- 'throw',
31
- 'try',
32
- 'catch',
33
- 'finally',
34
- 'debugger',
35
- 'with',
36
- 'yield',
37
- 'async',
38
- 'await',
39
- 'class',
40
- 'extends',
41
- 'super',
42
- 'import',
43
- 'export',
44
- 'from',
45
- 'static',
46
- ])
3
+ import {
4
+ parse,
5
+ render,
6
+ } from './core.js'
7
+ import { languages } from './lang.js'
47
8
 
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
-
81
- const Signs = new Set([
82
- '+',
83
- '-',
84
- '*',
85
- '/',
86
- '%',
87
- '=',
88
- '!',
89
- '&',
90
- '|',
91
- '^',
92
- '~',
93
- '!',
94
- '?',
95
- ':',
96
- '.',
97
- ',',
98
- ';',
99
- `'`,
100
- '"',
101
- '.',
102
- '(',
103
- ')',
104
- '[',
105
- ']',
106
- '#',
107
- '@',
108
- '\\',
109
- ...JSXBrackets,
110
- ])
111
-
112
- const DefaultOptions = {
113
- keywords: Keywords_Js,
114
- onCommentStart: isCommentStart_Js,
115
- onCommentEnd: isCommentEnd_Js,
116
- }
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
-
188
- /**
189
- *
190
- * 0 - identifier
191
- * 1 - keyword
192
- * 2 - string
193
- * 3 - Class, number and null
194
- * 4 - property
195
- * 5 - entity
196
- * 6 - jsx literals
197
- * 7 - sign
198
- * 8 - comment
199
- * 9 - break
200
- * 10 - space
201
- *
202
- */
203
- const TokenTypes = /** @type {const} */ ([
204
- 'identifier',
205
- 'keyword',
206
- 'string',
207
- 'class',
208
- 'property',
209
- 'entity',
210
- 'jsxliterals',
211
- 'sign',
212
- 'comment',
213
- 'break',
214
- 'space',
215
- ])
216
- const [
217
- T_IDENTIFIER,
218
- T_KEYWORD,
219
- T_STRING,
220
- T_CLS_NUMBER,
221
- T_PROPERTY,
222
- T_ENTITY,
223
- T_JSX_LITERALS,
224
- T_SIGN,
225
- T_COMMENT,
226
- T_BREAK,
227
- T_SPACE,
228
- ] = /** @types {const} */ TokenTypes.map((_, i) => i)
229
-
230
- function isSpaces(str) {
231
- return /^[^\S\r\n]+$/g.test(str)
232
- }
233
-
234
- function isSign(ch) {
235
- return Signs.has(ch)
236
- }
237
-
238
- const HtmlEntities = /** @type {Record<string, string>} */ ({
239
- '&': '&amp;',
240
- '<': '&lt;',
241
- '>': '&gt;',
242
- '"': '&quot;',
243
- "'": '&#039;',
244
- })
245
-
246
- /** @param {string} str */
247
- function encode(str) {
248
- if (!/[&<>"']/.test(str)) return str
249
- return str.replace(/[&<>"']/g, chr => HtmlEntities[chr])
250
- }
251
-
252
- function isWord(chr) {
253
- return /^[\w_]+$/.test(chr) || hasUnicode(chr)
254
- }
255
-
256
- function isCls(str) {
257
- const chr0 = str[0]
258
- return isWord(chr0) &&
259
- chr0 === chr0.toUpperCase() ||
260
- str === 'null'
261
- }
262
-
263
- function hasUnicode(s) {
264
- return /[^\u0000-\u007f]/.test(s);
265
- }
266
-
267
- function isAlpha(chr) {
268
- return /^[a-zA-Z]$/.test(chr)
269
- }
270
-
271
- function isIdentifierChar(chr) {
272
- return isAlpha(chr) || hasUnicode(chr)
273
- }
274
-
275
- function isIdentifier(str) {
276
- return isIdentifierChar(str[0]) && (str.length === 1 || isWord(str.slice(1)))
277
- }
278
-
279
- function isStrTemplateChr(chr) {
280
- return chr === '`'
281
- }
282
-
283
- function isSingleQuotes(chr) {
284
- return chr === '"' || chr === "'"
285
- }
286
-
287
- function isStringQuotation(chr) {
288
- return isSingleQuotes(chr) || isStrTemplateChr(chr)
289
- }
290
-
291
- /** @returns {0|1|2} */
292
- function isCommentStart_Js(curr, next) {
293
- const str = curr + next
294
- if (str === '/*') return 2
295
- return str === '//' ? 1 : 0
296
- }
297
-
298
- /** @returns {0|1|2} */
299
- function isCommentEnd_Js(prev, curr) {
300
- return (prev + curr) === '*/'
301
- ? 2
302
- : curr === '\n' ? 1 : 0
303
- }
304
-
305
- function isRegexStart(str) {
306
- return str[0] === '/' && !isCommentStart_Js(str[0], str[1])
307
- }
308
-
309
- function isPropertyKey(code, quoteEnd) {
310
- let i = quoteEnd + 1
311
- while (i < code.length && /\s/.test(code[i])) i++
312
- return code[i] === ':'
313
- }
314
-
315
- /**
316
- * @param {string} code
317
- * @param {{
318
- * keywords?: Set<string>
319
- * typeKeywords?: Set<string>
320
- * onCommentStart?: (curr: string, next: string) => number | boolean
321
- * onCommentEnd?: (prev: string, curr: string) => number | boolean
322
- * onQuote?: (curr: string, i: number, code: string) => number | null | undefined
323
- * quotedKeys?: boolean
324
- * lineClassName?: (line: string, index: number) => string | null | undefined
325
- * } | undefined} options
326
- * Optional `onQuote(curr, i, code)` at `code[i] === "'"`: return length to consume from `i` (>= 1),
327
- * or null/undefined/below 1 for default JS single-quoted strings. No substring allocation.
328
- * @return {Array<[number, string]>}
329
- */
330
- function tokenize(code, options) {
331
- const mergedOptions = { ...DefaultOptions, ...options }
332
- const hasCustomKeywords = !!(options && options.keywords instanceof Set)
333
- const isTs = isLikelyTypeScript(code)
334
- const resolvedKeywords = hasCustomKeywords
335
- ? mergedOptions.keywords
336
- : (isTs ? Keywords_Ts : Keywords_Js)
337
-
338
- const {
339
- onCommentStart,
340
- onCommentEnd,
341
- } = mergedOptions
342
-
343
- const resolvedTypeKeywords =
344
- mergedOptions.typeKeywords instanceof Set ? mergedOptions.typeKeywords : null
345
-
346
- let current = ''
347
- let type = -1
348
- /** @type {[number, string]} */
349
- let last = [-1, '']
350
- /** @type {[number, string]} */
351
- let beforeLast = [-2, '']
352
- /** @type {Array<[number, string]>} */
353
- const tokens = []
354
-
355
- /**
356
- * TS generics (`Map<string>`) and JSX (`<div>`) share the same `<Name …>` lexical shape. We use
357
- * one tag-lexer mode (__jsxTag + __jsxStack) for both when we enter it; isTsTypeArgStart and
358
- * isTypeParameterListStart only decide when *not* to enter (e.g. `foo<T>`, `<T>(x)=>`).
359
- * __jsxEnter gates that mode so a latched __jsxTag (from `<` in `"a<b"`) does not run the tag
360
- * lexer until we are in real JSX/TSX surface (not inside strings).
361
- * @type {boolean}
362
- */
363
- let __jsxEnter = false
364
- /** @type {0 | 1 | 2} 0 = none; 1 = inside `<open`; 2 = inside `</close` */
365
- let __jsxTag = 0
366
- let __jsxExpr = false
367
-
368
- /** Nested `<open>…</open>` depth (content between tags, including nested elements). */
369
- let __jsxStack = 0
370
-
371
- const __jsxChild = () => __jsxEnter && !__jsxExpr && !__jsxTag
372
- // < __content__ >
373
- const inJsxTag = () => __jsxTag && !__jsxChild()
374
- // {'__content__'}
375
- const inJsxLiterals = () => !__jsxTag && __jsxChild() && !__jsxExpr && __jsxStack > 0
376
-
377
- /** @type {string | null} */
378
- let __strQuote = null
379
- let __strTokenStart = 0
380
- let __regexQuoteStart = false
381
- let __strTemplateExprStack = 0
382
- let __strTemplateQuoteStack = 0
383
- const inStringQuotes = () => __strQuote !== null
384
- const inRegexQuotes = () => __regexQuoteStart
385
- const inStrTemplateLiterals = () => (__strTemplateQuoteStack > __strTemplateExprStack)
386
- const inStrTemplateExpr = () => __strTemplateQuoteStack > 0 && (__strTemplateQuoteStack === __strTemplateExprStack)
387
- const inStringContent = () => inStringQuotes() || inStrTemplateLiterals()
388
-
389
- /**
390
- *
391
- * @param {string} token
392
- * @returns {number}
393
- */
394
- function classify(token) {
395
- const isLineBreak = token === '\n'
396
- // First checking if they're attributes values
397
- if (inJsxTag()) {
398
- if (inStringQuotes()) {
399
- return T_STRING
400
- }
401
-
402
- const [, lastToken] = last
403
- if (isIdentifier(token)) {
404
- // classify jsx open tag
405
- if ((lastToken === '<' || lastToken === '</'))
406
- return T_ENTITY
407
- }
408
- }
409
- // Then determine if they're jsx literals
410
- const isJsxLiterals = inJsxLiterals()
411
- if (isJsxLiterals) return T_JSX_LITERALS
412
-
413
- // Determine strings first before other types
414
- if (inStringQuotes() || inStrTemplateLiterals()) {
415
- return T_STRING
416
- } else if (resolvedTypeKeywords && resolvedTypeKeywords.has(token)) {
417
- return last[1] === '.' ? T_IDENTIFIER : T_CLS_NUMBER
418
- } else if (resolvedKeywords.has(token)) {
419
- return last[1] === '.' ? T_IDENTIFIER : T_KEYWORD
420
- } else if (isLineBreak) {
421
- return T_BREAK
422
- } else if (isSpaces(token)) {
423
- return T_SPACE
424
- } else if (token.split('').every(isSign)) {
425
- return T_SIGN
426
- } else if (isCls(token)) {
427
- return inJsxTag() ? T_IDENTIFIER : T_CLS_NUMBER
428
- } else {
429
- if (isIdentifier(token)) {
430
- const isLastPropDot = last[1] === '.' && isIdentifier(beforeLast[1])
431
-
432
- if (!inStringContent() && !isLastPropDot) return T_IDENTIFIER
433
- if (isLastPropDot) return T_PROPERTY
434
- }
435
- return T_STRING
436
- }
437
- }
438
-
439
- /**
440
- *
441
- * @param {number | undefined} [type_]
442
- * @param {string | undefined} [token_]
443
- */
444
- const append = (type_, token_) => {
445
- if (token_) {
446
- current = token_
447
- }
448
- if (current) {
449
- type = typeof type_ === 'number' ? type_ : classify(current)
450
- /** @type [number, string] */
451
- const pair = [type, current]
452
- if (type !== T_SPACE && type !== T_BREAK) {
453
- beforeLast = last
454
- last = pair
455
- }
456
- tokens.push(pair)
457
- }
458
- current = ''
459
- }
460
- for (let i = 0; i < code.length; i++) {
461
- const curr = code[i]
462
- const prev = code[i - 1]
463
- const next = code[i + 1]
464
- const p_c = prev + curr // previous and current
465
- const c_n = curr + next // current and next
466
-
467
- // onQuote(curr, i, code): length from i; end = i + len (capped).
468
- if (
469
- typeof mergedOptions.onQuote === 'function' &&
470
- curr === "'" &&
471
- !inStringQuotes() &&
472
- !inJsxLiterals() &&
473
- !inStrTemplateLiterals()
474
- ) {
475
- const rawLen = mergedOptions.onQuote(curr, i, code)
476
- if (
477
- typeof rawLen === 'number' &&
478
- rawLen >= 1 &&
479
- !Number.isNaN(rawLen)
480
- ) {
481
- const len = Math.min(rawLen, code.length - i)
482
- const end = i + len
483
- append()
484
- current = code.slice(i, end)
485
- append(T_IDENTIFIER)
486
- i = end - 1
487
- continue
488
- }
489
- }
490
-
491
- // Determine string quotation outside of jsx literals and template literals.
492
- // Inside jsx literals or template literals, string quotation is still part of it.
493
- if (isSingleQuotes(curr) && !inJsxLiterals() && !inStrTemplateLiterals()) {
494
- append()
495
- let isStringClose = false
496
- if (prev !== `\\`) {
497
- if (__strQuote && curr === __strQuote) {
498
- __strQuote = null
499
- isStringClose = true
500
- } else if (!__strQuote) {
501
- __strQuote = curr
502
- __strTokenStart = tokens.length
503
- }
504
- }
505
-
506
- append(T_STRING, curr)
507
- if (mergedOptions.quotedKeys && isStringClose && isPropertyKey(code, i)) {
508
- for (let tokenIndex = __strTokenStart; tokenIndex < tokens.length; tokenIndex++) {
509
- tokens[tokenIndex][0] = T_PROPERTY
510
- }
511
- }
512
- continue
513
- }
514
-
515
- if (!inStrTemplateLiterals()) {
516
- if (prev !== '\\n' && isStrTemplateChr(curr)) {
517
- append()
518
- append(T_STRING, curr)
519
- __strTemplateQuoteStack++
520
- continue
521
- }
522
- }
523
-
524
- if (inStrTemplateLiterals()) {
525
- if (prev !== '\\n' && isStrTemplateChr(curr)) {
526
- if (__strTemplateQuoteStack > 0) {
527
- append()
528
- __strTemplateQuoteStack--
529
- append(T_STRING, curr)
530
- continue
531
- }
532
- }
533
-
534
- if (c_n === '${') {
535
- __strTemplateExprStack++
536
- append(T_STRING)
537
- append(T_SIGN, c_n)
538
- i++
539
- continue
540
- }
541
- }
542
-
543
- if (inStrTemplateExpr() && curr === '}') {
544
- append()
545
- __strTemplateExprStack--
546
- append(T_SIGN, curr)
547
- continue
548
- }
549
-
550
- if (__jsxChild()) {
551
- if (curr === '{') {
552
- append()
553
- append(T_SIGN, curr)
554
- __jsxExpr = true
555
- continue
556
- }
557
- }
558
-
559
- if (__jsxEnter) {
560
- // <: open tag sign
561
- // new '<' not inside jsx
562
- if (!__jsxTag && curr === '<') {
563
- append()
564
- if (next === '/') {
565
- // close tag
566
- __jsxTag = 2
567
- current = c_n
568
- i++
569
- } else {
570
- // open tag
571
- __jsxTag = 1
572
- current = curr
573
- }
574
- append(T_SIGN)
575
- continue
576
- }
577
- if (__jsxTag) {
578
- // >: open tag close sign or closing tag closing sign
579
- // and it's not `=>` or `/>`
580
- // `curr` could be `>` or `/`
581
- if ((curr === '>' && !'/='.includes(prev))) {
582
- append()
583
- if (__jsxTag === 1) {
584
- __jsxTag = 0
585
- __jsxStack++
586
- } else {
587
- __jsxTag = 0
588
- __jsxEnter = false
589
- }
590
- append(T_SIGN, curr)
591
- continue
592
- }
593
-
594
- // >: tag self close sign or close tag sign
595
- if (c_n === '/>' || c_n === '</') {
596
- // if current token is not part of close tag sign, push it first
597
- if (current !== '<' && current !== '/') {
598
- append()
599
- }
600
-
601
- if (c_n === '/>') {
602
- __jsxTag = 0
603
- } else {
604
- // is '</'
605
- __jsxStack--
606
- }
607
-
608
- if (!__jsxStack)
609
- __jsxEnter = false
610
-
611
- current = c_n
612
- i++
613
- append(T_SIGN)
614
- continue
615
- }
616
-
617
- // <: open tag sign
618
- if (curr === '<') {
619
- append()
620
- current = curr
621
- append(T_SIGN)
622
- continue
623
- }
624
-
625
- // jsx property
626
- // `-` in data-prop
627
- if (next === '-' && !inStringContent() && !inJsxLiterals()) {
628
- if (current) {
629
- append(T_PROPERTY, current + curr + next)
630
- i++
631
- continue
632
- }
633
- }
634
- // `=` in property=<value>
635
- if (next === '=' && !inStringContent()) {
636
- // if current is not a space, ensure `prop` is a property
637
- if (!isSpaces(curr)) {
638
- // If there're leading spaces, append them first
639
- if (isSpaces(current)) {
640
- append()
641
- }
642
-
643
- // Now check if the accumulated token is a property
644
- const prop = current + curr
645
- if (isIdentifier(prop)) {
646
- append(T_PROPERTY, prop)
647
- continue
648
- }
649
- }
650
- }
651
- }
652
- }
653
-
654
- // if it's not in a jsx tag declaration or a string, close child if next is jsx close tag
655
- if (!__jsxTag && (curr === '<' && isIdentifierChar(next) || c_n === '</')) {
656
- let prevNonSpace = i - 1
657
- while (prevNonSpace >= 0 && /\s/.test(code[prevNonSpace])) prevNonSpace--
658
- const prevChar = prevNonSpace >= 0 ? code[prevNonSpace] : ''
659
-
660
- const [lastType, lastTok] = last
661
- // Without a space before `<`, the LHS is often still in `current` (not flushed), so `last` is stale.
662
- let typeArgFromPending = false
663
- let jsxFromPending = false
664
- if (current && !isSpaces(current)) {
665
- const w = current
666
- // Unflushed LHS before `<`: numbers/null/Upper (isCls), and booleans, behave like `foo<` (type
667
- // args / `<`). Any other keyword (`return`, `void`, …) is JSX-friendly — no keyword allowlist.
668
- if (isCls(w) || w === 'true' || w === 'false') {
669
- typeArgFromPending = true
670
- } else if (resolvedKeywords.has(w) && isIdentifier(w)) {
671
- jsxFromPending = true
672
- } else if (isIdentifier(w)) {
673
- typeArgFromPending = true
674
- }
675
- }
676
-
677
- const isTsTypeArgStart =
678
- curr === '<' &&
679
- /[$\w\]\)]/.test(prevChar) &&
680
- (typeArgFromPending ||
681
- (!jsxFromPending &&
682
- (lastType === T_IDENTIFIER ||
683
- lastType === T_CLS_NUMBER ||
684
- (lastType === T_SIGN && (lastTok === ')' || lastTok === ']')))))
685
- const isTsGenericStart = curr === '<' && isTypeParameterListStart(code, i)
686
- if (!isTsTypeArgStart && !isTsGenericStart) {
687
- __jsxTag = next === '/' ? 2 : 1
688
- }
689
-
690
- if (curr === '<' && (next === '/' || isAlpha(next))) {
691
- if (
692
- !isTsTypeArgStart &&
693
- !isTsGenericStart &&
694
- !inStringContent() &&
695
- !inJsxLiterals() &&
696
- !inRegexQuotes()
697
- ) {
698
- __jsxEnter = true
699
- }
700
- }
701
- }
702
-
703
- const isQuotationChar = isStringQuotation(curr)
704
- const isStringTemplateLiterals = inStrTemplateLiterals()
705
- const isRegexChar = !__jsxEnter && isRegexStart(c_n)
706
- const isJsxLiterals = inJsxLiterals()
707
-
708
- // string quotation
709
- if (isQuotationChar || isStringTemplateLiterals || isSingleQuotes(__strQuote)) {
710
- current += curr
711
- } else if (isRegexChar) {
712
- append()
713
- const [lastType, lastToken] = last
714
- // Special cases that are not considered as regex:
715
- // * (expr1) / expr2: `)` before `/` operator is still in expression
716
- // * <non comment start>/ expr: non comment start before `/` is not regex
717
- if (
718
- isRegexChar &&
719
- lastType !== -1 &&
720
- !(
721
- (lastType === T_SIGN && ')' !== lastToken) ||
722
- lastType === T_COMMENT
723
- )
724
- ) {
725
- current = curr
726
- append()
727
- continue
728
- }
729
-
730
- __regexQuoteStart = true
731
- const start = i++
732
-
733
- // end of line of end of file
734
- const isEof = () => i >= code.length
735
- const isEol = () => isEof() || code[i] === '\n'
736
-
737
- let foundClose = false
738
-
739
- // `/` is literal inside regex character classes, e.g. `[/]`.
740
- let inCharClass = false
741
-
742
- // traverse to find closing regex slash
743
- for (; !isEol(); i++) {
744
- const ch = code[i]
745
- const escaped = code[i - 1] === '\\'
746
- if (!escaped && ch === '[') inCharClass = true
747
- if (!escaped && ch === ']') inCharClass = false
748
- if (ch === '/' && !inCharClass && !escaped) {
749
- foundClose = true
750
- // end of regex, append regex flags
751
- while (start !== i && /^[a-z]$/.test(code[i + 1]) && !isEol()) {
752
- i++
753
- }
754
- break
755
- }
756
- }
757
- __regexQuoteStart = false
758
-
759
- if (start !== i && foundClose) {
760
- // If current line is fully closed with string quotes or regex slashes,
761
- // add them to tokens
762
- current = code.slice(start, i + 1)
763
- append(T_STRING)
764
- } else {
765
- // If it doesn't match any of the above, just leave it as operator and move on
766
- current = curr
767
- append()
768
- i = start
769
- }
770
- } else if (onCommentStart(curr, next)) {
771
- append()
772
- const start = i
773
- const startCommentType = onCommentStart(curr, next)
774
-
775
- // just match the comment, commentType === true
776
- // inline comment, commentType === 1
777
- // block comment, commentType === 2
778
- if (startCommentType) {
779
- for (; i < code.length; i++) {
780
- const endCommentType = onCommentEnd(code[i - 1], code[i])
781
- if (endCommentType == startCommentType) break
782
- }
783
- }
784
- current = code.slice(start, i + 1)
785
- append(T_COMMENT)
786
- } else if (curr === ' ' || curr === '\n') {
787
- if (
788
- curr === ' ' &&
789
- (
790
- (isSpaces(current) || !current) ||
791
- isJsxLiterals
792
- )
793
- ) {
794
- let end = i + 1
795
- while (code[end] === ' ') end++
796
- current += code.slice(i, end)
797
- i = end - 1
798
- if (code[end] === '<') {
799
- append()
800
- }
801
- } else {
802
- append()
803
- current = curr
804
- append()
805
- }
806
- } else {
807
- if (__jsxExpr && curr === '}') {
808
- append()
809
- current = curr
810
- append()
811
- __jsxExpr = false
812
- } else if (
813
- // it's jsx literals and is not a jsx bracket
814
- (isJsxLiterals && !JSXBrackets.has(curr)) ||
815
- // it's template literal content (including quotes)
816
- inStrTemplateLiterals() ||
817
- // same type char as previous one in current token
818
- ((isWord(curr) === isWord(current[current.length - 1]) || __jsxChild()) && !Signs.has(curr))
819
- ) {
820
- current += curr
821
- } else {
822
- if (p_c === '</') {
823
- current = p_c
824
- }
825
- append()
826
-
827
- if (p_c !== '</') {
828
- current = curr
829
-
830
- }
831
- if ((c_n === '</' || c_n === '/>')) {
832
- current = c_n
833
- append()
834
- i++
835
- }
836
- else if (JSXBrackets.has(curr)) append()
837
- }
838
- }
839
- }
840
-
841
- append()
842
-
843
- return tokens
9
+ /** @param {string | undefined} name */
10
+ function configFor(name) {
11
+ return languages.find(({ id }) => id === (name || 'javascript'))?.config
844
12
  }
845
13
 
846
- /**
847
- * @param {Array<[number, string]>} tokens
848
- * @param {{
849
- * lineClassName?: (line: string, index: number) => string | null | undefined
850
- * } | undefined} options
851
- * @return {Array<{type: string, tagName: string, children: any[], properties: Record<string, string>}>}
852
- */
853
- function generate(tokens, options) {
854
- const lines = []
855
- const lineClassName = options && typeof options.lineClassName === 'function'
856
- ? options.lineClassName
857
- : null
858
- let lineIndex = 0
859
- /**
860
- * @param {any} children
861
- * @param {string} text
862
- * @return {{type: string, tagName: string, children: any[], properties: Record<string, string>}}
863
- */
864
- const createLine = (children, text) => {
865
- const extraClassName = lineClassName ? lineClassName(text, lineIndex) : ''
866
- lineIndex++
867
-
868
- return ({
869
- type: 'element',
870
- tagName: 'span',
871
- children,
872
- properties: {
873
- className: extraClassName ? `sh__line ${extraClassName}` : 'sh__line',
874
- },
875
- })
876
- }
877
-
878
- /**
879
- * @param {Array<[number, string]>} tokens
880
- * @returns {void}
881
- */
882
- function flushLine(tokens) {
883
- const lineText = tokens.map(([, value]) => value).join('')
884
- /** @type {Array<any>} */
885
- const lineTokens = (
886
- tokens
887
- .map(([type, value]) => {
888
- const tokenType = TokenTypes[type]
889
- return {
890
- type: 'element',
891
- tagName: 'span',
892
- children: [{
893
- type: 'text', // text node
894
- value, // to encode
895
- }],
896
- properties: {
897
- className: `sh__token--${tokenType}`,
898
- style: { color: `var(--sh-${tokenType})` },
899
- },
900
- }
901
- })
902
- )
903
- lines.push(createLine(lineTokens, lineText))
904
- }
905
- /** @type {Array<[number, string]>} */
906
- const lineTokens = []
907
- let lastWasBreak = false
908
-
909
- for (let i = 0; i < tokens.length; i++) {
910
- const token = tokens[i]
911
- const [type, value] = token
912
- const isLastToken = i === tokens.length - 1
913
-
914
- if (type !== T_BREAK) {
915
- // Divide multi-line token into multi-line code
916
- if (value.includes('\n')) {
917
- const lines = value.split('\n')
918
- for (let j = 0; j < lines.length; j++) {
919
- lineTokens.push([type, lines[j]])
920
- if (j < lines.length - 1) {
921
- flushLine(lineTokens)
922
- lineTokens.length = 0
923
- }
924
- }
925
- } else {
926
- lineTokens.push(token)
927
- }
928
- lastWasBreak = false
929
- } else {
930
- if (lastWasBreak) {
931
- // Consecutive break - create empty line
932
- flushLine([])
933
- } else {
934
- // First break after content - flush current line
935
- flushLine(lineTokens)
936
- lineTokens.length = 0
937
- }
938
-
939
- // If this is the last token and it's a break, create an empty line
940
- if (isLastToken) {
941
- flushLine([])
942
- }
943
-
944
- lastWasBreak = true
945
- }
946
- }
947
-
948
- // Flush remaining tokens if any
949
- if (lineTokens.length) {
950
- flushLine(lineTokens)
951
- }
952
-
953
- return lines
14
+ /** @param {string} code @param {HighlightOptions | undefined} options */
15
+ function highlight(code, options) {
16
+ const { lang, cx, mark, markLine } = options || {}
17
+ const parsed = parse(code, configFor(lang))
18
+ return render(parsed, { cx, mark, markLine })
954
19
  }
955
20
 
956
- /** @param {{ className: string, style?: Record<string, string> }} props */
957
- const propsToString = (props) => {
958
- let str = `class="${props.className}"`
959
-
960
- if (props.style) {
961
- const style = Object.entries(props.style)
962
- .map(([key, value]) => `${key}:${value}`)
963
- .join(';')
964
- str += ` style="${style}"`
965
- }
966
- return str
967
- }
968
-
969
- function toHtml(lines) {
970
- return lines
971
- .map(line => {
972
- const { tagName: lineTag } = line
973
- const tokens = line.children
974
- .map(child => {
975
- const { tagName, children, properties } = child
976
- return `<${tagName} ${propsToString(properties)}>${encode(children[0].value)}</${tagName}>`
977
- })
978
- .join('')
979
- return `<${lineTag} class="${line.properties.className}">${tokens}</${lineTag}>`
980
- })
981
- .join('\n')
982
- }
21
+ export { highlight }
983
22
 
984
23
  /**
985
- *
986
- * @param {string} code
987
- * @param {{
988
- * keywords?: Set<string>
989
- * typeKeywords?: Set<string>
990
- * onCommentStart?: (curr: string, next: string) => number | boolean
991
- * onCommentEnd?: (curr: string, prev: string) => number | boolean
992
- * onQuote?: (curr: string, i: number, code: string) => number | null | undefined
993
- * quotedKeys?: boolean
994
- * lineClassName?: (line: string, index: number) => string | null | undefined
995
- * } | undefined} options
996
- * `onQuote` same as `tokenize`.
997
- * @returns {string}
24
+ * @typedef {import('./core.js').DisplayOptions & { lang?: string }} HighlightOptions
998
25
  */
999
- function highlight(code, options) {
1000
- const tokens = tokenize(code, options)
1001
- const lines = generate(tokens, options)
1002
- const output = toHtml(lines)
1003
- return output
1004
- }
1005
-
1006
- // namespace
1007
- const SugarHigh = /** @type {const} */ {
1008
- TokenTypes,
1009
- TokenMap: new Map(TokenTypes.map((type, i) => [type, i])),
1010
- }
1011
-
1012
- export {
1013
- highlight,
1014
- tokenize,
1015
- generate,
1016
- SugarHigh,
1017
- }