@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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +159 -0
  3. package/dist/codegen/createElement.d.ts +10 -0
  4. package/dist/codegen/index.d.ts +1 -0
  5. package/dist/compiler.d.ts +26 -0
  6. package/dist/docPipe/index.d.ts +40 -0
  7. package/dist/docPipe/scan.d.ts +6 -0
  8. package/dist/index.d.ts +13 -0
  9. package/dist/index.mjs +2302 -0
  10. package/dist/index.mjs.map +1 -0
  11. package/dist/index.umd.cjs +13 -0
  12. package/dist/index.umd.cjs.map +1 -0
  13. package/dist/parser/ast.d.ts +207 -0
  14. package/dist/parser/lexer.d.ts +109 -0
  15. package/dist/parser/parser.d.ts +79 -0
  16. package/dist/parser/tokens.d.ts +51 -0
  17. package/dist/transform/directives.d.ts +30 -0
  18. package/dist/transform/index.d.ts +14 -0
  19. package/dist/transform/optimize.d.ts +21 -0
  20. package/package.json +53 -0
  21. package/src/codegen/createElement.ts +661 -0
  22. package/src/codegen/index.ts +1 -0
  23. package/src/compiler.ts +70 -0
  24. package/src/docPipe/index.ts +104 -0
  25. package/src/docPipe/scan.ts +318 -0
  26. package/src/index.ts +16 -0
  27. package/src/parser/ast.ts +478 -0
  28. package/src/parser/lexer.ts +561 -0
  29. package/src/parser/parser.ts +514 -0
  30. package/src/parser/tokens.ts +84 -0
  31. package/src/playground/main.ts +206 -0
  32. package/src/playground/style.css +135 -0
  33. package/src/shims/babel-env.ts +17 -0
  34. package/src/tests/compiler.test.ts +434 -0
  35. package/src/tests/docPipe.test.ts +41 -0
  36. package/src/tests/helpers.ts +96 -0
  37. package/src/tests/lexer.test.ts +282 -0
  38. package/src/tests/parser.test.ts +691 -0
  39. package/src/transform/directives.ts +160 -0
  40. package/src/transform/index.ts +29 -0
  41. package/src/transform/optimize.ts +203 -0
@@ -0,0 +1,104 @@
1
+ import MagicString, { type SourceMap } from 'magic-string';
2
+ import { compile, type CompileError, type CompileOptions } from '../compiler';
3
+ import { scanTemplates, type TemplateMatch } from './scan';
4
+
5
+ export interface DocumentTransformOptions {
6
+ /**
7
+ * Include original source name in generated source map.
8
+ */
9
+ source?: string;
10
+ /**
11
+ * Emit a source map when true.
12
+ */
13
+ generateSourceMap?: boolean;
14
+ /**
15
+ * Forwarded compile options for each template block.
16
+ */
17
+ compileOptions?: CompileOptions;
18
+ }
19
+
20
+ export interface DocumentTransformError {
21
+ match: TemplateMatch;
22
+ error: CompileError;
23
+ }
24
+
25
+ export interface DocumentTransformResult {
26
+ code: string;
27
+ map: SourceMap | null;
28
+ /**
29
+ * `true` when at least one replacement succeeded.
30
+ */
31
+ transformed: boolean;
32
+ /**
33
+ * Errors produced while compiling templates.
34
+ */
35
+ errors: DocumentTransformError[];
36
+ /**
37
+ * All template matches discovered in the source.
38
+ */
39
+ matches: TemplateMatch[];
40
+ }
41
+
42
+ export const transformDocument = (
43
+ source: string,
44
+ options: DocumentTransformOptions = {}
45
+ ): DocumentTransformResult => {
46
+ const matches = scanTemplates(source);
47
+ if (matches.length === 0) {
48
+ return {
49
+ code: source,
50
+ map: null,
51
+ transformed: false,
52
+ errors: [],
53
+ matches: [],
54
+ };
55
+ }
56
+
57
+ const ms = new MagicString(source);
58
+ const errors: DocumentTransformError[] = [];
59
+ let transformed = false;
60
+
61
+ for (const match of matches) {
62
+ const result = compile(match.snippet, options.compileOptions);
63
+ if (result.errors.length > 0 || !result.code) {
64
+ for (const error of result.errors) {
65
+ errors.push({ match, error });
66
+ }
67
+ continue;
68
+ }
69
+
70
+ ms.overwrite(match.start, match.end, result.code);
71
+ transformed = true;
72
+ }
73
+
74
+ if (!transformed) {
75
+ return {
76
+ code: source,
77
+ map: null,
78
+ transformed: false,
79
+ errors,
80
+ matches,
81
+ };
82
+ }
83
+
84
+ const code = ms.toString();
85
+ let map: SourceMap | null = null;
86
+
87
+ if (options.generateSourceMap) {
88
+ const mapConfig = options.source
89
+ ? { source: options.source, hires: true }
90
+ : { hires: true };
91
+ map = ms.generateMap(mapConfig);
92
+ }
93
+
94
+ return {
95
+ code,
96
+ map,
97
+ transformed: true,
98
+ errors,
99
+ matches,
100
+ };
101
+ };
102
+
103
+ export { scanTemplates };
104
+ export type { TemplateMatch };
@@ -0,0 +1,318 @@
1
+ export interface TemplateMatch {
2
+ start: number;
3
+ end: number;
4
+ snippet: string;
5
+ }
6
+
7
+ const DEFAULT_ALPHA_NUMERIC = new Set<number>();
8
+
9
+ for (let code = 48; code <= 57; code++) {
10
+ DEFAULT_ALPHA_NUMERIC.add(code);
11
+ }
12
+ for (let code = 65; code <= 90; code++) {
13
+ DEFAULT_ALPHA_NUMERIC.add(code);
14
+ }
15
+ for (let code = 97; code <= 122; code++) {
16
+ DEFAULT_ALPHA_NUMERIC.add(code);
17
+ }
18
+ DEFAULT_ALPHA_NUMERIC.add(95); // _
19
+ DEFAULT_ALPHA_NUMERIC.add(36); // $
20
+
21
+ const isAlphaNumeric = (code: number): boolean => {
22
+ return DEFAULT_ALPHA_NUMERIC.has(code);
23
+ };
24
+
25
+ const shouldAttemptTemplate = (source: string, index: number): boolean => {
26
+ if (source[index] !== '<') {
27
+ return false;
28
+ }
29
+
30
+ const next = source.charCodeAt(index + 1);
31
+ if (Number.isNaN(next)) {
32
+ return false;
33
+ }
34
+
35
+ // Allow fragments <>, closing </, comments <!-, normal tags <a
36
+ const isValidNext =
37
+ next === 47 || // /
38
+ next === 62 || // >
39
+ next === 33 || // ! (e.g. <!-- -->)
40
+ isAlphaNumeric(next);
41
+ if (!isValidNext) {
42
+ return false;
43
+ }
44
+
45
+ // Ensure previous meaningful char is not part of an identifier (e.g. a<b)
46
+ let i = index - 1;
47
+ while (i >= 0) {
48
+ const code = source.charCodeAt(i);
49
+ if (code === 32 || code === 9 || code === 10 || code === 13) {
50
+ i--;
51
+ continue;
52
+ }
53
+ if (isAlphaNumeric(code)) {
54
+ // Identifier characters => likely relational operator (e.g. a<b)
55
+ return false;
56
+ }
57
+ break;
58
+ }
59
+
60
+ return true;
61
+ };
62
+
63
+ const skipQuoted = (source: string, index: number, quote: string): number => {
64
+ let i = index + 1;
65
+ while (i < source.length) {
66
+ const ch = source[i];
67
+ if (ch === '\\') {
68
+ i += 2;
69
+ continue;
70
+ }
71
+ if (ch === quote) {
72
+ return i + 1;
73
+ }
74
+ i++;
75
+ }
76
+ return source.length;
77
+ };
78
+
79
+ const skipTemplateLiteral = (source: string, index: number): number => {
80
+ let i = index + 1;
81
+ while (i < source.length) {
82
+ const ch = source[i];
83
+ if (ch === '\\') {
84
+ i += 2;
85
+ continue;
86
+ }
87
+ if (ch === '`') {
88
+ return i + 1;
89
+ }
90
+ if (ch === '$' && source[i + 1] === '{') {
91
+ const result = skipJsExpression(source, i + 2);
92
+ if (result == null) {
93
+ return source.length;
94
+ }
95
+ i = result;
96
+ continue;
97
+ }
98
+ i++;
99
+ }
100
+ return source.length;
101
+ };
102
+
103
+ const skipLineComment = (source: string, index: number): number => {
104
+ let i = index;
105
+ while (i < source.length && source[i] !== '\n') {
106
+ i++;
107
+ }
108
+ return i;
109
+ };
110
+
111
+ const skipBlockComment = (source: string, index: number): number => {
112
+ const end = source.indexOf('*/', index);
113
+ return end === -1 ? source.length : end + 2;
114
+ };
115
+
116
+ const skipHtmlComment = (source: string, index: number): number => {
117
+ const end = source.indexOf('-->', index);
118
+ return end === -1 ? source.length : end + 3;
119
+ };
120
+
121
+ const skipJsExpression = (source: string, index: number): number | null => {
122
+ let depth = 1;
123
+ let i = index;
124
+ while (i < source.length) {
125
+ const ch = source[i];
126
+ if (ch === '\\') {
127
+ i += 2;
128
+ continue;
129
+ }
130
+ if (ch === "'" || ch === '"') {
131
+ i = skipQuoted(source, i, ch);
132
+ continue;
133
+ }
134
+ if (ch === '`') {
135
+ i = skipTemplateLiteral(source, i);
136
+ continue;
137
+ }
138
+ if (ch === '/' && source[i + 1] === '*') {
139
+ i = skipBlockComment(source, i + 2);
140
+ continue;
141
+ }
142
+ if (ch === '/' && source[i + 1] === '/') {
143
+ i = skipLineComment(source, i + 2);
144
+ continue;
145
+ }
146
+ if (ch === '{') {
147
+ depth++;
148
+ i++;
149
+ continue;
150
+ }
151
+ if (ch === '}') {
152
+ depth--;
153
+ i++;
154
+ if (depth === 0) {
155
+ return i;
156
+ }
157
+ continue;
158
+ }
159
+ i++;
160
+ }
161
+ return null;
162
+ };
163
+
164
+ interface SkipTagResult {
165
+ position: number;
166
+ selfClosing: boolean;
167
+ }
168
+
169
+ const skipTag = (
170
+ source: string,
171
+ index: number,
172
+ closing: boolean
173
+ ): SkipTagResult | null => {
174
+ let i = index;
175
+ while (i < source.length) {
176
+ const ch = source[i];
177
+ if (ch === "'" || ch === '"') {
178
+ i = skipQuoted(source, i, ch);
179
+ continue;
180
+ }
181
+ if (ch === '{') {
182
+ const result = skipJsExpression(source, i + 1);
183
+ if (result == null) {
184
+ return null;
185
+ }
186
+ i = result;
187
+ continue;
188
+ }
189
+ if (ch === '/' && source[i + 1] === '*') {
190
+ i = skipBlockComment(source, i + 2);
191
+ continue;
192
+ }
193
+ if (ch === '/' && source[i + 1] === '/') {
194
+ i = skipLineComment(source, i + 2);
195
+ continue;
196
+ }
197
+ if (ch === '>') {
198
+ const selfClosing = !closing && source[i - 1] === '/';
199
+ return { position: i + 1, selfClosing };
200
+ }
201
+ i++;
202
+ }
203
+ return null;
204
+ };
205
+
206
+ const extractTemplate = (
207
+ source: string,
208
+ start: number
209
+ ): TemplateMatch | null => {
210
+ const len = source.length;
211
+ let pos = start;
212
+ let depth = 0;
213
+
214
+ while (pos < len) {
215
+ const ch = source[pos];
216
+
217
+ if (ch === '/' && source[pos + 1] === '*') {
218
+ pos = skipBlockComment(source, pos + 2);
219
+ continue;
220
+ }
221
+ if (ch === '/' && source[pos + 1] === '/') {
222
+ pos = skipLineComment(source, pos + 2);
223
+ continue;
224
+ }
225
+ if (ch === '{') {
226
+ const result = skipJsExpression(source, pos + 1);
227
+ if (result != null) {
228
+ pos = result;
229
+ continue;
230
+ }
231
+ }
232
+ if (ch === '<') {
233
+ if (source.startsWith('<!--', pos)) {
234
+ pos = skipHtmlComment(source, pos + 4);
235
+ continue;
236
+ }
237
+
238
+ if (source[pos + 1] === '/') {
239
+ depth--;
240
+ const res = skipTag(source, pos + 2, true);
241
+ if (!res) {
242
+ return null;
243
+ }
244
+ pos = res.position;
245
+ if (depth === 0) {
246
+ return {
247
+ start,
248
+ end: pos,
249
+ snippet: source.slice(start, pos),
250
+ };
251
+ }
252
+ continue;
253
+ } else {
254
+ depth++;
255
+ const res = skipTag(source, pos + 1, false);
256
+ if (!res) {
257
+ return null;
258
+ }
259
+ pos = res.position;
260
+ if (res.selfClosing) {
261
+ depth--;
262
+ if (depth === 0) {
263
+ return {
264
+ start,
265
+ end: pos,
266
+ snippet: source.slice(start, pos),
267
+ };
268
+ }
269
+ }
270
+ continue;
271
+ }
272
+ }
273
+
274
+ pos++;
275
+ }
276
+
277
+ return null;
278
+ };
279
+
280
+ export const scanTemplates = (code: string): TemplateMatch[] => {
281
+ const matches: TemplateMatch[] = [];
282
+ const len = code.length;
283
+ let index = 0;
284
+
285
+ while (index < len) {
286
+ const ch = code[index];
287
+
288
+ if (ch === "'" || ch === '"') {
289
+ index = skipQuoted(code, index, ch);
290
+ continue;
291
+ }
292
+ if (ch === '`') {
293
+ index = skipTemplateLiteral(code, index);
294
+ continue;
295
+ }
296
+ if (ch === '/' && code[index + 1] === '*') {
297
+ index = skipBlockComment(code, index + 2);
298
+ continue;
299
+ }
300
+ if (ch === '/' && code[index + 1] === '/') {
301
+ index = skipLineComment(code, index + 2);
302
+ continue;
303
+ }
304
+
305
+ if (ch === '<' && shouldAttemptTemplate(code, index)) {
306
+ const match = extractTemplate(code, index);
307
+ if (match) {
308
+ matches.push(match);
309
+ index = match.end;
310
+ continue;
311
+ }
312
+ }
313
+
314
+ index++;
315
+ }
316
+
317
+ return matches;
318
+ };
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Lithent Template Parser
3
+ * A Vue-like template parser for Lithent
4
+ */
5
+
6
+ export { compile } from './compiler';
7
+ export type { CompileOptions, CompileResult, CompileError } from './compiler';
8
+
9
+ // Export all modules for advanced usage
10
+ export * from './parser/tokens';
11
+ export * from './parser/lexer';
12
+ export * from './parser/parser';
13
+ export * from './parser/ast';
14
+ export * from './transform';
15
+ export * from './codegen';
16
+ export * from './docPipe';