@falsefalse/prettier-plugin-handlebars 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,274 @@
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.tagNameTerminator = exports.attributeNameCharacter = exports.leadingWhitespace = exports.parseMustacheToken = exports.isEscapedOpen = exports.openDelimiter = void 0;
37
+ exports.startsTemplateTag = startsTemplateTag;
38
+ exports.skipTemplateTag = skipTemplateTag;
39
+ exports.scanTag = scanTag;
40
+ exports.sameTag = sameTag;
41
+ exports.startsCloseTag = startsCloseTag;
42
+ exports.readCloseTagSource = readCloseTagSource;
43
+ exports.trimTrailingWhitespace = trimTrailingWhitespace;
44
+ exports.isTagStart = isTagStart;
45
+ exports.readAttributeValue = readAttributeValue;
46
+ exports.skipWhitespace = skipWhitespace;
47
+ exports.readAttributeName = readAttributeName;
48
+ exports.readName = readName;
49
+ exports.consumeTagLikeChunk = consumeTagLikeChunk;
50
+ /* Character-level readers: where a tag, a name or a value ends. Nothing here builds a node or
51
+ * knows what a block is, so everything above can be read without this file open. */
52
+ const html_1 = require("../core/html");
53
+ const scan_1 = require("../core/scan");
54
+ const whitespace = __importStar(require("../core/whitespace"));
55
+ const tokens_1 = require("../dialects/handlebars/tokens");
56
+ ({ openDelimiter: exports.openDelimiter, isEscapedOpen: exports.isEscapedOpen, parseToken: exports.parseMustacheToken } = tokens_1.handlebarsDialect);
57
+ /* Built from the shared class so the character list stays written in one place. */
58
+ exports.leadingWhitespace = new RegExp(`^${whitespace.htmlRun.source}`, 'u');
59
+ /* HTML's lexical classes, composed from the whitespace list rather than repeating it - both
60
+ * embed it, and a second hand-written copy is what `whitespace.ts` exists to prevent. They
61
+ * live here because the tokenizer below is the only thing that reads them. */
62
+ /** What an attribute name is made of: anything but whitespace and the characters that end one. */
63
+ exports.attributeNameCharacter = new RegExp(`[^${whitespace.htmlCharacters}"'<>/=]`, 'u');
64
+ /** What ends a tag name. HTML's tag-name state leaves on whitespace, `/` or `>`, and nothing else. */
65
+ exports.tagNameTerminator = new RegExp(`[${whitespace.htmlCharacters}/>]`, 'u');
66
+ function startsTemplateTag(text, position) {
67
+ return text.startsWith(exports.openDelimiter, position) && !(0, exports.isEscapedOpen)(text, position);
68
+ }
69
+ /**
70
+ * Where a tag ends, what it is called and whether it closed - without building a single node and
71
+ * without rejecting anything.
72
+ *
73
+ * Lookahead has to be total: callers scan regions they may go on to skip, including a
74
+ * `{{! prettier-ignore }}` body, so a `parseTag` here let the directive reject the very file it
75
+ * was written to protect. `terminated` is false when the tag ran to EOF, which is also how an
76
+ * unterminated attribute value shows up.
77
+ */
78
+ /**
79
+ * Past the mustache at `position`. A token reporting an end at or before where it started would
80
+ * leave the caller's loop standing still, so the opening delimiter is the smallest step taken.
81
+ */
82
+ function skipTemplateTag(text, position) {
83
+ return Math.max((0, exports.parseMustacheToken)(text, position).end, position + 2);
84
+ }
85
+ function scanTag(text, position) {
86
+ let pos = position + 1;
87
+ const closing = text[pos] === '/';
88
+ if (closing) {
89
+ pos += 1;
90
+ }
91
+ const { value: tag, next } = readName(text, pos);
92
+ pos = next;
93
+ const kindAt = (selfClosed) => {
94
+ if (closing) {
95
+ return 'close';
96
+ }
97
+ return selfClosed || (0, html_1.isVoidElement)(tag) ? 'selfClosing' : 'open';
98
+ };
99
+ /* A quote only delimits a value directly after `=`, whitespace aside. Treating every quote as
100
+ * a delimiter would make `title=a"b'c>` swallow the rest of the file hunting a closing `"`. */
101
+ let afterEquals = false;
102
+ while (pos < text.length) {
103
+ if (startsTemplateTag(text, pos)) {
104
+ pos = skipTemplateTag(text, pos);
105
+ continue;
106
+ }
107
+ const char = text[pos];
108
+ if (whitespace.html.test(char)) {
109
+ pos += 1;
110
+ continue;
111
+ }
112
+ if (char === '=') {
113
+ afterEquals = true;
114
+ pos += 1;
115
+ continue;
116
+ }
117
+ if (afterEquals && char !== '>') {
118
+ pos = readAttributeValue(text, pos).end;
119
+ afterEquals = false;
120
+ continue;
121
+ }
122
+ if (isSelfClosingSlash(text, pos)) {
123
+ return { kind: kindAt(true), tag, end: pos + 2, terminated: true };
124
+ }
125
+ if (char === '>') {
126
+ return { kind: kindAt(false), tag, end: pos + 1, terminated: true };
127
+ }
128
+ afterEquals = false;
129
+ pos += 1;
130
+ }
131
+ return { kind: kindAt(false), tag, end: pos, terminated: false };
132
+ }
133
+ /* HTML tag names are case-insensitive, so `<DIV>x</div>` is one element. Comparing them
134
+ * verbatim rejected it as unclosed, while the `voidElements` and `rawTextElements` lookups two
135
+ * lines away had been lowercasing all along. */
136
+ function sameTag(one, other) {
137
+ return one.toLowerCase() === other.toLowerCase();
138
+ }
139
+ /**
140
+ * Whether a close tag for exactly `tag` starts here.
141
+ *
142
+ * The name has to end where `tag` does. On a prefix comparison `</bdi>` would close a `<b>`,
143
+ * deleting `di` from the source and pointing any error at the next, well-formed close tag.
144
+ */
145
+ function startsCloseTag(text, position, tag) {
146
+ if (!text.startsWith('</', position)) {
147
+ return false;
148
+ }
149
+ const { value: name, next } = readName(text, position + 2);
150
+ return sameTag(name, tag) && (next >= text.length || exports.tagNameTerminator.test(text[next]));
151
+ }
152
+ /* Everything between `</` and `>`. HTML keeps only the name and throws the rest away, but it is
153
+ * still the author's source: `</h{{level}}>` has to come back out spelled that way. Whitespace
154
+ * runs collapse so a close tag can never put a raw newline into a doc. */
155
+ function readCloseTagSource(text, position, closeIdx) {
156
+ return text
157
+ .slice(position + 2, closeIdx >= 0 ? closeIdx : text.length)
158
+ .trim()
159
+ .replace(whitespace.htmlRunGlobal, ' ');
160
+ }
161
+ /* One past the last non-whitespace character, leaving the author's trailing whitespace to the
162
+ * caller instead of burying it inside a node that prints verbatim. */
163
+ function trimTrailingWhitespace(text, from) {
164
+ let end = text.length;
165
+ while (end > from && whitespace.html.test(text[end - 1])) {
166
+ end -= 1;
167
+ }
168
+ return end;
169
+ }
170
+ function isTagStart(text, position) {
171
+ if (text[position] !== '<') {
172
+ return false;
173
+ }
174
+ return /[A-Za-z!/]/u.test(text[position + 1] ?? '');
175
+ }
176
+ /**
177
+ * Where an unquoted attribute value ends. HTML's unquoted-value state ends at whitespace or `>`
178
+ * and nowhere else, so a `/` is content: breaking on it would drop the trailing slash of
179
+ * `src=/a/b/` and make `<a href=/path/>t</a>` a self-closing `<a>` that rejects its own `</a>`.
180
+ * `scanTag` reads values with this too, so its idea of where a tag ends matches the parser's;
181
+ * were they to disagree, a `{{! prettier-ignore }}` region could stop mid-tag.
182
+ */
183
+ function readUnquotedValueEnd(text, position) {
184
+ let pos = position;
185
+ while (pos < text.length && text[pos] !== '>' && !whitespace.html.test(text[pos])) {
186
+ if (startsTemplateTag(text, pos)) {
187
+ pos = skipTemplateTag(text, pos);
188
+ continue;
189
+ }
190
+ pos += 1;
191
+ }
192
+ return pos;
193
+ }
194
+ /**
195
+ * An attribute value's extent and its text, whichever way it was written. The quotes are not
196
+ * part of the value, so `start` is inside them - getting that `+ 1` wrong shifts every range a
197
+ * mustache inside the value reports.
198
+ */
199
+ function readAttributeValue(text, position) {
200
+ const quote = text[position];
201
+ if (quote === '"' || quote === "'") {
202
+ const quoted = readQuotedAttributeValue(text, position + 1, quote);
203
+ return { raw: quoted.value, start: position + 1, end: quoted.position };
204
+ }
205
+ const end = readUnquotedValueEnd(text, position);
206
+ return { raw: text.slice(position, end), start: position, end };
207
+ }
208
+ function readQuotedAttributeValue(text, position, quote) {
209
+ let pos = position;
210
+ while (pos < text.length) {
211
+ if (startsTemplateTag(text, pos)) {
212
+ pos = skipTemplateTag(text, pos);
213
+ continue;
214
+ }
215
+ if (text[pos] === quote) {
216
+ return { value: text.slice(position, pos), position: pos + 1 };
217
+ }
218
+ pos += 1;
219
+ }
220
+ return { value: text.slice(position), position: text.length };
221
+ }
222
+ /* One past the whitespace run starting at `position`. Every caller wants an index, and taking
223
+ * one instead of a pair of closures is what let the open-coded copies of this loop go. */
224
+ function skipWhitespace(text, position) {
225
+ let pos = position;
226
+ while (pos < text.length && whitespace.html.test(text[pos])) {
227
+ pos += 1;
228
+ }
229
+ return pos;
230
+ }
231
+ /**
232
+ * HTML's attribute-name state ends at whitespace, `/`, `>` or `=`, and nowhere else.
233
+ *
234
+ * Matching a tag-name charset instead stepped over one character and carried on: `@click` came
235
+ * back as `click` and `(click)="go()"` as two boolean attributes, value gone, silently.
236
+ */
237
+ /* Stops at a mustache as well as at the characters HTML ends a name on. `parseDynamicAttribute`
238
+ * has already had its go by the time this runs, so what is left is a block or a partial glued to
239
+ * the name - `<div data-{{#if a}}x{{/if}}>`. Reading `data-{{#if` as the name desynchronised the
240
+ * tag loop, which then reported the `/` of `{{/if}}` as an unexpected character. Left here, the
241
+ * tag loop takes the block as its own glued attribute and the two print back together. */
242
+ function readAttributeName(text, position) {
243
+ let pos = position;
244
+ while (pos < text.length && exports.attributeNameCharacter.test(text[pos]) && !startsTemplateTag(text, pos)) {
245
+ pos += 1;
246
+ }
247
+ return { value: text.slice(position, pos), next: pos };
248
+ }
249
+ function readName(text, position) {
250
+ let pos = position;
251
+ while (pos < text.length && /[A-Za-z0-9_:-]/.test(text[pos])) {
252
+ pos += 1;
253
+ }
254
+ return { value: text.slice(position, pos), next: pos };
255
+ }
256
+ function isSelfClosingSlash(text, position) {
257
+ return text[position] === '/' && text[position + 1] === '>';
258
+ }
259
+ /** Whether the character before `index`, whitespace aside, is `=`. */
260
+ function follows(text, index, char) {
261
+ let at = index - 1;
262
+ while (at >= 0 && whitespace.html.test(text[at]))
263
+ at -= 1;
264
+ return text[at] === char;
265
+ }
266
+ function consumeTagLikeChunk(text, position) {
267
+ /* Same rule as a real tag head: a quote delimits a value only after `=`. `<{{t}} a=it's>`
268
+ * otherwise runs to EOF and swallows the rest of the file into one verbatim node. */
269
+ const end = (0, scan_1.scanPastQuotes)(text, position + 1, {
270
+ stopsAt: (index) => text[index] === '>',
271
+ opensQuote: (index) => follows(text, index, '='),
272
+ });
273
+ return end === -1 ? text.length : end + 1;
274
+ }
@@ -0,0 +1,35 @@
1
+ import type { HandlebarsToken as MustacheToken } from '../dialects/handlebars/tokens';
2
+ export declare const openDelimiter: string, parseMustacheToken: (text: string, position: number) => MustacheToken, findNextHandlebarsOpen: (text: string, position: number) => number, isDynamicTagStart: (text: string, position: number) => boolean, consumeRawBlock: (text: string, position: number) => number | null;
3
+ export declare function consumeTerminatedRawBlock(text: string, position: number, rangeOffset: number): number | null;
4
+ export declare function hasMatchingBlockEnd(text: string, token: MustacheToken): boolean;
5
+ /**
6
+ * The mustaches in `text` from `from` onwards, minus those inside a `{{{{raw}}}}` body:
7
+ * Handlebars does not parse one, so a `{{#if}}` in there opens nothing.
8
+ *
9
+ * Deliberately does *not* skip HTML comments or `<script>`: Handlebars has no idea what HTML is
10
+ * and rejects `{{#if a}}<!-- {{#if b}} -->{{/if}}`, so these scans must see that `{{#if b}}`.
11
+ */
12
+ export declare function mustachesFrom(text: string, from: number): Generator<MustacheToken>;
13
+ /** Where the block opened by `token` closes, or null if it never does. */
14
+ export declare function findMatchingBlockEnd(text: string, token: MustacheToken): number | null;
15
+ /**
16
+ * How far `{{! prettier-ignore }}` reaches: to the end of the one node that follows it, or
17
+ * nowhere if that node's extent cannot be determined.
18
+ *
19
+ * It scans rather than parses: a nested `parseChildren` would run past the enclosing container,
20
+ * handing an element its own `</div>`, and could `fail()` - leaving a directive meant to
21
+ * suppress formatting able to reject the file. `position` means "nothing to ignore".
22
+ */
23
+ export declare function consumeNextNode(text: string, position: number): number;
24
+ export declare function findNextMarkup(text: string, position: number): number;
25
+ export declare function findCurrentBlockBoundary(text: string, position: number, endBlock: string): number;
26
+ export declare function findMatchingTagClose(text: string, tag: string, position: number, limit?: number): number | null;
27
+ /**
28
+ * Raw text ends at the first `</tag`, whatever it appears to sit inside.
29
+ *
30
+ * A browser's tokenizer does not parse the script or style body looking for string literals -
31
+ * that is exactly why `"<\\/script>"` has to be escaped in JS. Tracking quotes here instead would
32
+ * let an apostrophe in a comment hide the closing tag.
33
+ */
34
+ export declare function findRawTextClose(text: string, position: number, tag: string): number;
35
+ export declare function consumeDynamicElement(text: string, position: number): number | null;
@@ -0,0 +1,305 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.consumeRawBlock = exports.isDynamicTagStart = exports.findNextHandlebarsOpen = exports.parseMustacheToken = exports.openDelimiter = void 0;
4
+ exports.consumeTerminatedRawBlock = consumeTerminatedRawBlock;
5
+ exports.hasMatchingBlockEnd = hasMatchingBlockEnd;
6
+ exports.mustachesFrom = mustachesFrom;
7
+ exports.findMatchingBlockEnd = findMatchingBlockEnd;
8
+ exports.consumeNextNode = consumeNextNode;
9
+ exports.findNextMarkup = findNextMarkup;
10
+ exports.findCurrentBlockBoundary = findCurrentBlockBoundary;
11
+ exports.findMatchingTagClose = findMatchingTagClose;
12
+ exports.findRawTextClose = findRawTextClose;
13
+ exports.consumeDynamicElement = consumeDynamicElement;
14
+ /* Lookahead: where a construct ends, without building anything. Every answer is an index, so
15
+ * the tree builder can decide whether to refuse before it has committed to a node. */
16
+ const html_1 = require("../core/html");
17
+ const errors_1 = require("../core/errors");
18
+ const tokens_1 = require("../dialects/handlebars/tokens");
19
+ const lex_1 = require("./lex");
20
+ ({ openDelimiter: exports.openDelimiter, parseToken: exports.parseMustacheToken, findNextOpen: exports.findNextHandlebarsOpen, isDynamicElementStart: exports.isDynamicTagStart, consumeRawBlock: exports.consumeRawBlock } = tokens_1.handlebarsDialect);
21
+ /* Where a raw block at `position` ends, or null if there is not one there. A body Handlebars
22
+ * emits literally is copied through wherever it appears; one that never closes is rejected
23
+ * wherever it appears too. */
24
+ function consumeTerminatedRawBlock(text, position, rangeOffset) {
25
+ const end = (0, exports.consumeRawBlock)(text, position);
26
+ if (end === null) {
27
+ return null;
28
+ }
29
+ /* Same as for a mustache, except the closer carries the block's own name - and the name is
30
+ * read once here rather than once to decide and again to name it in the message. */
31
+ const openEnd = text.indexOf('}}}}', position + 4);
32
+ const name = openEnd === -1 ? '' : (0, tokens_1.handlebarsRawBlockName)(text, position, openEnd);
33
+ if (name === '' || !text.slice(position, end).endsWith((0, tokens_1.handlebarsRawBlockCloser)(name))) {
34
+ (0, errors_1.fail)(`unterminated raw block: expected ${(0, tokens_1.handlebarsRawBlockCloser)(name)}`, rangeOffset + position, rangeOffset + end);
35
+ }
36
+ return end;
37
+ }
38
+ function hasMatchingBlockEnd(text, token) {
39
+ return findMatchingBlockEnd(text, token) !== null;
40
+ }
41
+ /**
42
+ * The mustaches in `text` from `from` onwards, minus those inside a `{{{{raw}}}}` body:
43
+ * Handlebars does not parse one, so a `{{#if}}` in there opens nothing.
44
+ *
45
+ * Deliberately does *not* skip HTML comments or `<script>`: Handlebars has no idea what HTML is
46
+ * and rejects `{{#if a}}<!-- {{#if b}} -->{{/if}}`, so these scans must see that `{{#if b}}`.
47
+ */
48
+ function* mustachesFrom(text, from) {
49
+ let pos = from;
50
+ while (pos < text.length) {
51
+ const next = (0, exports.findNextHandlebarsOpen)(text, pos);
52
+ if (next === -1) {
53
+ return;
54
+ }
55
+ const rawBlockEnd = (0, exports.consumeRawBlock)(text, next);
56
+ if (rawBlockEnd !== null && rawBlockEnd > next) {
57
+ pos = rawBlockEnd;
58
+ continue;
59
+ }
60
+ const token = (0, exports.parseMustacheToken)(text, next);
61
+ yield token;
62
+ pos = token.end > next ? token.end : next + 2;
63
+ }
64
+ }
65
+ /** Where the block opened by `token` closes, or null if it never does. */
66
+ function findMatchingBlockEnd(text, token) {
67
+ if (!token.name) {
68
+ return null;
69
+ }
70
+ let depth = 0;
71
+ /* From the end of the opening tag, which the token already knows - a caller passing a start
72
+ * position instead would have `findNextHandlebarsOpen` land on a `{{` inside the tag's own
73
+ * string literal, reading `{{#if (eq a "{{")}}` as a mustache that never closes. */
74
+ for (const candidate of mustachesFrom(text, token.end)) {
75
+ if (candidate.kind === 'blockStart' && candidate.name === token.name) {
76
+ depth += 1;
77
+ }
78
+ else if (candidate.kind === 'blockEnd' && candidate.name === token.name) {
79
+ if (depth === 0) {
80
+ return candidate.end;
81
+ }
82
+ depth -= 1;
83
+ }
84
+ }
85
+ return null;
86
+ }
87
+ /**
88
+ * How far `{{! prettier-ignore }}` reaches: to the end of the one node that follows it, or
89
+ * nowhere if that node's extent cannot be determined.
90
+ *
91
+ * It scans rather than parses: a nested `parseChildren` would run past the enclosing container,
92
+ * handing an element its own `</div>`, and could `fail()` - leaving a directive meant to
93
+ * suppress formatting able to reject the file. `position` means "nothing to ignore".
94
+ */
95
+ function consumeNextNode(text, position) {
96
+ if (position >= text.length) {
97
+ return position;
98
+ }
99
+ if ((0, lex_1.startsTemplateTag)(text, position)) {
100
+ const token = (0, exports.parseMustacheToken)(text, position);
101
+ /* A terminator belongs to whatever opened it, never to the node being skipped. */
102
+ if (token.kind === 'blockEnd' || token.kind === 'else') {
103
+ return position;
104
+ }
105
+ return token.kind === 'blockStart' ? findMatchingBlockEnd(text, token) ?? position : token.end;
106
+ }
107
+ if (text[position] === '<') {
108
+ const tagResult = (0, lex_1.scanTag)(text, position);
109
+ if (!tagResult.terminated || tagResult.kind === 'close') {
110
+ return position;
111
+ }
112
+ if (tagResult.kind === 'selfClosing') {
113
+ return tagResult.end;
114
+ }
115
+ const closeStart = findMatchingTagClose(text, tagResult.tag, tagResult.end);
116
+ if (closeStart === null) {
117
+ return position;
118
+ }
119
+ const closeEnd = text.indexOf('>', closeStart);
120
+ return closeEnd < 0 ? position : closeEnd + 1;
121
+ }
122
+ const nextMarkup = findNextMarkup(text, position);
123
+ if (nextMarkup <= position) {
124
+ return nextMarkup;
125
+ }
126
+ /* Only whitespace is stepped over on the way to the node being ignored - a run of text is a
127
+ * node in its own right, and is the thing to ignore. */
128
+ if (text.slice(position, nextMarkup).trim() !== '' || nextMarkup >= text.length) {
129
+ return nextMarkup;
130
+ }
131
+ return consumeNextNode(text, nextMarkup);
132
+ }
133
+ function findNextMarkup(text, position) {
134
+ let next = text.length;
135
+ let searchPos = position;
136
+ while (searchPos < text.length) {
137
+ const candidate = text.indexOf('<', searchPos);
138
+ if (candidate === -1) {
139
+ break;
140
+ }
141
+ if ((0, exports.isDynamicTagStart)(text, candidate)) {
142
+ next = candidate;
143
+ break;
144
+ }
145
+ if ((0, lex_1.isTagStart)(text, candidate)) {
146
+ next = candidate;
147
+ break;
148
+ }
149
+ searchPos = candidate + 1;
150
+ }
151
+ const hb = (0, exports.findNextHandlebarsOpen)(text, position);
152
+ if (hb !== -1 && hb < next) {
153
+ next = hb;
154
+ }
155
+ return next;
156
+ }
157
+ function findCurrentBlockBoundary(text, position, endBlock) {
158
+ let depth = 0;
159
+ for (const token of mustachesFrom(text, position)) {
160
+ if (token.kind === 'blockStart') {
161
+ depth += 1;
162
+ }
163
+ else if (token.kind === 'blockEnd') {
164
+ if (depth === 0 && token.name === endBlock) {
165
+ return token.start;
166
+ }
167
+ if (depth > 0) {
168
+ depth -= 1;
169
+ }
170
+ }
171
+ else if (token.kind === 'else' && depth === 0) {
172
+ return token.start;
173
+ }
174
+ }
175
+ return -1;
176
+ }
177
+ /* Past one mustache, or past a whole raw block: a raw block's body is emitted literally, so the
178
+ * markup inside it is not markup either. Never returns `position`, so callers cannot spin. */
179
+ function skipMustache(text, position) {
180
+ const rawBlockEnd = (0, exports.consumeRawBlock)(text, position);
181
+ if (rawBlockEnd !== null && rawBlockEnd > position) {
182
+ return rawBlockEnd;
183
+ }
184
+ return (0, lex_1.skipTemplateTag)(text, position);
185
+ }
186
+ function findMatchingTagClose(text, tag, position, limit = -1) {
187
+ if ((0, html_1.isRawTextElement)(tag)) {
188
+ const closeStart = findRawTextClose(text, position, tag);
189
+ if (closeStart === -1 || (limit >= 0 && closeStart >= limit)) {
190
+ return null;
191
+ }
192
+ return closeStart;
193
+ }
194
+ let depth = 0;
195
+ let pos = position;
196
+ while (pos < text.length) {
197
+ const next = text.indexOf('<', pos);
198
+ if (next === -1 || (limit >= 0 && next >= limit)) {
199
+ return null;
200
+ }
201
+ /* A `<` inside a mustache is not markup, so the dialect is consulted first, as every other
202
+ * scanner here does. Otherwise `{{t "<div>"}}` reads as an open tag, leaving the scan a level
203
+ * too deep and the real `</div>` closing it - refusing the file as unclosed. */
204
+ const mustache = (0, exports.findNextHandlebarsOpen)(text, pos);
205
+ if (mustache !== -1 && mustache < next) {
206
+ pos = skipMustache(text, mustache);
207
+ continue;
208
+ }
209
+ if (text.startsWith('<!--', next)) {
210
+ const closeIdx = text.indexOf('-->', next + 4);
211
+ pos = closeIdx >= 0 ? closeIdx + 3 : text.length;
212
+ continue;
213
+ }
214
+ if (text.startsWith('<!', next) && !text.startsWith('<!--', next)) {
215
+ const closeIdx = text.indexOf('>', next + 2);
216
+ pos = closeIdx >= 0 ? closeIdx + 1 : text.length;
217
+ continue;
218
+ }
219
+ const dynamicEnd = consumeDynamicElement(text, next);
220
+ if (dynamicEnd !== null) {
221
+ pos = dynamicEnd;
222
+ continue;
223
+ }
224
+ if (!(0, lex_1.isTagStart)(text, next)) {
225
+ pos = next + 1;
226
+ continue;
227
+ }
228
+ const tagResult = (0, lex_1.scanTag)(text, next);
229
+ if (tagResult.kind === 'close') {
230
+ if ((0, lex_1.sameTag)(tagResult.tag, tag)) {
231
+ if (depth === 0) {
232
+ return next;
233
+ }
234
+ depth -= 1;
235
+ }
236
+ pos = tagResult.end;
237
+ continue;
238
+ }
239
+ if (tagResult.kind === 'open' && (0, html_1.isRawTextElement)(tagResult.tag)) {
240
+ const closeStart = findRawTextClose(text, tagResult.end, tagResult.tag);
241
+ const closeIdx = closeStart >= 0 ? text.indexOf('>', closeStart) : -1;
242
+ pos = closeIdx >= 0 ? closeIdx + 1 : text.length;
243
+ continue;
244
+ }
245
+ if (tagResult.kind === 'open' && (0, lex_1.sameTag)(tagResult.tag, tag)) {
246
+ depth += 1;
247
+ }
248
+ pos = tagResult.end;
249
+ }
250
+ return null;
251
+ }
252
+ /**
253
+ * Raw text ends at the first `</tag`, whatever it appears to sit inside.
254
+ *
255
+ * A browser's tokenizer does not parse the script or style body looking for string literals -
256
+ * that is exactly why `"<\\/script>"` has to be escaped in JS. Tracking quotes here instead would
257
+ * let an apostrophe in a comment hide the closing tag.
258
+ */
259
+ function findRawTextClose(text, position, tag) {
260
+ const needle = `</${tag.toLowerCase()}`;
261
+ /* The name has to end there: HTML's script-data end-tag state needs whitespace, `/` or `>`
262
+ * after it, so `"</scriptx>"` inside a script body does not close the element. */
263
+ /* Scanning case-insensitively rather than lowercasing the whole template: this runs once per
264
+ * raw-text element and again inside every close-tag scan, so a copy of the file each time
265
+ * turns a page of `<script>`s into quadratic work. */
266
+ for (let index = text.indexOf('<', position); index !== -1; index = text.indexOf('<', index + 1)) {
267
+ if (text.slice(index, index + needle.length).toLowerCase() === needle && lex_1.tagNameTerminator.test(text[index + needle.length] ?? '>')) {
268
+ return index;
269
+ }
270
+ }
271
+ return -1;
272
+ }
273
+ function consumeDynamicElement(text, position) {
274
+ if (!(0, exports.isDynamicTagStart)(text, position)) {
275
+ return null;
276
+ }
277
+ const dynamicOpen = `<${exports.openDelimiter}`;
278
+ const dynamicClose = `</${exports.openDelimiter}`;
279
+ if (text.startsWith(dynamicClose, position)) {
280
+ return (0, lex_1.consumeTagLikeChunk)(text, position);
281
+ }
282
+ const openEnd = (0, lex_1.consumeTagLikeChunk)(text, position);
283
+ let depth = 0;
284
+ let pos = openEnd;
285
+ while (pos < text.length) {
286
+ const nextOpen = text.indexOf(dynamicOpen, pos);
287
+ const nextClose = text.indexOf(dynamicClose, pos);
288
+ const candidates = [nextOpen, nextClose].filter((value) => value !== -1);
289
+ const next = candidates.length > 0 ? Math.min(...candidates) : -1;
290
+ if (next === -1) {
291
+ return openEnd;
292
+ }
293
+ if (next === nextClose) {
294
+ if (depth === 0) {
295
+ return (0, lex_1.consumeTagLikeChunk)(text, nextClose);
296
+ }
297
+ depth -= 1;
298
+ pos = (0, lex_1.consumeTagLikeChunk)(text, nextClose);
299
+ continue;
300
+ }
301
+ depth += 1;
302
+ pos = (0, lex_1.consumeTagLikeChunk)(text, nextOpen);
303
+ }
304
+ return openEnd;
305
+ }
@@ -0,0 +1,38 @@
1
+ import type { HandlebarsToken as MustacheToken } from '../dialects/handlebars/tokens';
2
+ import type { CommentStatement, DecoratorStatement, MustacheStatement, PartialStatement, TextNode, UnmatchedNode } from '../types';
3
+ type PrettierIgnoreDirective = 'next' | 'start' | 'end' | null;
4
+ /**
5
+ * The directive has to *be* the comment, not appear somewhere inside it: on `includes`, a
6
+ * comment merely mentioning `prettier-ignore` would silently suppress the next node, and one
7
+ * mentioning `prettier-ignore-start` would open a region.
8
+ */
9
+ export declare function getPrettierIgnoreDirective(rawContent: string): PrettierIgnoreDirective;
10
+ export declare function findPrettierIgnoreEnd(text: string, position: number): number | null;
11
+ /** A run of source kept as text. `verbatim` means the printer reproduces it rather than reflowing. */
12
+ export declare function textNode(text: string, start: number, end: number, rangeOffset: number, verbatim?: boolean): TextNode;
13
+ export declare function createUnmatchedNode(text: string, start: number, end: number, rangeOffset: number): UnmatchedNode;
14
+ /** Where `content` begins inside the tag spanning [tagStart, tagEnd), for absolute expression ranges. */
15
+ export declare function contentOffset(text: string, tagStart: number, tagEnd: number, content: string): number;
16
+ /**
17
+ * A mustache built from whatever token is in hand, whether or not it reads as one.
18
+ *
19
+ * The recovery paths use it for a block that never closes and for a stray `{{else}}` or
20
+ * `{{/if}}` in a position that cannot reject them.
21
+ */
22
+ export declare function createMustache(text: string, token: MustacheToken, position: number, rangeOffset: number): MustacheStatement;
23
+ /**
24
+ * The node for a token that stands on its own, or null for the three kinds - a block and the two
25
+ * terminators - whose handling depends on where they appear.
26
+ *
27
+ * Every context that reads a mustache needs this dispatch: a program body, an attribute list,
28
+ * the inside of a value. Written out three times, they had drifted at the recovery arms.
29
+ */
30
+ export declare function createStatement(text: string, token: MustacheToken, position: number, rangeOffset: number): MustacheStatement | PartialStatement | DecoratorStatement | CommentStatement | null;
31
+ /**
32
+ * A comment's body, with the tag's own `~` markers taken off. They are whitespace control, not
33
+ * text: printing `rawContent` straight through emits them as body, turning `{{~! x ~}}` into
34
+ * `{{! ~! x ~ }}` and dropping the stripping the author asked for.
35
+ */
36
+ export declare function commentBody(token: MustacheToken): string;
37
+ export declare function createComment(token: MustacheToken, start?: number, end?: number): CommentStatement;
38
+ export {};