juxscript 1.1.43 → 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 +27 -0
- package/lib/components/code.d.ts.map +1 -1
- package/lib/components/code.js +135 -23
- package/lib/components/code.ts +163 -26
- 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
|
@@ -2,22 +2,49 @@ import { BaseComponent } from './base/BaseComponent.js';
|
|
|
2
2
|
export interface CodeOptions {
|
|
3
3
|
content?: string;
|
|
4
4
|
language?: string;
|
|
5
|
+
showLineNumbers?: boolean;
|
|
6
|
+
startLine?: number;
|
|
7
|
+
highlightLines?: number[];
|
|
5
8
|
style?: string;
|
|
6
9
|
class?: string;
|
|
7
10
|
}
|
|
8
11
|
type CodeState = {
|
|
9
12
|
content: string;
|
|
10
13
|
language: string;
|
|
14
|
+
showLineNumbers: boolean;
|
|
15
|
+
startLine: number;
|
|
16
|
+
highlightLines: number[];
|
|
11
17
|
style: string;
|
|
12
18
|
class: string;
|
|
13
19
|
};
|
|
14
20
|
export declare class Code extends BaseComponent<CodeState> {
|
|
21
|
+
private _codeContainer;
|
|
22
|
+
private static _syntaxCSSInjected;
|
|
15
23
|
constructor(id: string, options?: CodeOptions);
|
|
16
24
|
protected getTriggerEvents(): readonly string[];
|
|
17
25
|
protected getCallbackEvents(): readonly string[];
|
|
18
26
|
update(prop: string, value: any): void;
|
|
19
27
|
content(value: string): this;
|
|
20
28
|
language(value: string): this;
|
|
29
|
+
showLineNumbers(value: boolean): this;
|
|
30
|
+
startLine(value: number): this;
|
|
31
|
+
highlightLines(lines: number[]): this;
|
|
32
|
+
/**
|
|
33
|
+
* Parse code content into individual lines with tokens
|
|
34
|
+
*/
|
|
35
|
+
private _parseLines;
|
|
36
|
+
/**
|
|
37
|
+
* Rebuild all line elements
|
|
38
|
+
*/
|
|
39
|
+
private _rebuildLines;
|
|
40
|
+
/**
|
|
41
|
+
* Create a single line element with parsed tokens
|
|
42
|
+
*/
|
|
43
|
+
private _createLineElement;
|
|
44
|
+
/**
|
|
45
|
+
* Update highlighted lines without full rebuild
|
|
46
|
+
*/
|
|
47
|
+
private _updateHighlights;
|
|
21
48
|
render(targetId?: string | HTMLElement | BaseComponent<any>): this;
|
|
22
49
|
}
|
|
23
50
|
export declare function code(id: string, options?: CodeOptions): Code;
|
|
@@ -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 = [];
|
|
@@ -7,9 +8,13 @@ export class Code extends BaseComponent {
|
|
|
7
8
|
super(id, {
|
|
8
9
|
content: options.content ?? '',
|
|
9
10
|
language: options.language ?? 'javascript',
|
|
11
|
+
showLineNumbers: options.showLineNumbers ?? true,
|
|
12
|
+
startLine: options.startLine ?? 1,
|
|
13
|
+
highlightLines: options.highlightLines ?? [],
|
|
10
14
|
style: options.style ?? '',
|
|
11
15
|
class: options.class ?? ''
|
|
12
16
|
});
|
|
17
|
+
this._codeContainer = null;
|
|
13
18
|
}
|
|
14
19
|
getTriggerEvents() {
|
|
15
20
|
return TRIGGER_EVENTS;
|
|
@@ -18,7 +23,20 @@ export class Code extends BaseComponent {
|
|
|
18
23
|
return CALLBACK_EVENTS;
|
|
19
24
|
}
|
|
20
25
|
update(prop, value) {
|
|
21
|
-
|
|
26
|
+
super.update(prop, value);
|
|
27
|
+
if (!this._codeContainer)
|
|
28
|
+
return;
|
|
29
|
+
switch (prop) {
|
|
30
|
+
case 'content':
|
|
31
|
+
this._rebuildLines();
|
|
32
|
+
break;
|
|
33
|
+
case 'showLineNumbers':
|
|
34
|
+
this._codeContainer.classList.toggle('jux-code-no-numbers', !value);
|
|
35
|
+
break;
|
|
36
|
+
case 'highlightLines':
|
|
37
|
+
this._updateHighlights();
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
22
40
|
}
|
|
23
41
|
/* ═════════════════════════════════════════════════════════════════
|
|
24
42
|
* FLUENT API
|
|
@@ -31,23 +49,123 @@ export class Code extends BaseComponent {
|
|
|
31
49
|
this.state.language = value;
|
|
32
50
|
return this;
|
|
33
51
|
}
|
|
52
|
+
showLineNumbers(value) {
|
|
53
|
+
this.state.showLineNumbers = value;
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
startLine(value) {
|
|
57
|
+
this.state.startLine = value;
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
highlightLines(lines) {
|
|
61
|
+
this.state.highlightLines = lines;
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
65
|
+
* PARSING & RENDERING
|
|
66
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
67
|
+
/**
|
|
68
|
+
* Parse code content into individual lines with tokens
|
|
69
|
+
*/
|
|
70
|
+
_parseLines() {
|
|
71
|
+
return parseCode(this.state.content, this.state.language);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Rebuild all line elements
|
|
75
|
+
*/
|
|
76
|
+
_rebuildLines() {
|
|
77
|
+
if (!this._codeContainer)
|
|
78
|
+
return;
|
|
79
|
+
const codeEl = this._codeContainer.querySelector('.jux-code-lines');
|
|
80
|
+
if (!codeEl)
|
|
81
|
+
return;
|
|
82
|
+
codeEl.innerHTML = '';
|
|
83
|
+
const parsedLines = this._parseLines();
|
|
84
|
+
const { startLine, highlightLines, showLineNumbers } = this.state;
|
|
85
|
+
parsedLines.forEach((parsedLine, index) => {
|
|
86
|
+
const lineNumber = startLine + index;
|
|
87
|
+
const isHighlighted = highlightLines.includes(lineNumber);
|
|
88
|
+
const lineEl = this._createLineElement(parsedLine, lineNumber, isHighlighted);
|
|
89
|
+
codeEl.appendChild(lineEl);
|
|
90
|
+
});
|
|
91
|
+
// Re-run Prism if available
|
|
92
|
+
if (window.Prism && this._codeContainer) {
|
|
93
|
+
window.Prism.highlightAllUnder(this._codeContainer);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Create a single line element with parsed tokens
|
|
98
|
+
*/
|
|
99
|
+
_createLineElement(parsedLine, lineNumber, highlighted) {
|
|
100
|
+
const lineEl = document.createElement('div');
|
|
101
|
+
lineEl.className = 'jux-code-line';
|
|
102
|
+
lineEl.setAttribute('data-line', String(lineNumber));
|
|
103
|
+
if (highlighted) {
|
|
104
|
+
lineEl.classList.add('jux-code-line-highlight');
|
|
105
|
+
}
|
|
106
|
+
// Line number
|
|
107
|
+
if (this.state.showLineNumbers) {
|
|
108
|
+
const lineNum = document.createElement('span');
|
|
109
|
+
lineNum.className = 'jux-code-line-number';
|
|
110
|
+
lineNum.textContent = String(lineNumber);
|
|
111
|
+
lineEl.appendChild(lineNum);
|
|
112
|
+
}
|
|
113
|
+
// Line content with syntax highlighting
|
|
114
|
+
const lineCode = document.createElement('span');
|
|
115
|
+
lineCode.className = 'jux-code-line-content';
|
|
116
|
+
lineCode.innerHTML = renderLineWithTokens(parsedLine);
|
|
117
|
+
lineEl.appendChild(lineCode);
|
|
118
|
+
return lineEl;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Update highlighted lines without full rebuild
|
|
122
|
+
*/
|
|
123
|
+
_updateHighlights() {
|
|
124
|
+
if (!this._codeContainer)
|
|
125
|
+
return;
|
|
126
|
+
const lines = this._codeContainer.querySelectorAll('.jux-code-line');
|
|
127
|
+
lines.forEach((line) => {
|
|
128
|
+
const lineNum = parseInt(line.getAttribute('data-line') || '0', 10);
|
|
129
|
+
const isHighlighted = this.state.highlightLines.includes(lineNum);
|
|
130
|
+
line.classList.toggle('jux-code-line-highlight', isHighlighted);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
34
133
|
/* ═════════════════════════════════════════════════════════════════
|
|
35
134
|
* RENDER
|
|
36
135
|
* ═════════════════════════════════════════════════════════════════ */
|
|
37
136
|
render(targetId) {
|
|
38
137
|
const container = this._setupContainer(targetId);
|
|
39
|
-
|
|
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
|
+
}
|
|
146
|
+
const { language, showLineNumbers, style, class: className } = this.state;
|
|
40
147
|
const wrapper = document.createElement('div');
|
|
41
148
|
wrapper.className = 'jux-code';
|
|
42
149
|
wrapper.id = this._id;
|
|
150
|
+
if (!showLineNumbers)
|
|
151
|
+
wrapper.classList.add('jux-code-no-numbers');
|
|
43
152
|
if (className)
|
|
44
153
|
wrapper.className += ` ${className}`;
|
|
45
154
|
if (style)
|
|
46
155
|
wrapper.setAttribute('style', style);
|
|
47
156
|
const pre = document.createElement('pre');
|
|
157
|
+
pre.className = `language-${language}`;
|
|
48
158
|
const codeEl = document.createElement('code');
|
|
49
|
-
codeEl.className =
|
|
50
|
-
|
|
159
|
+
codeEl.className = 'jux-code-lines';
|
|
160
|
+
// Build initial lines with parsed tokens
|
|
161
|
+
const parsedLines = this._parseLines();
|
|
162
|
+
const { startLine, highlightLines } = this.state;
|
|
163
|
+
parsedLines.forEach((parsedLine, index) => {
|
|
164
|
+
const lineNumber = startLine + index;
|
|
165
|
+
const isHighlighted = highlightLines.includes(lineNumber);
|
|
166
|
+
const lineEl = this._createLineElement(parsedLine, lineNumber, isHighlighted);
|
|
167
|
+
codeEl.appendChild(lineEl);
|
|
168
|
+
});
|
|
51
169
|
pre.appendChild(codeEl);
|
|
52
170
|
wrapper.appendChild(pre);
|
|
53
171
|
this._wireStandardEvents(wrapper);
|
|
@@ -57,40 +175,34 @@ export class Code extends BaseComponent {
|
|
|
57
175
|
const transform = toComponent || ((v) => String(v));
|
|
58
176
|
stateObj.subscribe((val) => {
|
|
59
177
|
const transformed = transform(val);
|
|
60
|
-
codeEl.textContent = transformed;
|
|
61
178
|
this.state.content = transformed;
|
|
62
|
-
|
|
63
|
-
window.Prism.highlightElement(codeEl);
|
|
64
|
-
}
|
|
179
|
+
this._rebuildLines();
|
|
65
180
|
});
|
|
66
181
|
}
|
|
67
182
|
else if (property === 'language') {
|
|
68
183
|
const transform = toComponent || ((v) => String(v));
|
|
69
184
|
stateObj.subscribe((val) => {
|
|
70
185
|
const transformed = transform(val);
|
|
71
|
-
codeEl.className = `language-${transformed}`;
|
|
72
186
|
this.state.language = transformed;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
187
|
+
pre.className = `language-${transformed}`;
|
|
188
|
+
this._rebuildLines();
|
|
76
189
|
});
|
|
77
190
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// ✅ Helpful hint in development
|
|
86
|
-
console.log(`💡 Tip: Add Prism.js for syntax highlighting:\n` +
|
|
87
|
-
` <link href="https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism.min.css" rel="stylesheet" />\n` +
|
|
88
|
-
` <script src="https://cdn.jsdelivr.net/npm/prismjs@1/prism.min.js"></script>`);
|
|
191
|
+
else if (property === 'highlightLines') {
|
|
192
|
+
const transform = toComponent || ((v) => (Array.isArray(v) ? v : []));
|
|
193
|
+
stateObj.subscribe((val) => {
|
|
194
|
+
const transformed = transform(val);
|
|
195
|
+
this.state.highlightLines = transformed;
|
|
196
|
+
this._updateHighlights();
|
|
197
|
+
});
|
|
89
198
|
}
|
|
90
199
|
});
|
|
200
|
+
container.appendChild(wrapper);
|
|
201
|
+
this._codeContainer = wrapper;
|
|
91
202
|
return this;
|
|
92
203
|
}
|
|
93
204
|
}
|
|
205
|
+
Code._syntaxCSSInjected = false;
|
|
94
206
|
export function code(id, options = {}) {
|
|
95
207
|
return new Code(id, options);
|
|
96
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;
|
|
@@ -7,6 +8,9 @@ const CALLBACK_EVENTS = [] as const;
|
|
|
7
8
|
export interface CodeOptions {
|
|
8
9
|
content?: string;
|
|
9
10
|
language?: string;
|
|
11
|
+
showLineNumbers?: boolean;
|
|
12
|
+
startLine?: number;
|
|
13
|
+
highlightLines?: number[];
|
|
10
14
|
style?: string;
|
|
11
15
|
class?: string;
|
|
12
16
|
}
|
|
@@ -14,15 +18,24 @@ export interface CodeOptions {
|
|
|
14
18
|
type CodeState = {
|
|
15
19
|
content: string;
|
|
16
20
|
language: string;
|
|
21
|
+
showLineNumbers: boolean;
|
|
22
|
+
startLine: number;
|
|
23
|
+
highlightLines: number[];
|
|
17
24
|
style: string;
|
|
18
25
|
class: string;
|
|
19
26
|
};
|
|
20
27
|
|
|
21
28
|
export class Code extends BaseComponent<CodeState> {
|
|
29
|
+
private _codeContainer: HTMLElement | null = null;
|
|
30
|
+
private static _syntaxCSSInjected = false;
|
|
31
|
+
|
|
22
32
|
constructor(id: string, options: CodeOptions = {}) {
|
|
23
33
|
super(id, {
|
|
24
34
|
content: options.content ?? '',
|
|
25
35
|
language: options.language ?? 'javascript',
|
|
36
|
+
showLineNumbers: options.showLineNumbers ?? true,
|
|
37
|
+
startLine: options.startLine ?? 1,
|
|
38
|
+
highlightLines: options.highlightLines ?? [],
|
|
26
39
|
style: options.style ?? '',
|
|
27
40
|
class: options.class ?? ''
|
|
28
41
|
});
|
|
@@ -37,7 +50,21 @@ export class Code extends BaseComponent<CodeState> {
|
|
|
37
50
|
}
|
|
38
51
|
|
|
39
52
|
update(prop: string, value: any): void {
|
|
40
|
-
|
|
53
|
+
super.update(prop, value);
|
|
54
|
+
|
|
55
|
+
if (!this._codeContainer) return;
|
|
56
|
+
|
|
57
|
+
switch (prop) {
|
|
58
|
+
case 'content':
|
|
59
|
+
this._rebuildLines();
|
|
60
|
+
break;
|
|
61
|
+
case 'showLineNumbers':
|
|
62
|
+
this._codeContainer.classList.toggle('jux-code-no-numbers', !value);
|
|
63
|
+
break;
|
|
64
|
+
case 'highlightLines':
|
|
65
|
+
this._updateHighlights();
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
41
68
|
}
|
|
42
69
|
|
|
43
70
|
/* ═════════════════════════════════════════════════════════════════
|
|
@@ -54,6 +81,103 @@ export class Code extends BaseComponent<CodeState> {
|
|
|
54
81
|
return this;
|
|
55
82
|
}
|
|
56
83
|
|
|
84
|
+
showLineNumbers(value: boolean): this {
|
|
85
|
+
this.state.showLineNumbers = value;
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
startLine(value: number): this {
|
|
90
|
+
this.state.startLine = value;
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
highlightLines(lines: number[]): this {
|
|
95
|
+
this.state.highlightLines = lines;
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
100
|
+
* PARSING & RENDERING
|
|
101
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Parse code content into individual lines with tokens
|
|
105
|
+
*/
|
|
106
|
+
private _parseLines(): ReturnType<typeof parseCode> {
|
|
107
|
+
return parseCode(this.state.content, this.state.language);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Rebuild all line elements
|
|
112
|
+
*/
|
|
113
|
+
private _rebuildLines(): void {
|
|
114
|
+
if (!this._codeContainer) return;
|
|
115
|
+
|
|
116
|
+
const codeEl = this._codeContainer.querySelector('.jux-code-lines');
|
|
117
|
+
if (!codeEl) return;
|
|
118
|
+
|
|
119
|
+
codeEl.innerHTML = '';
|
|
120
|
+
|
|
121
|
+
const parsedLines = this._parseLines();
|
|
122
|
+
const { startLine, highlightLines, showLineNumbers } = this.state;
|
|
123
|
+
|
|
124
|
+
parsedLines.forEach((parsedLine, index) => {
|
|
125
|
+
const lineNumber = startLine + index;
|
|
126
|
+
const isHighlighted = highlightLines.includes(lineNumber);
|
|
127
|
+
|
|
128
|
+
const lineEl = this._createLineElement(parsedLine, lineNumber, isHighlighted);
|
|
129
|
+
codeEl.appendChild(lineEl);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Re-run Prism if available
|
|
133
|
+
if ((window as any).Prism && this._codeContainer) {
|
|
134
|
+
(window as any).Prism.highlightAllUnder(this._codeContainer);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Create a single line element with parsed tokens
|
|
140
|
+
*/
|
|
141
|
+
private _createLineElement(parsedLine: ReturnType<typeof parseCode>[0], lineNumber: number, highlighted: boolean): HTMLElement {
|
|
142
|
+
const lineEl = document.createElement('div');
|
|
143
|
+
lineEl.className = 'jux-code-line';
|
|
144
|
+
lineEl.setAttribute('data-line', String(lineNumber));
|
|
145
|
+
|
|
146
|
+
if (highlighted) {
|
|
147
|
+
lineEl.classList.add('jux-code-line-highlight');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Line number
|
|
151
|
+
if (this.state.showLineNumbers) {
|
|
152
|
+
const lineNum = document.createElement('span');
|
|
153
|
+
lineNum.className = 'jux-code-line-number';
|
|
154
|
+
lineNum.textContent = String(lineNumber);
|
|
155
|
+
lineEl.appendChild(lineNum);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Line content with syntax highlighting
|
|
159
|
+
const lineCode = document.createElement('span');
|
|
160
|
+
lineCode.className = 'jux-code-line-content';
|
|
161
|
+
lineCode.innerHTML = renderLineWithTokens(parsedLine);
|
|
162
|
+
lineEl.appendChild(lineCode);
|
|
163
|
+
|
|
164
|
+
return lineEl;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Update highlighted lines without full rebuild
|
|
169
|
+
*/
|
|
170
|
+
private _updateHighlights(): void {
|
|
171
|
+
if (!this._codeContainer) return;
|
|
172
|
+
|
|
173
|
+
const lines = this._codeContainer.querySelectorAll('.jux-code-line');
|
|
174
|
+
lines.forEach((line) => {
|
|
175
|
+
const lineNum = parseInt(line.getAttribute('data-line') || '0', 10);
|
|
176
|
+
const isHighlighted = this.state.highlightLines.includes(lineNum);
|
|
177
|
+
line.classList.toggle('jux-code-line-highlight', isHighlighted);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
57
181
|
/* ═════════════════════════════════════════════════════════════════
|
|
58
182
|
* RENDER
|
|
59
183
|
* ═════════════════════════════════════════════════════════════════ */
|
|
@@ -61,18 +185,41 @@ export class Code extends BaseComponent<CodeState> {
|
|
|
61
185
|
render(targetId?: string | HTMLElement | BaseComponent<any>): this {
|
|
62
186
|
const container = this._setupContainer(targetId);
|
|
63
187
|
|
|
64
|
-
|
|
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
|
+
|
|
197
|
+
const { language, showLineNumbers, style, class: className } = this.state;
|
|
65
198
|
|
|
66
199
|
const wrapper = document.createElement('div');
|
|
67
200
|
wrapper.className = 'jux-code';
|
|
68
201
|
wrapper.id = this._id;
|
|
202
|
+
if (!showLineNumbers) wrapper.classList.add('jux-code-no-numbers');
|
|
69
203
|
if (className) wrapper.className += ` ${className}`;
|
|
70
204
|
if (style) wrapper.setAttribute('style', style);
|
|
71
205
|
|
|
72
206
|
const pre = document.createElement('pre');
|
|
207
|
+
pre.className = `language-${language}`;
|
|
208
|
+
|
|
73
209
|
const codeEl = document.createElement('code');
|
|
74
|
-
codeEl.className =
|
|
75
|
-
|
|
210
|
+
codeEl.className = 'jux-code-lines';
|
|
211
|
+
|
|
212
|
+
// Build initial lines with parsed tokens
|
|
213
|
+
const parsedLines = this._parseLines();
|
|
214
|
+
const { startLine, highlightLines } = this.state;
|
|
215
|
+
|
|
216
|
+
parsedLines.forEach((parsedLine, index) => {
|
|
217
|
+
const lineNumber = startLine + index;
|
|
218
|
+
const isHighlighted = highlightLines.includes(lineNumber);
|
|
219
|
+
const lineEl = this._createLineElement(parsedLine, lineNumber, isHighlighted);
|
|
220
|
+
codeEl.appendChild(lineEl);
|
|
221
|
+
});
|
|
222
|
+
|
|
76
223
|
pre.appendChild(codeEl);
|
|
77
224
|
wrapper.appendChild(pre);
|
|
78
225
|
|
|
@@ -85,12 +232,8 @@ export class Code extends BaseComponent<CodeState> {
|
|
|
85
232
|
|
|
86
233
|
stateObj.subscribe((val: any) => {
|
|
87
234
|
const transformed = transform(val);
|
|
88
|
-
codeEl.textContent = transformed;
|
|
89
235
|
this.state.content = transformed;
|
|
90
|
-
|
|
91
|
-
if ((window as any).Prism) {
|
|
92
|
-
(window as any).Prism.highlightElement(codeEl);
|
|
93
|
-
}
|
|
236
|
+
this._rebuildLines();
|
|
94
237
|
});
|
|
95
238
|
}
|
|
96
239
|
else if (property === 'language') {
|
|
@@ -98,30 +241,24 @@ export class Code extends BaseComponent<CodeState> {
|
|
|
98
241
|
|
|
99
242
|
stateObj.subscribe((val: any) => {
|
|
100
243
|
const transformed = transform(val);
|
|
101
|
-
codeEl.className = `language-${transformed}`;
|
|
102
244
|
this.state.language = transformed;
|
|
245
|
+
pre.className = `language-${transformed}`;
|
|
246
|
+
this._rebuildLines();
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
else if (property === 'highlightLines') {
|
|
250
|
+
const transform = toComponent || ((v: any) => (Array.isArray(v) ? v : []));
|
|
103
251
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
252
|
+
stateObj.subscribe((val: any) => {
|
|
253
|
+
const transformed = transform(val);
|
|
254
|
+
this.state.highlightLines = transformed;
|
|
255
|
+
this._updateHighlights();
|
|
107
256
|
});
|
|
108
257
|
}
|
|
109
258
|
});
|
|
110
259
|
|
|
111
260
|
container.appendChild(wrapper);
|
|
112
|
-
|
|
113
|
-
requestAnimationFrame(() => {
|
|
114
|
-
if ((window as any).Prism) {
|
|
115
|
-
(window as any).Prism.highlightElement(codeEl);
|
|
116
|
-
} else if (process.env.NODE_ENV !== 'production') {
|
|
117
|
-
// ✅ Helpful hint in development
|
|
118
|
-
console.log(
|
|
119
|
-
`💡 Tip: Add Prism.js for syntax highlighting:\n` +
|
|
120
|
-
` <link href="https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism.min.css" rel="stylesheet" />\n` +
|
|
121
|
-
` <script src="https://cdn.jsdelivr.net/npm/prismjs@1/prism.min.js"></script>`
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
});
|
|
261
|
+
this._codeContainer = wrapper;
|
|
125
262
|
|
|
126
263
|
return this;
|
|
127
264
|
}
|
|
@@ -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
|
+
}
|