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