@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,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
|
+
}
|