juxscript 1.1.48 → 1.1.50
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.map +1 -1
- package/lib/components/code.js +8 -11
- package/lib/components/code.ts +10 -14
- package/lib/utils/codeparser.d.ts +9 -46
- package/lib/utils/codeparser.d.ts.map +1 -1
- package/lib/utils/codeparser.js +106 -228
- package/lib/utils/codeparser.ts +112 -271
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;CA4EnE;AAED,wBAAgB,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,IAAI,CAEhE"}
|
package/lib/components/code.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseComponent } from './base/BaseComponent.js';
|
|
2
|
-
import {
|
|
2
|
+
import { parseCode, renderLineWithTokens, getSyntaxHighlightCSS } from '../utils/codeparser.js';
|
|
3
3
|
// Event definitions
|
|
4
4
|
const TRIGGER_EVENTS = [];
|
|
5
5
|
const CALLBACK_EVENTS = [];
|
|
@@ -68,7 +68,7 @@ export class Code extends BaseComponent {
|
|
|
68
68
|
* Parse code content into individual lines with tokens
|
|
69
69
|
*/
|
|
70
70
|
_parseLines() {
|
|
71
|
-
return
|
|
71
|
+
return parseCode(this.state.content, this.state.language);
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
74
|
* Rebuild all line elements
|
|
@@ -81,7 +81,7 @@ export class Code extends BaseComponent {
|
|
|
81
81
|
return;
|
|
82
82
|
codeEl.innerHTML = '';
|
|
83
83
|
const parsedLines = this._parseLines();
|
|
84
|
-
const { startLine, highlightLines
|
|
84
|
+
const { startLine, highlightLines } = this.state;
|
|
85
85
|
parsedLines.forEach((parsedLine, index) => {
|
|
86
86
|
const lineNumber = startLine + index;
|
|
87
87
|
const isHighlighted = highlightLines.includes(lineNumber);
|
|
@@ -109,7 +109,7 @@ export class Code extends BaseComponent {
|
|
|
109
109
|
// Line content with syntax highlighting
|
|
110
110
|
const lineCode = document.createElement('span');
|
|
111
111
|
lineCode.className = 'jux-code-line-content';
|
|
112
|
-
lineCode.innerHTML =
|
|
112
|
+
lineCode.innerHTML = renderLineWithTokens(parsedLine);
|
|
113
113
|
lineEl.appendChild(lineCode);
|
|
114
114
|
return lineEl;
|
|
115
115
|
}
|
|
@@ -135,7 +135,7 @@ export class Code extends BaseComponent {
|
|
|
135
135
|
if (!Code._syntaxCSSInjected) {
|
|
136
136
|
const style = document.createElement('style');
|
|
137
137
|
style.id = 'jux-code-syntax-css';
|
|
138
|
-
style.textContent =
|
|
138
|
+
style.textContent = getSyntaxHighlightCSS();
|
|
139
139
|
document.head.appendChild(style);
|
|
140
140
|
Code._syntaxCSSInjected = true;
|
|
141
141
|
}
|
|
@@ -149,9 +149,8 @@ export class Code extends BaseComponent {
|
|
|
149
149
|
wrapper.className += ` ${className}`;
|
|
150
150
|
if (style)
|
|
151
151
|
wrapper.setAttribute('style', style);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const codeEl = document.createElement('code');
|
|
152
|
+
// ✅ Remove <pre>, just use a div with code styling
|
|
153
|
+
const codeEl = document.createElement('div');
|
|
155
154
|
codeEl.className = 'jux-code-lines';
|
|
156
155
|
// Build initial lines with parsed tokens
|
|
157
156
|
const parsedLines = this._parseLines();
|
|
@@ -162,8 +161,7 @@ export class Code extends BaseComponent {
|
|
|
162
161
|
const lineEl = this._createLineElement(parsedLine, lineNumber, isHighlighted);
|
|
163
162
|
codeEl.appendChild(lineEl);
|
|
164
163
|
});
|
|
165
|
-
|
|
166
|
-
wrapper.appendChild(pre);
|
|
164
|
+
wrapper.appendChild(codeEl);
|
|
167
165
|
this._wireStandardEvents(wrapper);
|
|
168
166
|
// Wire sync bindings
|
|
169
167
|
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
@@ -180,7 +178,6 @@ export class Code extends BaseComponent {
|
|
|
180
178
|
stateObj.subscribe((val) => {
|
|
181
179
|
const transformed = transform(val);
|
|
182
180
|
this.state.language = transformed;
|
|
183
|
-
pre.className = `language-${transformed}`;
|
|
184
181
|
this._rebuildLines();
|
|
185
182
|
});
|
|
186
183
|
}
|
package/lib/components/code.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseComponent } from './base/BaseComponent.js';
|
|
2
|
-
import {
|
|
2
|
+
import { parseCode, renderLineWithTokens, getSyntaxHighlightCSS } from '../utils/codeparser.js';
|
|
3
3
|
|
|
4
4
|
// Event definitions
|
|
5
5
|
const TRIGGER_EVENTS = [] as const;
|
|
@@ -103,8 +103,8 @@ export class Code extends BaseComponent<CodeState> {
|
|
|
103
103
|
/**
|
|
104
104
|
* Parse code content into individual lines with tokens
|
|
105
105
|
*/
|
|
106
|
-
private _parseLines(): ReturnType<typeof
|
|
107
|
-
return
|
|
106
|
+
private _parseLines(): ReturnType<typeof parseCode> {
|
|
107
|
+
return parseCode(this.state.content, this.state.language);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/**
|
|
@@ -119,7 +119,7 @@ export class Code extends BaseComponent<CodeState> {
|
|
|
119
119
|
codeEl.innerHTML = '';
|
|
120
120
|
|
|
121
121
|
const parsedLines = this._parseLines();
|
|
122
|
-
const { startLine, highlightLines
|
|
122
|
+
const { startLine, highlightLines } = this.state;
|
|
123
123
|
|
|
124
124
|
parsedLines.forEach((parsedLine, index) => {
|
|
125
125
|
const lineNumber = startLine + index;
|
|
@@ -133,7 +133,7 @@ export class Code extends BaseComponent<CodeState> {
|
|
|
133
133
|
/**
|
|
134
134
|
* Create a single line element with parsed tokens
|
|
135
135
|
*/
|
|
136
|
-
private _createLineElement(parsedLine: ReturnType<typeof
|
|
136
|
+
private _createLineElement(parsedLine: ReturnType<typeof parseCode>[0], lineNumber: number, highlighted: boolean): HTMLElement {
|
|
137
137
|
const lineEl = document.createElement('div');
|
|
138
138
|
lineEl.className = 'jux-code-line';
|
|
139
139
|
lineEl.setAttribute('data-line', String(lineNumber));
|
|
@@ -153,7 +153,7 @@ export class Code extends BaseComponent<CodeState> {
|
|
|
153
153
|
// Line content with syntax highlighting
|
|
154
154
|
const lineCode = document.createElement('span');
|
|
155
155
|
lineCode.className = 'jux-code-line-content';
|
|
156
|
-
lineCode.innerHTML =
|
|
156
|
+
lineCode.innerHTML = renderLineWithTokens(parsedLine);
|
|
157
157
|
lineEl.appendChild(lineCode);
|
|
158
158
|
|
|
159
159
|
return lineEl;
|
|
@@ -184,7 +184,7 @@ export class Code extends BaseComponent<CodeState> {
|
|
|
184
184
|
if (!Code._syntaxCSSInjected) {
|
|
185
185
|
const style = document.createElement('style');
|
|
186
186
|
style.id = 'jux-code-syntax-css';
|
|
187
|
-
style.textContent =
|
|
187
|
+
style.textContent = getSyntaxHighlightCSS();
|
|
188
188
|
document.head.appendChild(style);
|
|
189
189
|
Code._syntaxCSSInjected = true;
|
|
190
190
|
}
|
|
@@ -198,10 +198,8 @@ export class Code extends BaseComponent<CodeState> {
|
|
|
198
198
|
if (className) wrapper.className += ` ${className}`;
|
|
199
199
|
if (style) wrapper.setAttribute('style', style);
|
|
200
200
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const codeEl = document.createElement('code');
|
|
201
|
+
// ✅ Remove <pre>, just use a div with code styling
|
|
202
|
+
const codeEl = document.createElement('div');
|
|
205
203
|
codeEl.className = 'jux-code-lines';
|
|
206
204
|
|
|
207
205
|
// Build initial lines with parsed tokens
|
|
@@ -215,8 +213,7 @@ export class Code extends BaseComponent<CodeState> {
|
|
|
215
213
|
codeEl.appendChild(lineEl);
|
|
216
214
|
});
|
|
217
215
|
|
|
218
|
-
|
|
219
|
-
wrapper.appendChild(pre);
|
|
216
|
+
wrapper.appendChild(codeEl);
|
|
220
217
|
|
|
221
218
|
this._wireStandardEvents(wrapper);
|
|
222
219
|
|
|
@@ -237,7 +234,6 @@ export class Code extends BaseComponent<CodeState> {
|
|
|
237
234
|
stateObj.subscribe((val: any) => {
|
|
238
235
|
const transformed = transform(val);
|
|
239
236
|
this.state.language = transformed;
|
|
240
|
-
pre.className = `language-${transformed}`;
|
|
241
237
|
this._rebuildLines();
|
|
242
238
|
});
|
|
243
239
|
}
|
|
@@ -1,69 +1,32 @@
|
|
|
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
1
|
export interface ParsedLine {
|
|
15
2
|
lineNumber: number;
|
|
16
|
-
|
|
3
|
+
html: string;
|
|
17
4
|
raw: string;
|
|
18
5
|
}
|
|
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
|
-
* ✅ NEW: Strip TypeScript type annotations for parsing
|
|
28
|
-
* This is a simple regex-based approach - not perfect but works for most cases
|
|
29
|
-
*/
|
|
30
|
-
private static _stripTypeScriptTypes;
|
|
31
|
-
/**
|
|
32
|
-
* Clear cache (useful for development)
|
|
33
|
-
*/
|
|
34
|
-
static clearCache(): void;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Get CSS class for a token
|
|
38
|
-
*/
|
|
39
|
-
export declare function getTokenClass(token: any): string;
|
|
40
6
|
/**
|
|
41
|
-
*
|
|
7
|
+
* Parse code into lines with simple syntax highlighting
|
|
42
8
|
*/
|
|
43
|
-
export declare function
|
|
9
|
+
export declare function parseCode(code: string, language?: string): ParsedLine[];
|
|
44
10
|
/**
|
|
45
|
-
* Render a parsed line
|
|
11
|
+
* Render a parsed line (just returns the pre-generated HTML)
|
|
46
12
|
*/
|
|
47
13
|
export declare function renderLineWithTokens(parsedLine: ParsedLine): string;
|
|
48
14
|
/**
|
|
49
15
|
* Escape HTML entities
|
|
50
16
|
*/
|
|
51
|
-
|
|
17
|
+
declare function escapeHtml(text: string): string;
|
|
52
18
|
/**
|
|
53
19
|
* Generate CSS for syntax highlighting
|
|
54
20
|
*/
|
|
55
21
|
export declare function getSyntaxHighlightCSS(): string;
|
|
56
22
|
/**
|
|
57
|
-
* Main parser export
|
|
23
|
+
* Main parser export
|
|
58
24
|
*/
|
|
59
|
-
|
|
60
|
-
parse: typeof
|
|
25
|
+
declare const _default: {
|
|
26
|
+
parse: typeof parseCode;
|
|
61
27
|
renderLine: typeof renderLineWithTokens;
|
|
62
28
|
getCSS: typeof getSyntaxHighlightCSS;
|
|
63
|
-
getTokenClass: typeof getTokenClass;
|
|
64
|
-
isKeyword: typeof isKeyword;
|
|
65
29
|
escapeHtml: typeof escapeHtml;
|
|
66
|
-
TOKEN_CLASS_MAP: Record<string, string>;
|
|
67
30
|
};
|
|
68
|
-
export default
|
|
31
|
+
export default _default;
|
|
69
32
|
//# sourceMappingURL=codeparser.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codeparser.d.ts","sourceRoot":"","sources":["codeparser.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"codeparser.d.ts","sourceRoot":"","sources":["codeparser.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,UAAU;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAqB,GAAG,UAAU,EAAE,CAQrF;AAqCD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,UAAU,GAAG,MAAM,CAEnE;AAED;;GAEG;AACH,iBAAS,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOxC;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAiE9C;AAED;;GAEG;;;;;;;AACH,wBAKE"}
|
package/lib/utils/codeparser.js
CHANGED
|
@@ -1,226 +1,62 @@
|
|
|
1
|
-
import * as acorn from 'acorn';
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
2
|
+
* Simple keyword lists for highlighting
|
|
4
3
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
'
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
|
|
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
|
-
// ✅ Guard against undefined cache
|
|
45
|
-
if (!this._cache) {
|
|
46
|
-
this._cache = new Map();
|
|
47
|
-
}
|
|
48
|
-
const cacheKey = `${language}:${code}`;
|
|
49
|
-
if (this._cache.has(cacheKey)) {
|
|
50
|
-
return this._cache.get(cacheKey);
|
|
51
|
-
}
|
|
52
|
-
const result = this._parseCode(code, language);
|
|
53
|
-
this._cache.set(cacheKey, result);
|
|
54
|
-
return result;
|
|
55
|
-
}
|
|
56
|
-
static _parseCode(code, language) {
|
|
57
|
-
const lines = [];
|
|
58
|
-
const codeLines = code.split('\n');
|
|
59
|
-
// ✅ Skip parsing for non-JS languages
|
|
60
|
-
if (!['javascript', 'typescript', 'js', 'ts', 'jsx', 'tsx'].includes(language.toLowerCase())) {
|
|
61
|
-
codeLines.forEach((lineText, index) => {
|
|
62
|
-
lines.push({
|
|
63
|
-
lineNumber: index + 1,
|
|
64
|
-
tokens: [],
|
|
65
|
-
raw: lineText
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
return lines;
|
|
69
|
-
}
|
|
70
|
-
// ✅ Strip TypeScript types for parsing
|
|
71
|
-
const cleanedCode = language.toLowerCase().includes('typescript') || language.toLowerCase().includes('ts')
|
|
72
|
-
? this._stripTypeScriptTypes(code)
|
|
73
|
-
: code;
|
|
74
|
-
try {
|
|
75
|
-
// Parse with Acorn (supports ES2020+)
|
|
76
|
-
const tokens = [];
|
|
77
|
-
acorn.parse(cleanedCode, {
|
|
78
|
-
ecmaVersion: 2022,
|
|
79
|
-
sourceType: 'module',
|
|
80
|
-
locations: true,
|
|
81
|
-
ranges: true,
|
|
82
|
-
onToken: tokens,
|
|
83
|
-
onComment: (block, text, start, end, startLoc, endLoc) => {
|
|
84
|
-
tokens.push({
|
|
85
|
-
type: block ? 'BlockComment' : 'LineComment',
|
|
86
|
-
value: text,
|
|
87
|
-
start,
|
|
88
|
-
end,
|
|
89
|
-
loc: { start: startLoc, end: endLoc }
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
// Sort tokens by position
|
|
94
|
-
tokens.sort((a, b) => a.start - b.start);
|
|
95
|
-
// Group tokens by line
|
|
96
|
-
const tokensByLine = new Map();
|
|
97
|
-
tokens.forEach(token => {
|
|
98
|
-
const line = token.loc.start.line;
|
|
99
|
-
const parsedToken = {
|
|
100
|
-
type: token.type.label || token.type,
|
|
101
|
-
value: token.value !== undefined ? String(token.value) : cleanedCode.substring(token.start, token.end),
|
|
102
|
-
start: token.start,
|
|
103
|
-
end: token.end,
|
|
104
|
-
line: token.loc.start.line,
|
|
105
|
-
column: token.loc.start.column,
|
|
106
|
-
className: getTokenClass(token)
|
|
107
|
-
};
|
|
108
|
-
if (!tokensByLine.has(line)) {
|
|
109
|
-
tokensByLine.set(line, []);
|
|
110
|
-
}
|
|
111
|
-
tokensByLine.get(line).push(parsedToken);
|
|
112
|
-
});
|
|
113
|
-
// ✅ Build lines with tokens (use ORIGINAL code, not cleaned)
|
|
114
|
-
codeLines.forEach((lineText, index) => {
|
|
115
|
-
const lineNumber = index + 1;
|
|
116
|
-
const lineTokens = tokensByLine.get(lineNumber) || [];
|
|
117
|
-
lines.push({
|
|
118
|
-
lineNumber,
|
|
119
|
-
tokens: lineTokens,
|
|
120
|
-
raw: lineText // Use original line with types
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
catch (error) {
|
|
125
|
-
// Fallback: if parsing fails, return plain lines
|
|
126
|
-
console.warn('[CodeParser] Parse failed, using plain text:', error);
|
|
127
|
-
codeLines.forEach((lineText, index) => {
|
|
128
|
-
lines.push({
|
|
129
|
-
lineNumber: index + 1,
|
|
130
|
-
tokens: [],
|
|
131
|
-
raw: lineText
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
return lines;
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* ✅ NEW: Strip TypeScript type annotations for parsing
|
|
139
|
-
* This is a simple regex-based approach - not perfect but works for most cases
|
|
140
|
-
*/
|
|
141
|
-
static _stripTypeScriptTypes(code) {
|
|
142
|
-
return code
|
|
143
|
-
// Remove type annotations from parameters: (name: string) -> (name)
|
|
144
|
-
.replace(/(\w+)\s*:\s*[\w<>\[\]|&]+/g, '$1')
|
|
145
|
-
// Remove return type annotations: ): string -> )
|
|
146
|
-
.replace(/\)\s*:\s*[\w<>\[\]|&]+/g, ')')
|
|
147
|
-
// Remove interface/type declarations (keep as comments to preserve line numbers)
|
|
148
|
-
.replace(/^(\s*)(interface|type)\s+\w+.*$/gm, '$1// $2 declaration')
|
|
149
|
-
// Remove as type assertions: value as string -> value
|
|
150
|
-
.replace(/\s+as\s+[\w<>\[\]|&]+/g, '')
|
|
151
|
-
// Remove angle bracket generics: Array<string> -> Array
|
|
152
|
-
.replace(/<[\w<>\[\]|&,\s]+>/g, '');
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Clear cache (useful for development)
|
|
156
|
-
*/
|
|
157
|
-
static clearCache() {
|
|
158
|
-
if (this._cache) {
|
|
159
|
-
this._cache.clear();
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
CodeParser._cache = new Map(); // ✅ Initialize inline
|
|
4
|
+
const KEYWORDS = new Set([
|
|
5
|
+
'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue',
|
|
6
|
+
'debugger', 'default', 'delete', 'do', 'else', 'export', 'extends',
|
|
7
|
+
'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof',
|
|
8
|
+
'let', 'new', 'return', 'super', 'switch', 'this', 'throw', 'try',
|
|
9
|
+
'typeof', 'var', 'void', 'while', 'with', 'yield', 'from', 'of',
|
|
10
|
+
'static', 'get', 'set', 'as', 'interface', 'type', 'enum', 'namespace'
|
|
11
|
+
]);
|
|
164
12
|
/**
|
|
165
|
-
*
|
|
13
|
+
* Parse code into lines with simple syntax highlighting
|
|
166
14
|
*/
|
|
167
|
-
export function
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return TOKEN_CLASS_MAP[type] || 'token-default';
|
|
15
|
+
export function parseCode(code, language = 'javascript') {
|
|
16
|
+
const lines = code.split('\n');
|
|
17
|
+
return lines.map((line, index) => ({
|
|
18
|
+
lineNumber: index + 1,
|
|
19
|
+
html: highlightLine(line),
|
|
20
|
+
raw: line
|
|
21
|
+
}));
|
|
175
22
|
}
|
|
176
23
|
/**
|
|
177
|
-
*
|
|
24
|
+
* Highlight a single line of code
|
|
178
25
|
*/
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
26
|
+
function highlightLine(line) {
|
|
27
|
+
if (!line.trim())
|
|
28
|
+
return ' ';
|
|
29
|
+
let result = line;
|
|
30
|
+
// 1. Comments (do first to avoid highlighting keywords in comments)
|
|
31
|
+
result = result.replace(/(\/\/.*$)/g, '<span class="token-comment">$1</span>');
|
|
32
|
+
result = result.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="token-comment">$1</span>');
|
|
33
|
+
// 2. Strings (avoid highlighting keywords in strings)
|
|
34
|
+
result = result.replace(/(['"`])(?:(?=(\\?))\2.)*?\1/g, (match) => {
|
|
35
|
+
return `<span class="token-string">${escapeHtml(match)}</span>`;
|
|
36
|
+
});
|
|
37
|
+
// 3. Numbers
|
|
38
|
+
result = result.replace(/\b(\d+\.?\d*)\b/g, '<span class="token-number">$1</span>');
|
|
39
|
+
// 4. Keywords (only whole words)
|
|
40
|
+
KEYWORDS.forEach(keyword => {
|
|
41
|
+
const regex = new RegExp(`\\b(${keyword})\\b`, 'g');
|
|
42
|
+
result = result.replace(regex, '<span class="token-keyword">$1</span>');
|
|
43
|
+
});
|
|
44
|
+
// 5. Braces, brackets, parentheses
|
|
45
|
+
result = result.replace(/([{}[\]()])/g, '<span class="token-punctuation">$1</span>');
|
|
46
|
+
// 6. Operators
|
|
47
|
+
result = result.replace(/([+\-*/%=<>!&|^~?:])/g, '<span class="token-operator">$1</span>');
|
|
48
|
+
return result || ' ';
|
|
188
49
|
}
|
|
189
50
|
/**
|
|
190
|
-
* Render a parsed line
|
|
51
|
+
* Render a parsed line (just returns the pre-generated HTML)
|
|
191
52
|
*/
|
|
192
53
|
export function renderLineWithTokens(parsedLine) {
|
|
193
|
-
|
|
194
|
-
// No tokens, return plain text (escaped)
|
|
195
|
-
return escapeHtml(parsedLine.raw) || ' ';
|
|
196
|
-
}
|
|
197
|
-
const { raw, tokens } = parsedLine;
|
|
198
|
-
let html = '';
|
|
199
|
-
let lastIndex = 0;
|
|
200
|
-
// Calculate line start position in original code
|
|
201
|
-
const lineStartPos = tokens[0]?.start - tokens[0]?.column;
|
|
202
|
-
tokens.forEach(token => {
|
|
203
|
-
// Add any whitespace/text between tokens
|
|
204
|
-
const localStart = token.start - lineStartPos;
|
|
205
|
-
if (localStart > lastIndex) {
|
|
206
|
-
html += escapeHtml(raw.substring(lastIndex, localStart));
|
|
207
|
-
}
|
|
208
|
-
// Add token with span
|
|
209
|
-
const localEnd = token.end - lineStartPos;
|
|
210
|
-
const tokenText = raw.substring(localStart, localEnd);
|
|
211
|
-
html += `<span class="${token.className}">${escapeHtml(tokenText)}</span>`;
|
|
212
|
-
lastIndex = localEnd;
|
|
213
|
-
});
|
|
214
|
-
// Add any remaining text
|
|
215
|
-
if (lastIndex < raw.length) {
|
|
216
|
-
html += escapeHtml(raw.substring(lastIndex));
|
|
217
|
-
}
|
|
218
|
-
return html || ' ';
|
|
54
|
+
return parsedLine.html;
|
|
219
55
|
}
|
|
220
56
|
/**
|
|
221
57
|
* Escape HTML entities
|
|
222
58
|
*/
|
|
223
|
-
|
|
59
|
+
function escapeHtml(text) {
|
|
224
60
|
return text
|
|
225
61
|
.replace(/&/g, '&')
|
|
226
62
|
.replace(/</g, '<')
|
|
@@ -233,34 +69,76 @@ export function escapeHtml(text) {
|
|
|
233
69
|
*/
|
|
234
70
|
export function getSyntaxHighlightCSS() {
|
|
235
71
|
return `
|
|
236
|
-
/*
|
|
72
|
+
/* Simple Syntax Highlighting */
|
|
73
|
+
.jux-code {
|
|
74
|
+
font-family: 'Fira Code', 'Consolas', 'Monaco', monospace;
|
|
75
|
+
font-size: 14px;
|
|
76
|
+
line-height: 1.6;
|
|
77
|
+
background: #1e1e1e;
|
|
78
|
+
border-radius: 8px;
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
margin: 1rem 0;
|
|
81
|
+
padding: 1rem;
|
|
82
|
+
overflow-x: auto;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.jux-code-lines {
|
|
86
|
+
display: block;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.jux-code-line {
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: flex-start;
|
|
92
|
+
min-height: 1.6em;
|
|
93
|
+
transition: background-color 0.2s;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.jux-code-line:hover {
|
|
97
|
+
background-color: rgba(255, 255, 255, 0.05);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.jux-code-line-highlight {
|
|
101
|
+
background-color: rgba(255, 215, 0, 0.1);
|
|
102
|
+
border-left: 3px solid #ffd700;
|
|
103
|
+
padding-left: 0.5rem;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.jux-code-line-number {
|
|
107
|
+
display: inline-block;
|
|
108
|
+
min-width: 3rem;
|
|
109
|
+
text-align: right;
|
|
110
|
+
padding-right: 1rem;
|
|
111
|
+
color: #858585;
|
|
112
|
+
user-select: none;
|
|
113
|
+
flex-shrink: 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.jux-code-line-content {
|
|
117
|
+
flex: 1;
|
|
118
|
+
color: #d4d4d4;
|
|
119
|
+
white-space: pre;
|
|
120
|
+
overflow-x: auto;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.jux-code-no-numbers .jux-code-line-number {
|
|
124
|
+
display: none;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Token colors */
|
|
237
128
|
.token-keyword { color: #c678dd; font-weight: 600; }
|
|
238
129
|
.token-string { color: #98c379; }
|
|
239
130
|
.token-number { color: #d19a66; }
|
|
240
|
-
.token-boolean { color: #d19a66; }
|
|
241
|
-
.token-null { color: #d19a66; }
|
|
242
|
-
.token-regex { color: #e06c75; }
|
|
243
|
-
.token-identifier { color: #e5c07b; }
|
|
244
|
-
.token-punctuation { color: #abb2bf; }
|
|
245
131
|
.token-comment { color: #5c6370; font-style: italic; }
|
|
246
|
-
.token-
|
|
247
|
-
|
|
248
|
-
/* Function calls */
|
|
249
|
-
.token-identifier + .token-punctuation:has(+ .token-punctuation) {
|
|
250
|
-
color: #61afef; /* Function names in blue */
|
|
251
|
-
}
|
|
132
|
+
.token-punctuation { color: #abb2bf; }
|
|
133
|
+
.token-operator { color: #56b6c2; }
|
|
252
134
|
`;
|
|
253
135
|
}
|
|
254
136
|
/**
|
|
255
|
-
* Main parser export
|
|
137
|
+
* Main parser export
|
|
256
138
|
*/
|
|
257
|
-
export
|
|
258
|
-
parse:
|
|
139
|
+
export default {
|
|
140
|
+
parse: parseCode,
|
|
259
141
|
renderLine: renderLineWithTokens,
|
|
260
142
|
getCSS: getSyntaxHighlightCSS,
|
|
261
|
-
|
|
262
|
-
isKeyword,
|
|
263
|
-
escapeHtml,
|
|
264
|
-
TOKEN_CLASS_MAP
|
|
143
|
+
escapeHtml
|
|
265
144
|
};
|
|
266
|
-
export default CodeParser;
|
package/lib/utils/codeparser.ts
CHANGED
|
@@ -1,278 +1,80 @@
|
|
|
1
|
-
import * as acorn from 'acorn';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
*
|
|
2
|
+
* Simple keyword lists for highlighting
|
|
5
3
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
|
|
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
|
-
}
|
|
4
|
+
const KEYWORDS = new Set([
|
|
5
|
+
'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue',
|
|
6
|
+
'debugger', 'default', 'delete', 'do', 'else', 'export', 'extends',
|
|
7
|
+
'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof',
|
|
8
|
+
'let', 'new', 'return', 'super', 'switch', 'this', 'throw', 'try',
|
|
9
|
+
'typeof', 'var', 'void', 'while', 'with', 'yield', 'from', 'of',
|
|
10
|
+
'static', 'get', 'set', 'as', 'interface', 'type', 'enum', 'namespace'
|
|
11
|
+
]);
|
|
54
12
|
|
|
55
13
|
export interface ParsedLine {
|
|
56
14
|
lineNumber: number;
|
|
57
|
-
|
|
15
|
+
html: string;
|
|
58
16
|
raw: string;
|
|
59
17
|
}
|
|
60
18
|
|
|
61
|
-
export class CodeParser {
|
|
62
|
-
private static _cache: Map<string, ParsedLine[]> = new Map(); // ✅ Initialize inline
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Parse code with caching
|
|
66
|
-
*/
|
|
67
|
-
static parse(code: string, language: string = 'javascript'): ParsedLine[] {
|
|
68
|
-
// ✅ Guard against undefined cache
|
|
69
|
-
if (!this._cache) {
|
|
70
|
-
this._cache = new Map();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const cacheKey = `${language}:${code}`;
|
|
74
|
-
|
|
75
|
-
if (this._cache.has(cacheKey)) {
|
|
76
|
-
return this._cache.get(cacheKey)!;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const result = this._parseCode(code, language);
|
|
80
|
-
this._cache.set(cacheKey, result);
|
|
81
|
-
return result;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
private static _parseCode(code: string, language: string): ParsedLine[] {
|
|
85
|
-
const lines: ParsedLine[] = [];
|
|
86
|
-
const codeLines = code.split('\n');
|
|
87
|
-
|
|
88
|
-
// ✅ Skip parsing for non-JS languages
|
|
89
|
-
if (!['javascript', 'typescript', 'js', 'ts', 'jsx', 'tsx'].includes(language.toLowerCase())) {
|
|
90
|
-
codeLines.forEach((lineText, index) => {
|
|
91
|
-
lines.push({
|
|
92
|
-
lineNumber: index + 1,
|
|
93
|
-
tokens: [],
|
|
94
|
-
raw: lineText
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
return lines;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ✅ Strip TypeScript types for parsing
|
|
101
|
-
const cleanedCode = language.toLowerCase().includes('typescript') || language.toLowerCase().includes('ts')
|
|
102
|
-
? this._stripTypeScriptTypes(code)
|
|
103
|
-
: code;
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
// Parse with Acorn (supports ES2020+)
|
|
107
|
-
const tokens: any[] = [];
|
|
108
|
-
|
|
109
|
-
acorn.parse(cleanedCode, {
|
|
110
|
-
ecmaVersion: 2022,
|
|
111
|
-
sourceType: 'module',
|
|
112
|
-
locations: true,
|
|
113
|
-
ranges: true,
|
|
114
|
-
onToken: tokens,
|
|
115
|
-
onComment: (block, text, start, end, startLoc, endLoc) => {
|
|
116
|
-
tokens.push({
|
|
117
|
-
type: block ? 'BlockComment' : 'LineComment',
|
|
118
|
-
value: text,
|
|
119
|
-
start,
|
|
120
|
-
end,
|
|
121
|
-
loc: { start: startLoc, end: endLoc }
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// Sort tokens by position
|
|
127
|
-
tokens.sort((a, b) => a.start - b.start);
|
|
128
|
-
|
|
129
|
-
// Group tokens by line
|
|
130
|
-
const tokensByLine: Map<number, ParsedToken[]> = new Map();
|
|
131
|
-
|
|
132
|
-
tokens.forEach(token => {
|
|
133
|
-
const line = token.loc.start.line;
|
|
134
|
-
const parsedToken: ParsedToken = {
|
|
135
|
-
type: token.type.label || token.type,
|
|
136
|
-
value: token.value !== undefined ? String(token.value) : cleanedCode.substring(token.start, token.end),
|
|
137
|
-
start: token.start,
|
|
138
|
-
end: token.end,
|
|
139
|
-
line: token.loc.start.line,
|
|
140
|
-
column: token.loc.start.column,
|
|
141
|
-
className: getTokenClass(token)
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
if (!tokensByLine.has(line)) {
|
|
145
|
-
tokensByLine.set(line, []);
|
|
146
|
-
}
|
|
147
|
-
tokensByLine.get(line)!.push(parsedToken);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// ✅ Build lines with tokens (use ORIGINAL code, not cleaned)
|
|
151
|
-
codeLines.forEach((lineText, index) => {
|
|
152
|
-
const lineNumber = index + 1;
|
|
153
|
-
const lineTokens = tokensByLine.get(lineNumber) || [];
|
|
154
|
-
|
|
155
|
-
lines.push({
|
|
156
|
-
lineNumber,
|
|
157
|
-
tokens: lineTokens,
|
|
158
|
-
raw: lineText // Use original line with types
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
} catch (error) {
|
|
163
|
-
// Fallback: if parsing fails, return plain lines
|
|
164
|
-
console.warn('[CodeParser] Parse failed, using plain text:', error);
|
|
165
|
-
codeLines.forEach((lineText, index) => {
|
|
166
|
-
lines.push({
|
|
167
|
-
lineNumber: index + 1,
|
|
168
|
-
tokens: [],
|
|
169
|
-
raw: lineText
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return lines;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* ✅ NEW: Strip TypeScript type annotations for parsing
|
|
179
|
-
* This is a simple regex-based approach - not perfect but works for most cases
|
|
180
|
-
*/
|
|
181
|
-
private static _stripTypeScriptTypes(code: string): string {
|
|
182
|
-
return code
|
|
183
|
-
// Remove type annotations from parameters: (name: string) -> (name)
|
|
184
|
-
.replace(/(\w+)\s*:\s*[\w<>\[\]|&]+/g, '$1')
|
|
185
|
-
// Remove return type annotations: ): string -> )
|
|
186
|
-
.replace(/\)\s*:\s*[\w<>\[\]|&]+/g, ')')
|
|
187
|
-
// Remove interface/type declarations (keep as comments to preserve line numbers)
|
|
188
|
-
.replace(/^(\s*)(interface|type)\s+\w+.*$/gm, '$1// $2 declaration')
|
|
189
|
-
// Remove as type assertions: value as string -> value
|
|
190
|
-
.replace(/\s+as\s+[\w<>\[\]|&]+/g, '')
|
|
191
|
-
// Remove angle bracket generics: Array<string> -> Array
|
|
192
|
-
.replace(/<[\w<>\[\]|&,\s]+>/g, '');
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Clear cache (useful for development)
|
|
197
|
-
*/
|
|
198
|
-
static clearCache(): void {
|
|
199
|
-
if (this._cache) {
|
|
200
|
-
this._cache.clear();
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
19
|
/**
|
|
206
|
-
*
|
|
20
|
+
* Parse code into lines with simple syntax highlighting
|
|
207
21
|
*/
|
|
208
|
-
export function
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
// Map type to class
|
|
217
|
-
return TOKEN_CLASS_MAP[type] || 'token-default';
|
|
22
|
+
export function parseCode(code: string, language: string = 'javascript'): ParsedLine[] {
|
|
23
|
+
const lines = code.split('\n');
|
|
24
|
+
|
|
25
|
+
return lines.map((line, index) => ({
|
|
26
|
+
lineNumber: index + 1,
|
|
27
|
+
html: highlightLine(line),
|
|
28
|
+
raw: line
|
|
29
|
+
}));
|
|
218
30
|
}
|
|
219
31
|
|
|
220
32
|
/**
|
|
221
|
-
*
|
|
33
|
+
* Highlight a single line of code
|
|
222
34
|
*/
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger',
|
|
226
|
-
'default', 'delete', 'do', 'else', 'export', 'extends', 'finally', 'for',
|
|
227
|
-
'function', 'if', 'import', 'in', 'instanceof', 'let', 'new', 'return',
|
|
228
|
-
'super', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void',
|
|
229
|
-
'while', 'with', 'yield', 'async', 'of', 'static', 'get', 'set'
|
|
230
|
-
];
|
|
231
|
-
return keywords.includes(word);
|
|
232
|
-
}
|
|
35
|
+
function highlightLine(line: string): string {
|
|
36
|
+
if (!line.trim()) return ' ';
|
|
233
37
|
|
|
234
|
-
|
|
235
|
-
* Render a parsed line as HTML with spans
|
|
236
|
-
*/
|
|
237
|
-
export function renderLineWithTokens(parsedLine: ParsedLine): string {
|
|
238
|
-
if (parsedLine.tokens.length === 0) {
|
|
239
|
-
// No tokens, return plain text (escaped)
|
|
240
|
-
return escapeHtml(parsedLine.raw) || ' ';
|
|
241
|
-
}
|
|
38
|
+
let result = line;
|
|
242
39
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
40
|
+
// 1. Comments (do first to avoid highlighting keywords in comments)
|
|
41
|
+
result = result.replace(/(\/\/.*$)/g, '<span class="token-comment">$1</span>');
|
|
42
|
+
result = result.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="token-comment">$1</span>');
|
|
246
43
|
|
|
247
|
-
//
|
|
248
|
-
|
|
44
|
+
// 2. Strings (avoid highlighting keywords in strings)
|
|
45
|
+
result = result.replace(/(['"`])(?:(?=(\\?))\2.)*?\1/g, (match) => {
|
|
46
|
+
return `<span class="token-string">${escapeHtml(match)}</span>`;
|
|
47
|
+
});
|
|
249
48
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const localStart = token.start - lineStartPos;
|
|
253
|
-
if (localStart > lastIndex) {
|
|
254
|
-
html += escapeHtml(raw.substring(lastIndex, localStart));
|
|
255
|
-
}
|
|
49
|
+
// 3. Numbers
|
|
50
|
+
result = result.replace(/\b(\d+\.?\d*)\b/g, '<span class="token-number">$1</span>');
|
|
256
51
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
lastIndex = localEnd;
|
|
52
|
+
// 4. Keywords (only whole words)
|
|
53
|
+
KEYWORDS.forEach(keyword => {
|
|
54
|
+
const regex = new RegExp(`\\b(${keyword})\\b`, 'g');
|
|
55
|
+
result = result.replace(regex, '<span class="token-keyword">$1</span>');
|
|
262
56
|
});
|
|
263
57
|
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
58
|
+
// 5. Braces, brackets, parentheses
|
|
59
|
+
result = result.replace(/([{}[\]()])/g, '<span class="token-punctuation">$1</span>');
|
|
60
|
+
|
|
61
|
+
// 6. Operators
|
|
62
|
+
result = result.replace(/([+\-*/%=<>!&|^~?:])/g, '<span class="token-operator">$1</span>');
|
|
268
63
|
|
|
269
|
-
return
|
|
64
|
+
return result || ' ';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Render a parsed line (just returns the pre-generated HTML)
|
|
69
|
+
*/
|
|
70
|
+
export function renderLineWithTokens(parsedLine: ParsedLine): string {
|
|
71
|
+
return parsedLine.html;
|
|
270
72
|
}
|
|
271
73
|
|
|
272
74
|
/**
|
|
273
75
|
* Escape HTML entities
|
|
274
76
|
*/
|
|
275
|
-
|
|
77
|
+
function escapeHtml(text: string): string {
|
|
276
78
|
return text
|
|
277
79
|
.replace(/&/g, '&')
|
|
278
80
|
.replace(/</g, '<')
|
|
@@ -286,38 +88,77 @@ export function escapeHtml(text: string): string {
|
|
|
286
88
|
*/
|
|
287
89
|
export function getSyntaxHighlightCSS(): string {
|
|
288
90
|
return `
|
|
289
|
-
/*
|
|
91
|
+
/* Simple Syntax Highlighting */
|
|
92
|
+
.jux-code {
|
|
93
|
+
font-family: 'Fira Code', 'Consolas', 'Monaco', monospace;
|
|
94
|
+
font-size: 14px;
|
|
95
|
+
line-height: 1.6;
|
|
96
|
+
background: #1e1e1e;
|
|
97
|
+
border-radius: 8px;
|
|
98
|
+
overflow: hidden;
|
|
99
|
+
margin: 1rem 0;
|
|
100
|
+
padding: 1rem;
|
|
101
|
+
overflow-x: auto;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.jux-code-lines {
|
|
105
|
+
display: block;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.jux-code-line {
|
|
109
|
+
display: flex;
|
|
110
|
+
align-items: flex-start;
|
|
111
|
+
min-height: 1.6em;
|
|
112
|
+
transition: background-color 0.2s;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.jux-code-line:hover {
|
|
116
|
+
background-color: rgba(255, 255, 255, 0.05);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.jux-code-line-highlight {
|
|
120
|
+
background-color: rgba(255, 215, 0, 0.1);
|
|
121
|
+
border-left: 3px solid #ffd700;
|
|
122
|
+
padding-left: 0.5rem;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.jux-code-line-number {
|
|
126
|
+
display: inline-block;
|
|
127
|
+
min-width: 3rem;
|
|
128
|
+
text-align: right;
|
|
129
|
+
padding-right: 1rem;
|
|
130
|
+
color: #858585;
|
|
131
|
+
user-select: none;
|
|
132
|
+
flex-shrink: 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.jux-code-line-content {
|
|
136
|
+
flex: 1;
|
|
137
|
+
color: #d4d4d4;
|
|
138
|
+
white-space: pre;
|
|
139
|
+
overflow-x: auto;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.jux-code-no-numbers .jux-code-line-number {
|
|
143
|
+
display: none;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* Token colors */
|
|
290
147
|
.token-keyword { color: #c678dd; font-weight: 600; }
|
|
291
148
|
.token-string { color: #98c379; }
|
|
292
149
|
.token-number { color: #d19a66; }
|
|
293
|
-
.token-boolean { color: #d19a66; }
|
|
294
|
-
.token-null { color: #d19a66; }
|
|
295
|
-
.token-regex { color: #e06c75; }
|
|
296
|
-
.token-identifier { color: #e5c07b; }
|
|
297
|
-
.token-punctuation { color: #abb2bf; }
|
|
298
150
|
.token-comment { color: #5c6370; font-style: italic; }
|
|
299
|
-
.token-
|
|
300
|
-
|
|
301
|
-
/* Function calls */
|
|
302
|
-
.token-identifier + .token-punctuation:has(+ .token-punctuation) {
|
|
303
|
-
color: #61afef; /* Function names in blue */
|
|
304
|
-
}
|
|
151
|
+
.token-punctuation { color: #abb2bf; }
|
|
152
|
+
.token-operator { color: #56b6c2; }
|
|
305
153
|
`;
|
|
306
154
|
}
|
|
307
155
|
|
|
308
|
-
|
|
309
|
-
|
|
310
156
|
/**
|
|
311
|
-
* Main parser export
|
|
157
|
+
* Main parser export
|
|
312
158
|
*/
|
|
313
|
-
export
|
|
314
|
-
parse:
|
|
159
|
+
export default {
|
|
160
|
+
parse: parseCode,
|
|
315
161
|
renderLine: renderLineWithTokens,
|
|
316
162
|
getCSS: getSyntaxHighlightCSS,
|
|
317
|
-
|
|
318
|
-
isKeyword,
|
|
319
|
-
escapeHtml,
|
|
320
|
-
TOKEN_CLASS_MAP
|
|
163
|
+
escapeHtml
|
|
321
164
|
};
|
|
322
|
-
|
|
323
|
-
export default CodeParser;
|