@lithent/lithent-template-parser 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/dist/codegen/createElement.d.ts +10 -0
- package/dist/codegen/index.d.ts +1 -0
- package/dist/compiler.d.ts +26 -0
- package/dist/docPipe/index.d.ts +40 -0
- package/dist/docPipe/scan.d.ts +6 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.mjs +2302 -0
- package/dist/index.mjs.map +1 -0
- package/dist/index.umd.cjs +13 -0
- package/dist/index.umd.cjs.map +1 -0
- package/dist/parser/ast.d.ts +207 -0
- package/dist/parser/lexer.d.ts +109 -0
- package/dist/parser/parser.d.ts +79 -0
- package/dist/parser/tokens.d.ts +51 -0
- package/dist/transform/directives.d.ts +30 -0
- package/dist/transform/index.d.ts +14 -0
- package/dist/transform/optimize.d.ts +21 -0
- package/package.json +53 -0
- package/src/codegen/createElement.ts +661 -0
- package/src/codegen/index.ts +1 -0
- package/src/compiler.ts +70 -0
- package/src/docPipe/index.ts +104 -0
- package/src/docPipe/scan.ts +318 -0
- package/src/index.ts +16 -0
- package/src/parser/ast.ts +478 -0
- package/src/parser/lexer.ts +561 -0
- package/src/parser/parser.ts +514 -0
- package/src/parser/tokens.ts +84 -0
- package/src/playground/main.ts +206 -0
- package/src/playground/style.css +135 -0
- package/src/shims/babel-env.ts +17 -0
- package/src/tests/compiler.test.ts +434 -0
- package/src/tests/docPipe.test.ts +41 -0
- package/src/tests/helpers.ts +96 -0
- package/src/tests/lexer.test.ts +282 -0
- package/src/tests/parser.test.ts +691 -0
- package/src/transform/directives.ts +160 -0
- package/src/transform/index.ts +29 -0
- package/src/transform/optimize.ts +203 -0
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RootNode,
|
|
3
|
+
TemplateNode,
|
|
4
|
+
ElementNode,
|
|
5
|
+
FragmentNode,
|
|
6
|
+
TextNode,
|
|
7
|
+
InterpolationNode,
|
|
8
|
+
NodeType,
|
|
9
|
+
DirectiveForNode,
|
|
10
|
+
} from '../parser/ast';
|
|
11
|
+
import { tokenize } from '../parser/lexer';
|
|
12
|
+
import { parse } from '../parser/parser';
|
|
13
|
+
import { getConditionalGroup, getDirective, transform } from '../transform';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate JavaScript code from AST
|
|
17
|
+
*/
|
|
18
|
+
export function generate(ast: RootNode, options?: GenerateOptions): string {
|
|
19
|
+
const opts: Required<GenerateOptions> = {
|
|
20
|
+
templateFactory: 'h',
|
|
21
|
+
templateFragmentFactory: 'Fragment',
|
|
22
|
+
...options,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const code = generateChildren(ast.children, opts);
|
|
26
|
+
|
|
27
|
+
return code;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface GenerateOptions {
|
|
31
|
+
templateFactory?: string;
|
|
32
|
+
templateFragmentFactory?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generate code for children nodes
|
|
37
|
+
*/
|
|
38
|
+
function generateChildren(
|
|
39
|
+
children: TemplateNode[],
|
|
40
|
+
options: Required<GenerateOptions>
|
|
41
|
+
): string {
|
|
42
|
+
const childrenCode = children
|
|
43
|
+
.map(child => generateNode(child, options))
|
|
44
|
+
.filter(code => code !== '');
|
|
45
|
+
|
|
46
|
+
if (childrenCode.length === 0) {
|
|
47
|
+
return 'null';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (childrenCode.length === 1) {
|
|
51
|
+
return childrenCode[0];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Multiple children - wrap in array or Fragment
|
|
55
|
+
return `[${childrenCode.join(', ')}]`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generate code for a single node
|
|
60
|
+
*/
|
|
61
|
+
function generateNode(
|
|
62
|
+
node: TemplateNode,
|
|
63
|
+
options: Required<GenerateOptions>
|
|
64
|
+
): string {
|
|
65
|
+
switch (node.type) {
|
|
66
|
+
case NodeType.ELEMENT:
|
|
67
|
+
return generateElement(node, options);
|
|
68
|
+
case NodeType.FRAGMENT:
|
|
69
|
+
return generateFragment(node, options);
|
|
70
|
+
case NodeType.TEXT:
|
|
71
|
+
return generateText(node);
|
|
72
|
+
case NodeType.INTERPOLATION:
|
|
73
|
+
return generateInterpolation(node, options);
|
|
74
|
+
case NodeType.COMMENT:
|
|
75
|
+
// Comments are not rendered
|
|
76
|
+
return '';
|
|
77
|
+
default:
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Generate code for an element node
|
|
84
|
+
*/
|
|
85
|
+
function generateElement(
|
|
86
|
+
node: ElementNode,
|
|
87
|
+
options: Required<GenerateOptions>
|
|
88
|
+
): string {
|
|
89
|
+
// Check if this element is part of a conditional group
|
|
90
|
+
const conditionalGroup = getConditionalGroup(node);
|
|
91
|
+
if (conditionalGroup) {
|
|
92
|
+
return generateConditionalGroup(conditionalGroup, options);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check if this element has l-for directive
|
|
96
|
+
const forDirective = node.directives.find(
|
|
97
|
+
d => d.type === NodeType.DIRECTIVE_FOR
|
|
98
|
+
) as DirectiveForNode | undefined;
|
|
99
|
+
|
|
100
|
+
if (forDirective) {
|
|
101
|
+
return generateForLoop(node, forDirective, options);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Normal element
|
|
105
|
+
return generateNormalElement(node, options);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Generate code for a normal element (without directives)
|
|
110
|
+
*/
|
|
111
|
+
function generateNormalElement(
|
|
112
|
+
node: ElementNode,
|
|
113
|
+
options: Required<GenerateOptions>
|
|
114
|
+
): string {
|
|
115
|
+
const factory = options.templateFactory;
|
|
116
|
+
|
|
117
|
+
// Tag name (component or string)
|
|
118
|
+
const tag = node.isComponent ? node.tag : `'${node.tag}'`;
|
|
119
|
+
|
|
120
|
+
// Props
|
|
121
|
+
const props = generateProps(node, options);
|
|
122
|
+
|
|
123
|
+
// Children
|
|
124
|
+
const children = node.children
|
|
125
|
+
.map(child => generateNode(child, options))
|
|
126
|
+
.filter(code => code !== '');
|
|
127
|
+
|
|
128
|
+
// Build h() call
|
|
129
|
+
if (children.length === 0) {
|
|
130
|
+
return `${factory}(${tag}, ${props})`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return `${factory}(${tag}, ${props}, ${children.join(', ')})`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Generate code for a fragment node
|
|
138
|
+
*/
|
|
139
|
+
function generateFragment(
|
|
140
|
+
node: FragmentNode,
|
|
141
|
+
options: Required<GenerateOptions>
|
|
142
|
+
): string {
|
|
143
|
+
const factory = options.templateFactory;
|
|
144
|
+
const fragment = options.templateFragmentFactory;
|
|
145
|
+
|
|
146
|
+
const children = node.children
|
|
147
|
+
.map(child => generateNode(child, options))
|
|
148
|
+
.filter(code => code !== '');
|
|
149
|
+
|
|
150
|
+
if (children.length === 0) {
|
|
151
|
+
return `${factory}(${fragment}, null)`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return `${factory}(${fragment}, null, ${children.join(', ')})`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Generate props object
|
|
159
|
+
*/
|
|
160
|
+
function generateProps(
|
|
161
|
+
node: ElementNode,
|
|
162
|
+
options: Required<GenerateOptions>
|
|
163
|
+
): string {
|
|
164
|
+
if (node.attributes.length === 0) {
|
|
165
|
+
return 'null';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const props = node.attributes.map(attr => {
|
|
169
|
+
const key = attr.name;
|
|
170
|
+
let value: string;
|
|
171
|
+
|
|
172
|
+
if (attr.value === null) {
|
|
173
|
+
// Boolean attribute (e.g., disabled, checked)
|
|
174
|
+
value = 'true';
|
|
175
|
+
} else if (typeof attr.value === 'string') {
|
|
176
|
+
// Static string value
|
|
177
|
+
value = `'${attr.value.replace(/'/g, "\\'")}'`;
|
|
178
|
+
} else {
|
|
179
|
+
// Dynamic expression
|
|
180
|
+
value = transformExpression(attr.value.expression, options);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return `${key}: ${value}`;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return `{ ${props.join(', ')} }`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Generate code for text node
|
|
191
|
+
*/
|
|
192
|
+
function generateText(node: TextNode): string {
|
|
193
|
+
// Escape single quotes in text
|
|
194
|
+
const escaped = node.content
|
|
195
|
+
.replace(/\\/g, '\\\\')
|
|
196
|
+
.replace(/\r/g, '\\r')
|
|
197
|
+
.replace(/\n/g, '\\n')
|
|
198
|
+
.replace(/'/g, "\\'");
|
|
199
|
+
return `'${escaped}'`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Generate code for interpolation node
|
|
204
|
+
*/
|
|
205
|
+
function generateInterpolation(
|
|
206
|
+
node: InterpolationNode,
|
|
207
|
+
options: Required<GenerateOptions>
|
|
208
|
+
): string {
|
|
209
|
+
return transformExpression(node.expression, options);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Generate code for l-for loop
|
|
214
|
+
*/
|
|
215
|
+
function generateForLoop(
|
|
216
|
+
node: ElementNode,
|
|
217
|
+
directive: DirectiveForNode,
|
|
218
|
+
options: Required<GenerateOptions>
|
|
219
|
+
): string {
|
|
220
|
+
const { item, index, list } = directive;
|
|
221
|
+
|
|
222
|
+
// Generate the element code (without the for directive)
|
|
223
|
+
const elementWithoutFor: ElementNode = {
|
|
224
|
+
...node,
|
|
225
|
+
directives: node.directives.filter(d => d.type !== NodeType.DIRECTIVE_FOR),
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const elementCode = generateNormalElement(elementWithoutFor, options);
|
|
229
|
+
|
|
230
|
+
// Generate map function
|
|
231
|
+
if (index) {
|
|
232
|
+
return `(${list}).map((${item}, ${index}) => ${elementCode})`;
|
|
233
|
+
} else {
|
|
234
|
+
return `(${list}).map(${item} => ${elementCode})`;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Generate code for conditional group (l-if/l-else-if/l-else)
|
|
240
|
+
*/
|
|
241
|
+
function generateConditionalGroup(
|
|
242
|
+
group: any,
|
|
243
|
+
options: Required<GenerateOptions>
|
|
244
|
+
): string {
|
|
245
|
+
// Start with if condition
|
|
246
|
+
const ifDirective = getDirective(group.if, NodeType.DIRECTIVE_IF);
|
|
247
|
+
if (!ifDirective) {
|
|
248
|
+
return generateNormalElement(group.if, options);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const ifElementWithoutDirective: ElementNode = {
|
|
252
|
+
...group.if,
|
|
253
|
+
directives: group.if.directives.filter(
|
|
254
|
+
(d: any) => d.type !== NodeType.DIRECTIVE_IF
|
|
255
|
+
),
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
let code = `(${ifDirective.condition}) ? ${generateNormalElement(ifElementWithoutDirective, options)}`;
|
|
259
|
+
|
|
260
|
+
// Add else-if conditions
|
|
261
|
+
for (const elseIfNode of group.elseIfs || []) {
|
|
262
|
+
const elseIfDirective = getDirective(
|
|
263
|
+
elseIfNode,
|
|
264
|
+
NodeType.DIRECTIVE_ELSE_IF
|
|
265
|
+
);
|
|
266
|
+
if (!elseIfDirective) continue;
|
|
267
|
+
|
|
268
|
+
const elseIfElementWithoutDirective: ElementNode = {
|
|
269
|
+
...elseIfNode,
|
|
270
|
+
directives: elseIfNode.directives.filter(
|
|
271
|
+
(d: any) => d.type !== NodeType.DIRECTIVE_ELSE_IF
|
|
272
|
+
),
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
code += ` : (${elseIfDirective.condition}) ? ${generateNormalElement(elseIfElementWithoutDirective, options)}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Add else condition
|
|
279
|
+
if (group.else) {
|
|
280
|
+
const elseElementWithoutDirective: ElementNode = {
|
|
281
|
+
...group.else,
|
|
282
|
+
directives: group.else.directives.filter(
|
|
283
|
+
(d: any) => d.type !== NodeType.DIRECTIVE_ELSE
|
|
284
|
+
),
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
code += ` : ${generateNormalElement(elseElementWithoutDirective, options)}`;
|
|
288
|
+
} else {
|
|
289
|
+
code += ' : null';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return code;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function formatCode(code: string): string {
|
|
296
|
+
// Basic formatting - this is a simple implementation
|
|
297
|
+
// For production, you might want to use prettier or similar
|
|
298
|
+
return code;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
interface TemplateMatch {
|
|
302
|
+
start: number;
|
|
303
|
+
end: number;
|
|
304
|
+
snippet: string;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const isAlphaNumeric = (code: number): boolean => {
|
|
308
|
+
return (
|
|
309
|
+
(code >= 48 && code <= 57) || // 0-9
|
|
310
|
+
(code >= 65 && code <= 90) || // A-Z
|
|
311
|
+
(code >= 97 && code <= 122) || // a-z
|
|
312
|
+
code === 95 || // _
|
|
313
|
+
code === 36 // $
|
|
314
|
+
);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const shouldAttemptTemplate = (source: string, index: number): boolean => {
|
|
318
|
+
if (source[index] !== '<') {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const next = source.charCodeAt(index + 1);
|
|
323
|
+
if (Number.isNaN(next)) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const isValidNext =
|
|
328
|
+
next === 47 || // /
|
|
329
|
+
next === 62 || // >
|
|
330
|
+
next === 33 || // !
|
|
331
|
+
isAlphaNumeric(next);
|
|
332
|
+
|
|
333
|
+
if (!isValidNext) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let i = index - 1;
|
|
338
|
+
while (i >= 0) {
|
|
339
|
+
const code = source.charCodeAt(i);
|
|
340
|
+
if (code === 32 || code === 9 || code === 10 || code === 13) {
|
|
341
|
+
i--;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (isAlphaNumeric(code)) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return true;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const skipQuoted = (source: string, index: number, quote: string): number => {
|
|
356
|
+
let i = index + 1;
|
|
357
|
+
while (i < source.length) {
|
|
358
|
+
const ch = source[i];
|
|
359
|
+
if (ch === '\\') {
|
|
360
|
+
i += 2;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (ch === quote) {
|
|
364
|
+
return i + 1;
|
|
365
|
+
}
|
|
366
|
+
i++;
|
|
367
|
+
}
|
|
368
|
+
return source.length;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const skipTemplateLiteral = (source: string, index: number): number => {
|
|
372
|
+
let i = index + 1;
|
|
373
|
+
while (i < source.length) {
|
|
374
|
+
const ch = source[i];
|
|
375
|
+
if (ch === '\\') {
|
|
376
|
+
i += 2;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (ch === '`') {
|
|
380
|
+
return i + 1;
|
|
381
|
+
}
|
|
382
|
+
if (ch === '$' && source[i + 1] === '{') {
|
|
383
|
+
i = skipJsExpression(source, i + 2);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
i++;
|
|
387
|
+
}
|
|
388
|
+
return source.length;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const skipLineComment = (source: string, index: number): number => {
|
|
392
|
+
let i = index;
|
|
393
|
+
while (i < source.length && source[i] !== '\\n') {
|
|
394
|
+
i++;
|
|
395
|
+
}
|
|
396
|
+
return i;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const skipBlockComment = (source: string, index: number): number => {
|
|
400
|
+
const end = source.indexOf('*/', index);
|
|
401
|
+
return end === -1 ? source.length : end + 2;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const skipHtmlComment = (source: string, index: number): number => {
|
|
405
|
+
const end = source.indexOf('-->', index);
|
|
406
|
+
return end === -1 ? source.length : end + 3;
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const skipJsExpression = (source: string, index: number): number => {
|
|
410
|
+
let depth = 1;
|
|
411
|
+
let i = index;
|
|
412
|
+
|
|
413
|
+
while (i < source.length) {
|
|
414
|
+
const ch = source[i];
|
|
415
|
+
if (ch === '\\') {
|
|
416
|
+
i += 2;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (ch === "'" || ch === '"') {
|
|
420
|
+
i = skipQuoted(source, i, ch);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (ch === '`') {
|
|
424
|
+
i = skipTemplateLiteral(source, i);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (ch === '/' && source[i + 1] === '*') {
|
|
428
|
+
i = skipBlockComment(source, i + 2);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (ch === '/' && source[i + 1] === '/') {
|
|
432
|
+
i = skipLineComment(source, i + 2);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (ch === '{') {
|
|
436
|
+
depth++;
|
|
437
|
+
i++;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (ch === '}') {
|
|
441
|
+
depth--;
|
|
442
|
+
i++;
|
|
443
|
+
if (depth === 0) {
|
|
444
|
+
return i;
|
|
445
|
+
}
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
i++;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return source.length;
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
interface SkipTagResult {
|
|
455
|
+
position: number;
|
|
456
|
+
selfClosing: boolean;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const skipTag = (
|
|
460
|
+
source: string,
|
|
461
|
+
index: number,
|
|
462
|
+
closing: boolean
|
|
463
|
+
): SkipTagResult | null => {
|
|
464
|
+
let i = index;
|
|
465
|
+
while (i < source.length) {
|
|
466
|
+
const ch = source[i];
|
|
467
|
+
if (ch === "'" || ch === '"') {
|
|
468
|
+
i = skipQuoted(source, i, ch);
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (ch === '`') {
|
|
472
|
+
i = skipTemplateLiteral(source, i);
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (ch === '{') {
|
|
476
|
+
i = skipJsExpression(source, i + 1);
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (ch === '/' && source[i + 1] === '*') {
|
|
480
|
+
i = skipBlockComment(source, i + 2);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (ch === '/' && source[i + 1] === '/') {
|
|
484
|
+
i = skipLineComment(source, i + 2);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (ch === '>') {
|
|
488
|
+
const selfClosing = !closing && source[i - 1] === '/';
|
|
489
|
+
return { position: i + 1, selfClosing };
|
|
490
|
+
}
|
|
491
|
+
i++;
|
|
492
|
+
}
|
|
493
|
+
return null;
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const extractTemplate = (
|
|
497
|
+
source: string,
|
|
498
|
+
start: number
|
|
499
|
+
): TemplateMatch | null => {
|
|
500
|
+
const len = source.length;
|
|
501
|
+
let pos = start;
|
|
502
|
+
let depth = 0;
|
|
503
|
+
|
|
504
|
+
while (pos < len) {
|
|
505
|
+
const ch = source[pos];
|
|
506
|
+
|
|
507
|
+
if (ch === "'" || ch === '"') {
|
|
508
|
+
pos = skipQuoted(source, pos, ch);
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (ch === '`') {
|
|
512
|
+
pos = skipTemplateLiteral(source, pos);
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (ch === '/' && source[pos + 1] === '*') {
|
|
516
|
+
pos = skipBlockComment(source, pos + 2);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (ch === '/' && source[pos + 1] === '/') {
|
|
520
|
+
pos = skipLineComment(source, pos + 2);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (ch === '<') {
|
|
525
|
+
if (source.startsWith('<!--', pos)) {
|
|
526
|
+
pos = skipHtmlComment(source, pos + 4);
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (source[pos + 1] === '/') {
|
|
531
|
+
depth--;
|
|
532
|
+
const res = skipTag(source, pos + 2, true);
|
|
533
|
+
if (!res) {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
pos = res.position;
|
|
537
|
+
if (depth === 0) {
|
|
538
|
+
return {
|
|
539
|
+
start,
|
|
540
|
+
end: pos,
|
|
541
|
+
snippet: source.slice(start, pos),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
continue;
|
|
545
|
+
} else {
|
|
546
|
+
depth++;
|
|
547
|
+
const res = skipTag(source, pos + 1, false);
|
|
548
|
+
if (!res) {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
pos = res.position;
|
|
552
|
+
if (res.selfClosing) {
|
|
553
|
+
depth--;
|
|
554
|
+
if (depth === 0) {
|
|
555
|
+
return {
|
|
556
|
+
start,
|
|
557
|
+
end: pos,
|
|
558
|
+
snippet: source.slice(start, pos),
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
pos++;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return null;
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const scanTemplates = (code: string): TemplateMatch[] => {
|
|
573
|
+
const matches: TemplateMatch[] = [];
|
|
574
|
+
const len = code.length;
|
|
575
|
+
let index = 0;
|
|
576
|
+
|
|
577
|
+
while (index < len) {
|
|
578
|
+
const ch = code[index];
|
|
579
|
+
|
|
580
|
+
if (ch === "'" || ch === '"') {
|
|
581
|
+
index = skipQuoted(code, index, ch);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (ch === '`') {
|
|
585
|
+
index = skipTemplateLiteral(code, index);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (ch === '/' && code[index + 1] === '*') {
|
|
589
|
+
index = skipBlockComment(code, index + 2);
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
if (ch === '/' && code[index + 1] === '/') {
|
|
593
|
+
index = skipLineComment(code, index + 2);
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (ch === '<' && shouldAttemptTemplate(code, index)) {
|
|
598
|
+
const match = extractTemplate(code, index);
|
|
599
|
+
if (match) {
|
|
600
|
+
matches.push(match);
|
|
601
|
+
index = match.end;
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
index++;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return matches;
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
function transformExpression(
|
|
613
|
+
expression: string,
|
|
614
|
+
options: Required<GenerateOptions>
|
|
615
|
+
): string {
|
|
616
|
+
if (!expression.includes('<')) {
|
|
617
|
+
return expression;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const matches = scanTemplates(expression);
|
|
621
|
+
if (matches.length === 0) {
|
|
622
|
+
return expression;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
let result = '';
|
|
626
|
+
let lastIndex = 0;
|
|
627
|
+
let changed = false;
|
|
628
|
+
|
|
629
|
+
for (const match of matches) {
|
|
630
|
+
let replacement: string | null = null;
|
|
631
|
+
try {
|
|
632
|
+
replacement = compileEmbeddedTemplate(match.snippet, options);
|
|
633
|
+
} catch {
|
|
634
|
+
replacement = null;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (!replacement) {
|
|
638
|
+
result += expression.slice(lastIndex, match.end);
|
|
639
|
+
lastIndex = match.end;
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
result += expression.slice(lastIndex, match.start) + replacement;
|
|
644
|
+
lastIndex = match.end;
|
|
645
|
+
changed = true;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
result += expression.slice(lastIndex);
|
|
649
|
+
|
|
650
|
+
return changed ? result : expression;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function compileEmbeddedTemplate(
|
|
654
|
+
templateSource: string,
|
|
655
|
+
options: Required<GenerateOptions>
|
|
656
|
+
): string {
|
|
657
|
+
const tokens = tokenize(templateSource);
|
|
658
|
+
const ast = parse(tokens);
|
|
659
|
+
const transformedAst = transform(ast);
|
|
660
|
+
return generate(transformedAst, options);
|
|
661
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './createElement';
|
package/src/compiler.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { tokenize } from './parser/lexer';
|
|
2
|
+
import { parse } from './parser/parser';
|
|
3
|
+
import { transform, TransformOptions } from './transform';
|
|
4
|
+
import { generate, GenerateOptions } from './codegen';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Compile a Lithent template to JavaScript code
|
|
8
|
+
*/
|
|
9
|
+
export function compile(
|
|
10
|
+
template: string,
|
|
11
|
+
options?: CompileOptions
|
|
12
|
+
): CompileResult {
|
|
13
|
+
try {
|
|
14
|
+
// 1. Tokenize
|
|
15
|
+
const tokens = tokenize(template);
|
|
16
|
+
|
|
17
|
+
// 2. Parse
|
|
18
|
+
const ast = parse(tokens);
|
|
19
|
+
|
|
20
|
+
// 3. Transform
|
|
21
|
+
const transformedAst = transform(ast, options?.transform);
|
|
22
|
+
|
|
23
|
+
// 4. Generate
|
|
24
|
+
const code = generate(transformedAst, options?.generate);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
code,
|
|
28
|
+
ast: transformedAst,
|
|
29
|
+
errors: [],
|
|
30
|
+
};
|
|
31
|
+
} catch (error) {
|
|
32
|
+
const err = error as Error & { line?: number; column?: number };
|
|
33
|
+
return {
|
|
34
|
+
code: '',
|
|
35
|
+
ast: null,
|
|
36
|
+
errors: [
|
|
37
|
+
{
|
|
38
|
+
message: error instanceof Error ? error.message : String(error),
|
|
39
|
+
line: err.line ?? 0,
|
|
40
|
+
column: err.column ?? 0,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface CompileOptions {
|
|
48
|
+
transform?: TransformOptions;
|
|
49
|
+
generate?: GenerateOptions;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CompileResult {
|
|
53
|
+
code: string;
|
|
54
|
+
ast: any;
|
|
55
|
+
errors: CompileError[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface CompileError {
|
|
59
|
+
message: string;
|
|
60
|
+
line: number;
|
|
61
|
+
column: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Re-export all modules
|
|
65
|
+
export * from './parser/tokens';
|
|
66
|
+
export * from './parser/lexer';
|
|
67
|
+
export * from './parser/parser';
|
|
68
|
+
export * from './parser/ast';
|
|
69
|
+
export * from './transform';
|
|
70
|
+
export * from './codegen';
|