ember-estree 0.5.1 → 0.6.1
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/README.md +29 -8
- package/package.json +2 -1
- package/src/parse.js +83 -42
- package/src/tokens.js +177 -0
- package/src/transforms.js +14 -86
package/README.md
CHANGED
|
@@ -63,14 +63,35 @@ Both `toTree` and `parse` accept an options object as their second argument.
|
|
|
63
63
|
|
|
64
64
|
All options are optional.
|
|
65
65
|
|
|
66
|
-
| Option | Type | Description
|
|
67
|
-
| -------------- | ------------------------------------------------- |
|
|
68
|
-
| `filePath` | `string` | Used for language detection.
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
|
|
73
|
-
|
|
66
|
+
| Option | Type | Description |
|
|
67
|
+
| -------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
68
|
+
| `filePath` | `string` | Used for language detection. |
|
|
69
|
+
| `tokens` | `boolean` | Generate a flat `ast.tokens` array. Required by ESLint; skipped by default so codemods and type-checkers pay nothing. |
|
|
70
|
+
| `templateOnly` | `boolean` | Parse the source as a raw Glimmer template. Use for `.hbs` files. |
|
|
71
|
+
| `parser` | `(placeholderJS: string) => { ast, ... }` | Use a custom JS/TS parser instead of the default oxc-parser. See [Custom parser](#custom-parser). |
|
|
72
|
+
| `visitors` | `VisitorMap` <br /> or `(outerAst) => VisitorMap` | Callbacks fired on every node during traversal — JS/TS and Glimmer — in a single pass. See [Visitors](#visitors). |
|
|
73
|
+
|
|
74
|
+
Handler signature is `(node, path) => void`, where `path = { node, parent, parentPath }` — a linked list that walks all the way back through the JS/TS root, so visitors can locate the enclosing scope or class from within a Glimmer subtree.
|
|
75
|
+
|
|
76
|
+
### Token stream
|
|
77
|
+
|
|
78
|
+
Pass `tokens: true` to populate `ast.tokens` with a flat, position-sorted array of lexemes spanning the full file — including Glimmer tokens spliced in place of each `<template>` region. This is what ESLint's `SourceCode` needs; omit it for codemods or type-checkers that don't use the token stream.
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
import { toTree } from "ember-estree";
|
|
82
|
+
|
|
83
|
+
const result = toTree(source, {
|
|
84
|
+
tokens: true,
|
|
85
|
+
parser: myTsParser,
|
|
86
|
+
});
|
|
87
|
+
// result.ast.program.tokens now contains JS + Glimmer tokens in source order
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For `.hbs` files via `templateOnly`, pass both flags:
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
toTree(hbsSource, { templateOnly: true, tokens: true });
|
|
94
|
+
```
|
|
74
95
|
|
|
75
96
|
### Custom parser
|
|
76
97
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ember-estree",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "ESTree generator for gjs and gts file used by ember",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"AST",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@tsconfig/node-lts": "^22.0.2",
|
|
41
|
+
"@typescript-eslint/parser": "^8.59.0",
|
|
41
42
|
"mitata": "^1.0.34",
|
|
42
43
|
"oxfmt": "^0.40.0",
|
|
43
44
|
"oxlint": "^1.55.0",
|
package/src/parse.js
CHANGED
|
@@ -13,7 +13,29 @@ import { parseSync } from "oxc-parser";
|
|
|
13
13
|
import { Preprocessor } from "content-tag";
|
|
14
14
|
import { walk } from "zimmerframe";
|
|
15
15
|
|
|
16
|
-
import { processTemplate, DocumentLines, glimmerVisitorKeys } from "./transforms.js";
|
|
16
|
+
import { processTemplate, DocumentLines, glimmerVisitorKeys, setParent } from "./transforms.js";
|
|
17
|
+
|
|
18
|
+
// Swap `oldNode` for `newNode` in whichever slot of `parent` currently holds it.
|
|
19
|
+
// Used to splice a GlimmerTemplate directly into the outer AST without
|
|
20
|
+
// allocating new ancestor objects — keeps WeakMap-keyed data (scope manager,
|
|
21
|
+
// esTreeNodeToTSNodeMap) attached to the existing nodes.
|
|
22
|
+
function replaceInParent(parent, oldNode, newNode) {
|
|
23
|
+
for (const key of Object.keys(parent)) {
|
|
24
|
+
const v = parent[key];
|
|
25
|
+
if (v === oldNode) {
|
|
26
|
+
parent[key] = newNode;
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(v)) {
|
|
30
|
+
const idx = v.indexOf(oldNode);
|
|
31
|
+
if (idx !== -1) {
|
|
32
|
+
v[idx] = newNode;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
17
39
|
|
|
18
40
|
const preprocessor = new Preprocessor();
|
|
19
41
|
|
|
@@ -31,6 +53,7 @@ const PLACEHOLDER_TYPES = new Set([
|
|
|
31
53
|
* @param {string} source
|
|
32
54
|
* @param {object} [options]
|
|
33
55
|
* @param {string} [options.filePath] - File path for language detection
|
|
56
|
+
* @param {boolean} [options.tokens] - Generate a flat token stream on the AST (needed by ESLint; skipped by default)
|
|
34
57
|
* @param {boolean} [options.templateOnly] - Parse as raw Glimmer template content (for .hbs)
|
|
35
58
|
* @param {function} [options.parser] - Custom JS/TS parser: (placeholderJS) => { ast, scopeManager?, visitorKeys?, services?, ... }
|
|
36
59
|
* @param {object|function} [options.visitors] - Either a map of `{ [Type]: (node, path) => void }`
|
|
@@ -41,8 +64,13 @@ const PLACEHOLDER_TYPES = new Set([
|
|
|
41
64
|
* @return {object}
|
|
42
65
|
*/
|
|
43
66
|
export function toTree(source, options = {}) {
|
|
67
|
+
const generateTokens = !!options.tokens;
|
|
68
|
+
|
|
44
69
|
if (options.templateOnly) {
|
|
45
|
-
return processTemplate(source, new DocumentLines(source),
|
|
70
|
+
return processTemplate(source, new DocumentLines(source), {
|
|
71
|
+
templateRange: [0, source.length],
|
|
72
|
+
tokens: generateTokens,
|
|
73
|
+
});
|
|
46
74
|
}
|
|
47
75
|
|
|
48
76
|
let parseResults = preprocessor.parse(source);
|
|
@@ -106,8 +134,11 @@ export function toTree(source, options = {}) {
|
|
|
106
134
|
? new Map(parseResults.map((r) => [r.range.startUtf16Codepoint, r]))
|
|
107
135
|
: null;
|
|
108
136
|
|
|
109
|
-
// Process a matched placeholder node: create Glimmer AST and tokens
|
|
110
|
-
|
|
137
|
+
// Process a matched placeholder node: create Glimmer AST and tokens.
|
|
138
|
+
// `placeholderNode` is the original JS/TS node being swapped out; we stash
|
|
139
|
+
// it on templateInfos so consumers can forward its parser-services mapping
|
|
140
|
+
// (e.g. esTreeNodeToTSNodeMap) onto the GlimmerTemplate that replaces it.
|
|
141
|
+
function processPlaceholder(parseResult, placeholderNode) {
|
|
111
142
|
let templateContent = parseResult.contents;
|
|
112
143
|
let contentRange = [
|
|
113
144
|
parseResult.contentRange.startUtf16Codepoint,
|
|
@@ -115,7 +146,10 @@ export function toTree(source, options = {}) {
|
|
|
115
146
|
];
|
|
116
147
|
let fullRange = [parseResult.range.startUtf16Codepoint, parseResult.range.endUtf16Codepoint];
|
|
117
148
|
|
|
118
|
-
const { ast } = processTemplate(templateContent, codeLines,
|
|
149
|
+
const { ast } = processTemplate(templateContent, codeLines, {
|
|
150
|
+
templateRange: contentRange,
|
|
151
|
+
tokens: generateTokens,
|
|
152
|
+
});
|
|
119
153
|
|
|
120
154
|
// Fix the Template root to cover the full <template>...</template> range
|
|
121
155
|
ast.range = fullRange;
|
|
@@ -126,29 +160,31 @@ export function toTree(source, options = {}) {
|
|
|
126
160
|
end: codeLines.offsetToPosition(fullRange[1]),
|
|
127
161
|
};
|
|
128
162
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
163
|
+
if (generateTokens) {
|
|
164
|
+
// Add tokens for the <template> and </template> tags
|
|
165
|
+
const openEnd = contentRange[0];
|
|
166
|
+
const closeStart = contentRange[1];
|
|
167
|
+
const openTag = source.slice(fullRange[0], openEnd);
|
|
168
|
+
const closeTag = source.slice(closeStart, fullRange[1]);
|
|
169
|
+
const makeToken = (value, range) => ({
|
|
170
|
+
type: "Punctuator",
|
|
171
|
+
value,
|
|
172
|
+
range,
|
|
173
|
+
start: range[0],
|
|
174
|
+
end: range[1],
|
|
175
|
+
loc: {
|
|
176
|
+
start: codeLines.offsetToPosition(range[0]),
|
|
177
|
+
end: codeLines.offsetToPosition(range[1]),
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
ast.tokens = [
|
|
181
|
+
makeToken(openTag, [fullRange[0], openEnd]),
|
|
182
|
+
...(ast.tokens || []),
|
|
183
|
+
makeToken(closeTag, [closeStart, fullRange[1]]),
|
|
184
|
+
];
|
|
185
|
+
}
|
|
150
186
|
|
|
151
|
-
templateInfos.push({ utf16Range: fullRange, ast });
|
|
187
|
+
templateInfos.push({ utf16Range: fullRange, ast, placeholder: placeholderNode });
|
|
152
188
|
return ast;
|
|
153
189
|
}
|
|
154
190
|
|
|
@@ -175,21 +211,23 @@ export function toTree(source, options = {}) {
|
|
|
175
211
|
if (hasTemplates && PLACEHOLDER_TYPES.has(node.type)) {
|
|
176
212
|
const parseResult = matchPlaceholder(node);
|
|
177
213
|
if (parseResult) {
|
|
178
|
-
const ast = processPlaceholder(parseResult);
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
214
|
+
const ast = processPlaceholder(parseResult, node);
|
|
215
|
+
// Splice in place: write the GlimmerTemplate directly into the
|
|
216
|
+
// parent's slot instead of returning it from the visitor. Returning
|
|
217
|
+
// would trigger zimmerframe's apply_mutations, which shallow-clones
|
|
218
|
+
// every ancestor up to the root — orphaning any WeakMap-keyed data
|
|
219
|
+
// held by custom parsers (scope manager, esTreeNodeToTSNodeMap).
|
|
220
|
+
// In-place mutation preserves node identity for all ancestors.
|
|
221
|
+
const parent = state?.parentPath?.node ?? null;
|
|
222
|
+
if (parent) replaceInParent(parent, node, ast);
|
|
223
|
+
setParent(ast, parent);
|
|
224
|
+
// Dispatch visitors on the Glimmer subtree. We pass `state` so the
|
|
225
|
+
// Glimmer root's parentPath reflects its true JS parent — the
|
|
188
226
|
// placeholder (TemplateLiteral / StaticBlock) is an internal
|
|
189
|
-
// artifact
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
return
|
|
227
|
+
// artifact. Not returning anything keeps apply_mutations from
|
|
228
|
+
// firing up the ancestor chain.
|
|
229
|
+
if (hasVisitors) visit(ast, state);
|
|
230
|
+
return;
|
|
193
231
|
}
|
|
194
232
|
}
|
|
195
233
|
|
|
@@ -229,8 +267,11 @@ export function toTree(source, options = {}) {
|
|
|
229
267
|
// original source byte-for-byte across JS and Glimmer regions.
|
|
230
268
|
//
|
|
231
269
|
// Tokens are sorted by range, so use binary search for O(log n) lookup.
|
|
270
|
+
// Only splice if the caller asked for tokens — otherwise `ti.ast.tokens`
|
|
271
|
+
// wasn't populated by processPlaceholder, and a custom parser may still
|
|
272
|
+
// have returned its own token stream we shouldn't touch.
|
|
232
273
|
const astRoot = result.ast.program || result.ast;
|
|
233
|
-
if (astRoot.tokens) {
|
|
274
|
+
if (generateTokens && astRoot.tokens) {
|
|
234
275
|
for (const ti of templateInfos) {
|
|
235
276
|
const [tStart, tEnd] = ti.utf16Range;
|
|
236
277
|
const tokens = astRoot.tokens;
|
package/src/tokens.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token stream generation for Glimmer templates.
|
|
3
|
+
*
|
|
4
|
+
* Only needed by ESLint consumers — codemods, type-checkers, and formatters
|
|
5
|
+
* do not use the flat token stream and skip this entirely by omitting
|
|
6
|
+
* `{ tokens: true }` from processTemplate options.
|
|
7
|
+
*
|
|
8
|
+
* ESLint-specific note: comment tokens are placed at range[0]+1 rather than
|
|
9
|
+
* range[0] because ESLint's createIndexMap infinite-loops when a token and an
|
|
10
|
+
* ast.comments entry share range[0] — both inner loops fail the strict-less-than
|
|
11
|
+
* guard and the outer loop never advances. Tracked upstream as eslint/eslint#20492.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function isAlphaNumeric(code) {
|
|
15
|
+
return (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Lex a Glimmer template into a flat token array.
|
|
20
|
+
* Tracks line/column incrementally from a single seed call to
|
|
21
|
+
* doc.offsetToPosition — O(n + log L) instead of O(n log L).
|
|
22
|
+
*/
|
|
23
|
+
export function tokenize(template, doc, startOffset) {
|
|
24
|
+
const tokens = [];
|
|
25
|
+
let wordStart = -1;
|
|
26
|
+
|
|
27
|
+
// Seed position from the start offset — one binary search total.
|
|
28
|
+
// Then track line/column incrementally (DocumentLines counts only \n
|
|
29
|
+
// as line separators, so this matches offsetToPosition exactly).
|
|
30
|
+
let { line: curLine, column: curCol } = doc.offsetToPosition(startOffset);
|
|
31
|
+
let wordLine = 0,
|
|
32
|
+
wordCol = 0;
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < template.length; i++) {
|
|
35
|
+
const code = template.charCodeAt(i);
|
|
36
|
+
if (isAlphaNumeric(code)) {
|
|
37
|
+
if (wordStart < 0) {
|
|
38
|
+
wordStart = i;
|
|
39
|
+
wordLine = curLine;
|
|
40
|
+
wordCol = curCol;
|
|
41
|
+
}
|
|
42
|
+
curCol++;
|
|
43
|
+
} else {
|
|
44
|
+
if (wordStart >= 0) {
|
|
45
|
+
const absStart = startOffset + wordStart;
|
|
46
|
+
const absEnd = startOffset + i;
|
|
47
|
+
tokens.push({
|
|
48
|
+
type: "word",
|
|
49
|
+
value: template.slice(wordStart, i),
|
|
50
|
+
range: [absStart, absEnd],
|
|
51
|
+
start: absStart,
|
|
52
|
+
end: absEnd,
|
|
53
|
+
loc: {
|
|
54
|
+
start: { line: wordLine, column: wordCol, index: absStart },
|
|
55
|
+
end: { line: curLine, column: curCol, index: absEnd },
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
wordStart = -1;
|
|
59
|
+
}
|
|
60
|
+
if (code === 10 /* \n */) {
|
|
61
|
+
curLine++;
|
|
62
|
+
curCol = 0;
|
|
63
|
+
} else {
|
|
64
|
+
if (code !== 32 && code !== 9 && code !== 13 && code !== 11 /* non-whitespace */) {
|
|
65
|
+
const absPos = startOffset + i;
|
|
66
|
+
tokens.push({
|
|
67
|
+
type: "Punctuator",
|
|
68
|
+
value: template[i],
|
|
69
|
+
range: [absPos, absPos + 1],
|
|
70
|
+
start: absPos,
|
|
71
|
+
end: absPos + 1,
|
|
72
|
+
loc: {
|
|
73
|
+
start: { line: curLine, column: curCol, index: absPos },
|
|
74
|
+
end: { line: curLine, column: curCol + 1, index: absPos + 1 },
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
curCol++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (wordStart >= 0) {
|
|
84
|
+
const absStart = startOffset + wordStart;
|
|
85
|
+
const absEnd = startOffset + template.length;
|
|
86
|
+
tokens.push({
|
|
87
|
+
type: "word",
|
|
88
|
+
value: template.slice(wordStart),
|
|
89
|
+
range: [absStart, absEnd],
|
|
90
|
+
start: absStart,
|
|
91
|
+
end: absEnd,
|
|
92
|
+
loc: {
|
|
93
|
+
start: { line: wordLine, column: wordCol, index: absStart },
|
|
94
|
+
end: { line: curLine, column: curCol, index: absEnd },
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return tokens;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Merge the raw Glimmer token stream with text nodes and comment tokens,
|
|
104
|
+
* dropping raw tokens that fall inside comment or text-node intervals.
|
|
105
|
+
*
|
|
106
|
+
* All inputs are sorted by range[0] (sequential document scan), so the
|
|
107
|
+
* entire operation is a single O(n+m+k) pass with advancing pointers
|
|
108
|
+
* rather than O(n log m) per-token binary searches.
|
|
109
|
+
*/
|
|
110
|
+
export function buildTokenStream(rawTokens, comments, textNodes, templateContent, offset) {
|
|
111
|
+
// Comment tokens shifted by 1 to avoid the ESLint createIndexMap conflict
|
|
112
|
+
const commentTokens = comments.map((c) => {
|
|
113
|
+
const start = c.range[0] + 1;
|
|
114
|
+
const end = c.range[1];
|
|
115
|
+
return {
|
|
116
|
+
type: "Block",
|
|
117
|
+
value: templateContent.slice(start - offset, end - offset),
|
|
118
|
+
range: [start, end],
|
|
119
|
+
start,
|
|
120
|
+
end,
|
|
121
|
+
loc: c.loc,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const spliceables = linearMerge(textNodes, commentTokens);
|
|
126
|
+
|
|
127
|
+
const result = [];
|
|
128
|
+
let ri = 0; // rawTokens
|
|
129
|
+
let si = 0; // spliceables
|
|
130
|
+
let ci = 0; // comment intervals (for skip detection)
|
|
131
|
+
let ni = 0; // text-node intervals (for skip detection)
|
|
132
|
+
|
|
133
|
+
while (ri < rawTokens.length || si < spliceables.length) {
|
|
134
|
+
if (ri >= rawTokens.length) {
|
|
135
|
+
result.push(spliceables[si++]);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const tok = rawTokens[ri];
|
|
140
|
+
|
|
141
|
+
// Advance interval pointers past intervals that end before this token
|
|
142
|
+
while (ci < comments.length && comments[ci].range[1] <= tok.range[0]) ci++;
|
|
143
|
+
while (ni < textNodes.length && textNodes[ni].range[1] <= tok.range[0]) ni++;
|
|
144
|
+
|
|
145
|
+
// Skip raw token if it falls inside a comment or text-node interval
|
|
146
|
+
if (
|
|
147
|
+
(ci < comments.length && comments[ci].range[0] <= tok.range[0]) ||
|
|
148
|
+
(ni < textNodes.length && textNodes[ni].range[0] <= tok.range[0])
|
|
149
|
+
) {
|
|
150
|
+
ri++;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Emit the earlier of the next spliceable or this raw token
|
|
155
|
+
if (si < spliceables.length && spliceables[si].range[0] < tok.range[0]) {
|
|
156
|
+
result.push(spliceables[si++]);
|
|
157
|
+
} else {
|
|
158
|
+
result.push(tok);
|
|
159
|
+
ri++;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function linearMerge(a, b) {
|
|
167
|
+
const result = Array.from({ length: a.length + b.length });
|
|
168
|
+
let ai = 0,
|
|
169
|
+
bi = 0,
|
|
170
|
+
ri = 0;
|
|
171
|
+
while (ai < a.length && bi < b.length) {
|
|
172
|
+
result[ri++] = a[ai].range[0] <= b[bi].range[0] ? a[ai++] : b[bi++];
|
|
173
|
+
}
|
|
174
|
+
while (ai < a.length) result[ri++] = a[ai++];
|
|
175
|
+
while (bi < b.length) result[ri++] = b[bi++];
|
|
176
|
+
return result;
|
|
177
|
+
}
|
package/src/transforms.js
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
preprocess as glimmerPreprocess,
|
|
8
8
|
} from "@glimmer/syntax";
|
|
9
9
|
|
|
10
|
+
import { tokenize, buildTokenStream } from "./tokens.js";
|
|
11
|
+
|
|
10
12
|
/**
|
|
11
13
|
* Converts between character offsets and line/column positions.
|
|
12
14
|
* Lines are 1-based, columns are 0-based (matching ESTree & Glimmer conventions).
|
|
@@ -62,7 +64,7 @@ export const glimmerVisitorKeys = (() => {
|
|
|
62
64
|
// Block: blockParams
|
|
63
65
|
const _desc = { value: undefined, configurable: true, enumerable: true, writable: true };
|
|
64
66
|
const _parentDesc = { value: null, configurable: true, enumerable: false, writable: true };
|
|
65
|
-
function setParent(node, parent) {
|
|
67
|
+
export function setParent(node, parent) {
|
|
66
68
|
_parentDesc.value = parent;
|
|
67
69
|
Object.defineProperty(node, "parent", _parentDesc);
|
|
68
70
|
}
|
|
@@ -82,89 +84,6 @@ function removeFromParent(nodes) {
|
|
|
82
84
|
}
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
function isAlphaNumeric(code) {
|
|
86
|
-
return !(!(code > 47 && code < 58) && !(code > 64 && code < 91) && !(code > 96 && code < 123));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function isWhiteSpaceCode(code) {
|
|
90
|
-
return code === 32 || code === 9 || code === 13 || code === 10 || code === 11;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function tokenize(template, doc, startOffset) {
|
|
94
|
-
const tokens = [];
|
|
95
|
-
let wordStart = -1;
|
|
96
|
-
function pushToken(value, type, range) {
|
|
97
|
-
tokens.push({
|
|
98
|
-
type,
|
|
99
|
-
value,
|
|
100
|
-
range,
|
|
101
|
-
start: range[0],
|
|
102
|
-
end: range[1],
|
|
103
|
-
loc: {
|
|
104
|
-
start: { ...doc.offsetToPosition(range[0]), index: range[0] },
|
|
105
|
-
end: { ...doc.offsetToPosition(range[1]), index: range[1] },
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
for (let i = 0; i < template.length; i++) {
|
|
110
|
-
const code = template.charCodeAt(i);
|
|
111
|
-
if (isAlphaNumeric(code)) {
|
|
112
|
-
if (wordStart < 0) wordStart = i;
|
|
113
|
-
} else {
|
|
114
|
-
if (wordStart >= 0) {
|
|
115
|
-
pushToken(template.slice(wordStart, i), "word", [startOffset + wordStart, startOffset + i]);
|
|
116
|
-
wordStart = -1;
|
|
117
|
-
}
|
|
118
|
-
if (!isWhiteSpaceCode(code)) {
|
|
119
|
-
pushToken(template[i], "Punctuator", [startOffset + i, startOffset + i + 1]);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (wordStart >= 0) {
|
|
124
|
-
pushToken(template.slice(wordStart), "word", [
|
|
125
|
-
startOffset + wordStart,
|
|
126
|
-
startOffset + template.length,
|
|
127
|
-
]);
|
|
128
|
-
}
|
|
129
|
-
return tokens;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function buildTokenStream(rawTokens, comments, textNodes) {
|
|
133
|
-
const commentIntervals = comments.map((c) => c.range).sort((a, b) => a[0] - b[0]);
|
|
134
|
-
const textNodeIntervals = textNodes.map((t) => t.range).sort((a, b) => a[0] - b[0]);
|
|
135
|
-
|
|
136
|
-
function isCovered(tokenRange, intervals) {
|
|
137
|
-
let lo = 0;
|
|
138
|
-
let hi = intervals.length - 1;
|
|
139
|
-
while (lo <= hi) {
|
|
140
|
-
const mid = (lo + hi) >> 1;
|
|
141
|
-
const iv = intervals[mid];
|
|
142
|
-
if (iv[0] <= tokenRange[0] && iv[1] >= tokenRange[1]) return true;
|
|
143
|
-
if (iv[0] > tokenRange[0]) hi = mid - 1;
|
|
144
|
-
else lo = mid + 1;
|
|
145
|
-
}
|
|
146
|
-
return false;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const filteredTokens = rawTokens.filter(
|
|
150
|
-
(t) => !isCovered(t.range, commentIntervals) && !isCovered(t.range, textNodeIntervals),
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
const sortedTextNodes = [...textNodes].sort((a, b) => a.range[0] - b.range[0]);
|
|
154
|
-
const result = [];
|
|
155
|
-
let ti = 0;
|
|
156
|
-
for (const token of filteredTokens) {
|
|
157
|
-
while (ti < sortedTextNodes.length && sortedTextNodes[ti].range[0] < token.range[0]) {
|
|
158
|
-
result.push(sortedTextNodes[ti++]);
|
|
159
|
-
}
|
|
160
|
-
result.push(token);
|
|
161
|
-
}
|
|
162
|
-
while (ti < sortedTextNodes.length) {
|
|
163
|
-
result.push(sortedTextNodes[ti++]);
|
|
164
|
-
}
|
|
165
|
-
return result;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
87
|
/**
|
|
169
88
|
* Parse and transform a Glimmer template into an ESTree-compatible AST.
|
|
170
89
|
* Internal — consumed by toTree.
|
|
@@ -173,7 +92,8 @@ function buildTokenStream(rawTokens, comments, textNodes) {
|
|
|
173
92
|
* positions, create parts/blockParamNodes, nullify empty hashes, and
|
|
174
93
|
* prefix types. No separate collect-then-transform loop.
|
|
175
94
|
*/
|
|
176
|
-
export function processTemplate(templateContent, codeLines,
|
|
95
|
+
export function processTemplate(templateContent, codeLines, options = {}) {
|
|
96
|
+
const { templateRange, tokens: generateTokens = false } = options;
|
|
177
97
|
const offset = templateRange[0];
|
|
178
98
|
const docLines = offset === 0 ? codeLines : new DocumentLines(templateContent);
|
|
179
99
|
|
|
@@ -335,7 +255,15 @@ export function processTemplate(templateContent, codeLines, templateRange) {
|
|
|
335
255
|
|
|
336
256
|
removeFromParent(emptyTextNodes);
|
|
337
257
|
|
|
338
|
-
|
|
258
|
+
if (generateTokens) {
|
|
259
|
+
ast.tokens = buildTokenStream(
|
|
260
|
+
tokenize(templateContent, codeLines, offset),
|
|
261
|
+
comments,
|
|
262
|
+
textNodes,
|
|
263
|
+
templateContent,
|
|
264
|
+
offset,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
339
267
|
ast.contents = templateContent;
|
|
340
268
|
|
|
341
269
|
return { ast, comments };
|