@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/LICENSE +2 -1
- package/README.md +19 -15
- package/dist/{errors.d.ts → core/errors.d.ts} +7 -0
- package/dist/{errors.js → core/errors.js} +10 -0
- package/dist/core/html.d.ts +2 -0
- package/dist/core/html.js +32 -0
- package/dist/core/source.d.ts +11 -0
- package/dist/core/source.js +27 -0
- package/dist/{whitespace.d.ts → core/whitespace.d.ts} +5 -0
- package/dist/{whitespace.js → core/whitespace.js} +18 -0
- package/dist/dialects/handlebars/tokens.d.ts +23 -6
- package/dist/dialects/handlebars/tokens.js +15 -8
- package/dist/expression.js +8 -8
- package/dist/parse/lex.d.ts +86 -0
- package/dist/parse/lex.js +274 -0
- package/dist/parse/lookahead.d.ts +35 -0
- package/dist/parse/lookahead.js +305 -0
- package/dist/parse/nodes.d.ts +38 -0
- package/dist/parse/nodes.js +161 -0
- package/dist/parser.d.ts +0 -2
- package/dist/parser.js +271 -931
- package/dist/plugin.d.ts +2 -1
- package/dist/plugin.js +3 -2
- package/dist/printer.d.ts +1 -6
- package/dist/printer.js +72 -35
- package/dist/types.d.ts +2 -2
- package/docs/EDITOR_SETUP.md +0 -1
- package/package.json +3 -6
- /package/dist/{scan.d.ts → core/scan.d.ts} +0 -0
- /package/dist/{scan.js → core/scan.js} +0 -0
|
@@ -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 {};
|