@falsefalse/prettier-plugin-handlebars 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/parser.js ADDED
@@ -0,0 +1,1275 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.locStart = exports.locEnd = void 0;
37
+ 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; } });
42
+ const expression_1 = require("./expression");
43
+ const scan_1 = require("./scan");
44
+ const errors_1 = require("./errors");
45
+ const whitespace = __importStar(require("./whitespace"));
46
+ 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
+ /* Destructured rather than wrapped: seven of these had a one-line function around them whose
57
+ * 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;
59
+ function parse(text) {
60
+ const normalizedText = (0, template_format_core_2.normalizeInput)(text);
61
+ try {
62
+ const { nodes } = parseChildren(normalizedText, 0, null, null);
63
+ return (0, template_format_core_2.withRange)({ type: 'Program', body: nodes }, 0, normalizedText.length);
64
+ }
65
+ catch (error) {
66
+ /* Offsets become line and column here, where the whole text is still in hand. */
67
+ throw error instanceof errors_1.TemplateSyntaxError ? error.locate(normalizedText) : error;
68
+ }
69
+ }
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
+ /* The dialect reports an unterminated token as one that ends at EOF, which is also what a token
80
+ * 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
+ function parseChildren(text, position, endTag, endBlock, rangeOffset = 0) {
102
+ const nodes = [];
103
+ 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 };
122
+ }
123
+ /* The current block's terminator does not move while this call runs, and every position the
124
+ * loop reaches is at depth 0 inside it, so it is hoisted: recomputing it per open tag is
125
+ * quadratic in the number of mustaches in the block's body. */
126
+ const blockBoundary = endBlock ? findCurrentBlockBoundary(text, pos, endBlock) : -1;
127
+ while (pos < text.length) {
128
+ const rawBlockEnd = consumeTerminatedRawBlock(text, pos, rangeOffset);
129
+ if (rawBlockEnd !== null) {
130
+ nodes.push(createUnmatchedNode(text, pos, rawBlockEnd, rangeOffset));
131
+ pos = rawBlockEnd;
132
+ continue;
133
+ }
134
+ const dynamicElementEnd = consumeDynamicElement(text, pos);
135
+ if (dynamicElementEnd !== null) {
136
+ nodes.push(createUnmatchedNode(text, pos, dynamicElementEnd, rangeOffset));
137
+ pos = dynamicElementEnd;
138
+ continue;
139
+ }
140
+ if (endTag && startsCloseTag(text, pos, endTag)) {
141
+ const contentEnd = pos;
142
+ const closeIdx = text.indexOf('>', pos);
143
+ if (closeIdx < 0) {
144
+ fail("unterminated tag: expected '>'", rangeOffset + pos, rangeOffset + text.length);
145
+ }
146
+ const closeTag = readCloseTagSource(text, pos, closeIdx);
147
+ pos = closeIdx + 1;
148
+ return { nodes, position: pos, endReason: 'tagClose', contentEnd, closeTag };
149
+ }
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;
217
+ continue;
218
+ }
219
+ 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;
294
+ continue;
295
+ }
296
+ /* Text node until the next markup. The run is kept verbatim, whitespace-only runs
297
+ * included: what renders is the printer's to decide, not the parser's to discard. */
298
+ const nextMarkup = findNextMarkup(text, pos);
299
+ if (nextMarkup > pos) {
300
+ nodes.push((0, template_format_core_2.withRange)({ type: 'TextNode', chars: text.slice(pos, nextMarkup) }, rangeOffset + pos, rangeOffset + nextMarkup));
301
+ }
302
+ pos = nextMarkup;
303
+ }
304
+ return { nodes, position: pos, endReason: null };
305
+ }
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;
331
+ }
332
+ }
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;
337
+ }
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;
349
+ }
350
+ depth -= 1;
351
+ }
352
+ }
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));
358
+ 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);
361
+ /* A program ends where its terminator begins, not after it, so the body tiles the range. */
362
+ const programBody = buildProgram(program, token.end, endToken?.start ?? afterProgram);
363
+ /* Set only when the author wrote a bare `{{else}}`; otherwise the empty inverse is built at
364
+ * the end, once the closer's position is known. Anchoring it at `afterProgram` up here put it
365
+ * inside the else-if chain - a point belonging to a different section of the block. */
366
+ let inverseBody;
367
+ const inverseChain = [];
368
+ let finalPos = afterProgram;
369
+ let closeToken = endReason === 'blockEnd' ? endToken : undefined;
370
+ let inverseTrimOpen = false;
371
+ let inverseTrimClose = false;
372
+ if (endReason === 'else' && endToken) {
373
+ let currentElseToken = endToken;
374
+ let currentPosition = afterProgram;
375
+ 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)({
380
+ type: 'ElseBranch',
381
+ program: buildProgram(branchNodes, currentElseToken.end, branchEndToken?.start ?? afterBranch),
382
+ trimOpen: currentElseToken.trimOpen,
383
+ trimClose: currentElseToken.trimClose,
384
+ ...branchExpression,
385
+ }, rangeOffset + currentElseToken.start, rangeOffset + afterBranch));
386
+ finalPos = afterBranch;
387
+ closeToken = branchEndReason === 'blockEnd' ? branchEndToken : undefined;
388
+ if (branchEndReason === 'else' && branchEndToken) {
389
+ currentElseToken = branchEndToken;
390
+ currentPosition = afterBranch;
391
+ continue;
392
+ }
393
+ currentElseToken = undefined;
394
+ }
395
+ 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);
400
+ finalPos = afterInverse;
401
+ closeToken = inverseEndReason === 'blockEnd' ? inverseEndToken : undefined;
402
+ }
403
+ }
404
+ const closerAnchor = closeToken?.start ?? finalPos;
405
+ const node = (0, template_format_core_2.withRange)({
406
+ type: 'BlockStatement',
407
+ program: programBody,
408
+ ...(inverseChain.length > 0 ? { inverseChain } : {}),
409
+ /* An empty inverse sits where the block's closer starts: after every branch, before
410
+ * `{{/if}}`. It is a zero-width point, so it has to be a position the block actually
411
+ * owns. */
412
+ inverse: inverseBody ?? buildProgram([], closerAnchor, closerAnchor),
413
+ ...(inverseTrimOpen ? { inverseTrimOpen } : {}),
414
+ ...(inverseTrimClose ? { inverseTrimClose } : {}),
415
+ blockPrefix,
416
+ trimOpen: token.trimOpen,
417
+ trimClose: token.trimClose,
418
+ closeTrimOpen: closeToken?.trimOpen,
419
+ closeTrimClose: closeToken?.trimClose,
420
+ ...openInfo,
421
+ }, rangeOffset + token.start, rangeOffset + finalPos);
422
+ return { node, next: finalPos, closed: Boolean(closeToken) };
423
+ }
424
+ /**
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.
428
+ */
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;
439
+ }
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
+ }
448
+ }
449
+ return null;
450
+ }
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) {
562
+ let pos = position + 1; // skip '<'
563
+ if (text[pos] === '/') {
564
+ pos += 1;
565
+ const { value: tag, next } = readName(text, pos);
566
+ const closeIdx = text.indexOf('>', next);
567
+ return {
568
+ kind: 'close',
569
+ tag,
570
+ source: readCloseTagSource(text, position, closeIdx),
571
+ end: closeIdx >= 0 ? closeIdx + 1 : text.length,
572
+ terminated: closeIdx >= 0,
573
+ };
574
+ }
575
+ const { value: tag, next } = readName(text, pos);
576
+ pos = next;
577
+ const attributes = [];
578
+ const headStart = pos;
579
+ const span = (headEnd) => [rangeOffset + headStart, rangeOffset + headEnd];
580
+ let glued = false;
581
+ let attrStart = pos;
582
+ /* `glued` is whether the author left a space before this attribute. That includes the first
583
+ * one: running into the tag name is what makes `<h{{level}}>` a heading rather than an `<h>`
584
+ * with an attribute. The span is what lets `findTilingViolations` see that an attribute
585
+ * accounts for all of the source it was read from. */
586
+ const add = (attribute, end) => {
587
+ const marked = glued ? { ...attribute, glued: true } : attribute;
588
+ attributes.push((0, template_format_core_2.withRange)(marked, rangeOffset + attrStart, rangeOffset + end));
589
+ };
590
+ while (pos < text.length) {
591
+ pos = skipWhitespace(text, pos);
592
+ /* Trailing whitespace can run out the input. Falling through would ask `parseAttribute` to
593
+ * read past the end and report `unexpected undefined`, when the tag is simply unterminated. */
594
+ if (pos >= text.length) {
595
+ break;
596
+ }
597
+ /* Look at the character before the attribute rather than at whether whitespace was skipped
598
+ * here: some of the attribute readers consume their own trailing space. */
599
+ glued = pos > 0 && !whitespace.html.test(text[pos - 1]);
600
+ attrStart = pos;
601
+ const dynamicAttribute = parseDynamicAttribute(text, pos);
602
+ if (dynamicAttribute) {
603
+ add(dynamicAttribute.attribute, dynamicAttribute.position);
604
+ pos = dynamicAttribute.position;
605
+ continue;
606
+ }
607
+ if (startsTemplateTag(text, pos)) {
608
+ 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;
627
+ continue;
628
+ }
629
+ if (text[pos] === '/' && text[pos + 1] === '>') {
630
+ 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 };
639
+ }
640
+ const attr = parseAttribute(text, pos, rangeOffset);
641
+ /* Every remaining character is one an attribute name may start with, so there is nothing
642
+ * left to skip over - and skipping is what quietly deleted the author's markup. */
643
+ if (!attr) {
644
+ fail(`unexpected ${text[pos]} in <${tag}>: expected an attribute name or '>'`, rangeOffset + pos, rangeOffset + pos + 1);
645
+ }
646
+ add(attr.attribute, attr.position);
647
+ pos = attr.position;
648
+ }
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 };
651
+ }
652
+ function consumeInvalidVoidElementClose(text, position, tag) {
653
+ if (!template_format_core_1.voidElements.has(tag.toLowerCase())) {
654
+ return null;
655
+ }
656
+ const afterGap = skipWhitespace(text, position);
657
+ if (!text.startsWith('</', afterGap)) {
658
+ return null;
659
+ }
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;
725
+ }
726
+ function parseAttribute(text, position, rangeOffset = 0) {
727
+ let pos = position;
728
+ pos = skipWhitespace(text, pos);
729
+ const { value: name, next } = readAttributeName(text, pos);
730
+ pos = next;
731
+ if (!name) {
732
+ return null;
733
+ }
734
+ pos = skipWhitespace(text, pos);
735
+ // a boolean attribute: no "="
736
+ if (text[pos] !== '=') {
737
+ return { attribute: createAttribute(name, null), position: pos };
738
+ }
739
+ 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
+ }
757
+ /* A value holding both quote characters cannot be printed: whichever one the printer wraps it
758
+ * in ends the attribute early: `title=a"b'c` would print as `title='a"b'c'`, which HTML reads
759
+ * as two attributes. The reader skips over mustaches to find the closing quote, so it accepts
760
+ * values like `class="{{t 'a' "b"}}"` that a browser would cut short. */
761
+ if (rawValue.includes('"') && rawValue.includes("'")) {
762
+ fail('attribute value cannot contain both quote characters', rangeOffset + valueStart, rangeOffset + pos);
763
+ }
764
+ return { attribute: createAttribute(name, rawValue, rangeOffset + valueStart), position: pos };
765
+ }
766
+ function parseDynamicAttribute(text, position) {
767
+ let pos = position;
768
+ pos = skipWhitespace(text, pos);
769
+ const start = pos;
770
+ let hasDynamicPart = false;
771
+ let hasStaticPart = false;
772
+ while (pos < text.length) {
773
+ if (startsTemplateTag(text, pos)) {
774
+ const token = parseMustacheToken(text, pos);
775
+ /* A block in the middle of a name is part of the name, so it is consumed whole rather
776
+ * than refused. Unbalanced it is not a name at all, and the caller's error is better
777
+ * than a guess at where it ends. */
778
+ if (token.kind === 'blockStart') {
779
+ const blockEnd = findMatchingBlockEnd(text, token);
780
+ if (blockEnd === null) {
781
+ return null;
782
+ }
783
+ hasDynamicPart = true;
784
+ pos = blockEnd;
785
+ continue;
786
+ }
787
+ if (token.kind !== 'mustache') {
788
+ return null;
789
+ }
790
+ hasDynamicPart = true;
791
+ pos = token.end;
792
+ continue;
793
+ }
794
+ if (attributeNameCharacter.test(text[pos])) {
795
+ hasStaticPart = true;
796
+ pos += 1;
797
+ continue;
798
+ }
799
+ break;
800
+ }
801
+ if (!hasDynamicPart) {
802
+ return null;
803
+ }
804
+ const nameEnd = pos;
805
+ const afterName = skipWhitespace(text, pos);
806
+ if (text[afterName] !== '=') {
807
+ /* A static part is what makes this a name with a mustache in it rather than a mustache
808
+ * standing alone. Without one the caller's model is the better fit - `{{attrs}}` is an
809
+ * `AttributeMustache` and `{{#if a}}class="x"{{/if}}` an `AttributeBlock` wrapping whole
810
+ * attributes, whose body is worth formatting - so hand it back. */
811
+ if (!hasStaticPart) {
812
+ return null;
813
+ }
814
+ return {
815
+ attribute: createRawAttribute(text.slice(start, nameEnd)),
816
+ position: nameEnd,
817
+ };
818
+ }
819
+ /* A value overrides that: it attaches to the composite name, and once the name is split
820
+ * 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
+ }
830
+ return {
831
+ attribute: createRawAttribute(text.slice(start, pos)),
832
+ position: pos,
833
+ };
834
+ }
835
+ function createAttribute(name, rawValue, valueStart) {
836
+ if (rawValue == null) {
837
+ return {
838
+ type: 'Attribute',
839
+ name,
840
+ value: null,
841
+ };
842
+ }
843
+ const value = {
844
+ type: 'AttributeValue',
845
+ parts: parseAttributeValueParts(rawValue, valueStart ?? 0),
846
+ raw: rawValue,
847
+ };
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
+ };
853
+ }
854
+ function createRawAttribute(raw) {
855
+ return {
856
+ type: 'RawAttribute',
857
+ raw,
858
+ };
859
+ }
860
+ function parseAttributeValueParts(value, rangeOffset = 0) {
861
+ const parts = [];
862
+ let pos = 0;
863
+ while (pos < value.length) {
864
+ /* A raw block's body is emitted literally by Handlebars, so it is copied through here for
865
+ * the same reason it is between siblings: reformatting the `{{ x }}` inside one changes
866
+ * what the value renders. Only the sibling list guarded this. */
867
+ const rawBlockEnd = consumeTerminatedRawBlock(value, pos, rangeOffset);
868
+ if (rawBlockEnd !== null) {
869
+ parts.push((0, template_format_core_2.withRange)({ type: 'TextNode', chars: value.slice(pos, rawBlockEnd) }, rangeOffset + pos, rangeOffset + rawBlockEnd));
870
+ pos = rawBlockEnd;
871
+ continue;
872
+ }
873
+ if (startsTemplateTag(value, pos)) {
874
+ 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
+ /* A value is a string, so the recovery here keeps the source as text rather than as a
888
+ * 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;
891
+ continue;
892
+ }
893
+ const next = findNextHandlebarsOpen(value, pos);
894
+ const end = next === -1 ? value.length : next;
895
+ const rawText = value.slice(pos, end);
896
+ if (rawText.length > 0) {
897
+ parts.push((0, template_format_core_2.withRange)({ type: 'TextNode', chars: rawText }, rangeOffset + pos, rangeOffset + end));
898
+ }
899
+ pos = end;
900
+ }
901
+ preserveValueWhitespace(parts);
902
+ return parts;
903
+ }
904
+ /**
905
+ * An attribute value is a string, so every space in it renders - including the spaces inside a
906
+ * block's body. Without this the printer lays that body out at its own indent level, which is
907
+ * unrelated to the column the value sits at, and rewrites whitespace the author owns.
908
+ */
909
+ function preserveValueWhitespace(nodes) {
910
+ for (const node of nodes) {
911
+ if (node.type === 'TextNode') {
912
+ node.preserveWhitespace = true;
913
+ }
914
+ else if (node.type === 'BlockStatement') {
915
+ preserveValueWhitespace(node.program.body);
916
+ (node.inverseChain ?? []).forEach((branch) => preserveValueWhitespace(branch.program.body));
917
+ preserveValueWhitespace(node.inverse.body);
918
+ }
919
+ else if (node.type === 'ElementNode') {
920
+ node.preserveWhitespace = true;
921
+ preserveValueWhitespace(node.children);
922
+ }
923
+ else if (node.type === 'UnmatchedNode') {
924
+ node.preserveWhitespace = true;
925
+ }
926
+ }
927
+ }
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
+ }