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