juxscript 1.1.44 → 1.1.45
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/components/code.d.ts +3 -2
- package/lib/components/code.d.ts.map +1 -1
- package/lib/components/code.js +24 -19
- package/lib/components/code.ts +26 -21
- package/lib/utils/codeparser.d.ts +28 -0
- package/lib/utils/codeparser.d.ts.map +1 -0
- package/lib/utils/codeparser.js +198 -0
- package/lib/utils/codeparser.ts +242 -0
- package/package.json +1 -1
package/lib/components/code.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ type CodeState = {
|
|
|
19
19
|
};
|
|
20
20
|
export declare class Code extends BaseComponent<CodeState> {
|
|
21
21
|
private _codeContainer;
|
|
22
|
+
private static _syntaxCSSInjected;
|
|
22
23
|
constructor(id: string, options?: CodeOptions);
|
|
23
24
|
protected getTriggerEvents(): readonly string[];
|
|
24
25
|
protected getCallbackEvents(): readonly string[];
|
|
@@ -29,7 +30,7 @@ export declare class Code extends BaseComponent<CodeState> {
|
|
|
29
30
|
startLine(value: number): this;
|
|
30
31
|
highlightLines(lines: number[]): this;
|
|
31
32
|
/**
|
|
32
|
-
* Parse code content into individual lines
|
|
33
|
+
* Parse code content into individual lines with tokens
|
|
33
34
|
*/
|
|
34
35
|
private _parseLines;
|
|
35
36
|
/**
|
|
@@ -37,7 +38,7 @@ export declare class Code extends BaseComponent<CodeState> {
|
|
|
37
38
|
*/
|
|
38
39
|
private _rebuildLines;
|
|
39
40
|
/**
|
|
40
|
-
* Create a single line element
|
|
41
|
+
* Create a single line element with parsed tokens
|
|
41
42
|
*/
|
|
42
43
|
private _createLineElement;
|
|
43
44
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"code.d.ts","sourceRoot":"","sources":["code.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"code.d.ts","sourceRoot":"","sources":["code.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAOxD,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,KAAK,SAAS,GAAG;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,OAAO,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,qBAAa,IAAK,SAAQ,aAAa,CAAC,SAAS,CAAC;IAChD,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAS;gBAE9B,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB;IAYjD,SAAS,CAAC,gBAAgB,IAAI,SAAS,MAAM,EAAE;IAI/C,SAAS,CAAC,iBAAiB,IAAI,SAAS,MAAM,EAAE;IAIhD,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI;IAsBtC,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAK5B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAK7B,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAKrC,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAK9B,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI;IASrC;;OAEG;IACH,OAAO,CAAC,WAAW;IAInB;;OAEG;IACH,OAAO,CAAC,aAAa;IAyBrB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IA0B1B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAezB,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,IAAI;CAgFnE;AAED,wBAAgB,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,IAAI,CAEhE"}
|
package/lib/components/code.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseComponent } from './base/BaseComponent.js';
|
|
2
|
+
import { parseCode, renderLineWithTokens, getSyntaxHighlightCSS } from '../utils/codeparser.js';
|
|
2
3
|
// Event definitions
|
|
3
4
|
const TRIGGER_EVENTS = [];
|
|
4
5
|
const CALLBACK_EVENTS = [];
|
|
@@ -64,10 +65,10 @@ export class Code extends BaseComponent {
|
|
|
64
65
|
* PARSING & RENDERING
|
|
65
66
|
* ═════════════════════════════════════════════════════════════════ */
|
|
66
67
|
/**
|
|
67
|
-
* Parse code content into individual lines
|
|
68
|
+
* Parse code content into individual lines with tokens
|
|
68
69
|
*/
|
|
69
70
|
_parseLines() {
|
|
70
|
-
return this.state.content.
|
|
71
|
+
return parseCode(this.state.content, this.state.language);
|
|
71
72
|
}
|
|
72
73
|
/**
|
|
73
74
|
* Rebuild all line elements
|
|
@@ -79,12 +80,12 @@ export class Code extends BaseComponent {
|
|
|
79
80
|
if (!codeEl)
|
|
80
81
|
return;
|
|
81
82
|
codeEl.innerHTML = '';
|
|
82
|
-
const
|
|
83
|
+
const parsedLines = this._parseLines();
|
|
83
84
|
const { startLine, highlightLines, showLineNumbers } = this.state;
|
|
84
|
-
|
|
85
|
+
parsedLines.forEach((parsedLine, index) => {
|
|
85
86
|
const lineNumber = startLine + index;
|
|
86
87
|
const isHighlighted = highlightLines.includes(lineNumber);
|
|
87
|
-
const lineEl = this._createLineElement(
|
|
88
|
+
const lineEl = this._createLineElement(parsedLine, lineNumber, isHighlighted);
|
|
88
89
|
codeEl.appendChild(lineEl);
|
|
89
90
|
});
|
|
90
91
|
// Re-run Prism if available
|
|
@@ -93,9 +94,9 @@ export class Code extends BaseComponent {
|
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
/**
|
|
96
|
-
* Create a single line element
|
|
97
|
+
* Create a single line element with parsed tokens
|
|
97
98
|
*/
|
|
98
|
-
_createLineElement(
|
|
99
|
+
_createLineElement(parsedLine, lineNumber, highlighted) {
|
|
99
100
|
const lineEl = document.createElement('div');
|
|
100
101
|
lineEl.className = 'jux-code-line';
|
|
101
102
|
lineEl.setAttribute('data-line', String(lineNumber));
|
|
@@ -109,10 +110,10 @@ export class Code extends BaseComponent {
|
|
|
109
110
|
lineNum.textContent = String(lineNumber);
|
|
110
111
|
lineEl.appendChild(lineNum);
|
|
111
112
|
}
|
|
112
|
-
// Line content
|
|
113
|
+
// Line content with syntax highlighting
|
|
113
114
|
const lineCode = document.createElement('span');
|
|
114
|
-
lineCode.className =
|
|
115
|
-
lineCode.
|
|
115
|
+
lineCode.className = 'jux-code-line-content';
|
|
116
|
+
lineCode.innerHTML = renderLineWithTokens(parsedLine);
|
|
116
117
|
lineEl.appendChild(lineCode);
|
|
117
118
|
return lineEl;
|
|
118
119
|
}
|
|
@@ -134,6 +135,14 @@ export class Code extends BaseComponent {
|
|
|
134
135
|
* ═════════════════════════════════════════════════════════════════ */
|
|
135
136
|
render(targetId) {
|
|
136
137
|
const container = this._setupContainer(targetId);
|
|
138
|
+
// ✅ Inject syntax CSS once globally
|
|
139
|
+
if (!Code._syntaxCSSInjected) {
|
|
140
|
+
const style = document.createElement('style');
|
|
141
|
+
style.id = 'jux-code-syntax-css';
|
|
142
|
+
style.textContent = getSyntaxHighlightCSS();
|
|
143
|
+
document.head.appendChild(style);
|
|
144
|
+
Code._syntaxCSSInjected = true;
|
|
145
|
+
}
|
|
137
146
|
const { language, showLineNumbers, style, class: className } = this.state;
|
|
138
147
|
const wrapper = document.createElement('div');
|
|
139
148
|
wrapper.className = 'jux-code';
|
|
@@ -148,13 +157,13 @@ export class Code extends BaseComponent {
|
|
|
148
157
|
pre.className = `language-${language}`;
|
|
149
158
|
const codeEl = document.createElement('code');
|
|
150
159
|
codeEl.className = 'jux-code-lines';
|
|
151
|
-
// Build initial lines
|
|
152
|
-
const
|
|
160
|
+
// Build initial lines with parsed tokens
|
|
161
|
+
const parsedLines = this._parseLines();
|
|
153
162
|
const { startLine, highlightLines } = this.state;
|
|
154
|
-
|
|
163
|
+
parsedLines.forEach((parsedLine, index) => {
|
|
155
164
|
const lineNumber = startLine + index;
|
|
156
165
|
const isHighlighted = highlightLines.includes(lineNumber);
|
|
157
|
-
const lineEl = this._createLineElement(
|
|
166
|
+
const lineEl = this._createLineElement(parsedLine, lineNumber, isHighlighted);
|
|
158
167
|
codeEl.appendChild(lineEl);
|
|
159
168
|
});
|
|
160
169
|
pre.appendChild(codeEl);
|
|
@@ -190,14 +199,10 @@ export class Code extends BaseComponent {
|
|
|
190
199
|
});
|
|
191
200
|
container.appendChild(wrapper);
|
|
192
201
|
this._codeContainer = wrapper;
|
|
193
|
-
requestAnimationFrame(() => {
|
|
194
|
-
if (window.Prism) {
|
|
195
|
-
window.Prism.highlightAllUnder(wrapper);
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
202
|
return this;
|
|
199
203
|
}
|
|
200
204
|
}
|
|
205
|
+
Code._syntaxCSSInjected = false;
|
|
201
206
|
export function code(id, options = {}) {
|
|
202
207
|
return new Code(id, options);
|
|
203
208
|
}
|
package/lib/components/code.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseComponent } from './base/BaseComponent.js';
|
|
2
|
+
import { parseCode, renderLineWithTokens, getSyntaxHighlightCSS } from '../utils/codeparser.js';
|
|
2
3
|
|
|
3
4
|
// Event definitions
|
|
4
5
|
const TRIGGER_EVENTS = [] as const;
|
|
@@ -26,6 +27,7 @@ type CodeState = {
|
|
|
26
27
|
|
|
27
28
|
export class Code extends BaseComponent<CodeState> {
|
|
28
29
|
private _codeContainer: HTMLElement | null = null;
|
|
30
|
+
private static _syntaxCSSInjected = false;
|
|
29
31
|
|
|
30
32
|
constructor(id: string, options: CodeOptions = {}) {
|
|
31
33
|
super(id, {
|
|
@@ -99,10 +101,10 @@ export class Code extends BaseComponent<CodeState> {
|
|
|
99
101
|
* ═════════════════════════════════════════════════════════════════ */
|
|
100
102
|
|
|
101
103
|
/**
|
|
102
|
-
* Parse code content into individual lines
|
|
104
|
+
* Parse code content into individual lines with tokens
|
|
103
105
|
*/
|
|
104
|
-
private _parseLines():
|
|
105
|
-
return this.state.content.
|
|
106
|
+
private _parseLines(): ReturnType<typeof parseCode> {
|
|
107
|
+
return parseCode(this.state.content, this.state.language);
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
/**
|
|
@@ -116,14 +118,14 @@ export class Code extends BaseComponent<CodeState> {
|
|
|
116
118
|
|
|
117
119
|
codeEl.innerHTML = '';
|
|
118
120
|
|
|
119
|
-
const
|
|
121
|
+
const parsedLines = this._parseLines();
|
|
120
122
|
const { startLine, highlightLines, showLineNumbers } = this.state;
|
|
121
123
|
|
|
122
|
-
|
|
124
|
+
parsedLines.forEach((parsedLine, index) => {
|
|
123
125
|
const lineNumber = startLine + index;
|
|
124
126
|
const isHighlighted = highlightLines.includes(lineNumber);
|
|
125
127
|
|
|
126
|
-
const lineEl = this._createLineElement(
|
|
128
|
+
const lineEl = this._createLineElement(parsedLine, lineNumber, isHighlighted);
|
|
127
129
|
codeEl.appendChild(lineEl);
|
|
128
130
|
});
|
|
129
131
|
|
|
@@ -134,9 +136,9 @@ export class Code extends BaseComponent<CodeState> {
|
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
/**
|
|
137
|
-
* Create a single line element
|
|
139
|
+
* Create a single line element with parsed tokens
|
|
138
140
|
*/
|
|
139
|
-
private _createLineElement(
|
|
141
|
+
private _createLineElement(parsedLine: ReturnType<typeof parseCode>[0], lineNumber: number, highlighted: boolean): HTMLElement {
|
|
140
142
|
const lineEl = document.createElement('div');
|
|
141
143
|
lineEl.className = 'jux-code-line';
|
|
142
144
|
lineEl.setAttribute('data-line', String(lineNumber));
|
|
@@ -153,10 +155,10 @@ export class Code extends BaseComponent<CodeState> {
|
|
|
153
155
|
lineEl.appendChild(lineNum);
|
|
154
156
|
}
|
|
155
157
|
|
|
156
|
-
// Line content
|
|
158
|
+
// Line content with syntax highlighting
|
|
157
159
|
const lineCode = document.createElement('span');
|
|
158
|
-
lineCode.className =
|
|
159
|
-
lineCode.
|
|
160
|
+
lineCode.className = 'jux-code-line-content';
|
|
161
|
+
lineCode.innerHTML = renderLineWithTokens(parsedLine);
|
|
160
162
|
lineEl.appendChild(lineCode);
|
|
161
163
|
|
|
162
164
|
return lineEl;
|
|
@@ -183,6 +185,15 @@ export class Code extends BaseComponent<CodeState> {
|
|
|
183
185
|
render(targetId?: string | HTMLElement | BaseComponent<any>): this {
|
|
184
186
|
const container = this._setupContainer(targetId);
|
|
185
187
|
|
|
188
|
+
// ✅ Inject syntax CSS once globally
|
|
189
|
+
if (!Code._syntaxCSSInjected) {
|
|
190
|
+
const style = document.createElement('style');
|
|
191
|
+
style.id = 'jux-code-syntax-css';
|
|
192
|
+
style.textContent = getSyntaxHighlightCSS();
|
|
193
|
+
document.head.appendChild(style);
|
|
194
|
+
Code._syntaxCSSInjected = true;
|
|
195
|
+
}
|
|
196
|
+
|
|
186
197
|
const { language, showLineNumbers, style, class: className } = this.state;
|
|
187
198
|
|
|
188
199
|
const wrapper = document.createElement('div');
|
|
@@ -198,14 +209,14 @@ export class Code extends BaseComponent<CodeState> {
|
|
|
198
209
|
const codeEl = document.createElement('code');
|
|
199
210
|
codeEl.className = 'jux-code-lines';
|
|
200
211
|
|
|
201
|
-
// Build initial lines
|
|
202
|
-
const
|
|
212
|
+
// Build initial lines with parsed tokens
|
|
213
|
+
const parsedLines = this._parseLines();
|
|
203
214
|
const { startLine, highlightLines } = this.state;
|
|
204
215
|
|
|
205
|
-
|
|
216
|
+
parsedLines.forEach((parsedLine, index) => {
|
|
206
217
|
const lineNumber = startLine + index;
|
|
207
218
|
const isHighlighted = highlightLines.includes(lineNumber);
|
|
208
|
-
const lineEl = this._createLineElement(
|
|
219
|
+
const lineEl = this._createLineElement(parsedLine, lineNumber, isHighlighted);
|
|
209
220
|
codeEl.appendChild(lineEl);
|
|
210
221
|
});
|
|
211
222
|
|
|
@@ -249,12 +260,6 @@ export class Code extends BaseComponent<CodeState> {
|
|
|
249
260
|
container.appendChild(wrapper);
|
|
250
261
|
this._codeContainer = wrapper;
|
|
251
262
|
|
|
252
|
-
requestAnimationFrame(() => {
|
|
253
|
-
if ((window as any).Prism) {
|
|
254
|
-
(window as any).Prism.highlightAllUnder(wrapper);
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
|
|
258
263
|
return this;
|
|
259
264
|
}
|
|
260
265
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface ParsedToken {
|
|
2
|
+
type: string;
|
|
3
|
+
value: string;
|
|
4
|
+
start: number;
|
|
5
|
+
end: number;
|
|
6
|
+
line: number;
|
|
7
|
+
column: number;
|
|
8
|
+
className: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ParsedLine {
|
|
11
|
+
lineNumber: number;
|
|
12
|
+
tokens: ParsedToken[];
|
|
13
|
+
raw: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Parse JavaScript/TypeScript code using Acorn
|
|
17
|
+
* Returns token-level information for syntax highlighting
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseCode(code: string, language?: string): ParsedLine[];
|
|
20
|
+
/**
|
|
21
|
+
* Render a parsed line as HTML with spans
|
|
22
|
+
*/
|
|
23
|
+
export declare function renderLineWithTokens(parsedLine: ParsedLine): string;
|
|
24
|
+
/**
|
|
25
|
+
* Generate CSS for syntax highlighting
|
|
26
|
+
*/
|
|
27
|
+
export declare function getSyntaxHighlightCSS(): string;
|
|
28
|
+
//# sourceMappingURL=codeparser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codeparser.d.ts","sourceRoot":"","sources":["codeparser.ts"],"names":[],"mappings":"AA4CA,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAqB,GAAG,UAAU,EAAE,CA0ErF;AA+BD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,UAAU,GAAG,MAAM,CAiCnE;AAcD;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAmB9C"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import * as acorn from 'acorn';
|
|
2
|
+
/**
|
|
3
|
+
* Token type to CSS class mapping
|
|
4
|
+
*/
|
|
5
|
+
const TOKEN_CLASS_MAP = {
|
|
6
|
+
// Keywords
|
|
7
|
+
'Keyword': 'token-keyword',
|
|
8
|
+
'this': 'token-keyword',
|
|
9
|
+
'const': 'token-keyword',
|
|
10
|
+
'let': 'token-keyword',
|
|
11
|
+
'var': 'token-keyword',
|
|
12
|
+
'function': 'token-keyword',
|
|
13
|
+
'return': 'token-keyword',
|
|
14
|
+
'if': 'token-keyword',
|
|
15
|
+
'else': 'token-keyword',
|
|
16
|
+
'for': 'token-keyword',
|
|
17
|
+
'while': 'token-keyword',
|
|
18
|
+
'class': 'token-keyword',
|
|
19
|
+
'import': 'token-keyword',
|
|
20
|
+
'export': 'token-keyword',
|
|
21
|
+
'from': 'token-keyword',
|
|
22
|
+
'async': 'token-keyword',
|
|
23
|
+
'await': 'token-keyword',
|
|
24
|
+
// Literals
|
|
25
|
+
'String': 'token-string',
|
|
26
|
+
'Numeric': 'token-number',
|
|
27
|
+
'Boolean': 'token-boolean',
|
|
28
|
+
'Null': 'token-null',
|
|
29
|
+
'RegExp': 'token-regex',
|
|
30
|
+
// Identifiers
|
|
31
|
+
'Identifier': 'token-identifier',
|
|
32
|
+
'PrivateIdentifier': 'token-identifier',
|
|
33
|
+
// Operators
|
|
34
|
+
'Punctuator': 'token-punctuation',
|
|
35
|
+
// Comments
|
|
36
|
+
'LineComment': 'token-comment',
|
|
37
|
+
'BlockComment': 'token-comment',
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Parse JavaScript/TypeScript code using Acorn
|
|
41
|
+
* Returns token-level information for syntax highlighting
|
|
42
|
+
*/
|
|
43
|
+
export function parseCode(code, language = 'javascript') {
|
|
44
|
+
const lines = [];
|
|
45
|
+
const codeLines = code.split('\n');
|
|
46
|
+
try {
|
|
47
|
+
// Parse with Acorn (supports ES2020+)
|
|
48
|
+
const tokens = [];
|
|
49
|
+
acorn.parse(code, {
|
|
50
|
+
ecmaVersion: 2022,
|
|
51
|
+
sourceType: 'module',
|
|
52
|
+
locations: true,
|
|
53
|
+
ranges: true,
|
|
54
|
+
onToken: tokens,
|
|
55
|
+
onComment: (block, text, start, end, startLoc, endLoc) => {
|
|
56
|
+
tokens.push({
|
|
57
|
+
type: block ? 'BlockComment' : 'LineComment',
|
|
58
|
+
value: text,
|
|
59
|
+
start,
|
|
60
|
+
end,
|
|
61
|
+
loc: { start: startLoc, end: endLoc }
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
// Sort tokens by position
|
|
66
|
+
tokens.sort((a, b) => a.start - b.start);
|
|
67
|
+
// Group tokens by line
|
|
68
|
+
const tokensByLine = new Map();
|
|
69
|
+
tokens.forEach(token => {
|
|
70
|
+
const line = token.loc.start.line;
|
|
71
|
+
const parsedToken = {
|
|
72
|
+
type: token.type.label || token.type,
|
|
73
|
+
value: token.value !== undefined ? String(token.value) : code.substring(token.start, token.end),
|
|
74
|
+
start: token.start,
|
|
75
|
+
end: token.end,
|
|
76
|
+
line: token.loc.start.line,
|
|
77
|
+
column: token.loc.start.column,
|
|
78
|
+
className: getTokenClass(token)
|
|
79
|
+
};
|
|
80
|
+
if (!tokensByLine.has(line)) {
|
|
81
|
+
tokensByLine.set(line, []);
|
|
82
|
+
}
|
|
83
|
+
tokensByLine.get(line).push(parsedToken);
|
|
84
|
+
});
|
|
85
|
+
// Build lines with tokens
|
|
86
|
+
codeLines.forEach((lineText, index) => {
|
|
87
|
+
const lineNumber = index + 1;
|
|
88
|
+
const lineTokens = tokensByLine.get(lineNumber) || [];
|
|
89
|
+
lines.push({
|
|
90
|
+
lineNumber,
|
|
91
|
+
tokens: lineTokens,
|
|
92
|
+
raw: lineText
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
// Fallback: if parsing fails, return plain lines
|
|
98
|
+
console.warn('[CodeParser] Parse failed, using plain text:', error);
|
|
99
|
+
codeLines.forEach((lineText, index) => {
|
|
100
|
+
lines.push({
|
|
101
|
+
lineNumber: index + 1,
|
|
102
|
+
tokens: [],
|
|
103
|
+
raw: lineText
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return lines;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get CSS class for a token
|
|
111
|
+
*/
|
|
112
|
+
function getTokenClass(token) {
|
|
113
|
+
const type = token.type.label || token.type;
|
|
114
|
+
// Check if it's a keyword
|
|
115
|
+
if (type === 'name' && token.value && isKeyword(token.value)) {
|
|
116
|
+
return TOKEN_CLASS_MAP[token.value] || TOKEN_CLASS_MAP['Keyword'];
|
|
117
|
+
}
|
|
118
|
+
// Map type to class
|
|
119
|
+
return TOKEN_CLASS_MAP[type] || 'token-default';
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Check if a word is a JavaScript keyword
|
|
123
|
+
*/
|
|
124
|
+
function isKeyword(word) {
|
|
125
|
+
const keywords = [
|
|
126
|
+
'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger',
|
|
127
|
+
'default', 'delete', 'do', 'else', 'export', 'extends', 'finally', 'for',
|
|
128
|
+
'function', 'if', 'import', 'in', 'instanceof', 'let', 'new', 'return',
|
|
129
|
+
'super', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void',
|
|
130
|
+
'while', 'with', 'yield', 'async', 'of', 'static', 'get', 'set'
|
|
131
|
+
];
|
|
132
|
+
return keywords.includes(word);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Render a parsed line as HTML with spans
|
|
136
|
+
*/
|
|
137
|
+
export function renderLineWithTokens(parsedLine) {
|
|
138
|
+
if (parsedLine.tokens.length === 0) {
|
|
139
|
+
// No tokens, return plain text (escaped)
|
|
140
|
+
return escapeHtml(parsedLine.raw) || ' ';
|
|
141
|
+
}
|
|
142
|
+
const { raw, tokens } = parsedLine;
|
|
143
|
+
let html = '';
|
|
144
|
+
let lastIndex = 0;
|
|
145
|
+
// Calculate line start position in original code
|
|
146
|
+
const lineStartPos = tokens[0]?.start - tokens[0]?.column;
|
|
147
|
+
tokens.forEach(token => {
|
|
148
|
+
// Add any whitespace/text between tokens
|
|
149
|
+
const localStart = token.start - lineStartPos;
|
|
150
|
+
if (localStart > lastIndex) {
|
|
151
|
+
html += escapeHtml(raw.substring(lastIndex, localStart));
|
|
152
|
+
}
|
|
153
|
+
// Add token with span
|
|
154
|
+
const localEnd = token.end - lineStartPos;
|
|
155
|
+
const tokenText = raw.substring(localStart, localEnd);
|
|
156
|
+
html += `<span class="${token.className}">${escapeHtml(tokenText)}</span>`;
|
|
157
|
+
lastIndex = localEnd;
|
|
158
|
+
});
|
|
159
|
+
// Add any remaining text
|
|
160
|
+
if (lastIndex < raw.length) {
|
|
161
|
+
html += escapeHtml(raw.substring(lastIndex));
|
|
162
|
+
}
|
|
163
|
+
return html || ' ';
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Escape HTML entities
|
|
167
|
+
*/
|
|
168
|
+
function escapeHtml(text) {
|
|
169
|
+
return text
|
|
170
|
+
.replace(/&/g, '&')
|
|
171
|
+
.replace(/</g, '<')
|
|
172
|
+
.replace(/>/g, '>')
|
|
173
|
+
.replace(/"/g, '"')
|
|
174
|
+
.replace(/'/g, ''');
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Generate CSS for syntax highlighting
|
|
178
|
+
*/
|
|
179
|
+
export function getSyntaxHighlightCSS() {
|
|
180
|
+
return `
|
|
181
|
+
/* Code Parser Syntax Highlighting */
|
|
182
|
+
.token-keyword { color: #c678dd; font-weight: 600; }
|
|
183
|
+
.token-string { color: #98c379; }
|
|
184
|
+
.token-number { color: #d19a66; }
|
|
185
|
+
.token-boolean { color: #d19a66; }
|
|
186
|
+
.token-null { color: #d19a66; }
|
|
187
|
+
.token-regex { color: #e06c75; }
|
|
188
|
+
.token-identifier { color: #e5c07b; }
|
|
189
|
+
.token-punctuation { color: #abb2bf; }
|
|
190
|
+
.token-comment { color: #5c6370; font-style: italic; }
|
|
191
|
+
.token-default { color: #abb2bf; }
|
|
192
|
+
|
|
193
|
+
/* Function calls */
|
|
194
|
+
.token-identifier + .token-punctuation:has(+ .token-punctuation) {
|
|
195
|
+
color: #61afef; /* Function names in blue */
|
|
196
|
+
}
|
|
197
|
+
`;
|
|
198
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import * as acorn from 'acorn';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Token type to CSS class mapping
|
|
5
|
+
*/
|
|
6
|
+
const TOKEN_CLASS_MAP: Record<string, string> = {
|
|
7
|
+
// Keywords
|
|
8
|
+
'Keyword': 'token-keyword',
|
|
9
|
+
'this': 'token-keyword',
|
|
10
|
+
'const': 'token-keyword',
|
|
11
|
+
'let': 'token-keyword',
|
|
12
|
+
'var': 'token-keyword',
|
|
13
|
+
'function': 'token-keyword',
|
|
14
|
+
'return': 'token-keyword',
|
|
15
|
+
'if': 'token-keyword',
|
|
16
|
+
'else': 'token-keyword',
|
|
17
|
+
'for': 'token-keyword',
|
|
18
|
+
'while': 'token-keyword',
|
|
19
|
+
'class': 'token-keyword',
|
|
20
|
+
'import': 'token-keyword',
|
|
21
|
+
'export': 'token-keyword',
|
|
22
|
+
'from': 'token-keyword',
|
|
23
|
+
'async': 'token-keyword',
|
|
24
|
+
'await': 'token-keyword',
|
|
25
|
+
|
|
26
|
+
// Literals
|
|
27
|
+
'String': 'token-string',
|
|
28
|
+
'Numeric': 'token-number',
|
|
29
|
+
'Boolean': 'token-boolean',
|
|
30
|
+
'Null': 'token-null',
|
|
31
|
+
'RegExp': 'token-regex',
|
|
32
|
+
|
|
33
|
+
// Identifiers
|
|
34
|
+
'Identifier': 'token-identifier',
|
|
35
|
+
'PrivateIdentifier': 'token-identifier',
|
|
36
|
+
|
|
37
|
+
// Operators
|
|
38
|
+
'Punctuator': 'token-punctuation',
|
|
39
|
+
|
|
40
|
+
// Comments
|
|
41
|
+
'LineComment': 'token-comment',
|
|
42
|
+
'BlockComment': 'token-comment',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export interface ParsedToken {
|
|
46
|
+
type: string;
|
|
47
|
+
value: string;
|
|
48
|
+
start: number;
|
|
49
|
+
end: number;
|
|
50
|
+
line: number;
|
|
51
|
+
column: number;
|
|
52
|
+
className: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ParsedLine {
|
|
56
|
+
lineNumber: number;
|
|
57
|
+
tokens: ParsedToken[];
|
|
58
|
+
raw: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse JavaScript/TypeScript code using Acorn
|
|
63
|
+
* Returns token-level information for syntax highlighting
|
|
64
|
+
*/
|
|
65
|
+
export function parseCode(code: string, language: string = 'javascript'): ParsedLine[] {
|
|
66
|
+
const lines: ParsedLine[] = [];
|
|
67
|
+
const codeLines = code.split('\n');
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// Parse with Acorn (supports ES2020+)
|
|
71
|
+
const tokens: any[] = [];
|
|
72
|
+
|
|
73
|
+
acorn.parse(code, {
|
|
74
|
+
ecmaVersion: 2022,
|
|
75
|
+
sourceType: 'module',
|
|
76
|
+
locations: true,
|
|
77
|
+
ranges: true,
|
|
78
|
+
onToken: tokens,
|
|
79
|
+
onComment: (block, text, start, end, startLoc, endLoc) => {
|
|
80
|
+
tokens.push({
|
|
81
|
+
type: block ? 'BlockComment' : 'LineComment',
|
|
82
|
+
value: text,
|
|
83
|
+
start,
|
|
84
|
+
end,
|
|
85
|
+
loc: { start: startLoc, end: endLoc }
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Sort tokens by position
|
|
91
|
+
tokens.sort((a, b) => a.start - b.start);
|
|
92
|
+
|
|
93
|
+
// Group tokens by line
|
|
94
|
+
const tokensByLine: Map<number, ParsedToken[]> = new Map();
|
|
95
|
+
|
|
96
|
+
tokens.forEach(token => {
|
|
97
|
+
const line = token.loc.start.line;
|
|
98
|
+
const parsedToken: ParsedToken = {
|
|
99
|
+
type: token.type.label || token.type,
|
|
100
|
+
value: token.value !== undefined ? String(token.value) : code.substring(token.start, token.end),
|
|
101
|
+
start: token.start,
|
|
102
|
+
end: token.end,
|
|
103
|
+
line: token.loc.start.line,
|
|
104
|
+
column: token.loc.start.column,
|
|
105
|
+
className: getTokenClass(token)
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (!tokensByLine.has(line)) {
|
|
109
|
+
tokensByLine.set(line, []);
|
|
110
|
+
}
|
|
111
|
+
tokensByLine.get(line)!.push(parsedToken);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Build lines with tokens
|
|
115
|
+
codeLines.forEach((lineText, index) => {
|
|
116
|
+
const lineNumber = index + 1;
|
|
117
|
+
const lineTokens = tokensByLine.get(lineNumber) || [];
|
|
118
|
+
|
|
119
|
+
lines.push({
|
|
120
|
+
lineNumber,
|
|
121
|
+
tokens: lineTokens,
|
|
122
|
+
raw: lineText
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// Fallback: if parsing fails, return plain lines
|
|
128
|
+
console.warn('[CodeParser] Parse failed, using plain text:', error);
|
|
129
|
+
codeLines.forEach((lineText, index) => {
|
|
130
|
+
lines.push({
|
|
131
|
+
lineNumber: index + 1,
|
|
132
|
+
tokens: [],
|
|
133
|
+
raw: lineText
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return lines;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get CSS class for a token
|
|
143
|
+
*/
|
|
144
|
+
function getTokenClass(token: any): string {
|
|
145
|
+
const type = token.type.label || token.type;
|
|
146
|
+
|
|
147
|
+
// Check if it's a keyword
|
|
148
|
+
if (type === 'name' && token.value && isKeyword(token.value)) {
|
|
149
|
+
return TOKEN_CLASS_MAP[token.value] || TOKEN_CLASS_MAP['Keyword'];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Map type to class
|
|
153
|
+
return TOKEN_CLASS_MAP[type] || 'token-default';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Check if a word is a JavaScript keyword
|
|
158
|
+
*/
|
|
159
|
+
function isKeyword(word: string): boolean {
|
|
160
|
+
const keywords = [
|
|
161
|
+
'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger',
|
|
162
|
+
'default', 'delete', 'do', 'else', 'export', 'extends', 'finally', 'for',
|
|
163
|
+
'function', 'if', 'import', 'in', 'instanceof', 'let', 'new', 'return',
|
|
164
|
+
'super', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void',
|
|
165
|
+
'while', 'with', 'yield', 'async', 'of', 'static', 'get', 'set'
|
|
166
|
+
];
|
|
167
|
+
return keywords.includes(word);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Render a parsed line as HTML with spans
|
|
172
|
+
*/
|
|
173
|
+
export function renderLineWithTokens(parsedLine: ParsedLine): string {
|
|
174
|
+
if (parsedLine.tokens.length === 0) {
|
|
175
|
+
// No tokens, return plain text (escaped)
|
|
176
|
+
return escapeHtml(parsedLine.raw) || ' ';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const { raw, tokens } = parsedLine;
|
|
180
|
+
let html = '';
|
|
181
|
+
let lastIndex = 0;
|
|
182
|
+
|
|
183
|
+
// Calculate line start position in original code
|
|
184
|
+
const lineStartPos = tokens[0]?.start - tokens[0]?.column;
|
|
185
|
+
|
|
186
|
+
tokens.forEach(token => {
|
|
187
|
+
// Add any whitespace/text between tokens
|
|
188
|
+
const localStart = token.start - lineStartPos;
|
|
189
|
+
if (localStart > lastIndex) {
|
|
190
|
+
html += escapeHtml(raw.substring(lastIndex, localStart));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Add token with span
|
|
194
|
+
const localEnd = token.end - lineStartPos;
|
|
195
|
+
const tokenText = raw.substring(localStart, localEnd);
|
|
196
|
+
html += `<span class="${token.className}">${escapeHtml(tokenText)}</span>`;
|
|
197
|
+
lastIndex = localEnd;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Add any remaining text
|
|
201
|
+
if (lastIndex < raw.length) {
|
|
202
|
+
html += escapeHtml(raw.substring(lastIndex));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return html || ' ';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Escape HTML entities
|
|
210
|
+
*/
|
|
211
|
+
function escapeHtml(text: string): string {
|
|
212
|
+
return text
|
|
213
|
+
.replace(/&/g, '&')
|
|
214
|
+
.replace(/</g, '<')
|
|
215
|
+
.replace(/>/g, '>')
|
|
216
|
+
.replace(/"/g, '"')
|
|
217
|
+
.replace(/'/g, ''');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Generate CSS for syntax highlighting
|
|
222
|
+
*/
|
|
223
|
+
export function getSyntaxHighlightCSS(): string {
|
|
224
|
+
return `
|
|
225
|
+
/* Code Parser Syntax Highlighting */
|
|
226
|
+
.token-keyword { color: #c678dd; font-weight: 600; }
|
|
227
|
+
.token-string { color: #98c379; }
|
|
228
|
+
.token-number { color: #d19a66; }
|
|
229
|
+
.token-boolean { color: #d19a66; }
|
|
230
|
+
.token-null { color: #d19a66; }
|
|
231
|
+
.token-regex { color: #e06c75; }
|
|
232
|
+
.token-identifier { color: #e5c07b; }
|
|
233
|
+
.token-punctuation { color: #abb2bf; }
|
|
234
|
+
.token-comment { color: #5c6370; font-style: italic; }
|
|
235
|
+
.token-default { color: #abb2bf; }
|
|
236
|
+
|
|
237
|
+
/* Function calls */
|
|
238
|
+
.token-identifier + .token-punctuation:has(+ .token-punctuation) {
|
|
239
|
+
color: #61afef; /* Function names in blue */
|
|
240
|
+
}
|
|
241
|
+
`;
|
|
242
|
+
}
|