@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,160 @@
1
+ import {
2
+ RootNode,
3
+ TemplateNode,
4
+ ElementNode,
5
+ NodeType,
6
+ DirectiveIfNode,
7
+ DirectiveElseIfNode,
8
+ DirectiveElseNode,
9
+ } from '../parser/ast';
10
+
11
+ /**
12
+ * Group conditional elements (l-if/l-else-if/l-else chain)
13
+ * This transform marks related conditional elements for easier codegen
14
+ */
15
+ export interface ConditionalGroup {
16
+ if: ElementNode;
17
+ elseIfs: ElementNode[];
18
+ else?: ElementNode;
19
+ }
20
+
21
+ /**
22
+ * Transform directives in the AST
23
+ * Currently handles:
24
+ * - Grouping l-if/l-else-if/l-else chains
25
+ */
26
+ export function transformDirectives(ast: RootNode): RootNode {
27
+ return {
28
+ ...ast,
29
+ children: transformNodeList(ast.children),
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Transform a list of nodes, handling conditional grouping
35
+ */
36
+ function transformNodeList(nodes: TemplateNode[]): TemplateNode[] {
37
+ const result: TemplateNode[] = [];
38
+
39
+ for (let i = 0; i < nodes.length; i++) {
40
+ const node = nodes[i];
41
+
42
+ if (node.type === NodeType.ELEMENT) {
43
+ // Process element's children recursively
44
+ const transformedElement: ElementNode = {
45
+ ...node,
46
+ children: transformNodeList(node.children),
47
+ };
48
+
49
+ // Check if this starts a conditional chain
50
+ const ifDirective = node.directives.find(
51
+ d => d.type === NodeType.DIRECTIVE_IF
52
+ ) as DirectiveIfNode | undefined;
53
+
54
+ if (ifDirective) {
55
+ // This starts a conditional chain, look ahead for else-if/else
56
+ const conditionalGroup: ConditionalGroup = {
57
+ if: transformedElement,
58
+ elseIfs: [],
59
+ };
60
+
61
+ // Look ahead for else-if and else
62
+ let j = i + 1;
63
+ while (j < nodes.length) {
64
+ const nextNode = nodes[j];
65
+
66
+ if (nextNode.type === NodeType.ELEMENT) {
67
+ const elseIfDirective = nextNode.directives.find(
68
+ d => d.type === NodeType.DIRECTIVE_ELSE_IF
69
+ ) as DirectiveElseIfNode | undefined;
70
+
71
+ const elseDirective = nextNode.directives.find(
72
+ d => d.type === NodeType.DIRECTIVE_ELSE
73
+ ) as DirectiveElseNode | undefined;
74
+
75
+ if (elseIfDirective) {
76
+ conditionalGroup.elseIfs.push({
77
+ ...nextNode,
78
+ children: transformNodeList(nextNode.children),
79
+ });
80
+ j++;
81
+ continue;
82
+ }
83
+
84
+ if (elseDirective) {
85
+ conditionalGroup.else = {
86
+ ...nextNode,
87
+ children: transformNodeList(nextNode.children),
88
+ };
89
+ j++;
90
+ break;
91
+ }
92
+ }
93
+
94
+ // Not part of the conditional chain
95
+ break;
96
+ }
97
+
98
+ // Mark the conditional group metadata (for codegen)
99
+ // We'll store this in a symbol property that codegen can check
100
+ (transformedElement as any).__conditionalGroup = conditionalGroup;
101
+
102
+ // Skip the else-if/else nodes we've processed
103
+ i = j - 1;
104
+ }
105
+
106
+ result.push(transformedElement);
107
+ } else if (node.type === NodeType.FRAGMENT) {
108
+ result.push({
109
+ ...node,
110
+ children: transformNodeList(node.children),
111
+ });
112
+ } else {
113
+ // Non-element nodes pass through
114
+ result.push(node);
115
+ }
116
+ }
117
+
118
+ return result;
119
+ }
120
+
121
+ /**
122
+ * Get conditional group if element is part of one
123
+ */
124
+ export function getConditionalGroup(
125
+ element: ElementNode
126
+ ): ConditionalGroup | undefined {
127
+ return (element as any).__conditionalGroup;
128
+ }
129
+
130
+ /**
131
+ * Check if element has a specific directive type
132
+ */
133
+ export function hasDirective(
134
+ element: ElementNode,
135
+ directiveType: NodeType
136
+ ): boolean {
137
+ return element.directives.some(d => d.type === directiveType);
138
+ }
139
+
140
+ /**
141
+ * Get directive of specific type from element
142
+ */
143
+ export function getDirective(
144
+ element: ElementNode,
145
+ directiveType: NodeType.DIRECTIVE_IF
146
+ ): DirectiveIfNode | undefined;
147
+ export function getDirective(
148
+ element: ElementNode,
149
+ directiveType: NodeType.DIRECTIVE_ELSE_IF
150
+ ): DirectiveElseIfNode | undefined;
151
+ export function getDirective(
152
+ element: ElementNode,
153
+ directiveType: NodeType.DIRECTIVE_ELSE
154
+ ): DirectiveElseNode | undefined;
155
+ export function getDirective(
156
+ element: ElementNode,
157
+ directiveType: NodeType
158
+ ): DirectiveIfNode | DirectiveElseIfNode | DirectiveElseNode | undefined {
159
+ return element.directives.find(d => d.type === directiveType) as any;
160
+ }
@@ -0,0 +1,29 @@
1
+ import { RootNode } from '../parser/ast';
2
+ import { transformDirectives } from './directives';
3
+ import { optimize, OptimizeOptions } from './optimize';
4
+
5
+ /**
6
+ * Apply all transformations to the AST
7
+ */
8
+ export function transform(ast: RootNode, options?: TransformOptions): RootNode {
9
+ let result = ast;
10
+
11
+ // Transform directives (group conditionals)
12
+ result = transformDirectives(result);
13
+
14
+ // Optimize AST
15
+ if (options?.optimize !== false) {
16
+ result = optimize(result, options?.optimizeOptions);
17
+ }
18
+
19
+ return result;
20
+ }
21
+
22
+ export interface TransformOptions {
23
+ optimize?: boolean;
24
+ optimizeOptions?: OptimizeOptions;
25
+ }
26
+
27
+ export { transformDirectives, optimize };
28
+ export * from './directives';
29
+ export * from './optimize';
@@ -0,0 +1,203 @@
1
+ import {
2
+ RootNode,
3
+ TemplateNode,
4
+ ElementNode,
5
+ FragmentNode,
6
+ TextNode,
7
+ NodeType,
8
+ createTextNode,
9
+ } from '../parser/ast';
10
+
11
+ /**
12
+ * Optimize the AST
13
+ * - Merge consecutive text nodes
14
+ * - Remove empty text nodes (whitespace-only)
15
+ * - Trim text nodes at boundaries
16
+ */
17
+ export function optimize(ast: RootNode, options?: OptimizeOptions): RootNode {
18
+ const opts: Required<OptimizeOptions> = {
19
+ mergeTextNodes: true,
20
+ removeWhitespace: true,
21
+ trimText: true,
22
+ ...options,
23
+ };
24
+
25
+ return {
26
+ ...ast,
27
+ children: optimizeNodeList(ast.children, opts),
28
+ };
29
+ }
30
+
31
+ export interface OptimizeOptions {
32
+ mergeTextNodes?: boolean;
33
+ removeWhitespace?: boolean;
34
+ trimText?: boolean;
35
+ }
36
+
37
+ /**
38
+ * Optimize a list of nodes
39
+ */
40
+ function optimizeNodeList(
41
+ nodes: TemplateNode[],
42
+ options: Required<OptimizeOptions>
43
+ ): TemplateNode[] {
44
+ let result: TemplateNode[] = nodes.map(node => optimizeNode(node, options));
45
+
46
+ // Merge consecutive text nodes
47
+ if (options.mergeTextNodes) {
48
+ result = mergeTextNodes(result);
49
+ }
50
+
51
+ // Remove whitespace-only text nodes
52
+ if (options.removeWhitespace) {
53
+ result = removeWhitespaceNodes(result);
54
+ }
55
+
56
+ return result;
57
+ }
58
+
59
+ /**
60
+ * Optimize a single node
61
+ */
62
+ function optimizeNode(
63
+ node: TemplateNode,
64
+ options: Required<OptimizeOptions>
65
+ ): TemplateNode {
66
+ if (node.type === NodeType.ELEMENT) {
67
+ return optimizeElementNode(node, options);
68
+ }
69
+
70
+ if (node.type === NodeType.FRAGMENT) {
71
+ return optimizeFragmentNode(node, options);
72
+ }
73
+
74
+ if (node.type === NodeType.TEXT) {
75
+ return optimizeTextNode(node, options);
76
+ }
77
+
78
+ return node;
79
+ }
80
+
81
+ /**
82
+ * Optimize an element node
83
+ */
84
+ function optimizeElementNode(
85
+ node: ElementNode,
86
+ options: Required<OptimizeOptions>
87
+ ): ElementNode {
88
+ return {
89
+ ...node,
90
+ children: optimizeNodeList(node.children, options),
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Optimize a fragment node
96
+ */
97
+ function optimizeFragmentNode(
98
+ node: FragmentNode,
99
+ options: Required<OptimizeOptions>
100
+ ): FragmentNode {
101
+ return {
102
+ ...node,
103
+ children: optimizeNodeList(node.children, options),
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Optimize a text node
109
+ */
110
+ function optimizeTextNode(
111
+ node: TextNode,
112
+ options: Required<OptimizeOptions>
113
+ ): TextNode {
114
+ let content = node.content;
115
+
116
+ // Trim text if enabled
117
+ if (options.trimText) {
118
+ content = content.trim();
119
+ }
120
+
121
+ return {
122
+ ...node,
123
+ content,
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Merge consecutive text nodes
129
+ */
130
+ function mergeTextNodes(nodes: TemplateNode[]): TemplateNode[] {
131
+ const result: TemplateNode[] = [];
132
+ let currentText: TextNode | null = null;
133
+
134
+ for (const node of nodes) {
135
+ if (node.type === NodeType.TEXT) {
136
+ if (currentText) {
137
+ // Merge with previous text node
138
+ currentText = createTextNode(
139
+ currentText.content + node.content,
140
+ currentText.start,
141
+ node.end
142
+ );
143
+ } else {
144
+ currentText = node;
145
+ }
146
+ } else {
147
+ // Push accumulated text node if any
148
+ if (currentText) {
149
+ result.push(currentText);
150
+ currentText = null;
151
+ }
152
+ result.push(node);
153
+ }
154
+ }
155
+
156
+ // Don't forget the last text node
157
+ if (currentText) {
158
+ result.push(currentText);
159
+ }
160
+
161
+ return result;
162
+ }
163
+
164
+ /**
165
+ * Remove whitespace-only text nodes
166
+ */
167
+ function removeWhitespaceNodes(nodes: TemplateNode[]): TemplateNode[] {
168
+ return nodes.filter(node => {
169
+ if (node.type === NodeType.TEXT) {
170
+ return node.content.trim().length > 0;
171
+ }
172
+ return true;
173
+ });
174
+ }
175
+
176
+ /**
177
+ * Check if a text node is whitespace-only
178
+ */
179
+ export function isWhitespaceOnly(node: TextNode): boolean {
180
+ return node.content.trim().length === 0;
181
+ }
182
+
183
+ /**
184
+ * Count nodes in the tree
185
+ */
186
+ export function countNodes(node: RootNode | TemplateNode): number {
187
+ let count = 1;
188
+
189
+ if (node.type === NodeType.ROOT) {
190
+ node.children.forEach(child => {
191
+ count += countNodes(child);
192
+ });
193
+ } else if (
194
+ node.type === NodeType.ELEMENT ||
195
+ node.type === NodeType.FRAGMENT
196
+ ) {
197
+ node.children.forEach(child => {
198
+ count += countNodes(child);
199
+ });
200
+ }
201
+
202
+ return count;
203
+ }