git-trace 0.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.
Files changed (48) hide show
  1. package/.tracerc.example +38 -0
  2. package/README.md +136 -0
  3. package/bun.lock +511 -0
  4. package/bunchee.config.ts +11 -0
  5. package/cli/index.ts +251 -0
  6. package/cli/parser.ts +76 -0
  7. package/cli/tsconfig.json +6 -0
  8. package/dist/cli/index.d.ts +1 -0
  9. package/dist/cli/index.js +858 -0
  10. package/dist/config.cjs +66 -0
  11. package/dist/config.d.ts +15 -0
  12. package/dist/config.js +63 -0
  13. package/dist/highlight/index.cjs +770 -0
  14. package/dist/highlight/index.d.ts +26 -0
  15. package/dist/highlight/index.js +766 -0
  16. package/dist/index.cjs +849 -0
  17. package/dist/index.d.ts +52 -0
  18. package/dist/index.js +845 -0
  19. package/examples/demo/App.tsx +78 -0
  20. package/examples/demo/index.html +12 -0
  21. package/examples/demo/main.tsx +10 -0
  22. package/examples/demo/mockData.ts +170 -0
  23. package/examples/demo/styles.css +103 -0
  24. package/examples/demo/tsconfig.json +21 -0
  25. package/examples/demo/tsconfig.node.json +10 -0
  26. package/examples/demo/vite.config.ts +20 -0
  27. package/package.json +58 -0
  28. package/src/Trace.tsx +717 -0
  29. package/src/cache.ts +118 -0
  30. package/src/config.ts +51 -0
  31. package/src/entries/config.ts +7 -0
  32. package/src/entries/gitea.ts +4 -0
  33. package/src/entries/github.ts +5 -0
  34. package/src/entries/gitlab.ts +4 -0
  35. package/src/gitea.ts +58 -0
  36. package/src/github.ts +100 -0
  37. package/src/gitlab.ts +65 -0
  38. package/src/highlight/highlight.ts +119 -0
  39. package/src/highlight/index.ts +4 -0
  40. package/src/host.ts +32 -0
  41. package/src/index.ts +6 -0
  42. package/src/patterns.ts +6 -0
  43. package/src/shared.ts +108 -0
  44. package/src/themes.ts +98 -0
  45. package/src/types.ts +72 -0
  46. package/test/e2e.html +424 -0
  47. package/tsconfig.json +18 -0
  48. package/vercel.json +4 -0
@@ -0,0 +1,766 @@
1
+ // @ts-check
2
+ const JSXBrackets = new Set([
3
+ '<',
4
+ '>',
5
+ '{',
6
+ '}',
7
+ '[',
8
+ ']'
9
+ ]);
10
+ const Keywords_Js = new Set([
11
+ 'for',
12
+ 'do',
13
+ 'while',
14
+ 'if',
15
+ 'else',
16
+ 'return',
17
+ 'function',
18
+ 'var',
19
+ 'let',
20
+ 'const',
21
+ 'true',
22
+ 'false',
23
+ 'undefined',
24
+ 'this',
25
+ 'new',
26
+ 'delete',
27
+ 'typeof',
28
+ 'in',
29
+ 'instanceof',
30
+ 'void',
31
+ 'break',
32
+ 'continue',
33
+ 'switch',
34
+ 'case',
35
+ 'default',
36
+ 'throw',
37
+ 'try',
38
+ 'catch',
39
+ 'finally',
40
+ 'debugger',
41
+ 'with',
42
+ 'yield',
43
+ 'async',
44
+ 'await',
45
+ 'class',
46
+ 'extends',
47
+ 'super',
48
+ 'import',
49
+ 'export',
50
+ 'from',
51
+ 'static'
52
+ ]);
53
+ const Signs = new Set([
54
+ '+',
55
+ '-',
56
+ '*',
57
+ '/',
58
+ '%',
59
+ '=',
60
+ '!',
61
+ '&',
62
+ '|',
63
+ '^',
64
+ '~',
65
+ '!',
66
+ '?',
67
+ ':',
68
+ '.',
69
+ ',',
70
+ ';',
71
+ `'`,
72
+ '"',
73
+ '.',
74
+ '(',
75
+ ')',
76
+ '[',
77
+ ']',
78
+ '#',
79
+ '@',
80
+ '\\',
81
+ ...JSXBrackets
82
+ ]);
83
+ const DefaultOptions = {
84
+ keywords: Keywords_Js,
85
+ onCommentStart: isCommentStart_Js,
86
+ onCommentEnd: isCommentEnd_Js
87
+ };
88
+ /**
89
+ *
90
+ * 0 - identifier
91
+ * 1 - keyword
92
+ * 2 - string
93
+ * 3 - Class, number and null
94
+ * 4 - property
95
+ * 5 - entity
96
+ * 6 - jsx literals
97
+ * 7 - sign
98
+ * 8 - comment
99
+ * 9 - break
100
+ * 10 - space
101
+ *
102
+ */ const TokenTypes = /** @type {const} */ [
103
+ 'identifier',
104
+ 'keyword',
105
+ 'string',
106
+ 'class',
107
+ 'property',
108
+ 'entity',
109
+ 'jsxliterals',
110
+ 'sign',
111
+ 'comment',
112
+ 'break',
113
+ 'space'
114
+ ];
115
+ const [T_IDENTIFIER, T_KEYWORD, T_STRING, T_CLS_NUMBER, T_PROPERTY, T_ENTITY, T_JSX_LITERALS, T_SIGN, T_COMMENT, T_BREAK, T_SPACE] = /** @types {const} */ TokenTypes.map((_, i)=>i);
116
+ function isSpaces(str) {
117
+ return /^[^\S\r\n]+$/g.test(str);
118
+ }
119
+ function isSign(ch) {
120
+ return Signs.has(ch);
121
+ }
122
+ function encode(str) {
123
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
124
+ }
125
+ function isWord(chr) {
126
+ return /^[\w_]+$/.test(chr) || hasUnicode(chr);
127
+ }
128
+ function isCls(str) {
129
+ const chr0 = str[0];
130
+ return isWord(chr0) && chr0 === chr0.toUpperCase() || str === 'null';
131
+ }
132
+ function hasUnicode(s) {
133
+ return /[^\u0000-\u007f]/.test(s);
134
+ }
135
+ function isAlpha(chr) {
136
+ return /^[a-zA-Z]$/.test(chr);
137
+ }
138
+ function isIdentifierChar(chr) {
139
+ return isAlpha(chr) || hasUnicode(chr);
140
+ }
141
+ function isIdentifier(str) {
142
+ return isIdentifierChar(str[0]) && (str.length === 1 || isWord(str.slice(1)));
143
+ }
144
+ function isStrTemplateChr(chr) {
145
+ return chr === '`';
146
+ }
147
+ function isSingleQuotes(chr) {
148
+ return chr === '"' || chr === "'";
149
+ }
150
+ function isStringQuotation(chr) {
151
+ return isSingleQuotes(chr) || isStrTemplateChr(chr);
152
+ }
153
+ /** @returns {0|1|2} */ function isCommentStart_Js(curr, next) {
154
+ const str = curr + next;
155
+ if (str === '/*') return 2;
156
+ return str === '//' ? 1 : 0;
157
+ }
158
+ /** @returns {0|1|2} */ function isCommentEnd_Js(prev, curr) {
159
+ return prev + curr === '*/' ? 2 : curr === '\n' ? 1 : 0;
160
+ }
161
+ function isRegexStart(str) {
162
+ return str[0] === '/' && !isCommentStart_Js(str[0], str[1]);
163
+ }
164
+ /**
165
+ * @param {string} code
166
+ * @param {{ keywords: Set<string> }} options
167
+ * @return {Array<[number, string]>}
168
+ */ function tokenize(code, options) {
169
+ const { keywords, onCommentStart, onCommentEnd } = {
170
+ ...DefaultOptions,
171
+ ...options
172
+ };
173
+ let current = '';
174
+ let type = -1;
175
+ /** @type {[number, string]} */ let last = [
176
+ -1,
177
+ ''
178
+ ];
179
+ /** @type {[number, string]} */ let beforeLast = [
180
+ -2,
181
+ ''
182
+ ];
183
+ /** @type {Array<[number, string]>} */ const tokens = [];
184
+ /** @type boolean if entered jsx tag, inside <open tag> or </close tag> */ let __jsxEnter = false;
185
+ /**
186
+ * @type {0 | 1 | 2}
187
+ * @example
188
+ * 0 for not in jsx;
189
+ * 1 for open jsx tag;
190
+ * 2 for closing jsx tag;
191
+ **/ let __jsxTag = 0;
192
+ let __jsxExpr = false;
193
+ // only match paired (open + close) tags, not self-closing tags
194
+ let __jsxStack = 0;
195
+ const __jsxChild = ()=>__jsxEnter && !__jsxExpr && !__jsxTag;
196
+ // < __content__ >
197
+ const inJsxTag = ()=>__jsxTag && !__jsxChild();
198
+ // {'__content__'}
199
+ const inJsxLiterals = ()=>!__jsxTag && __jsxChild() && !__jsxExpr && __jsxStack > 0;
200
+ /** @type {string | null} */ let __strQuote = null;
201
+ let __regexQuoteStart = false;
202
+ let __strTemplateExprStack = 0;
203
+ let __strTemplateQuoteStack = 0;
204
+ const inStringQuotes = ()=>__strQuote !== null;
205
+ const inRegexQuotes = ()=>__regexQuoteStart;
206
+ const inStrTemplateLiterals = ()=>__strTemplateQuoteStack > __strTemplateExprStack;
207
+ const inStrTemplateExpr = ()=>__strTemplateQuoteStack > 0 && __strTemplateQuoteStack === __strTemplateExprStack;
208
+ const inStringContent = ()=>inStringQuotes() || inStrTemplateLiterals();
209
+ /**
210
+ *
211
+ * @param {string} token
212
+ * @returns {number}
213
+ */ function classify(token) {
214
+ const isLineBreak = token === '\n';
215
+ // First checking if they're attributes values
216
+ if (inJsxTag()) {
217
+ if (inStringQuotes()) {
218
+ return T_STRING;
219
+ }
220
+ const [, lastToken] = last;
221
+ if (isIdentifier(token)) {
222
+ // classify jsx open tag
223
+ if (lastToken === '<' || lastToken === '</') return T_ENTITY;
224
+ }
225
+ }
226
+ // Then determine if they're jsx literals
227
+ const isJsxLiterals = inJsxLiterals();
228
+ if (isJsxLiterals) return T_JSX_LITERALS;
229
+ // Determine strings first before other types
230
+ if (inStringQuotes() || inStrTemplateLiterals()) {
231
+ return T_STRING;
232
+ } else if (keywords.has(token)) {
233
+ return last[1] === '.' ? T_IDENTIFIER : T_KEYWORD;
234
+ } else if (isLineBreak) {
235
+ return T_BREAK;
236
+ } else if (isSpaces(token)) {
237
+ return T_SPACE;
238
+ } else if (token.split('').every(isSign)) {
239
+ return T_SIGN;
240
+ } else if (isCls(token)) {
241
+ return inJsxTag() ? T_IDENTIFIER : T_CLS_NUMBER;
242
+ } else {
243
+ if (isIdentifier(token)) {
244
+ const isLastPropDot = last[1] === '.' && isIdentifier(beforeLast[1]);
245
+ if (!inStringContent() && !isLastPropDot) return T_IDENTIFIER;
246
+ if (isLastPropDot) return T_PROPERTY;
247
+ }
248
+ return T_STRING;
249
+ }
250
+ }
251
+ /**
252
+ *
253
+ * @param {number} type_
254
+ * @param {string} token_
255
+ */ const append = (type_, token_)=>{
256
+ if (token_) {
257
+ current = token_;
258
+ }
259
+ if (current) {
260
+ type = type_ || classify(current);
261
+ /** @type [number, string] */ const pair = [
262
+ type,
263
+ current
264
+ ];
265
+ if (type !== T_SPACE && type !== T_BREAK) {
266
+ beforeLast = last;
267
+ last = pair;
268
+ }
269
+ tokens.push(pair);
270
+ }
271
+ current = '';
272
+ };
273
+ for(let i = 0; i < code.length; i++){
274
+ const curr = code[i];
275
+ const prev = code[i - 1];
276
+ const next = code[i + 1];
277
+ const p_c = prev + curr // previous and current
278
+ ;
279
+ const c_n = curr + next // current and next
280
+ ;
281
+ // Determine string quotation outside of jsx literals and template literals.
282
+ // Inside jsx literals or template literals, string quotation is still part of it.
283
+ if (isSingleQuotes(curr) && !inJsxLiterals() && !inStrTemplateLiterals()) {
284
+ append();
285
+ if (prev !== `\\`) {
286
+ if (__strQuote && curr === __strQuote) {
287
+ __strQuote = null;
288
+ } else if (!__strQuote) {
289
+ __strQuote = curr;
290
+ }
291
+ }
292
+ append(T_STRING, curr);
293
+ continue;
294
+ }
295
+ if (!inStrTemplateLiterals()) {
296
+ if (prev !== '\\n' && isStrTemplateChr(curr)) {
297
+ append();
298
+ append(T_STRING, curr);
299
+ __strTemplateQuoteStack++;
300
+ continue;
301
+ }
302
+ }
303
+ if (inStrTemplateLiterals()) {
304
+ if (prev !== '\\n' && isStrTemplateChr(curr)) {
305
+ if (__strTemplateQuoteStack > 0) {
306
+ append();
307
+ __strTemplateQuoteStack--;
308
+ append(T_STRING, curr);
309
+ continue;
310
+ }
311
+ }
312
+ if (c_n === '${') {
313
+ __strTemplateExprStack++;
314
+ append(T_STRING);
315
+ append(T_SIGN, c_n);
316
+ i++;
317
+ continue;
318
+ }
319
+ }
320
+ if (inStrTemplateExpr() && curr === '}') {
321
+ append();
322
+ __strTemplateExprStack--;
323
+ append(T_SIGN, curr);
324
+ continue;
325
+ }
326
+ if (__jsxChild()) {
327
+ if (curr === '{') {
328
+ append();
329
+ append(T_SIGN, curr);
330
+ __jsxExpr = true;
331
+ continue;
332
+ }
333
+ }
334
+ if (__jsxEnter) {
335
+ // <: open tag sign
336
+ // new '<' not inside jsx
337
+ if (!__jsxTag && curr === '<') {
338
+ append();
339
+ if (next === '/') {
340
+ // close tag
341
+ __jsxTag = 2;
342
+ current = c_n;
343
+ i++;
344
+ } else {
345
+ // open tag
346
+ __jsxTag = 1;
347
+ current = curr;
348
+ }
349
+ append(T_SIGN);
350
+ continue;
351
+ }
352
+ if (__jsxTag) {
353
+ // >: open tag close sign or closing tag closing sign
354
+ // and it's not `=>` or `/>`
355
+ // `curr` could be `>` or `/`
356
+ if (curr === '>' && !'/='.includes(prev)) {
357
+ append();
358
+ if (__jsxTag === 1) {
359
+ __jsxTag = 0;
360
+ __jsxStack++;
361
+ } else {
362
+ __jsxTag = 0;
363
+ __jsxEnter = false;
364
+ }
365
+ append(T_SIGN, curr);
366
+ continue;
367
+ }
368
+ // >: tag self close sign or close tag sign
369
+ if (c_n === '/>' || c_n === '</') {
370
+ // if current token is not part of close tag sign, push it first
371
+ if (current !== '<' && current !== '/') {
372
+ append();
373
+ }
374
+ if (c_n === '/>') {
375
+ __jsxTag = 0;
376
+ } else {
377
+ // is '</'
378
+ __jsxStack--;
379
+ }
380
+ if (!__jsxStack) __jsxEnter = false;
381
+ current = c_n;
382
+ i++;
383
+ append(T_SIGN);
384
+ continue;
385
+ }
386
+ // <: open tag sign
387
+ if (curr === '<') {
388
+ append();
389
+ current = curr;
390
+ append(T_SIGN);
391
+ continue;
392
+ }
393
+ // jsx property
394
+ // `-` in data-prop
395
+ if (next === '-' && !inStringContent() && !inJsxLiterals()) {
396
+ if (current) {
397
+ append(T_PROPERTY, current + curr + next);
398
+ i++;
399
+ continue;
400
+ }
401
+ }
402
+ // `=` in property=<value>
403
+ if (next === '=' && !inStringContent()) {
404
+ // if current is not a space, ensure `prop` is a property
405
+ if (!isSpaces(curr)) {
406
+ // If there're leading spaces, append them first
407
+ if (isSpaces(current)) {
408
+ append();
409
+ }
410
+ // Now check if the accumulated token is a property
411
+ const prop = current + curr;
412
+ if (isIdentifier(prop)) {
413
+ append(T_PROPERTY, prop);
414
+ continue;
415
+ }
416
+ }
417
+ }
418
+ }
419
+ }
420
+ // if it's not in a jsx tag declaration or a string, close child if next is jsx close tag
421
+ if (!__jsxTag && (curr === '<' && isIdentifierChar(next) || c_n === '</')) {
422
+ __jsxTag = next === '/' ? 2 : 1;
423
+ // current and next char can form a jsx open or close tag
424
+ if (curr === '<' && (next === '/' || isAlpha(next))) {
425
+ if (!inStringContent() && !inJsxLiterals() && !inRegexQuotes()) {
426
+ __jsxEnter = true;
427
+ }
428
+ }
429
+ }
430
+ const isQuotationChar = isStringQuotation(curr);
431
+ const isStringTemplateLiterals = inStrTemplateLiterals();
432
+ const isRegexChar = !__jsxEnter && isRegexStart(c_n);
433
+ const isJsxLiterals = inJsxLiterals();
434
+ // string quotation
435
+ if (isQuotationChar || isStringTemplateLiterals || isSingleQuotes(__strQuote)) {
436
+ current += curr;
437
+ } else if (isRegexChar) {
438
+ append();
439
+ const [lastType, lastToken] = last;
440
+ // Special cases that are not considered as regex:
441
+ // * (expr1) / expr2: `)` before `/` operator is still in expression
442
+ // * <non comment start>/ expr: non comment start before `/` is not regex
443
+ if (isRegexChar && lastType !== -1 && !(lastType === T_SIGN && ')' !== lastToken || lastType === T_COMMENT)) {
444
+ current = curr;
445
+ append();
446
+ continue;
447
+ }
448
+ __regexQuoteStart = true;
449
+ const start = i++;
450
+ // end of line of end of file
451
+ const isEof = ()=>i >= code.length;
452
+ const isEol = ()=>isEof() || code[i] === '\n';
453
+ let foundClose = false;
454
+ // traverse to find closing regex slash
455
+ for(; !isEol(); i++){
456
+ if (code[i] === '/' && code[i - 1] !== '\\') {
457
+ foundClose = true;
458
+ // end of regex, append regex flags
459
+ while(start !== i && /^[a-z]$/.test(code[i + 1]) && !isEol()){
460
+ i++;
461
+ }
462
+ break;
463
+ }
464
+ }
465
+ __regexQuoteStart = false;
466
+ if (start !== i && foundClose) {
467
+ // If current line is fully closed with string quotes or regex slashes,
468
+ // add them to tokens
469
+ current = code.slice(start, i + 1);
470
+ append(T_STRING);
471
+ } else {
472
+ // If it doesn't match any of the above, just leave it as operator and move on
473
+ current = curr;
474
+ append();
475
+ i = start;
476
+ }
477
+ } else if (onCommentStart(curr, next)) {
478
+ append();
479
+ const start = i;
480
+ const startCommentType = onCommentStart(curr, next);
481
+ // just match the comment, commentType === true
482
+ // inline comment, commentType === 1
483
+ // block comment, commentType === 2
484
+ if (startCommentType) {
485
+ for(; i < code.length; i++){
486
+ const endCommentType = onCommentEnd(code[i - 1], code[i]);
487
+ if (endCommentType == startCommentType) break;
488
+ }
489
+ }
490
+ current = code.slice(start, i + 1);
491
+ append(T_COMMENT);
492
+ } else if (curr === ' ' || curr === '\n') {
493
+ if (curr === ' ' && (isSpaces(current) || !current || isJsxLiterals)) {
494
+ current += curr;
495
+ if (next === '<') {
496
+ append();
497
+ }
498
+ } else {
499
+ append();
500
+ current = curr;
501
+ append();
502
+ }
503
+ } else {
504
+ if (__jsxExpr && curr === '}') {
505
+ append();
506
+ current = curr;
507
+ append();
508
+ __jsxExpr = false;
509
+ } else if (// it's jsx literals and is not a jsx bracket
510
+ isJsxLiterals && !JSXBrackets.has(curr) || // it's template literal content (including quotes)
511
+ inStrTemplateLiterals() || // same type char as previous one in current token
512
+ (isWord(curr) === isWord(current[current.length - 1]) || __jsxChild()) && !Signs.has(curr)) {
513
+ current += curr;
514
+ } else {
515
+ if (p_c === '</') {
516
+ current = p_c;
517
+ }
518
+ append();
519
+ if (p_c !== '</') {
520
+ current = curr;
521
+ }
522
+ if (c_n === '</' || c_n === '/>') {
523
+ current = c_n;
524
+ append();
525
+ i++;
526
+ } else if (JSXBrackets.has(curr)) append();
527
+ }
528
+ }
529
+ }
530
+ append();
531
+ return tokens;
532
+ }
533
+ /**
534
+ * @param {Array<[number, string]>} tokens
535
+ * @return {Array<{type: string, tagName: string, children: any[], properties: Record<string, string>}>}
536
+ */ function generate(tokens) {
537
+ const lines = [];
538
+ /**
539
+ * @param {any} children
540
+ * @return {{type: string, tagName: string, children: any[], properties: Record<string, string>}}
541
+ */ const createLine = (children)=>({
542
+ type: 'element',
543
+ tagName: 'span',
544
+ children,
545
+ properties: {
546
+ className: 'sh__line'
547
+ }
548
+ });
549
+ /**
550
+ * @param {Array<[number, string]>} tokens
551
+ * @returns {void}
552
+ */ function flushLine(tokens) {
553
+ /** @type {Array<any>} */ const lineTokens = tokens.map(([type, value])=>{
554
+ const tokenType = TokenTypes[type];
555
+ return {
556
+ type: 'element',
557
+ tagName: 'span',
558
+ children: [
559
+ {
560
+ type: 'text',
561
+ value
562
+ }
563
+ ],
564
+ properties: {
565
+ className: `sh__token--${tokenType}`,
566
+ style: {
567
+ color: `var(--sh-${tokenType})`
568
+ }
569
+ }
570
+ };
571
+ });
572
+ lines.push(createLine(lineTokens));
573
+ }
574
+ /** @type {Array<[number, string]>} */ const lineTokens = [];
575
+ let lastWasBreak = false;
576
+ for(let i = 0; i < tokens.length; i++){
577
+ const token = tokens[i];
578
+ const [type, value] = token;
579
+ const isLastToken = i === tokens.length - 1;
580
+ if (type !== T_BREAK) {
581
+ // Divide multi-line token into multi-line code
582
+ if (value.includes('\n')) {
583
+ const lines = value.split('\n');
584
+ for(let j = 0; j < lines.length; j++){
585
+ lineTokens.push([
586
+ type,
587
+ lines[j]
588
+ ]);
589
+ if (j < lines.length - 1) {
590
+ flushLine(lineTokens);
591
+ lineTokens.length = 0;
592
+ }
593
+ }
594
+ } else {
595
+ lineTokens.push(token);
596
+ }
597
+ lastWasBreak = false;
598
+ } else {
599
+ if (lastWasBreak) {
600
+ // Consecutive break - create empty line
601
+ flushLine([]);
602
+ } else {
603
+ // First break after content - flush current line
604
+ flushLine(lineTokens);
605
+ lineTokens.length = 0;
606
+ }
607
+ // If this is the last token and it's a break, create an empty line
608
+ if (isLastToken) {
609
+ flushLine([]);
610
+ }
611
+ lastWasBreak = true;
612
+ }
613
+ }
614
+ // Flush remaining tokens if any
615
+ if (lineTokens.length) {
616
+ flushLine(lineTokens);
617
+ }
618
+ return lines;
619
+ }
620
+ /** @param { className: string, style?: Record<string, string> } */ const propsToString = (props)=>{
621
+ let str = `class="${props.className}"`;
622
+ if (props.style) {
623
+ const style = Object.entries(props.style).map(([key, value])=>`${key}:${value}`).join(';');
624
+ str += ` style="${style}"`;
625
+ }
626
+ return str;
627
+ };
628
+ function toHtml(lines) {
629
+ return lines.map((line)=>{
630
+ const { tagName: lineTag } = line;
631
+ const tokens = line.children.map((child)=>{
632
+ const { tagName, children, properties } = child;
633
+ return `<${tagName} ${propsToString(properties)}>${encode(children[0].value)}</${tagName}>`;
634
+ }).join('');
635
+ return `<${lineTag} class="${line.properties.className}">${tokens}</${lineTag}>`;
636
+ }).join('\n');
637
+ }
638
+ /**
639
+ *
640
+ * @param {string} code
641
+ * @param {
642
+ * {
643
+ * keywords: Set<string>
644
+ * onCommentStart?: (curr: string, next: string) => number | boolean
645
+ * onCommentEnd?: (curr: string, prev: string) => number | boolean
646
+ * } | undefined} options
647
+ * @returns {string}
648
+ */ function highlight(code, options) {
649
+ const tokens = tokenize(code, options);
650
+ const lines = generate(tokens);
651
+ const output = toHtml(lines);
652
+ return output;
653
+ }
654
+ // namespace
655
+ /** @type {const} */ ({
656
+ TokenMap: new Map(TokenTypes.map((type, i)=>[
657
+ type,
658
+ i
659
+ ]))
660
+ });
661
+
662
+ // Syntax highlighting using sugar-high (~1KB, zero dependencies)
663
+ const MAX_CACHE_SIZE = 100;
664
+ /**
665
+ * Simple LRU (Least Recently Used) cache implementation
666
+ * Evicts oldest entries when capacity is reached
667
+ */ class LRUCache {
668
+ get(key) {
669
+ if (!this.cache.has(key)) return undefined;
670
+ const value = this.cache.get(key);
671
+ // Move to end (most recently used)
672
+ this.cache.delete(key);
673
+ this.cache.set(key, value);
674
+ return value;
675
+ }
676
+ set(key, value) {
677
+ // Remove oldest entry if at capacity
678
+ if (this.cache.size >= MAX_CACHE_SIZE) {
679
+ const firstKey = this.cache.keys().next().value;
680
+ if (firstKey !== undefined) {
681
+ this.cache.delete(firstKey);
682
+ }
683
+ }
684
+ this.cache.set(key, value);
685
+ }
686
+ clear() {
687
+ this.cache.clear();
688
+ }
689
+ constructor(){
690
+ this.cache = new Map();
691
+ }
692
+ }
693
+ // LRU cache for highlighted code to avoid re-calculating
694
+ const highlightCache = new LRUCache();
695
+ /**
696
+ * Highlight code using sugar-high
697
+ * Returns HTML string with token classes (sh__token--*)
698
+ * Results are cached for performance - same input produces same output
699
+ *
700
+ * SECURITY: sugar-high tokenizes code but does NOT escape HTML.
701
+ * Caller must escape user input BEFORE calling this function.
702
+ */ function highlightCode(code) {
703
+ if (!code) return '';
704
+ const cached = highlightCache.get(code);
705
+ if (cached) return cached;
706
+ const result = highlight(code);
707
+ highlightCache.set(code, result);
708
+ return result;
709
+ }
710
+ /**
711
+ * Clear the highlight cache (useful for testing or memory management)
712
+ */ function clearHighlightCache() {
713
+ highlightCache.clear();
714
+ }
715
+ /**
716
+ * Get language from file extension
717
+ * Useful for language-specific handling
718
+ */ const LANGUAGE_MAP = {
719
+ 'js': 'javascript',
720
+ 'jsx': 'javascript',
721
+ 'ts': 'typescript',
722
+ 'tsx': 'typescript',
723
+ 'py': 'python',
724
+ 'rs': 'rust',
725
+ 'go': 'go',
726
+ 'java': 'java',
727
+ 'c': 'c',
728
+ 'h': 'c',
729
+ 'cpp': 'cpp',
730
+ 'cc': 'cpp',
731
+ 'cxx': 'cpp',
732
+ 'hpp': 'cpp',
733
+ 'cs': 'csharp',
734
+ 'php': 'php',
735
+ 'rb': 'ruby',
736
+ 'swift': 'swift',
737
+ 'kt': 'kotlin',
738
+ 'scala': 'scala',
739
+ 'sh': 'shell',
740
+ 'bash': 'shell',
741
+ 'zsh': 'shell',
742
+ 'fish': 'shell',
743
+ 'json': 'json',
744
+ 'yaml': 'yaml',
745
+ 'yml': 'yaml',
746
+ 'toml': 'toml',
747
+ 'xml': 'xml',
748
+ 'html': 'html',
749
+ 'htm': 'html',
750
+ 'css': 'css',
751
+ 'scss': 'scss',
752
+ 'sass': 'sass',
753
+ 'md': 'markdown',
754
+ 'mdx': 'markdown',
755
+ 'sql': 'sql',
756
+ 'graphql': 'graphql',
757
+ 'gql': 'graphql',
758
+ 'vue': 'vue',
759
+ 'svelte': 'svelte'
760
+ };
761
+ function getLanguageFromPath(filePath) {
762
+ const ext = filePath.split('.').pop()?.toLowerCase();
763
+ return LANGUAGE_MAP[ext || ''] || 'text';
764
+ }
765
+
766
+ export { clearHighlightCache, getLanguageFromPath, highlightCode };