@falsefalse/prettier-plugin-handlebars 0.0.2 → 0.0.5

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/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 template_format_core_1 = require("template-format-core");
39
- const template_format_core_2 = require("template-format-core");
40
- Object.defineProperty(exports, "locEnd", { enumerable: true, get: function () { return template_format_core_2.locEnd; } });
41
- Object.defineProperty(exports, "locStart", { enumerable: true, get: function () { return template_format_core_2.locStart; } });
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 { openDelimiter, isEscapedOpen, parseToken: parseMustacheToken, findNextOpen: findNextHandlebarsOpen, isDynamicElementStart: isDynamicTagStart, consumeRawBlock, getBlockExpression, getBlockPrefix, shouldPreserveTokenVerbatim: shouldPreserveMustacheVerbatim, } = tokens_1.handlebarsDialect;
48
+ const { parseToken: parseMustacheToken, findNextOpen: findNextHandlebarsOpen, getBlockExpression, getBlockPrefix, shouldPreserveTokenVerbatim: shouldPreserveMustacheVerbatim, } = tokens_1.handlebarsDialect;
59
49
  function parse(text) {
60
- const normalizedText = (0, template_format_core_2.normalizeInput)(text);
50
+ const normalizedText = (0, source_1.normalizeInput)(text);
61
51
  try {
62
52
  const { nodes } = parseChildren(normalizedText, 0, null, null);
63
- return (0, template_format_core_2.withRange)({ type: 'Program', body: nodes }, 0, normalizedText.length);
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 && template_format_core_1.rawTextElements.has(endTag.toLowerCase())) {
105
- const closeStart = findRawTextClose(text, pos, endTag);
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 token = parseMustacheToken(text, pos);
152
- if (!token.terminated) {
153
- const [open, close] = (0, tokens_1.isHandlebarsBlockComment)(text, pos)
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
- if (text.startsWith('<!', pos) && !text.startsWith('<!--', pos)) {
221
- const closeIdx = text.indexOf('>', pos + 2);
222
- /* Unterminated, so the declaration runs to the end of the input - but its trailing
223
- * whitespace is still the author's. Folding that into the verbatim run makes the
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, template_format_core_2.withRange)({ type: 'TextNode', chars: text.slice(pos, nextMarkup) }, rangeOffset + pos, rangeOffset + nextMarkup));
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
- function hasMatchingBlockEnd(text, token) {
307
- return findMatchingBlockEnd(text, token) !== null;
308
- }
309
- /**
310
- * The mustaches in `text` from `from` onwards, minus those inside a `{{{{raw}}}}` body:
311
- * Handlebars does not parse one, so a `{{#if}}` in there opens nothing.
312
- *
313
- * Deliberately does *not* skip HTML comments or `<script>`: Handlebars has no idea what HTML is
314
- * and rejects `{{#if a}}<!-- {{#if b}} -->{{/if}}`, so these scans must see that `{{#if b}}`.
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
- /** Where the block opened by `token` closes, or null if it never does. */
334
- function findMatchingBlockEnd(text, token) {
335
- if (!token.name) {
336
- return null;
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
- let depth = 0;
339
- /* From the end of the opening tag, which the token already knows - a caller passing a start
340
- * position instead would have `findNextHandlebarsOpen` land on a `{{` inside the tag's own
341
- * string literal, reading `{{#if (eq a "{{")}}` as a mustache that never closes. */
342
- for (const candidate of mustachesFrom(text, token.end)) {
343
- if (candidate.kind === 'blockStart' && candidate.name === token.name) {
344
- depth += 1;
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
- depth -= 1;
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
- return null;
354
- }
355
- function parseBlock(text, token, rangeOffset = 0) {
356
- const blockExpression = getBlockExpression(token);
357
- const openInfo = (0, expression_1.parseCall)(blockExpression, rangeOffset + contentOffset(text, token.start, token.end, blockExpression));
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
- const { nodes: program, position: afterProgram, endReason, endToken } = parseChildren(text, token.end, null, openInfo.path.source, rangeOffset);
360
- const buildProgram = (nodes, start, end) => (0, template_format_core_2.withRange)({ type: 'Program', body: nodes }, rangeOffset + start, rangeOffset + end);
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 inverseBody;
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 branchExpressionText = currentElseToken.content.replace(/^else\s+/, '');
377
- const branchExpression = (0, expression_1.parseCall)(branchExpressionText, rangeOffset + contentOffset(text, currentElseToken.start, currentElseToken.end, branchExpressionText));
378
- const { nodes: branchNodes, position: afterBranch, endReason: branchEndReason, endToken: branchEndToken, } = parseChildren(text, currentPosition, null, openInfo.path.source, rangeOffset);
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
- inverseTrimOpen = currentElseToken.trimOpen;
397
- inverseTrimClose = currentElseToken.trimClose;
398
- const { nodes: inverseNodes, position: afterInverse, endReason: inverseEndReason, endToken: inverseEndToken, } = parseChildren(text, currentPosition, null, openInfo.path.source, rangeOffset);
399
- inverseBody = buildProgram(inverseNodes, currentElseToken.end, inverseEndToken?.start ?? afterInverse);
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, template_format_core_2.withRange)({
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: inverseBody ?? buildProgram([], closerAnchor, closerAnchor),
413
- ...(inverseTrimOpen ? { inverseTrimOpen } : {}),
414
- ...(inverseTrimClose ? { inverseTrimClose } : {}),
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 directive has to *be* the comment, not appear somewhere inside it: on `includes`, a
426
- * comment merely mentioning `prettier-ignore` would silently suppress the next node, and one
427
- * mentioning `prettier-ignore-start` would open a region.
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 getPrettierIgnoreDirective(rawContent) {
430
- switch (rawContent.toLowerCase().replace(/^\s*!(?:-{2})?/u, '').trim()) {
431
- case 'prettier-ignore-start':
432
- return 'start';
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
- function findPrettierIgnoreEnd(text, position) {
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, template_format_core_2.withRange)(marked, rangeOffset + attrStart, rangeOffset + end));
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
- const statement = createStatement(text, token, pos, rangeOffset);
610
- if (statement) {
611
- add({ type: 'AttributeBlock', block: statement }, token.end);
612
- pos = token.end;
613
- continue;
614
- }
615
- if (token.kind === 'blockStart' && hasMatchingBlockEnd(text, token)) {
616
- const { node, next } = parseBlock(text, token, rangeOffset);
617
- add({ type: 'AttributeBlock', block: node }, next);
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
- if (text[pos] === '/' && text[pos + 1] === '>') {
425
+ const selfClosed = text[pos] === '/' && text[pos + 1] === '>';
426
+ if (selfClosed || text[pos] === '>') {
630
427
  const headEnd = pos;
631
- pos += 2;
632
- return { kind: 'selfClosing', tag, attributes, attributesRange: span(headEnd), end: pos, terminated: true };
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
- const kind = template_format_core_1.voidElements.has(tag.toLowerCase()) ? 'selfClosing' : 'open';
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 (!template_format_core_1.voidElements.has(tag.toLowerCase())) {
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 = 0) {
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
- let rawValue = '';
742
- let valueStart = pos;
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 == null) {
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
- return {
849
- type: 'Attribute',
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 = 0) {
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, template_format_core_2.withRange)({ type: 'TextNode', chars: value.slice(pos, rawBlockEnd) }, rangeOffset + pos, rangeOffset + rawBlockEnd));
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
- parts.push((0, template_format_core_2.withRange)({ type: 'TextNode', chars: value.slice(pos, token.end) }, rangeOffset + pos, rangeOffset + token.end));
890
- pos = token.end;
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, template_format_core_2.withRange)({ type: 'TextNode', chars: rawText }, rangeOffset + pos, rangeOffset + end));
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
- }