ember-estree 0.2.0 → 0.4.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/package.json +2 -2
- package/src/index.d.ts +38 -69
- package/src/index.js +1 -1
- package/src/parse.js +298 -134
- package/src/transforms.js +212 -117
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ember-estree",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "ESTree generator for gjs and gts file used by ember",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"AST",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
+
"@glimmer/env": "^0.1.7",
|
|
33
34
|
"@glimmer/syntax": "^0.95.0",
|
|
34
35
|
"content-tag": "^4.1.0",
|
|
35
|
-
"ember-template-recast": "^6.1.5",
|
|
36
36
|
"oxc-parser": "^0.119.0",
|
|
37
37
|
"zimmerframe": "^1.1.4"
|
|
38
38
|
},
|
package/src/index.d.ts
CHANGED
|
@@ -1,23 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Options accepted by `parse` and `toTree`.
|
|
3
|
-
*/
|
|
4
|
-
export interface ParseOptions {
|
|
5
|
-
/** Path to the file being parsed, used to determine the language (js/ts). */
|
|
6
|
-
filePath?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* A 1-based line / 0-based column position, matching ESTree and Glimmer
|
|
11
|
-
* conventions.
|
|
12
|
-
*/
|
|
13
1
|
export interface Position {
|
|
14
2
|
line: number;
|
|
15
3
|
column: number;
|
|
16
4
|
}
|
|
17
5
|
|
|
18
|
-
/**
|
|
19
|
-
* Minimal shape shared by every AST node (ESTree, TypeScript, and Glimmer).
|
|
20
|
-
*/
|
|
21
6
|
export interface ASTNode {
|
|
22
7
|
type: string;
|
|
23
8
|
start?: number;
|
|
@@ -25,71 +10,55 @@ export interface ASTNode {
|
|
|
25
10
|
[key: string]: unknown;
|
|
26
11
|
}
|
|
27
12
|
|
|
28
|
-
/**
|
|
29
|
-
* The `File`-like wrapper returned by `toTree` and `parse`.
|
|
30
|
-
*
|
|
31
|
-
* Mirrors the shape produced internally:
|
|
32
|
-
* ```
|
|
33
|
-
* { type: "File", program: Program, comments: Comment[], start, end }
|
|
34
|
-
* ```
|
|
35
|
-
*/
|
|
36
13
|
export interface FileNode extends ASTNode {
|
|
37
14
|
type: "File";
|
|
38
15
|
program: ASTNode;
|
|
39
16
|
comments: ASTNode[];
|
|
40
17
|
}
|
|
41
18
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
19
|
+
export interface TemplateResult {
|
|
20
|
+
ast: ASTNode;
|
|
21
|
+
comments: ASTNode[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface VisitorPath {
|
|
25
|
+
node: ASTNode;
|
|
26
|
+
parent: ASTNode | null;
|
|
27
|
+
parentPath: VisitorPath | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ParseOptions {
|
|
31
|
+
filePath?: string;
|
|
32
|
+
templateOnly?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Include `parent` references on Glimmer AST nodes.
|
|
35
|
+
* Defaults to `true`. Set to `false` for JSON-serializable output.
|
|
36
|
+
*/
|
|
37
|
+
includeParentLinks?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Custom JS/TS parser. Called with the placeholder JS string
|
|
40
|
+
* (templates replaced with backtick expressions of equal length).
|
|
41
|
+
* Must return at least `{ ast }`.
|
|
42
|
+
*/
|
|
43
|
+
parser?: (placeholderJS: string) => { ast: ASTNode; [key: string]: unknown };
|
|
44
|
+
/**
|
|
45
|
+
* Callbacks invoked for Glimmer nodes during the AST splice traversal.
|
|
46
|
+
* Runs in DFS order, so parent nodes are visited before children.
|
|
47
|
+
*/
|
|
48
|
+
visitors?: {
|
|
49
|
+
[glimmerNodeType: string]: (node: ASTNode, path: VisitorPath) => void;
|
|
50
|
+
GlimmerBlockParams?: (node: ASTNode, path: VisitorPath) => void;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
export class DocumentLines {
|
|
47
55
|
constructor(source: string);
|
|
48
|
-
|
|
49
|
-
/** Converts a `{ line, column }` position to a character offset. */
|
|
50
56
|
positionToOffset(pos: Position): number;
|
|
51
|
-
|
|
52
|
-
/** Converts a character offset to a `{ line, column }` position. */
|
|
53
57
|
offsetToPosition(offset: number): Position;
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
* AST with embedded Glimmer template nodes.
|
|
59
|
-
*
|
|
60
|
-
* @param source The raw source code of the file.
|
|
61
|
-
* @param options Optional parse options.
|
|
62
|
-
* @returns A `File`-shaped object with a `.program` property.
|
|
63
|
-
*/
|
|
64
|
-
export function toTree(source: string, options?: ParseOptions): FileNode;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Parse Ember .gjs/.gts source code into an ESTree-compatible AST with
|
|
68
|
-
* embedded Glimmer template nodes.
|
|
69
|
-
*
|
|
70
|
-
* @param source The source code to parse.
|
|
71
|
-
* @param options Optional parse options.
|
|
72
|
-
* @returns The ESTree-compatible AST.
|
|
73
|
-
*/
|
|
74
|
-
export function parse(source: string, options?: ParseOptions): FileNode;
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Recursively print an AST node back to source code.
|
|
78
|
-
*
|
|
79
|
-
* Handles ESTree, TypeScript, and Glimmer template node types.
|
|
80
|
-
* JSX nodes are not supported — Ember uses Glimmer templates instead.
|
|
81
|
-
*
|
|
82
|
-
* @param node The AST node to print.
|
|
83
|
-
* @returns The printed source string.
|
|
84
|
-
*/
|
|
60
|
+
export function toTree(source: string, options?: ParseOptions): FileNode | TemplateResult;
|
|
61
|
+
export function parse(source: string, options?: ParseOptions): FileNode | TemplateResult;
|
|
85
62
|
export function print(node: ASTNode): string;
|
|
86
63
|
|
|
87
|
-
|
|
88
|
-
* Build and return the Glimmer visitor keys map with a `"Glimmer"` prefix on
|
|
89
|
-
* every key (e.g. `"GlimmerElementNode"`).
|
|
90
|
-
*
|
|
91
|
-
* The result is cached after the first call.
|
|
92
|
-
*
|
|
93
|
-
* @returns A map of Glimmer node type names to arrays of child-property names.
|
|
94
|
-
*/
|
|
95
|
-
export function buildGlimmerVisitorKeys(): Record<string, string[]>;
|
|
64
|
+
export const glimmerVisitorKeys: Record<string, string[]>;
|
package/src/index.js
CHANGED
package/src/parse.js
CHANGED
|
@@ -1,176 +1,340 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Strategy:
|
|
3
3
|
*
|
|
4
|
-
* 1. parse out the <template>...</template> regions
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* 3. parse the string/contents as js/ts to generate an ESTree
|
|
12
|
-
*
|
|
13
|
-
* 4. parse each template region to generate an AST from that
|
|
14
|
-
*
|
|
15
|
-
* 5. convert the AST from `@glimmer/syntax` to ESTree
|
|
16
|
-
* - NOTE: it may already be ESTree
|
|
17
|
-
*
|
|
18
|
-
* 6. splice in the template ESTrees into the JS/TS ESTree
|
|
19
|
-
*
|
|
20
|
-
* 7. Done
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Docs for dependencies:
|
|
25
|
-
* - https://github.com/embroider-build/content-tag/
|
|
4
|
+
* 1. parse out the <template>...</template> regions (content-tag)
|
|
5
|
+
* 2. create placeholder JS for the template regions (backtick/static-block, same char length)
|
|
6
|
+
* 3. parse as js/ts — default: oxc-parser, or a custom parser via options
|
|
7
|
+
* 4. splice in processed Glimmer ASTs, invoking visitors during traversal
|
|
8
|
+
* 5. Merge Glimmer visitor keys into the result
|
|
9
|
+
* 6. Done
|
|
26
10
|
*/
|
|
27
11
|
|
|
28
12
|
import { parseSync } from "oxc-parser";
|
|
29
|
-
import templateRecast from "ember-template-recast";
|
|
30
13
|
import { Preprocessor } from "content-tag";
|
|
31
14
|
import { walk } from "zimmerframe";
|
|
32
15
|
|
|
33
|
-
import {
|
|
16
|
+
import { processTemplate, DocumentLines, glimmerVisitorKeys } from "./transforms.js";
|
|
34
17
|
|
|
35
18
|
const preprocessor = new Preprocessor();
|
|
36
19
|
|
|
20
|
+
// Node types that placeholders parse into (backtick/static-block format)
|
|
21
|
+
const PLACEHOLDER_TYPES = new Set([
|
|
22
|
+
"ExpressionStatement",
|
|
23
|
+
"StaticBlock",
|
|
24
|
+
"TemplateLiteral",
|
|
25
|
+
"ExportDefaultDeclaration",
|
|
26
|
+
]);
|
|
27
|
+
|
|
37
28
|
/**
|
|
29
|
+
* Parse Ember source and return an ESTree-compatible AST.
|
|
30
|
+
*
|
|
38
31
|
* @param {string} source
|
|
39
|
-
* @param {object} options
|
|
40
|
-
* @
|
|
32
|
+
* @param {object} [options]
|
|
33
|
+
* @param {string} [options.filePath] - File path for language detection
|
|
34
|
+
* @param {boolean} [options.templateOnly] - Parse as raw Glimmer template content (for .hbs)
|
|
35
|
+
* @param {function} [options.parser] - Custom JS/TS parser: (placeholderJS) => { ast, scopeManager?, visitorKeys?, services?, ... }
|
|
36
|
+
* @param {object} [options.visitors] - Callbacks invoked for Glimmer nodes during traversal
|
|
37
|
+
* @return {object}
|
|
41
38
|
*/
|
|
42
39
|
export function toTree(source, options = {}) {
|
|
40
|
+
const templateOpts = options.includeParentLinks === false ? { includeParentLinks: false } : {};
|
|
41
|
+
|
|
42
|
+
if (options.templateOnly) {
|
|
43
|
+
return processTemplate(source, new DocumentLines(source), [0, source.length], templateOpts);
|
|
44
|
+
}
|
|
45
|
+
|
|
43
46
|
let parseResults = preprocessor.parse(source);
|
|
44
47
|
let js = toPlaceholderJS(source, parseResults);
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
let
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
49
|
+
const useCustomParser = !!options.parser;
|
|
50
|
+
const visitors = options.visitors || null;
|
|
51
|
+
|
|
52
|
+
// Parse the placeholder JS — use custom parser or default oxc
|
|
53
|
+
let result;
|
|
54
|
+
if (useCustomParser) {
|
|
55
|
+
result = options.parser(js);
|
|
56
|
+
if (!result.ast) {
|
|
57
|
+
result = { ast: result };
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
let filename = options.filePath || "input.ts";
|
|
61
|
+
let oxcResult = parseSync(filename, js);
|
|
62
|
+
result = {
|
|
63
|
+
ast: {
|
|
64
|
+
type: "File",
|
|
65
|
+
program: oxcResult.program,
|
|
66
|
+
comments: oxcResult.comments || [],
|
|
67
|
+
start: oxcResult.program.start,
|
|
68
|
+
end: oxcResult.program.end,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// If no templates, return early
|
|
74
|
+
if (!parseResults.length) {
|
|
75
|
+
if (useCustomParser) {
|
|
76
|
+
result.visitorKeys = { ...result.visitorKeys, ...glimmerVisitorKeys };
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
result.ast.visitorKeys = glimmerVisitorKeys;
|
|
80
|
+
return result.ast;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const codeLines = new DocumentLines(source);
|
|
84
|
+
const allComments = [];
|
|
85
|
+
const templateInfos = [];
|
|
86
|
+
|
|
87
|
+
// Build a map of template ranges for lookup
|
|
88
|
+
const templateRangeByStart = new Map(parseResults.map((r) => [r.range.startUtf16Codepoint, r]));
|
|
89
|
+
|
|
90
|
+
// Process a matched placeholder node: create Glimmer AST and tokens
|
|
91
|
+
function processPlaceholder(parseResult) {
|
|
92
|
+
let templateContent = parseResult.contents;
|
|
93
|
+
let contentRange = [
|
|
94
|
+
parseResult.contentRange.startUtf16Codepoint,
|
|
95
|
+
parseResult.contentRange.endUtf16Codepoint,
|
|
96
|
+
];
|
|
97
|
+
let fullRange = [parseResult.range.startUtf16Codepoint, parseResult.range.endUtf16Codepoint];
|
|
98
|
+
|
|
99
|
+
const { ast, comments } = processTemplate(
|
|
100
|
+
templateContent,
|
|
101
|
+
codeLines,
|
|
102
|
+
contentRange,
|
|
103
|
+
templateOpts,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Fix the Template root to cover the full <template>...</template> range
|
|
107
|
+
ast.range = fullRange;
|
|
108
|
+
ast.start = fullRange[0];
|
|
109
|
+
ast.end = fullRange[1];
|
|
110
|
+
ast.loc = {
|
|
111
|
+
start: codeLines.offsetToPosition(fullRange[0]),
|
|
112
|
+
end: codeLines.offsetToPosition(fullRange[1]),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Add tokens for the <template> and </template> tags
|
|
116
|
+
const openEnd = contentRange[0];
|
|
117
|
+
const closeStart = contentRange[1];
|
|
118
|
+
const openTag = source.slice(fullRange[0], openEnd);
|
|
119
|
+
const closeTag = source.slice(closeStart, fullRange[1]);
|
|
120
|
+
const makeToken = (value, range) => ({
|
|
121
|
+
type: "Punctuator",
|
|
122
|
+
value,
|
|
123
|
+
range,
|
|
124
|
+
start: range[0],
|
|
125
|
+
end: range[1],
|
|
126
|
+
loc: {
|
|
127
|
+
start: codeLines.offsetToPosition(range[0]),
|
|
128
|
+
end: codeLines.offsetToPosition(range[1]),
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
ast.tokens = [
|
|
132
|
+
makeToken(openTag, [fullRange[0], openEnd]),
|
|
133
|
+
...(ast.tokens || []),
|
|
134
|
+
makeToken(closeTag, [closeStart, fullRange[1]]),
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
allComments.push(...comments);
|
|
138
|
+
templateInfos.push({ utf16Range: fullRange, ast });
|
|
139
|
+
return ast;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check if a node matches a template range
|
|
143
|
+
function matchPlaceholder(node) {
|
|
144
|
+
let range = node.range || [node.start, node.end];
|
|
145
|
+
if (node.type === "ExportDefaultDeclaration" && node.declaration) {
|
|
146
|
+
const decl = node.declaration;
|
|
147
|
+
range = decl.range || [decl.start, decl.end];
|
|
148
|
+
}
|
|
149
|
+
const parseResult = templateRangeByStart.get(range[0]);
|
|
150
|
+
if (
|
|
151
|
+
!parseResult ||
|
|
152
|
+
(parseResult.range.endUtf16Codepoint !== range[1] &&
|
|
153
|
+
parseResult.range.endUtf16Codepoint !== range[1] + 1)
|
|
154
|
+
) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return parseResult;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Walk Glimmer subtree, invoking visitors with full path context
|
|
161
|
+
function walkGlimmerTree(node, parentPath) {
|
|
162
|
+
if (!node || typeof node !== "object" || !node.type) return;
|
|
163
|
+
const path = { node, parent: parentPath?.node ?? null, parentPath };
|
|
164
|
+
|
|
165
|
+
if (visitors && node.type.startsWith("Glimmer")) {
|
|
166
|
+
const handler = visitors[node.type];
|
|
167
|
+
if (handler) handler(node, path);
|
|
168
|
+
if ("blockParams" in node && visitors.GlimmerBlockParams) {
|
|
169
|
+
visitors.GlimmerBlockParams(node, path);
|
|
84
170
|
}
|
|
85
|
-
|
|
86
|
-
},
|
|
87
|
-
});
|
|
171
|
+
}
|
|
88
172
|
|
|
89
|
-
|
|
173
|
+
const keys = glimmerVisitorKeys[node.type];
|
|
174
|
+
if (!keys) return;
|
|
175
|
+
for (const key of keys) {
|
|
176
|
+
const child = node[key];
|
|
177
|
+
if (!child) continue;
|
|
178
|
+
if (Array.isArray(child)) {
|
|
179
|
+
for (const item of child) {
|
|
180
|
+
walkGlimmerTree(item, path);
|
|
181
|
+
}
|
|
182
|
+
} else if (typeof child === "object" && child.type) {
|
|
183
|
+
walkGlimmerTree(child, path);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
90
187
|
|
|
91
|
-
|
|
92
|
-
|
|
188
|
+
if (useCustomParser) {
|
|
189
|
+
// Custom parser path: mutate the parser's AST in-place, invoke visitors.
|
|
190
|
+
// Use the parser's visitorKeys to traverse efficiently (avoids Object.keys).
|
|
191
|
+
const parserVisitorKeys = result.visitorKeys || {};
|
|
93
192
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
* with embedded Glimmer template nodes.
|
|
97
|
-
*
|
|
98
|
-
* @param {string} source - The source code to parse
|
|
99
|
-
* @param {object} [options] - Parse options
|
|
100
|
-
* @return {object} The ESTree-compatible AST
|
|
101
|
-
*/
|
|
102
|
-
export function parse(source, options = {}) {
|
|
103
|
-
let ast = toTree(source, options);
|
|
193
|
+
function visitNode(node, parentPath) {
|
|
194
|
+
if (!node || typeof node !== "object" || !node.type) return;
|
|
104
195
|
|
|
105
|
-
|
|
106
|
-
}
|
|
196
|
+
const path = { node, parent: parentPath?.node ?? null, parentPath };
|
|
107
197
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
198
|
+
if (PLACEHOLDER_TYPES.has(node.type)) {
|
|
199
|
+
const parseResult = matchPlaceholder(node);
|
|
200
|
+
if (parseResult) {
|
|
201
|
+
const ast = processPlaceholder(parseResult);
|
|
202
|
+
for (const key of Object.keys(node)) {
|
|
203
|
+
if (!(key in ast) && key !== "parent") {
|
|
204
|
+
delete node[key];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
Object.assign(node, ast);
|
|
208
|
+
if (visitors) walkGlimmerTree(node, parentPath);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
113
212
|
|
|
114
|
-
|
|
115
|
-
|
|
213
|
+
// Use visitorKeys for efficient child traversal
|
|
214
|
+
const keys = parserVisitorKeys[node.type];
|
|
215
|
+
if (!keys) return;
|
|
216
|
+
for (const key of keys) {
|
|
217
|
+
const child = node[key];
|
|
218
|
+
if (!child) continue;
|
|
219
|
+
if (Array.isArray(child)) {
|
|
220
|
+
for (const item of child) {
|
|
221
|
+
if (item && typeof item === "object" && item.type) {
|
|
222
|
+
visitNode(item, path);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} else if (typeof child === "object" && child.type) {
|
|
226
|
+
visitNode(child, path);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
116
230
|
|
|
117
|
-
|
|
118
|
-
}
|
|
231
|
+
visitNode(result.ast, null);
|
|
232
|
+
} else {
|
|
233
|
+
// Default oxc path: use zimmerframe walk (returns new tree)
|
|
234
|
+
result.ast = walk(result.ast, null, {
|
|
235
|
+
_(node, { next }) {
|
|
236
|
+
if (PLACEHOLDER_TYPES.has(node.type)) {
|
|
237
|
+
const parseResult = matchPlaceholder(node);
|
|
238
|
+
if (parseResult) {
|
|
239
|
+
return processPlaceholder(parseResult);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
next();
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Walk Glimmer subtrees for visitors (after zimmerframe splicing)
|
|
247
|
+
if (visitors) {
|
|
248
|
+
for (const ti of templateInfos) {
|
|
249
|
+
walkGlimmerTree(ti.ast, null);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
119
253
|
|
|
120
|
-
|
|
121
|
-
|
|
254
|
+
// Splice template tokens into the AST token stream.
|
|
255
|
+
// Tokens are sorted by range, so use binary search for O(log n) lookup.
|
|
256
|
+
const astRoot = result.ast.program || result.ast;
|
|
257
|
+
if (astRoot.tokens) {
|
|
258
|
+
for (const ti of templateInfos) {
|
|
259
|
+
const [tStart, tEnd] = ti.utf16Range;
|
|
260
|
+
const tokens = astRoot.tokens;
|
|
261
|
+
// Binary search for first token with range[0] >= tStart
|
|
262
|
+
let lo = 0;
|
|
263
|
+
let hi = tokens.length;
|
|
264
|
+
while (lo < hi) {
|
|
265
|
+
const mid = (lo + hi) >>> 1;
|
|
266
|
+
if (tokens[mid].range[0] < tStart) lo = mid + 1;
|
|
267
|
+
else hi = mid;
|
|
268
|
+
}
|
|
269
|
+
const firstIdx = lo;
|
|
270
|
+
if (firstIdx >= tokens.length || tokens[firstIdx].range[0] >= tEnd) continue;
|
|
271
|
+
let lastIdx = firstIdx;
|
|
272
|
+
while (lastIdx < tokens.length && tokens[lastIdx].range[1] <= tEnd) {
|
|
273
|
+
lastIdx++;
|
|
274
|
+
}
|
|
275
|
+
tokens.splice(firstIdx, lastIdx - firstIdx, ...ti.ast.tokens);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
122
278
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
279
|
+
// Merge comments
|
|
280
|
+
if (allComments.length) {
|
|
281
|
+
if (!astRoot.comments) astRoot.comments = [];
|
|
282
|
+
astRoot.comments.push(...allComments);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (useCustomParser) {
|
|
286
|
+
result.visitorKeys = { ...result.visitorKeys, ...glimmerVisitorKeys };
|
|
287
|
+
result.templateInfos = templateInfos;
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Default path: return bare AST with visitorKeys attached
|
|
292
|
+
result.ast.visitorKeys = glimmerVisitorKeys;
|
|
293
|
+
return result.ast;
|
|
126
294
|
}
|
|
127
295
|
|
|
296
|
+
export const parse = toTree;
|
|
297
|
+
|
|
298
|
+
// ── Placeholder JS ────────────────────────────────────────────────────
|
|
299
|
+
|
|
128
300
|
/**
|
|
129
|
-
* Replaces <template>...</template> regions
|
|
130
|
-
*
|
|
131
|
-
* are valid JavaScript, so oxc-parser can parse them.
|
|
132
|
-
*
|
|
133
|
-
* Expression templates become: TEMPLATE_TEMPLATE(`...`)
|
|
134
|
-
* Class member templates become: [_TEMPLATE_(`...`)] = 0;
|
|
301
|
+
* Replaces <template>...</template> regions with placeholder expressions
|
|
302
|
+
* of the same character length that are valid JS/TS.
|
|
135
303
|
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* <template> (10) + </template> (11) = 21 character overhead.
|
|
304
|
+
* Expression templates become: `content ` (backtick, space-padded)
|
|
305
|
+
* Class member templates become: static{`content `} (static block, space-padded)
|
|
139
306
|
*
|
|
140
|
-
*
|
|
141
|
-
* @
|
|
142
|
-
* @returns {string}
|
|
307
|
+
* This format is compatible with all JS/TS parsers including
|
|
308
|
+
* oxc-parser, @typescript-eslint/parser, and @babel/eslint-parser.
|
|
143
309
|
*/
|
|
144
310
|
function toPlaceholderJS(source, parseResults) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
for (let pr of parseResults) {
|
|
149
|
-
let start = pr.range.startUtf16Codepoint;
|
|
150
|
-
let end = pr.range.endUtf16Codepoint;
|
|
151
|
-
|
|
152
|
-
let openingTag, closingTag;
|
|
153
|
-
switch (pr.type) {
|
|
154
|
-
case "expression":
|
|
155
|
-
openingTag = "TEMPLATE_TEMPLATE(`";
|
|
156
|
-
closingTag = "`)";
|
|
157
|
-
break;
|
|
158
|
-
case "class-member":
|
|
159
|
-
openingTag = "[_TEMPLATE_(`";
|
|
160
|
-
closingTag = "`)] = 0;";
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
311
|
+
// Build result in forward order using parts array (avoids intermediate string allocations)
|
|
312
|
+
const parts = [];
|
|
313
|
+
let cursor = 0;
|
|
163
314
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
315
|
+
for (const pr of parseResults) {
|
|
316
|
+
const start = pr.range.startUtf16Codepoint;
|
|
317
|
+
const end = pr.range.endUtf16Codepoint;
|
|
318
|
+
const tplLength = end - start;
|
|
319
|
+
|
|
320
|
+
parts.push(source.slice(cursor, start));
|
|
168
321
|
|
|
169
|
-
|
|
322
|
+
const content = source
|
|
323
|
+
.slice(pr.contentRange.startUtf16Codepoint, pr.contentRange.endUtf16Codepoint)
|
|
324
|
+
.replace(/`/g, "\\`")
|
|
325
|
+
.replace(/\$/g, "\\$");
|
|
326
|
+
|
|
327
|
+
if (pr.type === "class-member") {
|
|
328
|
+
const spaces = tplLength - content.length - 10; // "static{`" + "`}" = 10
|
|
329
|
+
parts.push(`static{\`${content}${" ".repeat(Math.max(0, spaces))}\`}`);
|
|
330
|
+
} else {
|
|
331
|
+
const spaces = tplLength - content.length - 2; // "`" + "`" = 2
|
|
332
|
+
parts.push(`\`${content}${" ".repeat(Math.max(0, spaces))}\``);
|
|
333
|
+
}
|
|
170
334
|
|
|
171
|
-
|
|
172
|
-
offset += replacement.length - (end - start);
|
|
335
|
+
cursor = end;
|
|
173
336
|
}
|
|
174
337
|
|
|
175
|
-
|
|
338
|
+
parts.push(source.slice(cursor));
|
|
339
|
+
return parts.join("");
|
|
176
340
|
}
|
package/src/transforms.js
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Glimmer AST → ESTree transform utilities.
|
|
3
|
-
*
|
|
4
|
-
* Ported from ember-eslint-parser's transforms.js, adapted for
|
|
5
|
-
* ember-estree's ESM architecture. Handles:
|
|
6
|
-
*
|
|
7
|
-
* - Type prefixing (all Glimmer types get a "Glimmer" prefix)
|
|
8
|
-
* - Range / loc fixing (converts template-local positions to file-level)
|
|
9
|
-
* - ElementNode `parts` and `name` fields
|
|
10
|
-
* - blockParams → virtual node creation
|
|
11
|
-
* - Empty hash nullification
|
|
12
|
-
* - Empty text node removal
|
|
13
3
|
*/
|
|
14
4
|
|
|
15
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
visitorKeys as rawGlimmerVisitorKeys,
|
|
7
|
+
preprocess as glimmerPreprocess,
|
|
8
|
+
} from "@glimmer/syntax";
|
|
16
9
|
|
|
17
10
|
/**
|
|
18
11
|
* Converts between character offsets and line/column positions.
|
|
@@ -45,38 +38,65 @@ export class DocumentLines {
|
|
|
45
38
|
}
|
|
46
39
|
|
|
47
40
|
/**
|
|
48
|
-
*
|
|
41
|
+
* Glimmer visitor keys map with "Glimmer" prefix.
|
|
42
|
+
* Computed once at module load.
|
|
49
43
|
*/
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
44
|
+
export const glimmerVisitorKeys = (() => {
|
|
45
|
+
const keys = {};
|
|
46
|
+
for (const [k, v] of Object.entries(rawGlimmerVisitorKeys)) {
|
|
47
|
+
keys[`Glimmer${k}`] = v;
|
|
48
|
+
}
|
|
49
|
+
keys.GlimmerElementNode = [...keys.GlimmerElementNode, "blockParamNodes", "parts"];
|
|
50
|
+
keys.GlimmerProgram = ["body", "blockParamNodes"];
|
|
51
|
+
keys.GlimmerTemplate = ["body"];
|
|
52
|
+
return keys;
|
|
53
|
+
})();
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
55
|
+
// ── Internal helpers ──────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Recursively collect all nodes in a Glimmer AST using visitor keys.
|
|
59
|
+
* Sets parent references during traversal.
|
|
60
|
+
*/
|
|
61
|
+
function collectNodes(node, parent, allNodes, comments, textNodes, emptyTextNodes) {
|
|
62
|
+
node.parent = parent;
|
|
63
|
+
allNodes.push(node);
|
|
64
|
+
if (node.type === "CommentStatement" || node.type === "MustacheCommentStatement") {
|
|
65
|
+
comments.push(node);
|
|
66
|
+
}
|
|
67
|
+
if (node.type === "TextNode") {
|
|
68
|
+
node.value = node.chars;
|
|
69
|
+
if (node.value.trim().length !== 0 || (parent && parent.type === "AttrNode")) {
|
|
70
|
+
textNodes.push(node);
|
|
71
|
+
} else {
|
|
72
|
+
emptyTextNodes.push(node);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const keys = rawGlimmerVisitorKeys[node.type];
|
|
76
|
+
if (!keys) return;
|
|
77
|
+
for (const key of keys) {
|
|
78
|
+
const child = node[key];
|
|
79
|
+
if (!child) continue;
|
|
80
|
+
if (Array.isArray(child)) {
|
|
81
|
+
for (const item of child) {
|
|
82
|
+
if (item && typeof item === "object" && item.type) {
|
|
83
|
+
collectNodes(item, node, allNodes, comments, textNodes, emptyTextNodes);
|
|
69
84
|
}
|
|
70
85
|
}
|
|
71
|
-
}
|
|
72
|
-
|
|
86
|
+
} else if (typeof child === "object" && child.type) {
|
|
87
|
+
collectNodes(child, node, allNodes, comments, textNodes, emptyTextNodes);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
73
91
|
|
|
74
|
-
|
|
92
|
+
// Reusable descriptor — shadows prototype getters with own properties.
|
|
93
|
+
// @glimmer/syntax nodes have getter chains that cause esrecurse infinite recursion.
|
|
94
|
+
const _desc = { value: undefined, configurable: true, enumerable: true, writable: true };
|
|
95
|
+
function defOwn(obj, key, val) {
|
|
96
|
+
_desc.value = val;
|
|
97
|
+
Object.defineProperty(obj, key, _desc);
|
|
75
98
|
}
|
|
76
99
|
|
|
77
|
-
/**
|
|
78
|
-
* Remove nodes from their parent's children/body/parts arrays.
|
|
79
|
-
*/
|
|
80
100
|
function removeFromParent(nodes) {
|
|
81
101
|
for (const node of nodes) {
|
|
82
102
|
const children =
|
|
@@ -88,117 +108,182 @@ function removeFromParent(nodes) {
|
|
|
88
108
|
}
|
|
89
109
|
}
|
|
90
110
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
111
|
+
function isAlphaNumeric(code) {
|
|
112
|
+
return !(!(code > 47 && code < 58) && !(code > 64 && code < 91) && !(code > 96 && code < 123));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isWhiteSpaceCode(code) {
|
|
116
|
+
return code === 32 || code === 9 || code === 13 || code === 10 || code === 11;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function tokenize(template, doc, startOffset) {
|
|
120
|
+
const tokens = [];
|
|
121
|
+
let wordStart = -1;
|
|
122
|
+
function pushToken(value, type, range) {
|
|
123
|
+
tokens.push({
|
|
124
|
+
type,
|
|
125
|
+
value,
|
|
126
|
+
range,
|
|
127
|
+
start: range[0],
|
|
128
|
+
end: range[1],
|
|
129
|
+
loc: {
|
|
130
|
+
start: { ...doc.offsetToPosition(range[0]), index: range[0] },
|
|
131
|
+
end: { ...doc.offsetToPosition(range[1]), index: range[1] },
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
for (let i = 0; i < template.length; i++) {
|
|
136
|
+
const code = template.charCodeAt(i);
|
|
137
|
+
if (isAlphaNumeric(code)) {
|
|
138
|
+
if (wordStart < 0) wordStart = i;
|
|
139
|
+
} else {
|
|
140
|
+
if (wordStart >= 0) {
|
|
141
|
+
pushToken(template.slice(wordStart, i), "word", [startOffset + wordStart, startOffset + i]);
|
|
142
|
+
wordStart = -1;
|
|
143
|
+
}
|
|
144
|
+
if (!isWhiteSpaceCode(code)) {
|
|
145
|
+
pushToken(template[i], "Punctuator", [startOffset + i, startOffset + i + 1]);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
101
148
|
}
|
|
102
|
-
if (
|
|
103
|
-
|
|
149
|
+
if (wordStart >= 0) {
|
|
150
|
+
pushToken(template.slice(wordStart), "word", [
|
|
151
|
+
startOffset + wordStart,
|
|
152
|
+
startOffset + template.length,
|
|
153
|
+
]);
|
|
104
154
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
155
|
+
return tokens;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function buildTokenStream(rawTokens, comments, textNodes) {
|
|
159
|
+
const commentIntervals = comments.map((c) => c.range).sort((a, b) => a[0] - b[0]);
|
|
160
|
+
const textNodeIntervals = textNodes.map((t) => t.range).sort((a, b) => a[0] - b[0]);
|
|
161
|
+
|
|
162
|
+
function isCovered(tokenRange, intervals) {
|
|
163
|
+
let lo = 0;
|
|
164
|
+
let hi = intervals.length - 1;
|
|
165
|
+
while (lo <= hi) {
|
|
166
|
+
const mid = (lo + hi) >> 1;
|
|
167
|
+
const iv = intervals[mid];
|
|
168
|
+
if (iv[0] <= tokenRange[0] && iv[1] >= tokenRange[1]) return true;
|
|
169
|
+
if (iv[0] > tokenRange[0]) hi = mid - 1;
|
|
170
|
+
else lo = mid + 1;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const filteredTokens = rawTokens.filter(
|
|
176
|
+
(t) => !isCovered(t.range, commentIntervals) && !isCovered(t.range, textNodeIntervals),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const sortedTextNodes = [...textNodes].sort((a, b) => a.range[0] - b.range[0]);
|
|
180
|
+
const result = [];
|
|
181
|
+
let ti = 0;
|
|
182
|
+
for (const token of filteredTokens) {
|
|
183
|
+
while (ti < sortedTextNodes.length && sortedTextNodes[ti].range[0] < token.range[0]) {
|
|
184
|
+
result.push(sortedTextNodes[ti++]);
|
|
185
|
+
}
|
|
186
|
+
result.push(token);
|
|
187
|
+
}
|
|
188
|
+
while (ti < sortedTextNodes.length) {
|
|
189
|
+
result.push(sortedTextNodes[ti++]);
|
|
190
|
+
}
|
|
191
|
+
return result;
|
|
109
192
|
}
|
|
110
193
|
|
|
111
194
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* @param {object} templateAST - The Glimmer AST (from ember-template-recast / @glimmer/syntax)
|
|
115
|
-
* @param {object} opts
|
|
116
|
-
* @param {number} opts.contentOffset - Byte offset where the template content begins in the full source
|
|
117
|
-
* @param {[number, number]} opts.templateRange - [start, end] byte range of the full <template>...</template> block
|
|
118
|
-
* @param {string} opts.source - The full source code
|
|
119
|
-
* @returns {object} The transformed AST
|
|
195
|
+
* Parse and transform a Glimmer template into an ESTree-compatible AST.
|
|
196
|
+
* Internal — consumed by toTree.
|
|
120
197
|
*/
|
|
121
|
-
export function
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
const toFileRange = (loc) => {
|
|
130
|
-
const locObj = loc.toJSON ? loc.toJSON() : loc;
|
|
131
|
-
return [
|
|
132
|
-
contentOffset + contentDoc.positionToOffset(locObj.start),
|
|
133
|
-
contentOffset + contentDoc.positionToOffset(locObj.end),
|
|
134
|
-
];
|
|
135
|
-
};
|
|
198
|
+
export function processTemplate(
|
|
199
|
+
templateContent,
|
|
200
|
+
codeLines,
|
|
201
|
+
templateRange,
|
|
202
|
+
{ includeParentLinks = true } = {},
|
|
203
|
+
) {
|
|
204
|
+
const offset = templateRange[0];
|
|
205
|
+
const docLines = new DocumentLines(templateContent);
|
|
136
206
|
|
|
207
|
+
const toFileRange = (loc) => [
|
|
208
|
+
offset + docLines.positionToOffset(loc.start),
|
|
209
|
+
offset + docLines.positionToOffset(loc.end),
|
|
210
|
+
];
|
|
137
211
|
const toFileLoc = (range) => ({
|
|
138
|
-
start:
|
|
139
|
-
end:
|
|
212
|
+
start: codeLines.offsetToPosition(range[0]),
|
|
213
|
+
end: codeLines.offsetToPosition(range[1]),
|
|
140
214
|
});
|
|
141
215
|
|
|
142
|
-
const
|
|
216
|
+
const ast = glimmerPreprocess(templateContent, { mode: "codemod" });
|
|
217
|
+
const allNodes = [];
|
|
218
|
+
const comments = [];
|
|
219
|
+
const textNodes = [];
|
|
220
|
+
const emptyTextNodes = [];
|
|
221
|
+
collectNodes(ast, null, allNodes, comments, textNodes, emptyTextNodes);
|
|
143
222
|
|
|
144
223
|
for (const n of allNodes) {
|
|
145
|
-
const loc = n.loc.toJSON ? n.loc.toJSON() : n.loc;
|
|
146
|
-
|
|
147
|
-
// Fix PathExpression head
|
|
148
224
|
if (n.type === "PathExpression") {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
head.range = toFileRange(headLoc);
|
|
154
|
-
head.start = head.range[0];
|
|
155
|
-
head.end = head.range[1];
|
|
156
|
-
head.loc = toFileLoc(head.range);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
225
|
+
n.head.range = toFileRange(n.head.loc);
|
|
226
|
+
n.head.start = n.head.range[0];
|
|
227
|
+
n.head.end = n.head.range[1];
|
|
228
|
+
n.head.loc = toFileLoc(n.head.range);
|
|
159
229
|
}
|
|
160
230
|
|
|
161
|
-
|
|
162
|
-
n.range = n.type === "Template" ? [...templateRange] : toFileRange(loc);
|
|
231
|
+
n.range = n.type === "Template" ? [...templateRange] : toFileRange(n.loc);
|
|
163
232
|
n.start = n.range[0];
|
|
164
233
|
n.end = n.range[1];
|
|
165
234
|
n.loc = toFileLoc(n.range);
|
|
166
235
|
|
|
167
|
-
// Add parts and name to ElementNode
|
|
168
236
|
if (n.type === "ElementNode") {
|
|
237
|
+
defOwn(n, "tag", n.tag);
|
|
169
238
|
n.name = n.tag;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
239
|
+
const p = n.path.head;
|
|
240
|
+
defOwn(p, "name", p.name);
|
|
241
|
+
defOwn(p, "original", p.original);
|
|
242
|
+
const partRange = toFileRange(p.loc);
|
|
174
243
|
n.parts = [
|
|
175
244
|
{
|
|
176
|
-
original: n.tag,
|
|
177
|
-
name: n.tag,
|
|
178
245
|
type: "GlimmerElementNodePart",
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
246
|
+
original: p.original,
|
|
247
|
+
name: p.original,
|
|
248
|
+
parent: n,
|
|
249
|
+
range: partRange,
|
|
250
|
+
start: partRange[0],
|
|
251
|
+
end: partRange[1],
|
|
252
|
+
loc: toFileLoc(partRange),
|
|
183
253
|
},
|
|
184
254
|
];
|
|
185
255
|
}
|
|
186
256
|
|
|
187
|
-
// Handle blockParams — create virtual nodes from the blockParams string array
|
|
188
257
|
if ("blockParams" in n && Array.isArray(n.blockParams)) {
|
|
189
|
-
n.
|
|
190
|
-
|
|
258
|
+
if (n.params && n.params.length === n.blockParams.length) {
|
|
259
|
+
n.blockParamNodes = n.params.map((p) => {
|
|
260
|
+
defOwn(p, "name", p.name);
|
|
261
|
+
defOwn(p, "original", p.original);
|
|
262
|
+
const range = toFileRange(p.loc);
|
|
263
|
+
return {
|
|
264
|
+
type: "GlimmerBlockParam",
|
|
265
|
+
name: p.original || p.name,
|
|
266
|
+
original: p.original,
|
|
267
|
+
parent: n,
|
|
268
|
+
range,
|
|
269
|
+
start: range[0],
|
|
270
|
+
end: range[1],
|
|
271
|
+
loc: toFileLoc(range),
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
} else {
|
|
275
|
+
n.blockParamNodes = n.blockParams.map((name) => ({
|
|
191
276
|
type: "GlimmerBlockParam",
|
|
192
277
|
name,
|
|
193
|
-
|
|
278
|
+
parent: n,
|
|
279
|
+
range: [n.range[0], n.range[1]],
|
|
194
280
|
start: n.range[0],
|
|
195
281
|
end: n.range[1],
|
|
196
282
|
loc: toFileLoc(n.range),
|
|
197
|
-
};
|
|
198
|
-
}
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
199
285
|
}
|
|
200
286
|
|
|
201
|
-
// Nullify empty hashes
|
|
202
287
|
if (
|
|
203
288
|
(n.type === "MustacheStatement" ||
|
|
204
289
|
n.type === "BlockStatement" ||
|
|
@@ -210,21 +295,31 @@ export function processGlimmerTemplate(templateAST, { contentOffset, templateRan
|
|
|
210
295
|
n.hash = null;
|
|
211
296
|
}
|
|
212
297
|
|
|
213
|
-
// Prefix type with "Glimmer"
|
|
214
298
|
n.type = `Glimmer${n.type}`;
|
|
299
|
+
|
|
300
|
+
// Snapshot PathExpression.head getters (name/original)
|
|
301
|
+
if (n.type === "GlimmerPathExpression" && n.head) {
|
|
302
|
+
defOwn(n.head, "name", n.head.name);
|
|
303
|
+
defOwn(n.head, "original", n.head.original);
|
|
304
|
+
}
|
|
215
305
|
}
|
|
216
306
|
|
|
217
|
-
// Clean up AST structure
|
|
218
307
|
removeFromParent(emptyTextNodes);
|
|
219
308
|
removeFromParent(comments);
|
|
220
309
|
for (const comment of comments) {
|
|
221
310
|
comment.type = "Block";
|
|
222
311
|
}
|
|
223
312
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
313
|
+
ast.tokens = buildTokenStream(tokenize(templateContent, codeLines, offset), comments, textNodes);
|
|
314
|
+
ast.contents = templateContent;
|
|
315
|
+
|
|
316
|
+
if (!includeParentLinks) {
|
|
317
|
+
for (const n of allNodes) {
|
|
318
|
+
delete n.parent;
|
|
319
|
+
if (n.parts) for (const p of n.parts) delete p.parent;
|
|
320
|
+
if (n.blockParamNodes) for (const p of n.blockParamNodes) delete p.parent;
|
|
321
|
+
}
|
|
227
322
|
}
|
|
228
323
|
|
|
229
|
-
return
|
|
324
|
+
return { ast, comments };
|
|
230
325
|
}
|