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