@falsefalse/prettier-plugin-handlebars 0.0.1 → 0.0.3
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 +2 -1
- package/README.md +19 -15
- package/dist/{errors.d.ts → core/errors.d.ts} +7 -0
- package/dist/{errors.js → core/errors.js} +10 -0
- package/dist/core/html.d.ts +2 -0
- package/dist/core/html.js +32 -0
- package/dist/core/source.d.ts +11 -0
- package/dist/core/source.js +27 -0
- package/dist/{whitespace.d.ts → core/whitespace.d.ts} +5 -0
- package/dist/{whitespace.js → core/whitespace.js} +18 -0
- package/dist/dialects/handlebars/tokens.d.ts +23 -6
- package/dist/dialects/handlebars/tokens.js +15 -8
- package/dist/expression.js +8 -8
- package/dist/parse/lex.d.ts +86 -0
- package/dist/parse/lex.js +274 -0
- package/dist/parse/lookahead.d.ts +35 -0
- package/dist/parse/lookahead.js +305 -0
- package/dist/parse/nodes.d.ts +38 -0
- package/dist/parse/nodes.js +161 -0
- package/dist/parser.d.ts +0 -2
- package/dist/parser.js +271 -931
- package/dist/plugin.d.ts +2 -1
- package/dist/plugin.js +3 -2
- package/dist/printer.d.ts +1 -6
- package/dist/printer.js +72 -35
- package/dist/types.d.ts +2 -2
- package/docs/EDITOR_SETUP.md +0 -1
- package/package.json +3 -6
- /package/dist/{scan.d.ts → core/scan.d.ts} +0 -0
- /package/dist/{scan.js → core/scan.js} +0 -0
package/dist/parser.js
CHANGED
|
@@ -33,350 +33,272 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.locStart = exports.locEnd = void 0;
|
|
37
36
|
exports.parse = parse;
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
const html_1 = require("./core/html");
|
|
38
|
+
const source_1 = require("./core/source");
|
|
39
|
+
const errors_1 = require("./core/errors");
|
|
40
|
+
const whitespace = __importStar(require("./core/whitespace"));
|
|
41
|
+
const lex_1 = require("./parse/lex");
|
|
42
|
+
const lookahead_1 = require("./parse/lookahead");
|
|
43
|
+
const nodes_1 = require("./parse/nodes");
|
|
42
44
|
const expression_1 = require("./expression");
|
|
43
|
-
const scan_1 = require("./scan");
|
|
44
|
-
const errors_1 = require("./errors");
|
|
45
|
-
const whitespace = __importStar(require("./whitespace"));
|
|
46
45
|
const tokens_1 = require("./dialects/handlebars/tokens");
|
|
47
|
-
/* Built from the shared class so the character list stays written in one place. */
|
|
48
|
-
const leadingWhitespace = new RegExp(`^${whitespace.htmlRun.source}`, 'u');
|
|
49
|
-
/* HTML's lexical classes, composed from the whitespace list rather than repeating it - both
|
|
50
|
-
* embed it, and a second hand-written copy is what `whitespace.ts` exists to prevent. They
|
|
51
|
-
* live here because the tokenizer below is the only thing that reads them. */
|
|
52
|
-
/** What an attribute name is made of: anything but whitespace and the characters that end one. */
|
|
53
|
-
const attributeNameCharacter = new RegExp(`[^${whitespace.htmlCharacters}"'<>/=]`, 'u');
|
|
54
|
-
/** What ends a tag name. HTML's tag-name state leaves on whitespace, `/` or `>`, and nothing else. */
|
|
55
|
-
const tagNameTerminator = new RegExp(`[${whitespace.htmlCharacters}/>]`, 'u');
|
|
56
46
|
/* Destructured rather than wrapped: seven of these had a one-line function around them whose
|
|
57
47
|
* only job was to give the dialect member a local name. */
|
|
58
|
-
const {
|
|
48
|
+
const { parseToken: parseMustacheToken, findNextOpen: findNextHandlebarsOpen, getBlockExpression, getBlockPrefix, shouldPreserveTokenVerbatim: shouldPreserveMustacheVerbatim, } = tokens_1.handlebarsDialect;
|
|
59
49
|
function parse(text) {
|
|
60
|
-
const normalizedText = (0,
|
|
50
|
+
const normalizedText = (0, source_1.normalizeInput)(text);
|
|
61
51
|
try {
|
|
62
52
|
const { nodes } = parseChildren(normalizedText, 0, null, null);
|
|
63
|
-
return (0,
|
|
53
|
+
return (0, source_1.withRange)({ type: 'Program', body: nodes }, 0, normalizedText.length);
|
|
64
54
|
}
|
|
65
55
|
catch (error) {
|
|
66
56
|
/* Offsets become line and column here, where the whole text is still in hand. */
|
|
67
57
|
throw error instanceof errors_1.TemplateSyntaxError ? error.locate(normalizedText) : error;
|
|
68
58
|
}
|
|
69
59
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Every malformed construct ends here. A formatter that guesses at a missing delimiter prints
|
|
72
|
-
* markup the author did not write; one that passes a mismatched tag through leaves the rest of
|
|
73
|
-
* the file unformatted with nothing to show for it. Refusing is the only honest option, and the
|
|
74
|
-
* offsets let an editor put the cursor on the offending place.
|
|
75
|
-
*/
|
|
76
|
-
function fail(message, start, end) {
|
|
77
|
-
throw new errors_1.TemplateSyntaxError(message, start, end);
|
|
78
|
-
}
|
|
79
60
|
/* The dialect reports an unterminated token as one that ends at EOF, which is also what a token
|
|
80
61
|
* ending the file looks like; the closing delimiter is what tells them apart. */
|
|
81
|
-
/* Where a raw block at `position` ends, or null if there is not one there. A body Handlebars
|
|
82
|
-
* emits literally is copied through wherever it appears; one that never closes is rejected
|
|
83
|
-
* wherever it appears too. */
|
|
84
|
-
function consumeTerminatedRawBlock(text, position, rangeOffset) {
|
|
85
|
-
const end = consumeRawBlock(text, position);
|
|
86
|
-
if (end === null) {
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
/* Same as for a mustache, except the closer carries the block's own name - and the name is
|
|
90
|
-
* read once here rather than once to decide and again to name it in the message. */
|
|
91
|
-
const openEnd = text.indexOf('}}}}', position + 4);
|
|
92
|
-
const name = openEnd === -1 ? '' : (0, tokens_1.handlebarsRawBlockName)(text, position, openEnd);
|
|
93
|
-
if (name === '' || !text.slice(position, end).endsWith((0, tokens_1.handlebarsRawBlockCloser)(name))) {
|
|
94
|
-
fail(`unterminated raw block: expected ${(0, tokens_1.handlebarsRawBlockCloser)(name)}`, rangeOffset + position, rangeOffset + end);
|
|
95
|
-
}
|
|
96
|
-
return end;
|
|
97
|
-
}
|
|
98
|
-
function startsTemplateTag(text, position) {
|
|
99
|
-
return text.startsWith(openDelimiter, position) && !isEscapedOpen(text, position);
|
|
100
|
-
}
|
|
101
62
|
function parseChildren(text, position, endTag, endBlock, rangeOffset = 0) {
|
|
102
63
|
const nodes = [];
|
|
103
64
|
let pos = position;
|
|
104
|
-
if (endTag &&
|
|
105
|
-
|
|
106
|
-
const contentEnd = closeStart >= 0 ? closeStart : text.length;
|
|
107
|
-
const rawContent = text.slice(pos, contentEnd);
|
|
108
|
-
if (rawContent.length > 0) {
|
|
109
|
-
nodes.push((0, template_format_core_2.withRange)({
|
|
110
|
-
type: 'TextNode',
|
|
111
|
-
chars: rawContent,
|
|
112
|
-
verbatim: true,
|
|
113
|
-
}, rangeOffset + pos, rangeOffset + contentEnd));
|
|
114
|
-
}
|
|
115
|
-
const closeIdx = closeStart >= 0 ? text.indexOf('>', closeStart) : -1;
|
|
116
|
-
if (closeStart >= 0 && closeIdx < 0) {
|
|
117
|
-
fail("unterminated tag: expected '>'", rangeOffset + closeStart, rangeOffset + text.length);
|
|
118
|
-
}
|
|
119
|
-
const nextPos = closeIdx >= 0 ? closeIdx + 1 : contentEnd;
|
|
120
|
-
const closeTag = closeStart >= 0 ? readCloseTagSource(text, closeStart, closeIdx) : undefined;
|
|
121
|
-
return { nodes, position: nextPos, endReason: closeStart >= 0 ? 'tagClose' : null, contentEnd, closeTag };
|
|
65
|
+
if (endTag && (0, html_1.isRawTextElement)(endTag)) {
|
|
66
|
+
return parseRawTextChildren(text, pos, endTag, rangeOffset);
|
|
122
67
|
}
|
|
123
68
|
/* The current block's terminator does not move while this call runs, and every position the
|
|
124
69
|
* loop reaches is at depth 0 inside it, so it is hoisted: recomputing it per open tag is
|
|
125
70
|
* quadratic in the number of mustaches in the block's body. */
|
|
126
|
-
const blockBoundary = endBlock ? findCurrentBlockBoundary(text, pos, endBlock) : -1;
|
|
71
|
+
const blockBoundary = endBlock ? (0, lookahead_1.findCurrentBlockBoundary)(text, pos, endBlock) : -1;
|
|
127
72
|
while (pos < text.length) {
|
|
128
|
-
const rawBlockEnd = consumeTerminatedRawBlock(text, pos, rangeOffset);
|
|
73
|
+
const rawBlockEnd = (0, lookahead_1.consumeTerminatedRawBlock)(text, pos, rangeOffset);
|
|
129
74
|
if (rawBlockEnd !== null) {
|
|
130
|
-
nodes.push(createUnmatchedNode(text, pos, rawBlockEnd, rangeOffset));
|
|
75
|
+
nodes.push((0, nodes_1.createUnmatchedNode)(text, pos, rawBlockEnd, rangeOffset));
|
|
131
76
|
pos = rawBlockEnd;
|
|
132
77
|
continue;
|
|
133
78
|
}
|
|
134
|
-
const dynamicElementEnd = consumeDynamicElement(text, pos);
|
|
79
|
+
const dynamicElementEnd = (0, lookahead_1.consumeDynamicElement)(text, pos);
|
|
135
80
|
if (dynamicElementEnd !== null) {
|
|
136
|
-
nodes.push(createUnmatchedNode(text, pos, dynamicElementEnd, rangeOffset));
|
|
81
|
+
nodes.push((0, nodes_1.createUnmatchedNode)(text, pos, dynamicElementEnd, rangeOffset));
|
|
137
82
|
pos = dynamicElementEnd;
|
|
138
83
|
continue;
|
|
139
84
|
}
|
|
140
|
-
if (endTag && startsCloseTag(text, pos, endTag)) {
|
|
85
|
+
if (endTag && (0, lex_1.startsCloseTag)(text, pos, endTag)) {
|
|
141
86
|
const contentEnd = pos;
|
|
142
87
|
const closeIdx = text.indexOf('>', pos);
|
|
143
88
|
if (closeIdx < 0) {
|
|
144
|
-
fail("unterminated tag: expected '>'", rangeOffset + pos, rangeOffset + text.length);
|
|
89
|
+
(0, errors_1.fail)("unterminated tag: expected '>'", rangeOffset + pos, rangeOffset + text.length);
|
|
145
90
|
}
|
|
146
|
-
const closeTag = readCloseTagSource(text, pos, closeIdx);
|
|
91
|
+
const closeTag = (0, lex_1.readCloseTagSource)(text, pos, closeIdx);
|
|
147
92
|
pos = closeIdx + 1;
|
|
148
93
|
return { nodes, position: pos, endReason: 'tagClose', contentEnd, closeTag };
|
|
149
94
|
}
|
|
150
|
-
if (startsTemplateTag(text, pos)) {
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
: token.triple
|
|
156
|
-
? ['{{{', '}}}']
|
|
157
|
-
: [text.startsWith('{{!', pos) ? '{{!' : '{{', '}}'];
|
|
158
|
-
fail(`unterminated ${open}: expected ${close}`, rangeOffset + pos, rangeOffset + token.end);
|
|
159
|
-
}
|
|
160
|
-
if (shouldPreserveMustacheVerbatim(token) && !(endBlock && token.kind === 'else')) {
|
|
161
|
-
nodes.push(createUnmatchedNode(text, pos, token.end, rangeOffset));
|
|
162
|
-
pos = token.end;
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
if (token.kind === 'comment') {
|
|
166
|
-
const ignoreDirective = getPrettierIgnoreDirective(commentBody(token));
|
|
167
|
-
if (ignoreDirective === 'start') {
|
|
168
|
-
const ignoreStart = pos;
|
|
169
|
-
const ignoreEnd = findPrettierIgnoreEnd(text, token.end);
|
|
170
|
-
if (ignoreEnd === null) {
|
|
171
|
-
fail('unterminated prettier-ignore region: expected {{! prettier-ignore-end }}', rangeOffset + ignoreStart, rangeOffset + token.end);
|
|
172
|
-
}
|
|
173
|
-
nodes.push(createUnmatchedNode(text, ignoreStart, ignoreEnd, rangeOffset));
|
|
174
|
-
pos = ignoreEnd;
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
if (ignoreDirective === 'next') {
|
|
178
|
-
const ignoredEnd = consumeNextNode(text, token.end);
|
|
179
|
-
/* Nothing follows to ignore, so the directive is only a comment. */
|
|
180
|
-
if (ignoredEnd <= token.end) {
|
|
181
|
-
nodes.push(createComment(token, rangeOffset + pos, rangeOffset + token.end));
|
|
182
|
-
pos = token.end;
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
nodes.push(createUnmatchedNode(text, pos, ignoredEnd, rangeOffset));
|
|
186
|
-
pos = ignoredEnd;
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
if (endBlock && token.kind === 'blockEnd' && token.name === endBlock) {
|
|
191
|
-
return { nodes, position: token.end, endReason: 'blockEnd', endToken: token };
|
|
192
|
-
}
|
|
193
|
-
if (endBlock && token.kind === 'else') {
|
|
194
|
-
return { nodes, position: token.end, endReason: 'else', endToken: token };
|
|
195
|
-
}
|
|
196
|
-
if (token.kind === 'blockStart') {
|
|
197
|
-
if (!hasMatchingBlockEnd(text, token)) {
|
|
198
|
-
fail(`unclosed block: expected {{/${token.name ?? ''}}}`, rangeOffset + pos, rangeOffset + token.end);
|
|
199
|
-
}
|
|
200
|
-
const { node, next, closed } = parseBlock(text, token, rangeOffset);
|
|
201
|
-
if (!closed) {
|
|
202
|
-
fail(`unclosed block: expected {{/${token.name ?? ''}}}`, rangeOffset + pos, rangeOffset + token.end);
|
|
203
|
-
}
|
|
204
|
-
nodes.push(node);
|
|
205
|
-
pos = next;
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
if (token.kind === 'blockEnd') {
|
|
209
|
-
fail(endBlock
|
|
210
|
-
? `unexpected {{/${token.name ?? ''}}}: expected {{/${endBlock}}}`
|
|
211
|
-
: `unexpected {{/${token.name ?? ''}}}: no block is open`, rangeOffset + pos, rangeOffset + token.end);
|
|
212
|
-
}
|
|
213
|
-
/* Blocks and terminators are handled above, so the only kind left that `createStatement`
|
|
214
|
-
* declines is a stray `{{else}}` with nothing open - kept as a mustache. */
|
|
215
|
-
nodes.push(createStatement(text, token, pos, rangeOffset) ?? createMustache(text, token, pos, rangeOffset));
|
|
216
|
-
pos = token.end;
|
|
95
|
+
if ((0, lex_1.startsTemplateTag)(text, pos)) {
|
|
96
|
+
const step = parseMustacheChild(text, pos, endBlock, rangeOffset, nodes);
|
|
97
|
+
if (typeof step !== 'number')
|
|
98
|
+
return step;
|
|
99
|
+
pos = step;
|
|
217
100
|
continue;
|
|
218
101
|
}
|
|
219
102
|
if (text[pos] === '<') {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
* printer's own final newline additive, and the file grows a line on every format. */
|
|
225
|
-
const end = closeIdx >= 0 ? closeIdx + 1 : trimTrailingWhitespace(text, pos);
|
|
226
|
-
nodes.push((0, template_format_core_2.withRange)({ type: 'TextNode', chars: text.slice(pos, end), verbatim: true }, rangeOffset + pos, rangeOffset + end));
|
|
227
|
-
pos = end;
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
if (!isTagStart(text, pos)) {
|
|
231
|
-
const nextMarkup = findNextMarkup(text, pos + 1);
|
|
232
|
-
nodes.push((0, template_format_core_2.withRange)({ type: 'TextNode', chars: text.slice(pos, nextMarkup) }, rangeOffset + pos, rangeOffset + nextMarkup));
|
|
233
|
-
pos = nextMarkup;
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
if (text.startsWith('<!--', pos)) {
|
|
237
|
-
const closeIdx = text.indexOf('-->', pos + 4);
|
|
238
|
-
if (closeIdx < 0) {
|
|
239
|
-
fail("unterminated HTML comment: expected '-->'", rangeOffset + pos, rangeOffset + text.length);
|
|
240
|
-
}
|
|
241
|
-
const end = closeIdx + 3;
|
|
242
|
-
nodes.push((0, template_format_core_2.withRange)({ type: 'TextNode', chars: text.slice(pos, end), verbatim: true }, rangeOffset + pos, rangeOffset + end));
|
|
243
|
-
pos = end;
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
const tagResult = parseTag(text, pos, rangeOffset);
|
|
247
|
-
if (!tagResult.terminated) {
|
|
248
|
-
fail("unterminated tag: expected '>'", rangeOffset + pos, rangeOffset + tagResult.end);
|
|
249
|
-
}
|
|
250
|
-
if (tagResult.kind === 'close') {
|
|
251
|
-
if (endTag && sameTag(tagResult.tag, endTag)) {
|
|
252
|
-
const contentEnd = pos;
|
|
253
|
-
pos = tagResult.end;
|
|
254
|
-
return { nodes, position: pos, endReason: 'tagClose', contentEnd, closeTag: tagResult.source };
|
|
255
|
-
}
|
|
256
|
-
fail(endTag
|
|
257
|
-
? `unexpected </${tagResult.tag}>: expected </${endTag}>`
|
|
258
|
-
: `unexpected </${tagResult.tag}>: no tag is open`, rangeOffset + pos, rangeOffset + tagResult.end);
|
|
259
|
-
}
|
|
260
|
-
if (tagResult.kind === 'selfClosing') {
|
|
261
|
-
const invalidVoidCloseEnd = consumeInvalidVoidElementClose(text, tagResult.end, tagResult.tag);
|
|
262
|
-
if (invalidVoidCloseEnd !== null) {
|
|
263
|
-
fail(`<${tagResult.tag}> is a void element and cannot be closed`, rangeOffset + tagResult.end, rangeOffset + invalidVoidCloseEnd);
|
|
264
|
-
}
|
|
265
|
-
nodes.push((0, template_format_core_2.withRange)({
|
|
266
|
-
type: 'ElementNode',
|
|
267
|
-
tag: tagResult.tag,
|
|
268
|
-
attributes: tagResult.attributes,
|
|
269
|
-
children: [],
|
|
270
|
-
selfClosing: true,
|
|
271
|
-
attributesRange: tagResult.attributesRange,
|
|
272
|
-
}, rangeOffset + pos, rangeOffset + tagResult.end));
|
|
273
|
-
pos = tagResult.end;
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
if (findMatchingTagClose(text, tagResult.tag, tagResult.end, blockBoundary) === null) {
|
|
277
|
-
fail(`unclosed tag: expected </${tagResult.tag}>`, rangeOffset + pos, rangeOffset + tagResult.end);
|
|
278
|
-
}
|
|
279
|
-
const { nodes: children, position: newPos, endReason: childEndReason, contentEnd, closeTag, } = parseChildren(text, tagResult.end, tagResult.tag, null, rangeOffset);
|
|
280
|
-
if (childEndReason !== 'tagClose') {
|
|
281
|
-
fail(`unclosed tag: expected </${tagResult.tag}>`, rangeOffset + pos, rangeOffset + tagResult.end);
|
|
282
|
-
}
|
|
283
|
-
nodes.push((0, template_format_core_2.withRange)({
|
|
284
|
-
type: 'ElementNode',
|
|
285
|
-
tag: tagResult.tag,
|
|
286
|
-
attributes: tagResult.attributes,
|
|
287
|
-
children,
|
|
288
|
-
selfClosing: false,
|
|
289
|
-
...(closeTag && closeTag !== tagResult.tag ? { closeTag } : {}),
|
|
290
|
-
attributesRange: tagResult.attributesRange,
|
|
291
|
-
contentRange: [rangeOffset + tagResult.end, rangeOffset + (contentEnd ?? newPos)],
|
|
292
|
-
}, rangeOffset + pos, rangeOffset + newPos));
|
|
293
|
-
pos = newPos;
|
|
103
|
+
const step = parseElementChild(text, pos, endTag, blockBoundary, rangeOffset, nodes);
|
|
104
|
+
if (typeof step !== 'number')
|
|
105
|
+
return step;
|
|
106
|
+
pos = step;
|
|
294
107
|
continue;
|
|
295
108
|
}
|
|
296
109
|
/* Text node until the next markup. The run is kept verbatim, whitespace-only runs
|
|
297
110
|
* included: what renders is the printer's to decide, not the parser's to discard. */
|
|
298
|
-
const nextMarkup = findNextMarkup(text, pos);
|
|
111
|
+
const nextMarkup = (0, lookahead_1.findNextMarkup)(text, pos);
|
|
299
112
|
if (nextMarkup > pos) {
|
|
300
|
-
nodes.push((0,
|
|
113
|
+
nodes.push((0, nodes_1.textNode)(text, pos, nextMarkup, rangeOffset));
|
|
301
114
|
}
|
|
302
115
|
pos = nextMarkup;
|
|
303
116
|
}
|
|
304
117
|
return { nodes, position: pos, endReason: null };
|
|
305
118
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
*/
|
|
316
|
-
function* mustachesFrom(text, from) {
|
|
317
|
-
let pos = from;
|
|
318
|
-
while (pos < text.length) {
|
|
319
|
-
const next = findNextHandlebarsOpen(text, pos);
|
|
320
|
-
if (next === -1) {
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
const rawBlockEnd = consumeRawBlock(text, next);
|
|
324
|
-
if (rawBlockEnd !== null && rawBlockEnd > next) {
|
|
325
|
-
pos = rawBlockEnd;
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
const token = parseMustacheToken(text, next);
|
|
329
|
-
yield token;
|
|
330
|
-
pos = token.end > next ? token.end : next + 2;
|
|
119
|
+
/* A raw text element has no children: `<` inside one is text, so the whole body is one run and
|
|
120
|
+
* the only thing to find is the closing tag. */
|
|
121
|
+
function parseRawTextChildren(text, position, endTag, rangeOffset) {
|
|
122
|
+
const closeStart = (0, lookahead_1.findRawTextClose)(text, position, endTag);
|
|
123
|
+
const contentEnd = closeStart >= 0 ? closeStart : text.length;
|
|
124
|
+
const nodes = contentEnd > position ? [(0, nodes_1.textNode)(text, position, contentEnd, rangeOffset, true)] : [];
|
|
125
|
+
const closeIdx = closeStart >= 0 ? text.indexOf('>', closeStart) : -1;
|
|
126
|
+
if (closeStart >= 0 && closeIdx < 0) {
|
|
127
|
+
(0, errors_1.fail)("unterminated tag: expected '>'", rangeOffset + closeStart, rangeOffset + text.length);
|
|
331
128
|
}
|
|
129
|
+
return {
|
|
130
|
+
nodes,
|
|
131
|
+
position: closeIdx >= 0 ? closeIdx + 1 : contentEnd,
|
|
132
|
+
endReason: closeStart >= 0 ? 'tagClose' : null,
|
|
133
|
+
contentEnd,
|
|
134
|
+
closeTag: closeStart >= 0 ? (0, lex_1.readCloseTagSource)(text, closeStart, closeIdx) : undefined,
|
|
135
|
+
};
|
|
332
136
|
}
|
|
333
|
-
|
|
334
|
-
function
|
|
335
|
-
|
|
336
|
-
|
|
137
|
+
/* Split out of `parseChildren` only for size; both read `nodes` as the list being built. */
|
|
138
|
+
function parseMustacheChild(text, pos, endBlock, rangeOffset, nodes) {
|
|
139
|
+
const token = parseMustacheToken(text, pos);
|
|
140
|
+
if (!token.terminated) {
|
|
141
|
+
const [open, close] = (0, tokens_1.isHandlebarsBlockComment)(text, pos)
|
|
142
|
+
? ['{{!--', '--}}']
|
|
143
|
+
: token.triple
|
|
144
|
+
? ['{{{', '}}}']
|
|
145
|
+
: [text.startsWith('{{!', pos) ? '{{!' : '{{', '}}'];
|
|
146
|
+
(0, errors_1.fail)(`unterminated ${open}: expected ${close}`, rangeOffset + pos, rangeOffset + token.end);
|
|
147
|
+
}
|
|
148
|
+
if (shouldPreserveMustacheVerbatim(token) && !(endBlock && token.kind === 'else')) {
|
|
149
|
+
nodes.push((0, nodes_1.createUnmatchedNode)(text, pos, token.end, rangeOffset));
|
|
150
|
+
return token.end;
|
|
337
151
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
else if (candidate.kind === 'blockEnd' && candidate.name === token.name) {
|
|
347
|
-
if (depth === 0) {
|
|
348
|
-
return candidate.end;
|
|
152
|
+
if (token.kind === 'comment') {
|
|
153
|
+
const ignoreDirective = (0, nodes_1.getPrettierIgnoreDirective)((0, nodes_1.commentBody)(token));
|
|
154
|
+
if (ignoreDirective === 'start') {
|
|
155
|
+
const ignoreStart = pos;
|
|
156
|
+
const ignoreEnd = (0, nodes_1.findPrettierIgnoreEnd)(text, token.end);
|
|
157
|
+
if (ignoreEnd === null) {
|
|
158
|
+
(0, errors_1.fail)('unterminated prettier-ignore region: expected {{! prettier-ignore-end }}', rangeOffset + ignoreStart, rangeOffset + token.end);
|
|
349
159
|
}
|
|
350
|
-
|
|
351
|
-
|
|
160
|
+
nodes.push((0, nodes_1.createUnmatchedNode)(text, ignoreStart, ignoreEnd, rangeOffset));
|
|
161
|
+
return ignoreEnd;
|
|
162
|
+
}
|
|
163
|
+
if (ignoreDirective === 'next') {
|
|
164
|
+
const ignoredEnd = (0, lookahead_1.consumeNextNode)(text, token.end);
|
|
165
|
+
/* Nothing follows to ignore, so the directive is only a comment. */
|
|
166
|
+
if (ignoredEnd <= token.end) {
|
|
167
|
+
nodes.push((0, nodes_1.createComment)(token, rangeOffset + pos, rangeOffset + token.end));
|
|
168
|
+
return token.end;
|
|
169
|
+
}
|
|
170
|
+
nodes.push((0, nodes_1.createUnmatchedNode)(text, pos, ignoredEnd, rangeOffset));
|
|
171
|
+
return ignoredEnd;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (endBlock && token.kind === 'blockEnd' && token.name === endBlock) {
|
|
175
|
+
return { nodes, position: token.end, endReason: 'blockEnd', endToken: token };
|
|
176
|
+
}
|
|
177
|
+
if (endBlock && token.kind === 'else') {
|
|
178
|
+
return { nodes, position: token.end, endReason: 'else', endToken: token };
|
|
179
|
+
}
|
|
180
|
+
if (token.kind === 'blockStart') {
|
|
181
|
+
if (!(0, lookahead_1.hasMatchingBlockEnd)(text, token)) {
|
|
182
|
+
(0, errors_1.fail)(`unclosed block: expected {{/${token.name ?? ''}}}`, rangeOffset + pos, rangeOffset + token.end);
|
|
183
|
+
}
|
|
184
|
+
const { node, next, closed } = parseBlock(text, token, rangeOffset);
|
|
185
|
+
if (!closed) {
|
|
186
|
+
(0, errors_1.fail)(`unclosed block: expected {{/${token.name ?? ''}}}`, rangeOffset + pos, rangeOffset + token.end);
|
|
187
|
+
}
|
|
188
|
+
nodes.push(node);
|
|
189
|
+
return next;
|
|
190
|
+
}
|
|
191
|
+
if (token.kind === 'blockEnd') {
|
|
192
|
+
(0, errors_1.fail)(endBlock
|
|
193
|
+
? `unexpected {{/${token.name ?? ''}}}: expected {{/${endBlock}}}`
|
|
194
|
+
: `unexpected {{/${token.name ?? ''}}}: no block is open`, rangeOffset + pos, rangeOffset + token.end);
|
|
195
|
+
}
|
|
196
|
+
/* Blocks and terminators are handled above, so the only kind left that `createStatement`
|
|
197
|
+
* declines is a stray `{{else}}` with nothing open - kept as a mustache. */
|
|
198
|
+
nodes.push((0, nodes_1.createStatement)(text, token, pos, rangeOffset) ?? (0, nodes_1.createMustache)(text, token, pos, rangeOffset));
|
|
199
|
+
return token.end;
|
|
200
|
+
}
|
|
201
|
+
function parseElementChild(text, pos, endTag, blockBoundary, rangeOffset, nodes) {
|
|
202
|
+
if (text.startsWith('<!', pos) && !text.startsWith('<!--', pos)) {
|
|
203
|
+
const closeIdx = text.indexOf('>', pos + 2);
|
|
204
|
+
/* Unterminated, so the declaration runs to the end of the input - but its trailing
|
|
205
|
+
* whitespace is still the author's. Folding that into the verbatim run makes the
|
|
206
|
+
* printer's own final newline additive, and the file grows a line on every format. */
|
|
207
|
+
const end = closeIdx >= 0 ? closeIdx + 1 : (0, lex_1.trimTrailingWhitespace)(text, pos);
|
|
208
|
+
nodes.push((0, nodes_1.textNode)(text, pos, end, rangeOffset, true));
|
|
209
|
+
return end;
|
|
210
|
+
}
|
|
211
|
+
if (!(0, lex_1.isTagStart)(text, pos)) {
|
|
212
|
+
const nextMarkup = (0, lookahead_1.findNextMarkup)(text, pos + 1);
|
|
213
|
+
nodes.push((0, nodes_1.textNode)(text, pos, nextMarkup, rangeOffset));
|
|
214
|
+
return nextMarkup;
|
|
352
215
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
216
|
+
if (text.startsWith('<!--', pos)) {
|
|
217
|
+
const closeIdx = text.indexOf('-->', pos + 4);
|
|
218
|
+
if (closeIdx < 0) {
|
|
219
|
+
(0, errors_1.fail)("unterminated HTML comment: expected '-->'", rangeOffset + pos, rangeOffset + text.length);
|
|
220
|
+
}
|
|
221
|
+
const end = closeIdx + 3;
|
|
222
|
+
nodes.push((0, nodes_1.textNode)(text, pos, end, rangeOffset, true));
|
|
223
|
+
return end;
|
|
224
|
+
}
|
|
225
|
+
return parseElement(text, pos, endTag, blockBoundary, rangeOffset, nodes);
|
|
226
|
+
}
|
|
227
|
+
/* Split from `parseElementChild` so the three things that open with `<` and are not an element -
|
|
228
|
+
* a declaration, a comment, a bare `<` - stay out of the way of the one that is. */
|
|
229
|
+
function parseElement(text, pos, endTag, blockBoundary, rangeOffset, nodes) {
|
|
230
|
+
const tagResult = parseTag(text, pos, rangeOffset);
|
|
231
|
+
if (!tagResult.terminated) {
|
|
232
|
+
(0, errors_1.fail)("unterminated tag: expected '>'", rangeOffset + pos, rangeOffset + tagResult.end);
|
|
233
|
+
}
|
|
234
|
+
if (tagResult.kind === 'close') {
|
|
235
|
+
if (endTag && (0, lex_1.sameTag)(tagResult.tag, endTag)) {
|
|
236
|
+
return { nodes, position: tagResult.end, endReason: 'tagClose', contentEnd: pos, closeTag: tagResult.source };
|
|
237
|
+
}
|
|
238
|
+
(0, errors_1.fail)(endTag
|
|
239
|
+
? `unexpected </${tagResult.tag}>: expected </${endTag}>`
|
|
240
|
+
: `unexpected </${tagResult.tag}>: no tag is open`, rangeOffset + pos, rangeOffset + tagResult.end);
|
|
241
|
+
}
|
|
242
|
+
if (tagResult.kind === 'selfClosing') {
|
|
243
|
+
const invalidVoidCloseEnd = consumeInvalidVoidElementClose(text, tagResult.end, tagResult.tag);
|
|
244
|
+
if (invalidVoidCloseEnd !== null) {
|
|
245
|
+
(0, errors_1.fail)(`<${tagResult.tag}> is a void element and cannot be closed`, rangeOffset + tagResult.end, rangeOffset + invalidVoidCloseEnd);
|
|
246
|
+
}
|
|
247
|
+
nodes.push((0, source_1.withRange)({
|
|
248
|
+
type: 'ElementNode',
|
|
249
|
+
tag: tagResult.tag,
|
|
250
|
+
attributes: tagResult.attributes,
|
|
251
|
+
children: [],
|
|
252
|
+
selfClosing: true,
|
|
253
|
+
attributesRange: tagResult.attributesRange,
|
|
254
|
+
}, rangeOffset + pos, rangeOffset + tagResult.end));
|
|
255
|
+
return tagResult.end;
|
|
256
|
+
}
|
|
257
|
+
if ((0, lookahead_1.findMatchingTagClose)(text, tagResult.tag, tagResult.end, blockBoundary) === null) {
|
|
258
|
+
(0, errors_1.fail)(`unclosed tag: expected </${tagResult.tag}>`, rangeOffset + pos, rangeOffset + tagResult.end);
|
|
259
|
+
}
|
|
260
|
+
const { nodes: children, position: newPos, endReason: childEndReason, contentEnd, closeTag, } = parseChildren(text, tagResult.end, tagResult.tag, null, rangeOffset);
|
|
261
|
+
if (childEndReason !== 'tagClose') {
|
|
262
|
+
(0, errors_1.fail)(`unclosed tag: expected </${tagResult.tag}>`, rangeOffset + pos, rangeOffset + tagResult.end);
|
|
263
|
+
}
|
|
264
|
+
nodes.push((0, source_1.withRange)({
|
|
265
|
+
type: 'ElementNode',
|
|
266
|
+
tag: tagResult.tag,
|
|
267
|
+
attributes: tagResult.attributes,
|
|
268
|
+
children,
|
|
269
|
+
selfClosing: false,
|
|
270
|
+
...(closeTag && closeTag !== tagResult.tag ? { closeTag } : {}),
|
|
271
|
+
attributesRange: tagResult.attributesRange,
|
|
272
|
+
contentRange: [rangeOffset + tagResult.end, rangeOffset + (contentEnd ?? newPos)],
|
|
273
|
+
}, rangeOffset + pos, rangeOffset + newPos));
|
|
274
|
+
return newPos;
|
|
275
|
+
}
|
|
276
|
+
function parseBlock(text, token, rangeOffset) {
|
|
277
|
+
/* A call's source is a slice of its own token, so its parts locate against the template. */
|
|
278
|
+
const callAt = (at, source) => (0, expression_1.parseCall)(source, rangeOffset + (0, nodes_1.contentOffset)(text, at.start, at.end, source));
|
|
279
|
+
const openInfo = callAt(token, getBlockExpression(token));
|
|
358
280
|
const blockPrefix = getBlockPrefix(token);
|
|
359
|
-
|
|
360
|
-
|
|
281
|
+
/* Every branch reads to the same terminator - this block's own closer - so they differ only
|
|
282
|
+
* in where they start. */
|
|
283
|
+
const parseBranch = (from) => parseChildren(text, from, null, openInfo.path.source, rangeOffset);
|
|
284
|
+
const { nodes: program, position: afterProgram, endReason, endToken } = parseBranch(token.end);
|
|
285
|
+
const buildProgram = (nodes, start, end) => (0, source_1.withRange)({ type: 'Program', body: nodes }, rangeOffset + start, rangeOffset + end);
|
|
361
286
|
/* A program ends where its terminator begins, not after it, so the body tiles the range. */
|
|
362
287
|
const programBody = buildProgram(program, token.end, endToken?.start ?? afterProgram);
|
|
363
288
|
/* Set only when the author wrote a bare `{{else}}`; otherwise the empty inverse is built at
|
|
364
289
|
* the end, once the closer's position is known. Anchoring it at `afterProgram` up here put it
|
|
365
290
|
* inside the else-if chain - a point belonging to a different section of the block. */
|
|
366
|
-
let
|
|
291
|
+
let inverse;
|
|
367
292
|
const inverseChain = [];
|
|
368
293
|
let finalPos = afterProgram;
|
|
369
294
|
let closeToken = endReason === 'blockEnd' ? endToken : undefined;
|
|
370
|
-
let inverseTrimOpen = false;
|
|
371
|
-
let inverseTrimClose = false;
|
|
372
295
|
if (endReason === 'else' && endToken) {
|
|
373
296
|
let currentElseToken = endToken;
|
|
374
297
|
let currentPosition = afterProgram;
|
|
375
298
|
while (currentElseToken?.specialForm === 'elseIf') {
|
|
376
|
-
const
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
inverseChain.push((0, template_format_core_2.withRange)({
|
|
299
|
+
const branchExpression = callAt(currentElseToken, currentElseToken.content.replace(/^else\s+/, ''));
|
|
300
|
+
const { nodes: branchNodes, position: afterBranch, endReason: branchEndReason, endToken: branchEndToken, } = parseBranch(currentPosition);
|
|
301
|
+
inverseChain.push((0, source_1.withRange)({
|
|
380
302
|
type: 'ElseBranch',
|
|
381
303
|
program: buildProgram(branchNodes, currentElseToken.end, branchEndToken?.start ?? afterBranch),
|
|
382
304
|
trimOpen: currentElseToken.trimOpen,
|
|
@@ -393,25 +315,27 @@ function parseBlock(text, token, rangeOffset = 0) {
|
|
|
393
315
|
currentElseToken = undefined;
|
|
394
316
|
}
|
|
395
317
|
if (currentElseToken) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
318
|
+
const { nodes: inverseNodes, position: afterInverse, endReason: inverseEndReason, endToken: inverseEndToken, } = parseBranch(currentPosition);
|
|
319
|
+
inverse = {
|
|
320
|
+
program: buildProgram(inverseNodes, currentElseToken.end, inverseEndToken?.start ?? afterInverse),
|
|
321
|
+
trimOpen: currentElseToken.trimOpen,
|
|
322
|
+
trimClose: currentElseToken.trimClose,
|
|
323
|
+
};
|
|
400
324
|
finalPos = afterInverse;
|
|
401
325
|
closeToken = inverseEndReason === 'blockEnd' ? inverseEndToken : undefined;
|
|
402
326
|
}
|
|
403
327
|
}
|
|
404
328
|
const closerAnchor = closeToken?.start ?? finalPos;
|
|
405
|
-
const node = (0,
|
|
329
|
+
const node = (0, source_1.withRange)({
|
|
406
330
|
type: 'BlockStatement',
|
|
407
331
|
program: programBody,
|
|
408
332
|
...(inverseChain.length > 0 ? { inverseChain } : {}),
|
|
409
333
|
/* An empty inverse sits where the block's closer starts: after every branch, before
|
|
410
334
|
* `{{/if}}`. It is a zero-width point, so it has to be a position the block actually
|
|
411
335
|
* owns. */
|
|
412
|
-
inverse:
|
|
413
|
-
...(
|
|
414
|
-
...(
|
|
336
|
+
inverse: inverse?.program ?? buildProgram([], closerAnchor, closerAnchor),
|
|
337
|
+
...(inverse?.trimOpen ? { inverseTrimOpen: true } : {}),
|
|
338
|
+
...(inverse?.trimClose ? { inverseTrimClose: true } : {}),
|
|
415
339
|
blockPrefix,
|
|
416
340
|
trimOpen: token.trimOpen,
|
|
417
341
|
trimClose: token.trimClose,
|
|
@@ -422,161 +346,42 @@ function parseBlock(text, token, rangeOffset = 0) {
|
|
|
422
346
|
return { node, next: finalPos, closed: Boolean(closeToken) };
|
|
423
347
|
}
|
|
424
348
|
/**
|
|
425
|
-
* The
|
|
426
|
-
*
|
|
427
|
-
*
|
|
349
|
+
* The node a mustache stands for and where it ends, or null when it stands for nothing on its
|
|
350
|
+
* own: a block that never closes, a stray `{{else}}` or `{{/if}}`. Recovering from that is the
|
|
351
|
+
* caller's, and the two callers disagree - in attribute position it stays a mustache, inside a
|
|
352
|
+
* value it goes back to being text.
|
|
428
353
|
*/
|
|
429
|
-
function
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
case 'prettier-ignore-end':
|
|
434
|
-
return 'end';
|
|
435
|
-
case 'prettier-ignore':
|
|
436
|
-
return 'next';
|
|
437
|
-
default:
|
|
438
|
-
return null;
|
|
354
|
+
function readTemplateNode(text, token, position, rangeOffset) {
|
|
355
|
+
const statement = (0, nodes_1.createStatement)(text, token, position, rangeOffset);
|
|
356
|
+
if (statement) {
|
|
357
|
+
return { node: statement, next: token.end };
|
|
439
358
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
for (const token of mustachesFrom(text, position)) {
|
|
443
|
-
/* Kind first: `commentBody` and the directive lookup are wasted on every mustache, block and
|
|
444
|
-
* partial the scan walks past on the way. */
|
|
445
|
-
if (token.kind === 'comment' && getPrettierIgnoreDirective(commentBody(token)) === 'end') {
|
|
446
|
-
return token.end;
|
|
447
|
-
}
|
|
359
|
+
if (token.kind === 'blockStart' && (0, lookahead_1.hasMatchingBlockEnd)(text, token)) {
|
|
360
|
+
return parseBlock(text, token, rangeOffset);
|
|
448
361
|
}
|
|
449
362
|
return null;
|
|
450
363
|
}
|
|
451
|
-
|
|
452
|
-
* How far `{{! prettier-ignore }}` reaches: to the end of the one node that follows it, or
|
|
453
|
-
* nowhere if that node's extent cannot be determined.
|
|
454
|
-
*
|
|
455
|
-
* It scans rather than parses: a nested `parseChildren` would run past the enclosing container,
|
|
456
|
-
* handing an element its own `</div>`, and could `fail()` - leaving a directive meant to
|
|
457
|
-
* suppress formatting able to reject the file. `position` means "nothing to ignore".
|
|
458
|
-
*/
|
|
459
|
-
function consumeNextNode(text, position) {
|
|
460
|
-
if (position >= text.length) {
|
|
461
|
-
return position;
|
|
462
|
-
}
|
|
463
|
-
if (startsTemplateTag(text, position)) {
|
|
464
|
-
const token = parseMustacheToken(text, position);
|
|
465
|
-
/* A terminator belongs to whatever opened it, never to the node being skipped. */
|
|
466
|
-
if (token.kind === 'blockEnd' || token.kind === 'else') {
|
|
467
|
-
return position;
|
|
468
|
-
}
|
|
469
|
-
return token.kind === 'blockStart' ? findMatchingBlockEnd(text, token) ?? position : token.end;
|
|
470
|
-
}
|
|
471
|
-
if (text[position] === '<') {
|
|
472
|
-
const tagResult = scanTag(text, position);
|
|
473
|
-
if (!tagResult.terminated || tagResult.kind === 'close') {
|
|
474
|
-
return position;
|
|
475
|
-
}
|
|
476
|
-
if (tagResult.kind === 'selfClosing') {
|
|
477
|
-
return tagResult.end;
|
|
478
|
-
}
|
|
479
|
-
const closeStart = findMatchingTagClose(text, tagResult.tag, tagResult.end);
|
|
480
|
-
if (closeStart === null) {
|
|
481
|
-
return position;
|
|
482
|
-
}
|
|
483
|
-
const closeEnd = text.indexOf('>', closeStart);
|
|
484
|
-
return closeEnd < 0 ? position : closeEnd + 1;
|
|
485
|
-
}
|
|
486
|
-
const nextMarkup = findNextMarkup(text, position);
|
|
487
|
-
if (nextMarkup <= position) {
|
|
488
|
-
return nextMarkup;
|
|
489
|
-
}
|
|
490
|
-
/* Only whitespace is stepped over on the way to the node being ignored - a run of text is a
|
|
491
|
-
* node in its own right, and is the thing to ignore. */
|
|
492
|
-
if (text.slice(position, nextMarkup).trim() !== '' || nextMarkup >= text.length) {
|
|
493
|
-
return nextMarkup;
|
|
494
|
-
}
|
|
495
|
-
return consumeNextNode(text, nextMarkup);
|
|
496
|
-
}
|
|
497
|
-
function createUnmatchedNode(text, start, end, rangeOffset) {
|
|
498
|
-
return (0, template_format_core_2.withRange)({ type: 'UnmatchedNode', raw: text.slice(start, end) }, rangeOffset + start, rangeOffset + end);
|
|
499
|
-
}
|
|
500
|
-
/**
|
|
501
|
-
* Where a tag ends, what it is called and whether it closed - without building a single node and
|
|
502
|
-
* without rejecting anything.
|
|
503
|
-
*
|
|
504
|
-
* Lookahead has to be total: callers scan regions they may go on to skip, including a
|
|
505
|
-
* `{{! prettier-ignore }}` body, so a `parseTag` here let the directive reject the very file it
|
|
506
|
-
* was written to protect. `terminated` is false when the tag ran to EOF, which is also how an
|
|
507
|
-
* unterminated attribute value shows up.
|
|
508
|
-
*/
|
|
509
|
-
function scanTag(text, position) {
|
|
510
|
-
let pos = position + 1;
|
|
511
|
-
const closing = text[pos] === '/';
|
|
512
|
-
if (closing) {
|
|
513
|
-
pos += 1;
|
|
514
|
-
}
|
|
515
|
-
const { value: tag, next } = readName(text, pos);
|
|
516
|
-
pos = next;
|
|
517
|
-
const kindAt = (selfClosed) => {
|
|
518
|
-
if (closing) {
|
|
519
|
-
return 'close';
|
|
520
|
-
}
|
|
521
|
-
return selfClosed || template_format_core_1.voidElements.has(tag.toLowerCase()) ? 'selfClosing' : 'open';
|
|
522
|
-
};
|
|
523
|
-
/* A quote only delimits a value directly after `=`, whitespace aside. Treating every quote as
|
|
524
|
-
* a delimiter would make `title=a"b'c>` swallow the rest of the file hunting a closing `"`. */
|
|
525
|
-
let afterEquals = false;
|
|
526
|
-
while (pos < text.length) {
|
|
527
|
-
if (startsTemplateTag(text, pos)) {
|
|
528
|
-
const token = parseMustacheToken(text, pos);
|
|
529
|
-
pos = token.end > pos ? token.end : pos + 2;
|
|
530
|
-
continue;
|
|
531
|
-
}
|
|
532
|
-
const char = text[pos];
|
|
533
|
-
if (whitespace.html.test(char)) {
|
|
534
|
-
pos += 1;
|
|
535
|
-
continue;
|
|
536
|
-
}
|
|
537
|
-
if (char === '=') {
|
|
538
|
-
afterEquals = true;
|
|
539
|
-
pos += 1;
|
|
540
|
-
continue;
|
|
541
|
-
}
|
|
542
|
-
if (afterEquals && char !== '>') {
|
|
543
|
-
pos =
|
|
544
|
-
char === '"' || char === "'"
|
|
545
|
-
? readQuotedAttributeValue(text, pos + 1, char).position
|
|
546
|
-
: readUnquotedValueEnd(text, pos);
|
|
547
|
-
afterEquals = false;
|
|
548
|
-
continue;
|
|
549
|
-
}
|
|
550
|
-
if (isSelfClosingSlash(text, pos)) {
|
|
551
|
-
return { kind: kindAt(true), tag, end: pos + 2, terminated: true };
|
|
552
|
-
}
|
|
553
|
-
if (char === '>') {
|
|
554
|
-
return { kind: kindAt(false), tag, end: pos + 1, terminated: true };
|
|
555
|
-
}
|
|
556
|
-
afterEquals = false;
|
|
557
|
-
pos += 1;
|
|
558
|
-
}
|
|
559
|
-
return { kind: kindAt(false), tag, end: pos, terminated: false };
|
|
560
|
-
}
|
|
561
|
-
function parseTag(text, position, rangeOffset = 0) {
|
|
364
|
+
function parseTag(text, position, rangeOffset) {
|
|
562
365
|
let pos = position + 1; // skip '<'
|
|
563
366
|
if (text[pos] === '/') {
|
|
564
367
|
pos += 1;
|
|
565
|
-
const { value: tag, next } = readName(text, pos);
|
|
368
|
+
const { value: tag, next } = (0, lex_1.readName)(text, pos);
|
|
566
369
|
const closeIdx = text.indexOf('>', next);
|
|
567
370
|
return {
|
|
568
371
|
kind: 'close',
|
|
569
372
|
tag,
|
|
570
|
-
source: readCloseTagSource(text, position, closeIdx),
|
|
373
|
+
source: (0, lex_1.readCloseTagSource)(text, position, closeIdx),
|
|
571
374
|
end: closeIdx >= 0 ? closeIdx + 1 : text.length,
|
|
572
375
|
terminated: closeIdx >= 0,
|
|
573
376
|
};
|
|
574
377
|
}
|
|
575
|
-
const { value: tag, next } = readName(text, pos);
|
|
378
|
+
const { value: tag, next } = (0, lex_1.readName)(text, pos);
|
|
576
379
|
pos = next;
|
|
577
380
|
const attributes = [];
|
|
578
381
|
const headStart = pos;
|
|
579
382
|
const span = (headEnd) => [rangeOffset + headStart, rangeOffset + headEnd];
|
|
383
|
+
/* A void element is self-closing however it was written, so `<br>` and `<br/>` agree. */
|
|
384
|
+
const kindOf = (selfClosed) => selfClosed || (0, html_1.isVoidElement)(tag) ? 'selfClosing' : 'open';
|
|
580
385
|
let glued = false;
|
|
581
386
|
let attrStart = pos;
|
|
582
387
|
/* `glued` is whether the author left a space before this attribute. That includes the first
|
|
@@ -585,10 +390,10 @@ function parseTag(text, position, rangeOffset = 0) {
|
|
|
585
390
|
* accounts for all of the source it was read from. */
|
|
586
391
|
const add = (attribute, end) => {
|
|
587
392
|
const marked = glued ? { ...attribute, glued: true } : attribute;
|
|
588
|
-
attributes.push((0,
|
|
393
|
+
attributes.push((0, source_1.withRange)(marked, rangeOffset + attrStart, rangeOffset + end));
|
|
589
394
|
};
|
|
590
395
|
while (pos < text.length) {
|
|
591
|
-
pos = skipWhitespace(text, pos);
|
|
396
|
+
pos = (0, lex_1.skipWhitespace)(text, pos);
|
|
592
397
|
/* Trailing whitespace can run out the input. Falling through would ask `parseAttribute` to
|
|
593
398
|
* read past the end and report `unexpected undefined`, when the tag is simply unterminated. */
|
|
594
399
|
if (pos >= text.length) {
|
|
@@ -604,179 +409,88 @@ function parseTag(text, position, rangeOffset = 0) {
|
|
|
604
409
|
pos = dynamicAttribute.position;
|
|
605
410
|
continue;
|
|
606
411
|
}
|
|
607
|
-
if (startsTemplateTag(text, pos)) {
|
|
412
|
+
if ((0, lex_1.startsTemplateTag)(text, pos)) {
|
|
608
413
|
const token = parseMustacheToken(text, pos);
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
pos = next;
|
|
619
|
-
continue;
|
|
620
|
-
}
|
|
621
|
-
/* A block that never closes, or a stray `{{else}}` / `{{/if}}`. Being unbalanced is not
|
|
622
|
-
* itself grounds to reject here - the tag's own extent is already fixed - so they are kept
|
|
623
|
-
* as a mustache. `createMustache` still parses the call, so what is *inside* one can be
|
|
624
|
-
* rejected the same as anywhere else. */
|
|
625
|
-
add({ type: 'AttributeBlock', block: createMustache(text, token, pos, rangeOffset) }, token.end);
|
|
626
|
-
pos = token.end;
|
|
414
|
+
/* Unbalanced is not itself grounds to reject here - the tag's own extent is already fixed
|
|
415
|
+
* - so it stays a mustache. `createMustache` still parses the call, so what is *inside*
|
|
416
|
+
* one can be rejected the same as anywhere else. */
|
|
417
|
+
const read = readTemplateNode(text, token, pos, rangeOffset) ?? {
|
|
418
|
+
node: (0, nodes_1.createMustache)(text, token, pos, rangeOffset),
|
|
419
|
+
next: token.end,
|
|
420
|
+
};
|
|
421
|
+
add({ type: 'AttributeBlock', block: read.node }, read.next);
|
|
422
|
+
pos = read.next;
|
|
627
423
|
continue;
|
|
628
424
|
}
|
|
629
|
-
|
|
425
|
+
const selfClosed = text[pos] === '/' && text[pos + 1] === '>';
|
|
426
|
+
if (selfClosed || text[pos] === '>') {
|
|
630
427
|
const headEnd = pos;
|
|
631
|
-
pos += 2;
|
|
632
|
-
return { kind:
|
|
633
|
-
}
|
|
634
|
-
if (text[pos] === '>') {
|
|
635
|
-
const headEnd = pos;
|
|
636
|
-
pos += 1;
|
|
637
|
-
const kind = template_format_core_1.voidElements.has(tag.toLowerCase()) ? 'selfClosing' : 'open';
|
|
638
|
-
return { kind, tag, attributes, attributesRange: span(headEnd), end: pos, terminated: true };
|
|
428
|
+
pos += selfClosed ? 2 : 1;
|
|
429
|
+
return { kind: kindOf(selfClosed), tag, attributes, attributesRange: span(headEnd), end: pos, terminated: true };
|
|
639
430
|
}
|
|
640
431
|
const attr = parseAttribute(text, pos, rangeOffset);
|
|
641
432
|
/* Every remaining character is one an attribute name may start with, so there is nothing
|
|
642
433
|
* left to skip over - and skipping is what quietly deleted the author's markup. */
|
|
643
434
|
if (!attr) {
|
|
644
|
-
fail(`unexpected ${text[pos]} in <${tag}>: expected an attribute name or '>'`, rangeOffset + pos, rangeOffset + pos + 1);
|
|
435
|
+
(0, errors_1.fail)(`unexpected ${text[pos]} in <${tag}>: expected an attribute name or '>'`, rangeOffset + pos, rangeOffset + pos + 1);
|
|
645
436
|
}
|
|
646
437
|
add(attr.attribute, attr.position);
|
|
647
438
|
pos = attr.position;
|
|
648
439
|
}
|
|
649
|
-
|
|
650
|
-
return { kind, tag, attributes, attributesRange: span(pos), end: pos, terminated: false };
|
|
440
|
+
return { kind: kindOf(false), tag, attributes, attributesRange: span(pos), end: pos, terminated: false };
|
|
651
441
|
}
|
|
652
442
|
function consumeInvalidVoidElementClose(text, position, tag) {
|
|
653
|
-
if (!
|
|
443
|
+
if (!(0, html_1.isVoidElement)(tag)) {
|
|
654
444
|
return null;
|
|
655
445
|
}
|
|
656
|
-
const afterGap = skipWhitespace(text, position);
|
|
446
|
+
const afterGap = (0, lex_1.skipWhitespace)(text, position);
|
|
657
447
|
if (!text.startsWith('</', afterGap)) {
|
|
658
448
|
return null;
|
|
659
449
|
}
|
|
660
|
-
const { value, next } = readName(text, afterGap + 2);
|
|
661
|
-
const end = skipWhitespace(text, next);
|
|
662
|
-
return sameTag(value, tag) && text[end] === '>' ? end + 1 : null;
|
|
663
|
-
}
|
|
664
|
-
/* HTML tag names are case-insensitive, so `<DIV>x</div>` is one element. Comparing them
|
|
665
|
-
* verbatim rejected it as unclosed, while the `voidElements` and `rawTextElements` lookups two
|
|
666
|
-
* lines away had been lowercasing all along. */
|
|
667
|
-
function sameTag(one, other) {
|
|
668
|
-
return one.toLowerCase() === other.toLowerCase();
|
|
669
|
-
}
|
|
670
|
-
/**
|
|
671
|
-
* Whether a close tag for exactly `tag` starts here.
|
|
672
|
-
*
|
|
673
|
-
* The name has to end where `tag` does. On a prefix comparison `</bdi>` would close a `<b>`,
|
|
674
|
-
* deleting `di` from the source and pointing any error at the next, well-formed close tag.
|
|
675
|
-
*/
|
|
676
|
-
function startsCloseTag(text, position, tag) {
|
|
677
|
-
if (!text.startsWith('</', position)) {
|
|
678
|
-
return false;
|
|
679
|
-
}
|
|
680
|
-
const { value: name, next } = readName(text, position + 2);
|
|
681
|
-
return sameTag(name, tag) && (next >= text.length || tagNameTerminator.test(text[next]));
|
|
682
|
-
}
|
|
683
|
-
/* Everything between `</` and `>`. HTML keeps only the name and throws the rest away, but it is
|
|
684
|
-
* still the author's source: `</h{{level}}>` has to come back out spelled that way. Whitespace
|
|
685
|
-
* runs collapse so a close tag can never put a raw newline into a doc. */
|
|
686
|
-
function readCloseTagSource(text, position, closeIdx) {
|
|
687
|
-
return text
|
|
688
|
-
.slice(position + 2, closeIdx >= 0 ? closeIdx : text.length)
|
|
689
|
-
.trim()
|
|
690
|
-
.replace(whitespace.htmlRunGlobal, ' ');
|
|
691
|
-
}
|
|
692
|
-
/* One past the last non-whitespace character, leaving the author's trailing whitespace to the
|
|
693
|
-
* caller instead of burying it inside a node that prints verbatim. */
|
|
694
|
-
function trimTrailingWhitespace(text, from) {
|
|
695
|
-
let end = text.length;
|
|
696
|
-
while (end > from && whitespace.html.test(text[end - 1])) {
|
|
697
|
-
end -= 1;
|
|
698
|
-
}
|
|
699
|
-
return end;
|
|
700
|
-
}
|
|
701
|
-
function isTagStart(text, position) {
|
|
702
|
-
if (text[position] !== '<') {
|
|
703
|
-
return false;
|
|
704
|
-
}
|
|
705
|
-
return /[A-Za-z!/]/u.test(text[position + 1] ?? '');
|
|
706
|
-
}
|
|
707
|
-
/**
|
|
708
|
-
* Where an unquoted attribute value ends. HTML's unquoted-value state ends at whitespace or `>`
|
|
709
|
-
* and nowhere else, so a `/` is content: breaking on it would drop the trailing slash of
|
|
710
|
-
* `src=/a/b/` and make `<a href=/path/>t</a>` a self-closing `<a>` that rejects its own `</a>`.
|
|
711
|
-
* `scanTag` reads values with this too, so its idea of where a tag ends matches the parser's;
|
|
712
|
-
* were they to disagree, a `{{! prettier-ignore }}` region could stop mid-tag.
|
|
713
|
-
*/
|
|
714
|
-
function readUnquotedValueEnd(text, position) {
|
|
715
|
-
let pos = position;
|
|
716
|
-
while (pos < text.length && text[pos] !== '>' && !whitespace.html.test(text[pos])) {
|
|
717
|
-
if (startsTemplateTag(text, pos)) {
|
|
718
|
-
const token = parseMustacheToken(text, pos);
|
|
719
|
-
pos = token.end > pos ? token.end : pos + 2;
|
|
720
|
-
continue;
|
|
721
|
-
}
|
|
722
|
-
pos += 1;
|
|
723
|
-
}
|
|
724
|
-
return pos;
|
|
450
|
+
const { value, next } = (0, lex_1.readName)(text, afterGap + 2);
|
|
451
|
+
const end = (0, lex_1.skipWhitespace)(text, next);
|
|
452
|
+
return (0, lex_1.sameTag)(value, tag) && text[end] === '>' ? end + 1 : null;
|
|
725
453
|
}
|
|
726
|
-
function parseAttribute(text, position, rangeOffset
|
|
454
|
+
function parseAttribute(text, position, rangeOffset) {
|
|
727
455
|
let pos = position;
|
|
728
|
-
pos = skipWhitespace(text, pos);
|
|
729
|
-
const { value: name, next } = readAttributeName(text, pos);
|
|
456
|
+
pos = (0, lex_1.skipWhitespace)(text, pos);
|
|
457
|
+
const { value: name, next } = (0, lex_1.readAttributeName)(text, pos);
|
|
730
458
|
pos = next;
|
|
731
459
|
if (!name) {
|
|
732
460
|
return null;
|
|
733
461
|
}
|
|
734
|
-
pos = skipWhitespace(text, pos);
|
|
462
|
+
pos = (0, lex_1.skipWhitespace)(text, pos);
|
|
735
463
|
// a boolean attribute: no "="
|
|
736
464
|
if (text[pos] !== '=') {
|
|
737
465
|
return { attribute: createAttribute(name, null), position: pos };
|
|
738
466
|
}
|
|
739
467
|
pos += 1;
|
|
740
|
-
pos = skipWhitespace(text, pos);
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
if (text[pos] === '"' || text[pos] === "'") {
|
|
744
|
-
const quote = text[pos];
|
|
745
|
-
pos += 1;
|
|
746
|
-
valueStart = pos;
|
|
747
|
-
const quoted = readQuotedAttributeValue(text, pos, quote);
|
|
748
|
-
rawValue = quoted.value;
|
|
749
|
-
pos = quoted.position;
|
|
750
|
-
}
|
|
751
|
-
else {
|
|
752
|
-
const start = pos;
|
|
753
|
-
valueStart = start;
|
|
754
|
-
pos = readUnquotedValueEnd(text, pos);
|
|
755
|
-
rawValue = text.slice(start, pos);
|
|
756
|
-
}
|
|
468
|
+
pos = (0, lex_1.skipWhitespace)(text, pos);
|
|
469
|
+
const { raw: rawValue, start: valueStart, end } = (0, lex_1.readAttributeValue)(text, pos);
|
|
470
|
+
pos = end;
|
|
757
471
|
/* A value holding both quote characters cannot be printed: whichever one the printer wraps it
|
|
758
472
|
* in ends the attribute early: `title=a"b'c` would print as `title='a"b'c'`, which HTML reads
|
|
759
473
|
* as two attributes. The reader skips over mustaches to find the closing quote, so it accepts
|
|
760
474
|
* values like `class="{{t 'a' "b"}}"` that a browser would cut short. */
|
|
761
475
|
if (rawValue.includes('"') && rawValue.includes("'")) {
|
|
762
|
-
fail('attribute value cannot contain both quote characters', rangeOffset + valueStart, rangeOffset + pos);
|
|
476
|
+
(0, errors_1.fail)('attribute value cannot contain both quote characters', rangeOffset + valueStart, rangeOffset + pos);
|
|
763
477
|
}
|
|
764
478
|
return { attribute: createAttribute(name, rawValue, rangeOffset + valueStart), position: pos };
|
|
765
479
|
}
|
|
766
480
|
function parseDynamicAttribute(text, position) {
|
|
767
481
|
let pos = position;
|
|
768
|
-
pos = skipWhitespace(text, pos);
|
|
482
|
+
pos = (0, lex_1.skipWhitespace)(text, pos);
|
|
769
483
|
const start = pos;
|
|
770
484
|
let hasDynamicPart = false;
|
|
771
485
|
let hasStaticPart = false;
|
|
772
486
|
while (pos < text.length) {
|
|
773
|
-
if (startsTemplateTag(text, pos)) {
|
|
487
|
+
if ((0, lex_1.startsTemplateTag)(text, pos)) {
|
|
774
488
|
const token = parseMustacheToken(text, pos);
|
|
775
489
|
/* A block in the middle of a name is part of the name, so it is consumed whole rather
|
|
776
490
|
* than refused. Unbalanced it is not a name at all, and the caller's error is better
|
|
777
491
|
* than a guess at where it ends. */
|
|
778
492
|
if (token.kind === 'blockStart') {
|
|
779
|
-
const blockEnd = findMatchingBlockEnd(text, token);
|
|
493
|
+
const blockEnd = (0, lookahead_1.findMatchingBlockEnd)(text, token);
|
|
780
494
|
if (blockEnd === null) {
|
|
781
495
|
return null;
|
|
782
496
|
}
|
|
@@ -791,7 +505,7 @@ function parseDynamicAttribute(text, position) {
|
|
|
791
505
|
pos = token.end;
|
|
792
506
|
continue;
|
|
793
507
|
}
|
|
794
|
-
if (attributeNameCharacter.test(text[pos])) {
|
|
508
|
+
if (lex_1.attributeNameCharacter.test(text[pos])) {
|
|
795
509
|
hasStaticPart = true;
|
|
796
510
|
pos += 1;
|
|
797
511
|
continue;
|
|
@@ -802,7 +516,7 @@ function parseDynamicAttribute(text, position) {
|
|
|
802
516
|
return null;
|
|
803
517
|
}
|
|
804
518
|
const nameEnd = pos;
|
|
805
|
-
const afterName = skipWhitespace(text, pos);
|
|
519
|
+
const afterName = (0, lex_1.skipWhitespace)(text, pos);
|
|
806
520
|
if (text[afterName] !== '=') {
|
|
807
521
|
/* A static part is what makes this a name with a mustache in it rather than a mustache
|
|
808
522
|
* standing alone. Without one the caller's model is the better fit - `{{attrs}}` is an
|
|
@@ -818,38 +532,23 @@ function parseDynamicAttribute(text, position) {
|
|
|
818
532
|
}
|
|
819
533
|
/* A value overrides that: it attaches to the composite name, and once the name is split
|
|
820
534
|
* there is nothing left to attach it to. */
|
|
821
|
-
pos = skipWhitespace(text, afterName + 1);
|
|
822
|
-
if (text[pos] === '"' || text[pos] === "'") {
|
|
823
|
-
const quote = text[pos];
|
|
824
|
-
pos += 1;
|
|
825
|
-
pos = readQuotedAttributeValue(text, pos, quote).position;
|
|
826
|
-
}
|
|
827
|
-
else {
|
|
828
|
-
pos = readUnquotedValueEnd(text, pos);
|
|
829
|
-
}
|
|
535
|
+
pos = (0, lex_1.readAttributeValue)(text, (0, lex_1.skipWhitespace)(text, afterName + 1)).end;
|
|
830
536
|
return {
|
|
831
537
|
attribute: createRawAttribute(text.slice(start, pos)),
|
|
832
538
|
position: pos,
|
|
833
539
|
};
|
|
834
540
|
}
|
|
835
541
|
function createAttribute(name, rawValue, valueStart) {
|
|
836
|
-
if (rawValue
|
|
837
|
-
return {
|
|
838
|
-
type: 'Attribute',
|
|
839
|
-
name,
|
|
840
|
-
value: null,
|
|
841
|
-
};
|
|
542
|
+
if (rawValue === null) {
|
|
543
|
+
return { type: 'Attribute', name, value: null };
|
|
842
544
|
}
|
|
843
545
|
const value = {
|
|
844
546
|
type: 'AttributeValue',
|
|
845
547
|
parts: parseAttributeValueParts(rawValue, valueStart ?? 0),
|
|
846
548
|
raw: rawValue,
|
|
847
549
|
};
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
name,
|
|
851
|
-
value: (0, template_format_core_2.withOptionalRange)(value, valueStart, typeof valueStart === 'number' ? valueStart + rawValue.length : undefined),
|
|
852
|
-
};
|
|
550
|
+
const valueEnd = valueStart === undefined ? undefined : valueStart + rawValue.length;
|
|
551
|
+
return { type: 'Attribute', name, value: (0, source_1.withOptionalRange)(value, valueStart, valueEnd) };
|
|
853
552
|
}
|
|
854
553
|
function createRawAttribute(raw) {
|
|
855
554
|
return {
|
|
@@ -857,44 +556,33 @@ function createRawAttribute(raw) {
|
|
|
857
556
|
raw,
|
|
858
557
|
};
|
|
859
558
|
}
|
|
860
|
-
function parseAttributeValueParts(value, rangeOffset
|
|
559
|
+
function parseAttributeValueParts(value, rangeOffset) {
|
|
861
560
|
const parts = [];
|
|
862
561
|
let pos = 0;
|
|
863
562
|
while (pos < value.length) {
|
|
864
563
|
/* A raw block's body is emitted literally by Handlebars, so it is copied through here for
|
|
865
564
|
* the same reason it is between siblings: reformatting the `{{ x }}` inside one changes
|
|
866
565
|
* what the value renders. Only the sibling list guarded this. */
|
|
867
|
-
const rawBlockEnd = consumeTerminatedRawBlock(value, pos, rangeOffset);
|
|
566
|
+
const rawBlockEnd = (0, lookahead_1.consumeTerminatedRawBlock)(value, pos, rangeOffset);
|
|
868
567
|
if (rawBlockEnd !== null) {
|
|
869
|
-
parts.push((0,
|
|
568
|
+
parts.push((0, nodes_1.textNode)(value, pos, rawBlockEnd, rangeOffset));
|
|
870
569
|
pos = rawBlockEnd;
|
|
871
570
|
continue;
|
|
872
571
|
}
|
|
873
|
-
if (startsTemplateTag(value, pos)) {
|
|
572
|
+
if ((0, lex_1.startsTemplateTag)(value, pos)) {
|
|
874
573
|
const token = parseMustacheToken(value, pos);
|
|
875
|
-
const statement = createStatement(value, token, pos, rangeOffset);
|
|
876
|
-
if (statement) {
|
|
877
|
-
parts.push(statement);
|
|
878
|
-
pos = token.end;
|
|
879
|
-
continue;
|
|
880
|
-
}
|
|
881
|
-
if (token.kind === 'blockStart' && hasMatchingBlockEnd(value, token)) {
|
|
882
|
-
const { node, next } = parseBlock(value, token, rangeOffset);
|
|
883
|
-
parts.push(node);
|
|
884
|
-
pos = next;
|
|
885
|
-
continue;
|
|
886
|
-
}
|
|
887
574
|
/* A value is a string, so the recovery here keeps the source as text rather than as a
|
|
888
575
|
* node - unlike attribute position, where an unreadable token stays a mustache. */
|
|
889
|
-
|
|
890
|
-
pos
|
|
576
|
+
const read = readTemplateNode(value, token, pos, rangeOffset);
|
|
577
|
+
parts.push(read?.node ?? (0, nodes_1.textNode)(value, pos, token.end, rangeOffset));
|
|
578
|
+
pos = read?.next ?? token.end;
|
|
891
579
|
continue;
|
|
892
580
|
}
|
|
893
581
|
const next = findNextHandlebarsOpen(value, pos);
|
|
894
582
|
const end = next === -1 ? value.length : next;
|
|
895
583
|
const rawText = value.slice(pos, end);
|
|
896
584
|
if (rawText.length > 0) {
|
|
897
|
-
parts.push((0,
|
|
585
|
+
parts.push((0, nodes_1.textNode)(value, pos, end, rangeOffset));
|
|
898
586
|
}
|
|
899
587
|
pos = end;
|
|
900
588
|
}
|
|
@@ -925,351 +613,3 @@ function preserveValueWhitespace(nodes) {
|
|
|
925
613
|
}
|
|
926
614
|
}
|
|
927
615
|
}
|
|
928
|
-
function readQuotedAttributeValue(text, position, quote) {
|
|
929
|
-
let pos = position;
|
|
930
|
-
while (pos < text.length) {
|
|
931
|
-
if (startsTemplateTag(text, pos)) {
|
|
932
|
-
const token = parseMustacheToken(text, pos);
|
|
933
|
-
pos = token.end > pos ? token.end : pos + 2;
|
|
934
|
-
continue;
|
|
935
|
-
}
|
|
936
|
-
if (text[pos] === quote) {
|
|
937
|
-
return { value: text.slice(position, pos), position: pos + 1 };
|
|
938
|
-
}
|
|
939
|
-
pos += 1;
|
|
940
|
-
}
|
|
941
|
-
return { value: text.slice(position), position: text.length };
|
|
942
|
-
}
|
|
943
|
-
/* One past the whitespace run starting at `position`. Every caller wants an index, and taking
|
|
944
|
-
* one instead of a pair of closures is what let the open-coded copies of this loop go. */
|
|
945
|
-
function skipWhitespace(text, position) {
|
|
946
|
-
let pos = position;
|
|
947
|
-
while (pos < text.length && whitespace.html.test(text[pos])) {
|
|
948
|
-
pos += 1;
|
|
949
|
-
}
|
|
950
|
-
return pos;
|
|
951
|
-
}
|
|
952
|
-
/**
|
|
953
|
-
* HTML's attribute-name state ends at whitespace, `/`, `>` or `=`, and nowhere else.
|
|
954
|
-
*
|
|
955
|
-
* Matching a tag-name charset instead stepped over one character and carried on: `@click` came
|
|
956
|
-
* back as `click` and `(click)="go()"` as two boolean attributes, value gone, silently.
|
|
957
|
-
*/
|
|
958
|
-
/* Stops at a mustache as well as at the characters HTML ends a name on. `parseDynamicAttribute`
|
|
959
|
-
* has already had its go by the time this runs, so what is left is a block or a partial glued to
|
|
960
|
-
* the name - `<div data-{{#if a}}x{{/if}}>`. Reading `data-{{#if` as the name desynchronised the
|
|
961
|
-
* tag loop, which then reported the `/` of `{{/if}}` as an unexpected character. Left here, the
|
|
962
|
-
* tag loop takes the block as its own glued attribute and the two print back together. */
|
|
963
|
-
function readAttributeName(text, position) {
|
|
964
|
-
let pos = position;
|
|
965
|
-
while (pos < text.length && attributeNameCharacter.test(text[pos]) && !startsTemplateTag(text, pos)) {
|
|
966
|
-
pos += 1;
|
|
967
|
-
}
|
|
968
|
-
return { value: text.slice(position, pos), next: pos };
|
|
969
|
-
}
|
|
970
|
-
function readName(text, position) {
|
|
971
|
-
let pos = position;
|
|
972
|
-
while (pos < text.length && /[A-Za-z0-9_:-]/.test(text[pos])) {
|
|
973
|
-
pos += 1;
|
|
974
|
-
}
|
|
975
|
-
return { value: text.slice(position, pos), next: pos };
|
|
976
|
-
}
|
|
977
|
-
function isSelfClosingSlash(text, position) {
|
|
978
|
-
return text[position] === '/' && text[position + 1] === '>';
|
|
979
|
-
}
|
|
980
|
-
function findNextMarkup(text, position) {
|
|
981
|
-
let next = text.length;
|
|
982
|
-
let searchPos = position;
|
|
983
|
-
while (searchPos < text.length) {
|
|
984
|
-
const candidate = text.indexOf('<', searchPos);
|
|
985
|
-
if (candidate === -1) {
|
|
986
|
-
break;
|
|
987
|
-
}
|
|
988
|
-
if (isDynamicTagStart(text, candidate)) {
|
|
989
|
-
next = candidate;
|
|
990
|
-
break;
|
|
991
|
-
}
|
|
992
|
-
if (isTagStart(text, candidate)) {
|
|
993
|
-
next = candidate;
|
|
994
|
-
break;
|
|
995
|
-
}
|
|
996
|
-
searchPos = candidate + 1;
|
|
997
|
-
}
|
|
998
|
-
const hb = findNextHandlebarsOpen(text, position);
|
|
999
|
-
if (hb !== -1 && hb < next) {
|
|
1000
|
-
next = hb;
|
|
1001
|
-
}
|
|
1002
|
-
return next;
|
|
1003
|
-
}
|
|
1004
|
-
function findCurrentBlockBoundary(text, position, endBlock) {
|
|
1005
|
-
let depth = 0;
|
|
1006
|
-
for (const token of mustachesFrom(text, position)) {
|
|
1007
|
-
if (token.kind === 'blockStart') {
|
|
1008
|
-
depth += 1;
|
|
1009
|
-
}
|
|
1010
|
-
else if (token.kind === 'blockEnd') {
|
|
1011
|
-
if (depth === 0 && token.name === endBlock) {
|
|
1012
|
-
return token.start;
|
|
1013
|
-
}
|
|
1014
|
-
if (depth > 0) {
|
|
1015
|
-
depth -= 1;
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
else if (token.kind === 'else' && depth === 0) {
|
|
1019
|
-
return token.start;
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
return -1;
|
|
1023
|
-
}
|
|
1024
|
-
/* Past one mustache, or past a whole raw block: a raw block's body is emitted literally, so the
|
|
1025
|
-
* markup inside it is not markup either. Never returns `position`, so callers cannot spin. */
|
|
1026
|
-
function skipMustache(text, position) {
|
|
1027
|
-
const rawBlockEnd = consumeRawBlock(text, position);
|
|
1028
|
-
if (rawBlockEnd !== null && rawBlockEnd > position) {
|
|
1029
|
-
return rawBlockEnd;
|
|
1030
|
-
}
|
|
1031
|
-
return Math.max(parseMustacheToken(text, position).end, position + 2);
|
|
1032
|
-
}
|
|
1033
|
-
function findMatchingTagClose(text, tag, position, limit = -1) {
|
|
1034
|
-
if (template_format_core_1.rawTextElements.has(tag.toLowerCase())) {
|
|
1035
|
-
const closeStart = findRawTextClose(text, position, tag);
|
|
1036
|
-
if (closeStart === -1 || (limit >= 0 && closeStart >= limit)) {
|
|
1037
|
-
return null;
|
|
1038
|
-
}
|
|
1039
|
-
return closeStart;
|
|
1040
|
-
}
|
|
1041
|
-
let depth = 0;
|
|
1042
|
-
let pos = position;
|
|
1043
|
-
while (pos < text.length) {
|
|
1044
|
-
const next = text.indexOf('<', pos);
|
|
1045
|
-
if (next === -1 || (limit >= 0 && next >= limit)) {
|
|
1046
|
-
return null;
|
|
1047
|
-
}
|
|
1048
|
-
/* A `<` inside a mustache is not markup, so the dialect is consulted first, as every other
|
|
1049
|
-
* scanner here does. Otherwise `{{t "<div>"}}` reads as an open tag, leaving the scan a level
|
|
1050
|
-
* too deep and the real `</div>` closing it - refusing the file as unclosed. */
|
|
1051
|
-
const mustache = findNextHandlebarsOpen(text, pos);
|
|
1052
|
-
if (mustache !== -1 && mustache < next) {
|
|
1053
|
-
pos = skipMustache(text, mustache);
|
|
1054
|
-
continue;
|
|
1055
|
-
}
|
|
1056
|
-
if (text.startsWith('<!--', next)) {
|
|
1057
|
-
const closeIdx = text.indexOf('-->', next + 4);
|
|
1058
|
-
pos = closeIdx >= 0 ? closeIdx + 3 : text.length;
|
|
1059
|
-
continue;
|
|
1060
|
-
}
|
|
1061
|
-
if (text.startsWith('<!', next) && !text.startsWith('<!--', next)) {
|
|
1062
|
-
const closeIdx = text.indexOf('>', next + 2);
|
|
1063
|
-
pos = closeIdx >= 0 ? closeIdx + 1 : text.length;
|
|
1064
|
-
continue;
|
|
1065
|
-
}
|
|
1066
|
-
const dynamicEnd = consumeDynamicElement(text, next);
|
|
1067
|
-
if (dynamicEnd !== null) {
|
|
1068
|
-
pos = dynamicEnd;
|
|
1069
|
-
continue;
|
|
1070
|
-
}
|
|
1071
|
-
if (!isTagStart(text, next)) {
|
|
1072
|
-
pos = next + 1;
|
|
1073
|
-
continue;
|
|
1074
|
-
}
|
|
1075
|
-
const tagResult = scanTag(text, next);
|
|
1076
|
-
if (tagResult.kind === 'close') {
|
|
1077
|
-
if (sameTag(tagResult.tag, tag)) {
|
|
1078
|
-
if (depth === 0) {
|
|
1079
|
-
return next;
|
|
1080
|
-
}
|
|
1081
|
-
depth -= 1;
|
|
1082
|
-
}
|
|
1083
|
-
pos = tagResult.end;
|
|
1084
|
-
continue;
|
|
1085
|
-
}
|
|
1086
|
-
if (tagResult.kind === 'open' && template_format_core_1.rawTextElements.has(tagResult.tag.toLowerCase())) {
|
|
1087
|
-
const closeStart = findRawTextClose(text, tagResult.end, tagResult.tag);
|
|
1088
|
-
const closeIdx = closeStart >= 0 ? text.indexOf('>', closeStart) : -1;
|
|
1089
|
-
pos = closeIdx >= 0 ? closeIdx + 1 : text.length;
|
|
1090
|
-
continue;
|
|
1091
|
-
}
|
|
1092
|
-
if (tagResult.kind === 'open' && sameTag(tagResult.tag, tag)) {
|
|
1093
|
-
depth += 1;
|
|
1094
|
-
}
|
|
1095
|
-
pos = tagResult.end;
|
|
1096
|
-
}
|
|
1097
|
-
return null;
|
|
1098
|
-
}
|
|
1099
|
-
/**
|
|
1100
|
-
* Raw text ends at the first `</tag`, whatever it appears to sit inside.
|
|
1101
|
-
*
|
|
1102
|
-
* A browser's tokenizer does not parse the script or style body looking for string literals -
|
|
1103
|
-
* that is exactly why `"<\\/script>"` has to be escaped in JS. Tracking quotes here instead would
|
|
1104
|
-
* let an apostrophe in a comment hide the closing tag.
|
|
1105
|
-
*/
|
|
1106
|
-
function findRawTextClose(text, position, tag) {
|
|
1107
|
-
const needle = `</${tag.toLowerCase()}`;
|
|
1108
|
-
/* The name has to end there: HTML's script-data end-tag state needs whitespace, `/` or `>`
|
|
1109
|
-
* after it, so `"</scriptx>"` inside a script body does not close the element. */
|
|
1110
|
-
/* Scanning case-insensitively rather than lowercasing the whole template: this runs once per
|
|
1111
|
-
* raw-text element and again inside every close-tag scan, so a copy of the file each time
|
|
1112
|
-
* turns a page of `<script>`s into quadratic work. */
|
|
1113
|
-
for (let index = text.indexOf('<', position); index !== -1; index = text.indexOf('<', index + 1)) {
|
|
1114
|
-
if (text.slice(index, index + needle.length).toLowerCase() === needle && tagNameTerminator.test(text[index + needle.length] ?? '>')) {
|
|
1115
|
-
return index;
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
return -1;
|
|
1119
|
-
}
|
|
1120
|
-
/** Whether the character before `index`, whitespace aside, is `=`. */
|
|
1121
|
-
function follows(text, index, char) {
|
|
1122
|
-
let at = index - 1;
|
|
1123
|
-
while (at >= 0 && whitespace.html.test(text[at]))
|
|
1124
|
-
at -= 1;
|
|
1125
|
-
return text[at] === char;
|
|
1126
|
-
}
|
|
1127
|
-
function consumeTagLikeChunk(text, position) {
|
|
1128
|
-
/* Same rule as a real tag head: a quote delimits a value only after `=`. `<{{t}} a=it's>`
|
|
1129
|
-
* otherwise runs to EOF and swallows the rest of the file into one verbatim node. */
|
|
1130
|
-
const end = (0, scan_1.scanPastQuotes)(text, position + 1, {
|
|
1131
|
-
stopsAt: (index) => text[index] === '>',
|
|
1132
|
-
opensQuote: (index) => follows(text, index, '='),
|
|
1133
|
-
});
|
|
1134
|
-
return end === -1 ? text.length : end + 1;
|
|
1135
|
-
}
|
|
1136
|
-
function consumeDynamicElement(text, position) {
|
|
1137
|
-
if (!isDynamicTagStart(text, position)) {
|
|
1138
|
-
return null;
|
|
1139
|
-
}
|
|
1140
|
-
const dynamicOpen = `<${openDelimiter}`;
|
|
1141
|
-
const dynamicClose = `</${openDelimiter}`;
|
|
1142
|
-
if (text.startsWith(dynamicClose, position)) {
|
|
1143
|
-
return consumeTagLikeChunk(text, position);
|
|
1144
|
-
}
|
|
1145
|
-
const openEnd = consumeTagLikeChunk(text, position);
|
|
1146
|
-
let depth = 0;
|
|
1147
|
-
let pos = openEnd;
|
|
1148
|
-
while (pos < text.length) {
|
|
1149
|
-
const nextOpen = text.indexOf(dynamicOpen, pos);
|
|
1150
|
-
const nextClose = text.indexOf(dynamicClose, pos);
|
|
1151
|
-
const candidates = [nextOpen, nextClose].filter((value) => value !== -1);
|
|
1152
|
-
const next = candidates.length > 0 ? Math.min(...candidates) : -1;
|
|
1153
|
-
if (next === -1) {
|
|
1154
|
-
return openEnd;
|
|
1155
|
-
}
|
|
1156
|
-
if (next === nextClose) {
|
|
1157
|
-
if (depth === 0) {
|
|
1158
|
-
return consumeTagLikeChunk(text, nextClose);
|
|
1159
|
-
}
|
|
1160
|
-
depth -= 1;
|
|
1161
|
-
pos = consumeTagLikeChunk(text, nextClose);
|
|
1162
|
-
continue;
|
|
1163
|
-
}
|
|
1164
|
-
depth += 1;
|
|
1165
|
-
pos = consumeTagLikeChunk(text, nextOpen);
|
|
1166
|
-
}
|
|
1167
|
-
return openEnd;
|
|
1168
|
-
}
|
|
1169
|
-
/** Where `content` begins inside the tag spanning [tagStart, tagEnd), for absolute expression ranges. */
|
|
1170
|
-
function contentOffset(text, tagStart, tagEnd, content) {
|
|
1171
|
-
const at = text.slice(tagStart, tagEnd).indexOf(content);
|
|
1172
|
-
return at === -1 ? tagStart : tagStart + at;
|
|
1173
|
-
}
|
|
1174
|
-
/* The parts every inline statement shares: its call, and the `~` markers on its delimiters. */
|
|
1175
|
-
function statementBase(text, token, position, rangeOffset, content) {
|
|
1176
|
-
return {
|
|
1177
|
-
...(0, expression_1.parseCall)(content, rangeOffset + contentOffset(text, position, token.end, content)),
|
|
1178
|
-
...(token.trimOpen ? { trimOpen: true } : {}),
|
|
1179
|
-
...(token.trimClose ? { trimClose: true } : {}),
|
|
1180
|
-
};
|
|
1181
|
-
}
|
|
1182
|
-
/**
|
|
1183
|
-
* A mustache built from whatever token is in hand, whether or not it reads as one.
|
|
1184
|
-
*
|
|
1185
|
-
* The recovery paths use it for a block that never closes and for a stray `{{else}}` or
|
|
1186
|
-
* `{{/if}}` in a position that cannot reject them.
|
|
1187
|
-
*/
|
|
1188
|
-
function createMustache(text, token, position, rangeOffset) {
|
|
1189
|
-
/* Annotated, not inferred: `withOptionalRange` is generic, so an unannotated literal widens
|
|
1190
|
-
* `type` to `string` and stops matching the node union. */
|
|
1191
|
-
const node = {
|
|
1192
|
-
type: 'MustacheStatement',
|
|
1193
|
-
triple: token.triple,
|
|
1194
|
-
...statementBase(text, token, position, rangeOffset, token.content),
|
|
1195
|
-
};
|
|
1196
|
-
return (0, template_format_core_2.withOptionalRange)(node, rangeOffset + position, rangeOffset + token.end);
|
|
1197
|
-
}
|
|
1198
|
-
/**
|
|
1199
|
-
* The node for a token that stands on its own, or null for the three kinds - a block and the two
|
|
1200
|
-
* terminators - whose handling depends on where they appear.
|
|
1201
|
-
*
|
|
1202
|
-
* Every context that reads a mustache needs this dispatch: a program body, an attribute list,
|
|
1203
|
-
* the inside of a value. Written out three times, they had drifted at the recovery arms.
|
|
1204
|
-
*/
|
|
1205
|
-
function createStatement(text, token, position, rangeOffset) {
|
|
1206
|
-
const start = rangeOffset + position;
|
|
1207
|
-
const end = rangeOffset + token.end;
|
|
1208
|
-
if (token.kind === 'comment') {
|
|
1209
|
-
return createComment(token, start, end);
|
|
1210
|
-
}
|
|
1211
|
-
if (token.kind === 'partial') {
|
|
1212
|
-
const node = {
|
|
1213
|
-
type: 'PartialStatement',
|
|
1214
|
-
...statementBase(text, token, position, rangeOffset, token.content),
|
|
1215
|
-
};
|
|
1216
|
-
return (0, template_format_core_2.withOptionalRange)(node, start, end);
|
|
1217
|
-
}
|
|
1218
|
-
/* Before the mustache arm: a decorator is a mustache token carrying a `*`. */
|
|
1219
|
-
if (token.specialForm === 'decorator') {
|
|
1220
|
-
const node = {
|
|
1221
|
-
type: 'DecoratorStatement',
|
|
1222
|
-
...statementBase(text, token, position, rangeOffset, token.content.slice(1).trim()),
|
|
1223
|
-
};
|
|
1224
|
-
return (0, template_format_core_2.withOptionalRange)(node, start, end);
|
|
1225
|
-
}
|
|
1226
|
-
return token.kind === 'mustache' ? createMustache(text, token, position, rangeOffset) : null;
|
|
1227
|
-
}
|
|
1228
|
-
/**
|
|
1229
|
-
* A comment's body, with the tag's own `~` markers taken off. They are whitespace control, not
|
|
1230
|
-
* text: printing `rawContent` straight through emits them as body, turning `{{~! x ~}}` into
|
|
1231
|
-
* `{{! ~! x ~ }}` and dropping the stripping the author asked for.
|
|
1232
|
-
*/
|
|
1233
|
-
function commentBody(token) {
|
|
1234
|
-
let content = token.rawContent;
|
|
1235
|
-
if (token.trimOpen) {
|
|
1236
|
-
content = content.replace(/^([\t ]*)~/u, '$1');
|
|
1237
|
-
}
|
|
1238
|
-
/* A block comment's closing `~` follows the `--`, so it never reached `rawContent`. */
|
|
1239
|
-
if (token.trimClose) {
|
|
1240
|
-
content = content.replace(/~([\t ]*)$/u, '$1');
|
|
1241
|
-
}
|
|
1242
|
-
return content;
|
|
1243
|
-
}
|
|
1244
|
-
function createComment(token, start, end) {
|
|
1245
|
-
const content = commentBody(token);
|
|
1246
|
-
const isBlockStyle = /^\s*!-{2}/.test(content);
|
|
1247
|
-
/* express-hbs' layout directive is `{{!< name}}`, with nothing between the `!` and the `<`, so
|
|
1248
|
-
* the gap is what distinguishes it. Recognising it by body alone would print the ordinary
|
|
1249
|
-
* comment `{{! < name}}` as a directive, silently wrapping the page in a layout. */
|
|
1250
|
-
const isLayout = /^!<\s*\S/u.test(content.trim());
|
|
1251
|
-
/* Only a block comment's `--` is a marker. Stripping up to two dashes regardless cannot tell
|
|
1252
|
-
* it from a body that opens with one, which turns `{{!-foo}}` into `{{! foo }}`.
|
|
1253
|
-
*
|
|
1254
|
-
* Nothing is stripped from the end: the tokenizer stops before the closing delimiter already,
|
|
1255
|
-
* so doing it again would delete a `--` the author wrote. */
|
|
1256
|
-
const body = content.replace(isBlockStyle ? /^[\t ]*!--/u : /^[\t ]*!/u, '');
|
|
1257
|
-
/* Trailing whitespace comes off first. A space between `{{!--` and the newline is invisible in
|
|
1258
|
-
* the source and left the body not *starting* with one, which silently turned off the
|
|
1259
|
-
* re-indent below - so `{{!-- \n x\n--}}` and `{{!--\n x\n--}}` printed differently. */
|
|
1260
|
-
const trimmed = body.replace(/[ \t]+$/gm, '');
|
|
1261
|
-
/* A body the author started on its own line keeps its leading newline; the printer reads that
|
|
1262
|
-
* to decide whether to re-indent it. ASCII whitespace, not `\s`: a non-breaking space is
|
|
1263
|
-
* content the author put there, and `\s` deleted one off the front of a comment body. */
|
|
1264
|
-
const value = trimmed.startsWith('\n') ? trimmed : trimmed.replace(leadingWhitespace, '');
|
|
1265
|
-
const isMultiline = /\n/.test(content);
|
|
1266
|
-
return (0, template_format_core_2.withOptionalRange)({
|
|
1267
|
-
type: 'CommentStatement',
|
|
1268
|
-
value,
|
|
1269
|
-
multiline: isMultiline,
|
|
1270
|
-
block: isBlockStyle || isMultiline,
|
|
1271
|
-
...(isLayout ? { layout: true } : {}),
|
|
1272
|
-
...(token.trimOpen ? { trimOpen: true } : {}),
|
|
1273
|
-
...(token.trimClose ? { trimClose: true } : {}),
|
|
1274
|
-
}, start, end);
|
|
1275
|
-
}
|