@reteps/tree-sitter-htmlmustache 0.8.1 → 0.9.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/README.md +1 -1
- package/browser/out/browser/index.d.ts +43 -0
- package/browser/out/browser/index.d.ts.map +1 -0
- package/browser/out/browser/index.mjs +3746 -0
- package/browser/out/browser/index.mjs.map +7 -0
- package/browser/out/core/collectErrors.d.ts +36 -0
- package/browser/out/core/collectErrors.d.ts.map +1 -0
- package/browser/out/core/configSchema.d.ts +63 -0
- package/browser/out/core/configSchema.d.ts.map +1 -0
- package/browser/out/core/customCodeTags.d.ts +34 -0
- package/browser/out/core/customCodeTags.d.ts.map +1 -0
- package/browser/out/core/diagnostic.d.ts +24 -0
- package/browser/out/core/diagnostic.d.ts.map +1 -0
- package/browser/out/core/embeddedRegions.d.ts +12 -0
- package/browser/out/core/embeddedRegions.d.ts.map +1 -0
- package/browser/out/core/formatting/classifier.d.ts +68 -0
- package/browser/out/core/formatting/classifier.d.ts.map +1 -0
- package/browser/out/core/formatting/embedded.d.ts +19 -0
- package/browser/out/core/formatting/embedded.d.ts.map +1 -0
- package/browser/out/core/formatting/formatters.d.ts +85 -0
- package/browser/out/core/formatting/formatters.d.ts.map +1 -0
- package/browser/out/core/formatting/index.d.ts +44 -0
- package/browser/out/core/formatting/index.d.ts.map +1 -0
- package/browser/out/core/formatting/ir.d.ts +100 -0
- package/browser/out/core/formatting/ir.d.ts.map +1 -0
- package/browser/out/core/formatting/mergeOptions.d.ts +18 -0
- package/browser/out/core/formatting/mergeOptions.d.ts.map +1 -0
- package/browser/out/core/formatting/printer.d.ts +18 -0
- package/browser/out/core/formatting/printer.d.ts.map +1 -0
- package/browser/out/core/formatting/utils.d.ts +39 -0
- package/browser/out/core/formatting/utils.d.ts.map +1 -0
- package/browser/out/core/grammar.d.ts +3 -0
- package/browser/out/core/grammar.d.ts.map +1 -0
- package/browser/out/core/htmlBalanceChecker.d.ts +23 -0
- package/browser/out/core/htmlBalanceChecker.d.ts.map +1 -0
- package/browser/out/core/mustacheChecks.d.ts +24 -0
- package/browser/out/core/mustacheChecks.d.ts.map +1 -0
- package/browser/out/core/nodeHelpers.d.ts +54 -0
- package/browser/out/core/nodeHelpers.d.ts.map +1 -0
- package/browser/out/core/ruleMetadata.d.ts +12 -0
- package/browser/out/core/ruleMetadata.d.ts.map +1 -0
- package/browser/out/core/selectorMatcher.d.ts +87 -0
- package/browser/out/core/selectorMatcher.d.ts.map +1 -0
- package/cli/out/main.js +333 -181
- package/package.json +21 -3
- package/src/browser/browser.test.ts +207 -0
- package/src/browser/index.ts +128 -0
- package/src/browser/tsconfig.json +18 -0
- package/src/core/collectErrors.ts +233 -0
- package/src/core/configSchema.ts +273 -0
- package/src/core/customCodeTags.ts +159 -0
- package/src/core/diagnostic.ts +45 -0
- package/src/core/embeddedRegions.ts +70 -0
- package/src/core/formatting/classifier.ts +549 -0
- package/src/core/formatting/embedded.ts +56 -0
- package/src/core/formatting/formatters.ts +1272 -0
- package/src/core/formatting/index.ts +185 -0
- package/src/core/formatting/ir.ts +202 -0
- package/src/core/formatting/mergeOptions.ts +34 -0
- package/src/core/formatting/printer.ts +242 -0
- package/src/core/formatting/utils.ts +193 -0
- package/src/core/grammar.ts +2 -0
- package/src/core/htmlBalanceChecker.ts +382 -0
- package/src/core/mustacheChecks.ts +504 -0
- package/src/core/nodeHelpers.ts +126 -0
- package/src/core/ruleMetadata.ts +63 -0
- package/src/core/selectorMatcher.ts +919 -0
|
@@ -0,0 +1,3746 @@
|
|
|
1
|
+
// src/browser/index.ts
|
|
2
|
+
import { Parser, Language } from "web-tree-sitter";
|
|
3
|
+
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
4
|
+
|
|
5
|
+
// src/core/nodeHelpers.ts
|
|
6
|
+
var MUSTACHE_SECTION_TYPES = /* @__PURE__ */ new Set([
|
|
7
|
+
"mustache_section",
|
|
8
|
+
"mustache_inverted_section"
|
|
9
|
+
]);
|
|
10
|
+
var RAW_CONTENT_ELEMENT_TYPES = /* @__PURE__ */ new Set([
|
|
11
|
+
"html_script_element",
|
|
12
|
+
"html_style_element",
|
|
13
|
+
"html_raw_element"
|
|
14
|
+
]);
|
|
15
|
+
var HTML_ELEMENT_TYPES = /* @__PURE__ */ new Set([
|
|
16
|
+
"html_element",
|
|
17
|
+
"html_script_element",
|
|
18
|
+
"html_style_element",
|
|
19
|
+
"html_raw_element"
|
|
20
|
+
]);
|
|
21
|
+
function isMustacheSection(node) {
|
|
22
|
+
return MUSTACHE_SECTION_TYPES.has(node.type);
|
|
23
|
+
}
|
|
24
|
+
function isRawContentElement(node) {
|
|
25
|
+
return RAW_CONTENT_ELEMENT_TYPES.has(node.type);
|
|
26
|
+
}
|
|
27
|
+
function isHtmlElementType(node) {
|
|
28
|
+
return HTML_ELEMENT_TYPES.has(node.type);
|
|
29
|
+
}
|
|
30
|
+
function getTagName(node) {
|
|
31
|
+
for (const child of node.children) {
|
|
32
|
+
if (child.type === "html_start_tag" || child.type === "html_self_closing_tag") {
|
|
33
|
+
const tagNameNode = child.children.find((c) => c.type === "html_tag_name");
|
|
34
|
+
if (tagNameNode) return tagNameNode.text;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function getSectionName(node) {
|
|
40
|
+
const beginNode = node.children.find(
|
|
41
|
+
(c) => c.type === "mustache_section_begin" || c.type === "mustache_inverted_section_begin"
|
|
42
|
+
);
|
|
43
|
+
if (!beginNode) return null;
|
|
44
|
+
const tagNameNode = beginNode.children.find((c) => c.type === "mustache_tag_name");
|
|
45
|
+
return tagNameNode?.text ?? null;
|
|
46
|
+
}
|
|
47
|
+
function getErroneousEndTagName(node) {
|
|
48
|
+
const nameNode = node.children.find((c) => c.type === "html_erroneous_end_tag_name");
|
|
49
|
+
return nameNode?.text?.toLowerCase() ?? null;
|
|
50
|
+
}
|
|
51
|
+
function getInterpolationPath(node) {
|
|
52
|
+
for (const child of node.children) {
|
|
53
|
+
if (child.type === "mustache_path_expression" || child.type === "mustache_identifier") {
|
|
54
|
+
return child.text;
|
|
55
|
+
}
|
|
56
|
+
if (child.type === ".") return ".";
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
function getCommentContent(node) {
|
|
61
|
+
const child = node.children.find((c) => c.type === "mustache_comment_content");
|
|
62
|
+
return child ? child.text.trim() : null;
|
|
63
|
+
}
|
|
64
|
+
function getPartialName(node) {
|
|
65
|
+
const child = node.children.find((c) => c.type === "mustache_partial_content");
|
|
66
|
+
return child ? child.text.trim() : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/core/htmlBalanceChecker.ts
|
|
70
|
+
function getTagNameLower(element) {
|
|
71
|
+
return getTagName(element)?.toLowerCase() ?? null;
|
|
72
|
+
}
|
|
73
|
+
function getErroneousEndTagNameLower(node) {
|
|
74
|
+
return getErroneousEndTagName(node)?.toLowerCase() ?? null;
|
|
75
|
+
}
|
|
76
|
+
function hasForcedEndTag(element) {
|
|
77
|
+
return element.children.some((c) => c.type === "html_forced_end_tag");
|
|
78
|
+
}
|
|
79
|
+
function extractFromNodes(nodes) {
|
|
80
|
+
const items = [];
|
|
81
|
+
for (const node of nodes) {
|
|
82
|
+
items.push(...extractFromNode(node));
|
|
83
|
+
}
|
|
84
|
+
return items;
|
|
85
|
+
}
|
|
86
|
+
function extractFromNode(node) {
|
|
87
|
+
if (node.type === "html_element") {
|
|
88
|
+
const contentChildren = node.children.filter(
|
|
89
|
+
(c) => c.type !== "html_start_tag" && c.type !== "html_end_tag" && c.type !== "html_forced_end_tag"
|
|
90
|
+
);
|
|
91
|
+
if (hasForcedEndTag(node)) {
|
|
92
|
+
const tagName = getTagNameLower(node);
|
|
93
|
+
const items = [];
|
|
94
|
+
if (tagName) {
|
|
95
|
+
const startTag = node.children.find((c) => c.type === "html_start_tag");
|
|
96
|
+
items.push({ type: "open", tagName, node: startTag ?? node });
|
|
97
|
+
}
|
|
98
|
+
items.push(...extractFromNodes(contentChildren));
|
|
99
|
+
return items;
|
|
100
|
+
}
|
|
101
|
+
return extractFromNodes(contentChildren);
|
|
102
|
+
}
|
|
103
|
+
if (node.type === "html_self_closing_tag") {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
if (node.type === "html_erroneous_end_tag") {
|
|
107
|
+
const tagName = getErroneousEndTagNameLower(node);
|
|
108
|
+
if (tagName) {
|
|
109
|
+
return [{ type: "close", tagName, node }];
|
|
110
|
+
}
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
if (node.type === "mustache_section") {
|
|
114
|
+
const sectionName = getSectionName(node);
|
|
115
|
+
if (sectionName) {
|
|
116
|
+
const contentChildren = node.children.filter(
|
|
117
|
+
(c) => c.type !== "mustache_section_begin" && c.type !== "mustache_section_end" && c.type !== "mustache_erroneous_section_end"
|
|
118
|
+
);
|
|
119
|
+
return [
|
|
120
|
+
{
|
|
121
|
+
type: "fork",
|
|
122
|
+
sectionName,
|
|
123
|
+
truthy: extractFromNodes(contentChildren),
|
|
124
|
+
falsy: []
|
|
125
|
+
}
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
if (node.type === "mustache_inverted_section") {
|
|
131
|
+
const sectionName = getSectionName(node);
|
|
132
|
+
if (sectionName) {
|
|
133
|
+
const contentChildren = node.children.filter(
|
|
134
|
+
(c) => c.type !== "mustache_inverted_section_begin" && c.type !== "mustache_inverted_section_end" && c.type !== "mustache_erroneous_inverted_section_end"
|
|
135
|
+
);
|
|
136
|
+
return [
|
|
137
|
+
{
|
|
138
|
+
type: "fork",
|
|
139
|
+
sectionName,
|
|
140
|
+
truthy: [],
|
|
141
|
+
falsy: extractFromNodes(contentChildren)
|
|
142
|
+
}
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
return extractFromNodes(node.children);
|
|
148
|
+
}
|
|
149
|
+
function mergeAdjacentForks(items) {
|
|
150
|
+
if (items.length === 0) return items;
|
|
151
|
+
const result = [];
|
|
152
|
+
let i = 0;
|
|
153
|
+
while (i < items.length) {
|
|
154
|
+
const item = items[i];
|
|
155
|
+
if (item.type !== "fork") {
|
|
156
|
+
result.push(item);
|
|
157
|
+
i++;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const truthy = [...item.truthy];
|
|
161
|
+
const falsy = [...item.falsy];
|
|
162
|
+
let j = i + 1;
|
|
163
|
+
while (j < items.length) {
|
|
164
|
+
const next = items[j];
|
|
165
|
+
if (next.type !== "fork" || next.sectionName !== item.sectionName) break;
|
|
166
|
+
truthy.push(...next.truthy);
|
|
167
|
+
falsy.push(...next.falsy);
|
|
168
|
+
j++;
|
|
169
|
+
}
|
|
170
|
+
result.push({
|
|
171
|
+
type: "fork",
|
|
172
|
+
sectionName: item.sectionName,
|
|
173
|
+
truthy: mergeAdjacentForks(truthy),
|
|
174
|
+
falsy: mergeAdjacentForks(falsy)
|
|
175
|
+
});
|
|
176
|
+
i = j;
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
function isBranchBalanced(items) {
|
|
181
|
+
const stack = [];
|
|
182
|
+
for (const item of items) {
|
|
183
|
+
if (item.type === "fork") {
|
|
184
|
+
if (!isBranchBalanced(item.truthy) || !isBranchBalanced(item.falsy)) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
} else if (item.type === "open") {
|
|
188
|
+
stack.push(item.tagName);
|
|
189
|
+
} else {
|
|
190
|
+
if (stack.length === 0 || stack[stack.length - 1] !== item.tagName) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
stack.pop();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return stack.length === 0;
|
|
197
|
+
}
|
|
198
|
+
function collectSectionNames(items) {
|
|
199
|
+
const names = /* @__PURE__ */ new Set();
|
|
200
|
+
for (const item of items) {
|
|
201
|
+
if (item.type === "fork") {
|
|
202
|
+
if (!isBranchBalanced(item.truthy) || !isBranchBalanced(item.falsy)) {
|
|
203
|
+
names.add(item.sectionName);
|
|
204
|
+
}
|
|
205
|
+
for (const name of collectSectionNames(item.truthy)) names.add(name);
|
|
206
|
+
for (const name of collectSectionNames(item.falsy)) names.add(name);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return names;
|
|
210
|
+
}
|
|
211
|
+
function flattenPath(items, assignment) {
|
|
212
|
+
const events = [];
|
|
213
|
+
for (const item of items) {
|
|
214
|
+
if (item.type === "fork") {
|
|
215
|
+
const value = assignment.get(item.sectionName) ?? true;
|
|
216
|
+
const branch = value ? item.truthy : item.falsy;
|
|
217
|
+
events.push(...flattenPath(branch, assignment));
|
|
218
|
+
} else {
|
|
219
|
+
events.push(item);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return events;
|
|
223
|
+
}
|
|
224
|
+
function formatCondition(assignment) {
|
|
225
|
+
if (assignment.size === 0) return "";
|
|
226
|
+
const parts = [];
|
|
227
|
+
for (const [name, value] of assignment) {
|
|
228
|
+
parts.push(`${name} is ${value ? "truthy" : "falsy"}`);
|
|
229
|
+
}
|
|
230
|
+
return ` (when ${parts.join(", ")})`;
|
|
231
|
+
}
|
|
232
|
+
function validateBalance(events, condition) {
|
|
233
|
+
const errors = [];
|
|
234
|
+
const stack = [];
|
|
235
|
+
for (const event of events) {
|
|
236
|
+
if (event.type === "open") {
|
|
237
|
+
stack.push(event);
|
|
238
|
+
} else {
|
|
239
|
+
if (stack.length === 0) {
|
|
240
|
+
errors.push({
|
|
241
|
+
node: event.node,
|
|
242
|
+
message: `Mismatched HTML end tag: </${event.tagName}>${condition}`
|
|
243
|
+
});
|
|
244
|
+
} else {
|
|
245
|
+
const top = stack[stack.length - 1];
|
|
246
|
+
if (top.tagName !== event.tagName) {
|
|
247
|
+
errors.push({
|
|
248
|
+
node: event.node,
|
|
249
|
+
message: `Mismatched HTML end tag: </${event.tagName}>${condition}`
|
|
250
|
+
});
|
|
251
|
+
} else {
|
|
252
|
+
stack.pop();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
for (const event of stack) {
|
|
258
|
+
errors.push({
|
|
259
|
+
node: event.node,
|
|
260
|
+
message: `Unclosed HTML tag: <${event.tagName}>${condition}`
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return errors;
|
|
264
|
+
}
|
|
265
|
+
var VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
266
|
+
"area",
|
|
267
|
+
"base",
|
|
268
|
+
"basefont",
|
|
269
|
+
"bgsound",
|
|
270
|
+
"br",
|
|
271
|
+
"col",
|
|
272
|
+
"command",
|
|
273
|
+
"embed",
|
|
274
|
+
"frame",
|
|
275
|
+
"hr",
|
|
276
|
+
"image",
|
|
277
|
+
"img",
|
|
278
|
+
"input",
|
|
279
|
+
"isindex",
|
|
280
|
+
"keygen",
|
|
281
|
+
"link",
|
|
282
|
+
"menuitem",
|
|
283
|
+
"meta",
|
|
284
|
+
"nextid",
|
|
285
|
+
"param",
|
|
286
|
+
"source",
|
|
287
|
+
"track",
|
|
288
|
+
"wbr"
|
|
289
|
+
]);
|
|
290
|
+
var OPTIONAL_END_TAG_ELEMENTS = /* @__PURE__ */ new Set([
|
|
291
|
+
"li",
|
|
292
|
+
"dt",
|
|
293
|
+
"dd",
|
|
294
|
+
"p",
|
|
295
|
+
"colgroup",
|
|
296
|
+
"rb",
|
|
297
|
+
"rt",
|
|
298
|
+
"rp",
|
|
299
|
+
"rtc",
|
|
300
|
+
"optgroup",
|
|
301
|
+
"option",
|
|
302
|
+
"tr",
|
|
303
|
+
"td",
|
|
304
|
+
"th",
|
|
305
|
+
"thead",
|
|
306
|
+
"tbody",
|
|
307
|
+
"tfoot",
|
|
308
|
+
"caption",
|
|
309
|
+
"html",
|
|
310
|
+
"head",
|
|
311
|
+
"body"
|
|
312
|
+
]);
|
|
313
|
+
function checkUnclosedTags(rootNode) {
|
|
314
|
+
const errors = [];
|
|
315
|
+
function visit(node) {
|
|
316
|
+
if (node.type === "html_element") {
|
|
317
|
+
const hasEndTag = node.children.some((c) => c.type === "html_end_tag");
|
|
318
|
+
const hasForcedEnd = node.children.some((c) => c.type === "html_forced_end_tag");
|
|
319
|
+
if (!hasEndTag && !hasForcedEnd) {
|
|
320
|
+
const tagName = getTagNameLower(node);
|
|
321
|
+
if (tagName && !VOID_ELEMENTS.has(tagName) && !OPTIONAL_END_TAG_ELEMENTS.has(tagName)) {
|
|
322
|
+
const startTag = node.children.find((c) => c.type === "html_start_tag");
|
|
323
|
+
errors.push({
|
|
324
|
+
node: startTag ?? node,
|
|
325
|
+
message: `Unclosed HTML tag: <${tagName}>`
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
for (const child of node.children) {
|
|
331
|
+
visit(child);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
visit(rootNode);
|
|
335
|
+
return errors;
|
|
336
|
+
}
|
|
337
|
+
var MAX_SECTION_NAMES = 15;
|
|
338
|
+
function checkHtmlBalance(rootNode) {
|
|
339
|
+
const rawItems = extractFromNode(rootNode);
|
|
340
|
+
const items = mergeAdjacentForks(rawItems);
|
|
341
|
+
const sectionNames = [...collectSectionNames(items)];
|
|
342
|
+
if (sectionNames.length > MAX_SECTION_NAMES) {
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
const allErrors = [];
|
|
346
|
+
const errorNodes = /* @__PURE__ */ new Set();
|
|
347
|
+
const totalPaths = 1 << sectionNames.length;
|
|
348
|
+
for (let mask = 0; mask < totalPaths; mask++) {
|
|
349
|
+
const assignment = /* @__PURE__ */ new Map();
|
|
350
|
+
for (let i = 0; i < sectionNames.length; i++) {
|
|
351
|
+
assignment.set(sectionNames[i], (mask & 1 << i) !== 0);
|
|
352
|
+
}
|
|
353
|
+
const events = flattenPath(items, assignment);
|
|
354
|
+
const condition = formatCondition(assignment);
|
|
355
|
+
const pathErrors = validateBalance(events, condition);
|
|
356
|
+
for (const error of pathErrors) {
|
|
357
|
+
if (!errorNodes.has(error.node)) {
|
|
358
|
+
errorNodes.add(error.node);
|
|
359
|
+
allErrors.push(error);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return allErrors;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/core/mustacheChecks.ts
|
|
367
|
+
function checkNestedSameNameSections(rootNode) {
|
|
368
|
+
const errors = [];
|
|
369
|
+
function visit(node, ancestors) {
|
|
370
|
+
if (isMustacheSection(node)) {
|
|
371
|
+
const name = getSectionName(node);
|
|
372
|
+
if (name) {
|
|
373
|
+
if (ancestors.has(name)) {
|
|
374
|
+
const beginNode = node.children.find(
|
|
375
|
+
(c) => c.type === "mustache_section_begin" || c.type === "mustache_inverted_section_begin"
|
|
376
|
+
);
|
|
377
|
+
errors.push({
|
|
378
|
+
node: beginNode ?? node,
|
|
379
|
+
message: `Nested duplicate section: {{#${name}}} is already open in an ancestor`
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
const next = new Set(ancestors);
|
|
383
|
+
next.add(name);
|
|
384
|
+
for (const child of node.children) {
|
|
385
|
+
visit(child, next);
|
|
386
|
+
}
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
for (const child of node.children) {
|
|
391
|
+
visit(child, ancestors);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
visit(rootNode, /* @__PURE__ */ new Set());
|
|
395
|
+
return errors;
|
|
396
|
+
}
|
|
397
|
+
function checkUnquotedMustacheAttributes(rootNode) {
|
|
398
|
+
const errors = [];
|
|
399
|
+
function visit(node) {
|
|
400
|
+
if (node.type === "html_attribute") {
|
|
401
|
+
const mustacheNode = node.children.find((c) => c.type === "mustache_interpolation");
|
|
402
|
+
if (mustacheNode) {
|
|
403
|
+
errors.push({
|
|
404
|
+
node: mustacheNode,
|
|
405
|
+
message: `Unquoted mustache attribute value: ${mustacheNode.text}`,
|
|
406
|
+
fix: [{
|
|
407
|
+
startIndex: mustacheNode.startIndex,
|
|
408
|
+
endIndex: mustacheNode.endIndex,
|
|
409
|
+
newText: `"${mustacheNode.text}"`
|
|
410
|
+
}],
|
|
411
|
+
fixDescription: "Wrap mustache value in quotes"
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
for (const child of node.children) {
|
|
417
|
+
visit(child);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
visit(rootNode);
|
|
421
|
+
return errors;
|
|
422
|
+
}
|
|
423
|
+
function checkConsecutiveSameNameSections(rootNode, sourceText) {
|
|
424
|
+
const errors = [];
|
|
425
|
+
function visit(node) {
|
|
426
|
+
const children = node.children;
|
|
427
|
+
for (let i = 0; i < children.length - 1; i++) {
|
|
428
|
+
const current = children[i];
|
|
429
|
+
const next = children[i + 1];
|
|
430
|
+
if (!isMustacheSection(current) || current.type !== next.type) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
const currentName = getSectionName(current);
|
|
434
|
+
const nextName = getSectionName(next);
|
|
435
|
+
if (!currentName || !nextName || currentName !== nextName) continue;
|
|
436
|
+
const gap = sourceText.slice(current.endIndex, next.startIndex);
|
|
437
|
+
if (gap.length > 0 && !/^\s*$/.test(gap)) continue;
|
|
438
|
+
const endTagType = current.type === "mustache_section" ? "mustache_section_end" : "mustache_inverted_section_end";
|
|
439
|
+
const beginTagType = next.type === "mustache_section" ? "mustache_section_begin" : "mustache_inverted_section_begin";
|
|
440
|
+
const currentEndTag = current.children.find((c) => c.type === endTagType);
|
|
441
|
+
const nextBeginTag = next.children.find((c) => c.type === beginTagType);
|
|
442
|
+
if (!currentEndTag || !nextBeginTag) continue;
|
|
443
|
+
const sectionTypeStr = current.type === "mustache_section" ? "#" : "^";
|
|
444
|
+
const nextBeginNode = next.children.find(
|
|
445
|
+
(c) => c.type === "mustache_section_begin" || c.type === "mustache_inverted_section_begin"
|
|
446
|
+
);
|
|
447
|
+
errors.push({
|
|
448
|
+
node: nextBeginNode ?? next,
|
|
449
|
+
message: `Consecutive duplicate section: {{${sectionTypeStr}${nextName}}} can be merged with previous {{${sectionTypeStr}${nextName}}}`,
|
|
450
|
+
severity: "warning",
|
|
451
|
+
fix: [{
|
|
452
|
+
startIndex: currentEndTag.startIndex,
|
|
453
|
+
endIndex: nextBeginTag.endIndex,
|
|
454
|
+
newText: ""
|
|
455
|
+
}],
|
|
456
|
+
fixDescription: "Merge consecutive sections"
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
for (const child of children) {
|
|
460
|
+
visit(child);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
visit(rootNode);
|
|
464
|
+
return errors;
|
|
465
|
+
}
|
|
466
|
+
var VOID_ELEMENTS2 = /* @__PURE__ */ new Set([
|
|
467
|
+
"area",
|
|
468
|
+
"base",
|
|
469
|
+
"basefont",
|
|
470
|
+
"bgsound",
|
|
471
|
+
"br",
|
|
472
|
+
"col",
|
|
473
|
+
"command",
|
|
474
|
+
"embed",
|
|
475
|
+
"frame",
|
|
476
|
+
"hr",
|
|
477
|
+
"image",
|
|
478
|
+
"img",
|
|
479
|
+
"input",
|
|
480
|
+
"isindex",
|
|
481
|
+
"keygen",
|
|
482
|
+
"link",
|
|
483
|
+
"menuitem",
|
|
484
|
+
"meta",
|
|
485
|
+
"nextid",
|
|
486
|
+
"param",
|
|
487
|
+
"source",
|
|
488
|
+
"track",
|
|
489
|
+
"wbr"
|
|
490
|
+
]);
|
|
491
|
+
function checkSelfClosingNonVoidTags(rootNode) {
|
|
492
|
+
const errors = [];
|
|
493
|
+
function visit(node) {
|
|
494
|
+
if (node.type === "html_self_closing_tag") {
|
|
495
|
+
const tagNameNode = node.children.find((c) => c.type === "html_tag_name");
|
|
496
|
+
const tagName = tagNameNode?.text.toLowerCase();
|
|
497
|
+
if (tagName && !VOID_ELEMENTS2.has(tagName)) {
|
|
498
|
+
errors.push({
|
|
499
|
+
node,
|
|
500
|
+
message: `Self-closing non-void element: <${tagNameNode.text}/>`,
|
|
501
|
+
fix: [{
|
|
502
|
+
startIndex: node.startIndex,
|
|
503
|
+
endIndex: node.endIndex,
|
|
504
|
+
newText: node.text.replace(/\s*\/>$/, ">") + `</${tagNameNode.text}>`
|
|
505
|
+
}],
|
|
506
|
+
fixDescription: "Replace self-closing syntax with explicit close tag"
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
for (const child of node.children) {
|
|
512
|
+
visit(child);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
visit(rootNode);
|
|
516
|
+
return errors;
|
|
517
|
+
}
|
|
518
|
+
function areMutuallyExclusive(a, b) {
|
|
519
|
+
for (const ac of a) {
|
|
520
|
+
for (const bc of b) {
|
|
521
|
+
if (ac.name === bc.name && ac.inverted !== bc.inverted) {
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
function formatConditionClause(a, b) {
|
|
529
|
+
const seen = /* @__PURE__ */ new Map();
|
|
530
|
+
for (const c of [...a, ...b]) {
|
|
531
|
+
if (!seen.has(c.name)) {
|
|
532
|
+
seen.set(c.name, c.inverted);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (seen.size === 0) return "";
|
|
536
|
+
const parts = [];
|
|
537
|
+
for (const [name, inverted] of seen) {
|
|
538
|
+
parts.push(`${name} is ${inverted ? "falsy" : "truthy"}`);
|
|
539
|
+
}
|
|
540
|
+
return ` (when ${parts.join(", ")})`;
|
|
541
|
+
}
|
|
542
|
+
function collectAttributes(node, conditions, out) {
|
|
543
|
+
for (const child of node.children) {
|
|
544
|
+
if (child.type === "html_attribute") {
|
|
545
|
+
const nameNode = child.children.find((c) => c.type === "html_attribute_name");
|
|
546
|
+
if (nameNode) {
|
|
547
|
+
out.push({ nameNode, conditions: [...conditions] });
|
|
548
|
+
}
|
|
549
|
+
} else if (child.type === "mustache_attribute") {
|
|
550
|
+
const section = child.children.find((c) => isMustacheSection(c));
|
|
551
|
+
if (section) {
|
|
552
|
+
const name = getSectionName(section);
|
|
553
|
+
if (name) {
|
|
554
|
+
const inverted = section.type === "mustache_inverted_section";
|
|
555
|
+
collectAttributes(section, [...conditions, { name, inverted }], out);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function checkUnescapedEntities(rootNode) {
|
|
562
|
+
const errors = [];
|
|
563
|
+
function visit(node) {
|
|
564
|
+
if (node.type === "text") {
|
|
565
|
+
if (node.text === "&") {
|
|
566
|
+
errors.push({
|
|
567
|
+
node,
|
|
568
|
+
message: 'Unescaped "&" in text content \u2014 use & instead',
|
|
569
|
+
severity: "warning",
|
|
570
|
+
fix: [{
|
|
571
|
+
startIndex: node.startIndex,
|
|
572
|
+
endIndex: node.endIndex,
|
|
573
|
+
newText: "&"
|
|
574
|
+
}],
|
|
575
|
+
fixDescription: "Replace & with &"
|
|
576
|
+
});
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (node.text.includes(">")) {
|
|
580
|
+
const fixes = [];
|
|
581
|
+
let searchFrom = 0;
|
|
582
|
+
const text2 = node.text;
|
|
583
|
+
while (true) {
|
|
584
|
+
const idx = text2.indexOf(">", searchFrom);
|
|
585
|
+
if (idx === -1) break;
|
|
586
|
+
fixes.push({
|
|
587
|
+
startIndex: node.startIndex + idx,
|
|
588
|
+
endIndex: node.startIndex + idx + 1,
|
|
589
|
+
newText: ">"
|
|
590
|
+
});
|
|
591
|
+
searchFrom = idx + 1;
|
|
592
|
+
}
|
|
593
|
+
errors.push({
|
|
594
|
+
node,
|
|
595
|
+
message: 'Unescaped ">" in text content \u2014 use > instead',
|
|
596
|
+
severity: "warning",
|
|
597
|
+
fix: fixes,
|
|
598
|
+
fixDescription: "Replace > with >"
|
|
599
|
+
});
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
for (const child of node.children) {
|
|
604
|
+
visit(child);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
visit(rootNode);
|
|
608
|
+
return errors;
|
|
609
|
+
}
|
|
610
|
+
function checkHtmlComments(rootNode) {
|
|
611
|
+
const errors = [];
|
|
612
|
+
function visit(node) {
|
|
613
|
+
if (node.type === "html_comment") {
|
|
614
|
+
const raw = node.text;
|
|
615
|
+
let content = raw;
|
|
616
|
+
if (content.startsWith("<!--")) content = content.slice(4);
|
|
617
|
+
if (content.endsWith("-->")) content = content.slice(0, -3);
|
|
618
|
+
content = content.trim();
|
|
619
|
+
errors.push({
|
|
620
|
+
node,
|
|
621
|
+
message: `HTML comment found \u2014 use mustache comment {{! ... }} instead`,
|
|
622
|
+
severity: "warning",
|
|
623
|
+
fix: [{
|
|
624
|
+
startIndex: node.startIndex,
|
|
625
|
+
endIndex: node.endIndex,
|
|
626
|
+
newText: `{{! ${content} }}`
|
|
627
|
+
}],
|
|
628
|
+
fixDescription: "Replace HTML comment with mustache comment"
|
|
629
|
+
});
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
for (const child of node.children) {
|
|
633
|
+
visit(child);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
visit(rootNode);
|
|
637
|
+
return errors;
|
|
638
|
+
}
|
|
639
|
+
var KNOWN_HTML_TAGS = /* @__PURE__ */ new Set([
|
|
640
|
+
// Void elements
|
|
641
|
+
"area",
|
|
642
|
+
"base",
|
|
643
|
+
"basefont",
|
|
644
|
+
"bgsound",
|
|
645
|
+
"br",
|
|
646
|
+
"col",
|
|
647
|
+
"command",
|
|
648
|
+
"embed",
|
|
649
|
+
"frame",
|
|
650
|
+
"hr",
|
|
651
|
+
"image",
|
|
652
|
+
"img",
|
|
653
|
+
"input",
|
|
654
|
+
"isindex",
|
|
655
|
+
"keygen",
|
|
656
|
+
"link",
|
|
657
|
+
"menuitem",
|
|
658
|
+
"meta",
|
|
659
|
+
"nextid",
|
|
660
|
+
"param",
|
|
661
|
+
"source",
|
|
662
|
+
"track",
|
|
663
|
+
"wbr",
|
|
664
|
+
// Non-void elements
|
|
665
|
+
"a",
|
|
666
|
+
"abbr",
|
|
667
|
+
"address",
|
|
668
|
+
"article",
|
|
669
|
+
"aside",
|
|
670
|
+
"audio",
|
|
671
|
+
"b",
|
|
672
|
+
"bdi",
|
|
673
|
+
"bdo",
|
|
674
|
+
"blockquote",
|
|
675
|
+
"body",
|
|
676
|
+
"button",
|
|
677
|
+
"canvas",
|
|
678
|
+
"caption",
|
|
679
|
+
"cite",
|
|
680
|
+
"code",
|
|
681
|
+
"colgroup",
|
|
682
|
+
"data",
|
|
683
|
+
"datalist",
|
|
684
|
+
"dd",
|
|
685
|
+
"del",
|
|
686
|
+
"details",
|
|
687
|
+
"dfn",
|
|
688
|
+
"dialog",
|
|
689
|
+
"div",
|
|
690
|
+
"dl",
|
|
691
|
+
"dt",
|
|
692
|
+
"em",
|
|
693
|
+
"fieldset",
|
|
694
|
+
"figcaption",
|
|
695
|
+
"figure",
|
|
696
|
+
"footer",
|
|
697
|
+
"form",
|
|
698
|
+
"h1",
|
|
699
|
+
"h2",
|
|
700
|
+
"h3",
|
|
701
|
+
"h4",
|
|
702
|
+
"h5",
|
|
703
|
+
"h6",
|
|
704
|
+
"head",
|
|
705
|
+
"header",
|
|
706
|
+
"hgroup",
|
|
707
|
+
"html",
|
|
708
|
+
"i",
|
|
709
|
+
"iframe",
|
|
710
|
+
"ins",
|
|
711
|
+
"kbd",
|
|
712
|
+
"label",
|
|
713
|
+
"legend",
|
|
714
|
+
"li",
|
|
715
|
+
"main",
|
|
716
|
+
"map",
|
|
717
|
+
"mark",
|
|
718
|
+
"math",
|
|
719
|
+
"menu",
|
|
720
|
+
"meter",
|
|
721
|
+
"nav",
|
|
722
|
+
"noscript",
|
|
723
|
+
"object",
|
|
724
|
+
"ol",
|
|
725
|
+
"optgroup",
|
|
726
|
+
"option",
|
|
727
|
+
"output",
|
|
728
|
+
"p",
|
|
729
|
+
"picture",
|
|
730
|
+
"pre",
|
|
731
|
+
"progress",
|
|
732
|
+
"q",
|
|
733
|
+
"rb",
|
|
734
|
+
"rp",
|
|
735
|
+
"rt",
|
|
736
|
+
"rtc",
|
|
737
|
+
"ruby",
|
|
738
|
+
"s",
|
|
739
|
+
"samp",
|
|
740
|
+
"script",
|
|
741
|
+
"search",
|
|
742
|
+
"section",
|
|
743
|
+
"select",
|
|
744
|
+
"slot",
|
|
745
|
+
"small",
|
|
746
|
+
"span",
|
|
747
|
+
"strong",
|
|
748
|
+
"style",
|
|
749
|
+
"sub",
|
|
750
|
+
"summary",
|
|
751
|
+
"sup",
|
|
752
|
+
"svg",
|
|
753
|
+
"table",
|
|
754
|
+
"tbody",
|
|
755
|
+
"td",
|
|
756
|
+
"template",
|
|
757
|
+
"textarea",
|
|
758
|
+
"tfoot",
|
|
759
|
+
"th",
|
|
760
|
+
"thead",
|
|
761
|
+
"time",
|
|
762
|
+
"title",
|
|
763
|
+
"tr",
|
|
764
|
+
"u",
|
|
765
|
+
"ul",
|
|
766
|
+
"var",
|
|
767
|
+
"video"
|
|
768
|
+
]);
|
|
769
|
+
function checkUnrecognizedHtmlTags(rootNode, customTagNames) {
|
|
770
|
+
const errors = [];
|
|
771
|
+
const customSet = customTagNames ? new Set(customTagNames.map((n) => n.toLowerCase())) : void 0;
|
|
772
|
+
function visit(node) {
|
|
773
|
+
if (node.type === "html_element" || node.type === "html_self_closing_tag") {
|
|
774
|
+
const tagNameNode = node.type === "html_self_closing_tag" ? node.children.find((c) => c.type === "html_tag_name") : node.children.find((c) => c.type === "html_start_tag")?.children.find((c) => c.type === "html_tag_name");
|
|
775
|
+
const tagName = tagNameNode?.text.toLowerCase();
|
|
776
|
+
if (tagName === "svg" || tagName === "math") return;
|
|
777
|
+
}
|
|
778
|
+
if (node.type === "html_start_tag" || node.type === "html_self_closing_tag") {
|
|
779
|
+
const tagNameNode = node.children.find((c) => c.type === "html_tag_name");
|
|
780
|
+
if (tagNameNode) {
|
|
781
|
+
const tagName = tagNameNode.text.toLowerCase();
|
|
782
|
+
if (!KNOWN_HTML_TAGS.has(tagName) && !customSet?.has(tagName)) {
|
|
783
|
+
errors.push({
|
|
784
|
+
node: tagNameNode,
|
|
785
|
+
message: `Unrecognized HTML tag: <${tagNameNode.text}>`
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
for (const child of node.children) {
|
|
792
|
+
visit(child);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
visit(rootNode);
|
|
796
|
+
return errors;
|
|
797
|
+
}
|
|
798
|
+
function checkDuplicateAttributes(rootNode) {
|
|
799
|
+
const errors = [];
|
|
800
|
+
function visit(node) {
|
|
801
|
+
if (node.type === "html_start_tag" || node.type === "html_self_closing_tag") {
|
|
802
|
+
const occurrences = [];
|
|
803
|
+
collectAttributes(node, [], occurrences);
|
|
804
|
+
const groups = /* @__PURE__ */ new Map();
|
|
805
|
+
for (const occ of occurrences) {
|
|
806
|
+
const key = occ.nameNode.text.toLowerCase();
|
|
807
|
+
let group2 = groups.get(key);
|
|
808
|
+
if (!group2) {
|
|
809
|
+
group2 = [];
|
|
810
|
+
groups.set(key, group2);
|
|
811
|
+
}
|
|
812
|
+
group2.push(occ);
|
|
813
|
+
}
|
|
814
|
+
for (const [, group2] of groups) {
|
|
815
|
+
if (group2.length < 2) continue;
|
|
816
|
+
for (let i = 1; i < group2.length; i++) {
|
|
817
|
+
let conflictIdx = -1;
|
|
818
|
+
for (let j = 0; j < i; j++) {
|
|
819
|
+
if (!areMutuallyExclusive(group2[i].conditions, group2[j].conditions)) {
|
|
820
|
+
conflictIdx = j;
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (conflictIdx >= 0) {
|
|
825
|
+
const clause = formatConditionClause(group2[conflictIdx].conditions, group2[i].conditions);
|
|
826
|
+
errors.push({
|
|
827
|
+
node: group2[i].nameNode,
|
|
828
|
+
message: `Duplicate attribute "${group2[i].nameNode.text}"${clause}`
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
for (const child of node.children) {
|
|
836
|
+
visit(child);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
visit(rootNode);
|
|
840
|
+
return errors;
|
|
841
|
+
}
|
|
842
|
+
function checkElementContentTooLong(rootNode, elements) {
|
|
843
|
+
const errors = [];
|
|
844
|
+
if (elements.length === 0) return errors;
|
|
845
|
+
const thresholds = /* @__PURE__ */ new Map();
|
|
846
|
+
for (const { tag, maxBytes } of elements) {
|
|
847
|
+
const key = tag.toLowerCase();
|
|
848
|
+
const existing = thresholds.get(key);
|
|
849
|
+
if (existing === void 0 || maxBytes < existing) thresholds.set(key, maxBytes);
|
|
850
|
+
}
|
|
851
|
+
function visit(node) {
|
|
852
|
+
if (node.type === "html_element") {
|
|
853
|
+
const startTag = node.children.find((c) => c.type === "html_start_tag");
|
|
854
|
+
const endTag = node.children.find((c) => c.type === "html_end_tag");
|
|
855
|
+
const tagNameNode = startTag?.children.find((c) => c.type === "html_tag_name");
|
|
856
|
+
const tagName = tagNameNode?.text.toLowerCase();
|
|
857
|
+
if (tagName && startTag && endTag) {
|
|
858
|
+
const maxBytes = thresholds.get(tagName);
|
|
859
|
+
if (maxBytes !== void 0) {
|
|
860
|
+
const innerBytes = endTag.startIndex - startTag.endIndex;
|
|
861
|
+
if (innerBytes > maxBytes) {
|
|
862
|
+
errors.push({
|
|
863
|
+
node: startTag,
|
|
864
|
+
message: `<${tagName}> content is ${innerBytes} bytes, exceeds limit of ${maxBytes}`
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
for (const child of node.children) {
|
|
871
|
+
visit(child);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
visit(rootNode);
|
|
875
|
+
return errors;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// src/core/ruleMetadata.ts
|
|
879
|
+
var RULES = [
|
|
880
|
+
{
|
|
881
|
+
name: "nestedDuplicateSections",
|
|
882
|
+
defaultSeverity: "error",
|
|
883
|
+
description: "Flags `{{#name}}` nested inside another `{{#name}}` with the same name"
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
name: "unquotedMustacheAttributes",
|
|
887
|
+
defaultSeverity: "error",
|
|
888
|
+
description: "Requires quotes around mustache expressions used as attribute values"
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
name: "consecutiveDuplicateSections",
|
|
892
|
+
defaultSeverity: "warning",
|
|
893
|
+
description: "Warns when adjacent same-name sections can be merged"
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
name: "selfClosingNonVoidTags",
|
|
897
|
+
defaultSeverity: "error",
|
|
898
|
+
description: "Disallows self-closing syntax on non-void HTML elements (e.g. `<div/>`)"
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
name: "duplicateAttributes",
|
|
902
|
+
defaultSeverity: "error",
|
|
903
|
+
description: "Detects duplicate HTML attributes on the same element"
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
name: "unescapedEntities",
|
|
907
|
+
defaultSeverity: "warning",
|
|
908
|
+
description: "Flags unescaped `&` and `>` characters in text content"
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
name: "preferMustacheComments",
|
|
912
|
+
defaultSeverity: "off",
|
|
913
|
+
description: "Suggests replacing HTML comments with mustache comments"
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
name: "unrecognizedHtmlTags",
|
|
917
|
+
defaultSeverity: "error",
|
|
918
|
+
description: "Flags HTML tags that are not standard HTML elements or valid custom elements"
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
name: "elementContentTooLong",
|
|
922
|
+
defaultSeverity: "off",
|
|
923
|
+
description: "Flags configured elements whose inner content exceeds a byte-length threshold (opt-in; requires `elements: [{ tag, maxBytes }]` option)"
|
|
924
|
+
}
|
|
925
|
+
];
|
|
926
|
+
var KNOWN_RULE_NAMES = new Set(RULES.map((r) => r.name));
|
|
927
|
+
var RULE_DEFAULTS = Object.fromEntries(
|
|
928
|
+
RULES.map((r) => [r.name, r.defaultSeverity])
|
|
929
|
+
);
|
|
930
|
+
|
|
931
|
+
// node_modules/parsel-js/dist/parsel.js
|
|
932
|
+
var TOKENS = {
|
|
933
|
+
attribute: /\[\s*(?:(?<namespace>\*|[-\w\P{ASCII}]*)\|)?(?<name>[-\w\P{ASCII}]+)\s*(?:(?<operator>\W?=)\s*(?<value>.+?)\s*(\s(?<caseSensitive>[iIsS]))?\s*)?\]/gu,
|
|
934
|
+
id: /#(?<name>[-\w\P{ASCII}]+)/gu,
|
|
935
|
+
class: /\.(?<name>[-\w\P{ASCII}]+)/gu,
|
|
936
|
+
comma: /\s*,\s*/g,
|
|
937
|
+
combinator: /\s*[\s>+~]\s*/g,
|
|
938
|
+
"pseudo-element": /::(?<name>[-\w\P{ASCII}]+)(?:\((?<argument>¶*)\))?/gu,
|
|
939
|
+
"pseudo-class": /:(?<name>[-\w\P{ASCII}]+)(?:\((?<argument>¶*)\))?/gu,
|
|
940
|
+
universal: /(?:(?<namespace>\*|[-\w\P{ASCII}]*)\|)?\*/gu,
|
|
941
|
+
type: /(?:(?<namespace>\*|[-\w\P{ASCII}]*)\|)?(?<name>[-\w\P{ASCII}]+)/gu
|
|
942
|
+
// this must be last
|
|
943
|
+
};
|
|
944
|
+
var TRIM_TOKENS = /* @__PURE__ */ new Set(["combinator", "comma"]);
|
|
945
|
+
var RECURSIVE_PSEUDO_CLASSES = /* @__PURE__ */ new Set([
|
|
946
|
+
"not",
|
|
947
|
+
"is",
|
|
948
|
+
"where",
|
|
949
|
+
"has",
|
|
950
|
+
"matches",
|
|
951
|
+
"-moz-any",
|
|
952
|
+
"-webkit-any",
|
|
953
|
+
"nth-child",
|
|
954
|
+
"nth-last-child"
|
|
955
|
+
]);
|
|
956
|
+
var nthChildRegExp = /(?<index>[\dn+-]+)\s+of\s+(?<subtree>.+)/;
|
|
957
|
+
var RECURSIVE_PSEUDO_CLASSES_ARGS = {
|
|
958
|
+
"nth-child": nthChildRegExp,
|
|
959
|
+
"nth-last-child": nthChildRegExp
|
|
960
|
+
};
|
|
961
|
+
var getArgumentPatternByType = (type) => {
|
|
962
|
+
switch (type) {
|
|
963
|
+
case "pseudo-element":
|
|
964
|
+
case "pseudo-class":
|
|
965
|
+
return new RegExp(TOKENS[type].source.replace("(?<argument>\xB6*)", "(?<argument>.*)"), "gu");
|
|
966
|
+
default:
|
|
967
|
+
return TOKENS[type];
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
function gobbleParens(text2, offset) {
|
|
971
|
+
let nesting = 0;
|
|
972
|
+
let result = "";
|
|
973
|
+
for (; offset < text2.length; offset++) {
|
|
974
|
+
const char = text2[offset];
|
|
975
|
+
switch (char) {
|
|
976
|
+
case "(":
|
|
977
|
+
++nesting;
|
|
978
|
+
break;
|
|
979
|
+
case ")":
|
|
980
|
+
--nesting;
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
result += char;
|
|
984
|
+
if (nesting === 0) {
|
|
985
|
+
return result;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return result;
|
|
989
|
+
}
|
|
990
|
+
function tokenizeBy(text2, grammar = TOKENS) {
|
|
991
|
+
if (!text2) {
|
|
992
|
+
return [];
|
|
993
|
+
}
|
|
994
|
+
const tokens = [text2];
|
|
995
|
+
for (const [type, pattern] of Object.entries(grammar)) {
|
|
996
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
997
|
+
const token = tokens[i];
|
|
998
|
+
if (typeof token !== "string") {
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
pattern.lastIndex = 0;
|
|
1002
|
+
const match = pattern.exec(token);
|
|
1003
|
+
if (!match) {
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
const from = match.index - 1;
|
|
1007
|
+
const args = [];
|
|
1008
|
+
const content = match[0];
|
|
1009
|
+
const before = token.slice(0, from + 1);
|
|
1010
|
+
if (before) {
|
|
1011
|
+
args.push(before);
|
|
1012
|
+
}
|
|
1013
|
+
args.push({
|
|
1014
|
+
...match.groups,
|
|
1015
|
+
type,
|
|
1016
|
+
content
|
|
1017
|
+
});
|
|
1018
|
+
const after = token.slice(from + content.length + 1);
|
|
1019
|
+
if (after) {
|
|
1020
|
+
args.push(after);
|
|
1021
|
+
}
|
|
1022
|
+
tokens.splice(i, 1, ...args);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
let offset = 0;
|
|
1026
|
+
for (const token of tokens) {
|
|
1027
|
+
switch (typeof token) {
|
|
1028
|
+
case "string":
|
|
1029
|
+
throw new Error(`Unexpected sequence ${token} found at index ${offset}`);
|
|
1030
|
+
case "object":
|
|
1031
|
+
offset += token.content.length;
|
|
1032
|
+
token.pos = [offset - token.content.length, offset];
|
|
1033
|
+
if (TRIM_TOKENS.has(token.type)) {
|
|
1034
|
+
token.content = token.content.trim() || " ";
|
|
1035
|
+
}
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return tokens;
|
|
1040
|
+
}
|
|
1041
|
+
var STRING_PATTERN = /(['"])([^\\\n]*?)\1/g;
|
|
1042
|
+
var ESCAPE_PATTERN = /\\./g;
|
|
1043
|
+
function tokenize(selector, grammar = TOKENS) {
|
|
1044
|
+
selector = selector.trim();
|
|
1045
|
+
if (selector === "") {
|
|
1046
|
+
return [];
|
|
1047
|
+
}
|
|
1048
|
+
const replacements = [];
|
|
1049
|
+
selector = selector.replace(ESCAPE_PATTERN, (value, offset) => {
|
|
1050
|
+
replacements.push({ value, offset });
|
|
1051
|
+
return "\uE000".repeat(value.length);
|
|
1052
|
+
});
|
|
1053
|
+
selector = selector.replace(STRING_PATTERN, (value, quote, content, offset) => {
|
|
1054
|
+
replacements.push({ value, offset });
|
|
1055
|
+
return `${quote}${"\uE001".repeat(content.length)}${quote}`;
|
|
1056
|
+
});
|
|
1057
|
+
{
|
|
1058
|
+
let pos = 0;
|
|
1059
|
+
let offset;
|
|
1060
|
+
while ((offset = selector.indexOf("(", pos)) > -1) {
|
|
1061
|
+
const value = gobbleParens(selector, offset);
|
|
1062
|
+
replacements.push({ value, offset });
|
|
1063
|
+
selector = `${selector.substring(0, offset)}(${"\xB6".repeat(value.length - 2)})${selector.substring(offset + value.length)}`;
|
|
1064
|
+
pos = offset + value.length;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
const tokens = tokenizeBy(selector, grammar);
|
|
1068
|
+
const changedTokens = /* @__PURE__ */ new Set();
|
|
1069
|
+
for (const replacement of replacements.reverse()) {
|
|
1070
|
+
for (const token of tokens) {
|
|
1071
|
+
const { offset, value } = replacement;
|
|
1072
|
+
if (!(token.pos[0] <= offset && offset + value.length <= token.pos[1])) {
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
const { content } = token;
|
|
1076
|
+
const tokenOffset = offset - token.pos[0];
|
|
1077
|
+
token.content = content.slice(0, tokenOffset) + value + content.slice(tokenOffset + value.length);
|
|
1078
|
+
if (token.content !== content) {
|
|
1079
|
+
changedTokens.add(token);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
for (const token of changedTokens) {
|
|
1084
|
+
const pattern = getArgumentPatternByType(token.type);
|
|
1085
|
+
if (!pattern) {
|
|
1086
|
+
throw new Error(`Unknown token type: ${token.type}`);
|
|
1087
|
+
}
|
|
1088
|
+
pattern.lastIndex = 0;
|
|
1089
|
+
const match = pattern.exec(token.content);
|
|
1090
|
+
if (!match) {
|
|
1091
|
+
throw new Error(`Unable to parse content for ${token.type}: ${token.content}`);
|
|
1092
|
+
}
|
|
1093
|
+
Object.assign(token, match.groups);
|
|
1094
|
+
}
|
|
1095
|
+
return tokens;
|
|
1096
|
+
}
|
|
1097
|
+
function nestTokens(tokens, { list = true } = {}) {
|
|
1098
|
+
if (list && tokens.find((t) => t.type === "comma")) {
|
|
1099
|
+
const selectors = [];
|
|
1100
|
+
const temp = [];
|
|
1101
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1102
|
+
if (tokens[i].type === "comma") {
|
|
1103
|
+
if (temp.length === 0) {
|
|
1104
|
+
throw new Error("Incorrect comma at " + i);
|
|
1105
|
+
}
|
|
1106
|
+
selectors.push(nestTokens(temp, { list: false }));
|
|
1107
|
+
temp.length = 0;
|
|
1108
|
+
} else {
|
|
1109
|
+
temp.push(tokens[i]);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
if (temp.length === 0) {
|
|
1113
|
+
throw new Error("Trailing comma");
|
|
1114
|
+
} else {
|
|
1115
|
+
selectors.push(nestTokens(temp, { list: false }));
|
|
1116
|
+
}
|
|
1117
|
+
return { type: "list", list: selectors };
|
|
1118
|
+
}
|
|
1119
|
+
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
1120
|
+
let token = tokens[i];
|
|
1121
|
+
if (token.type === "combinator") {
|
|
1122
|
+
let left = tokens.slice(0, i);
|
|
1123
|
+
let right = tokens.slice(i + 1);
|
|
1124
|
+
if (left.length === 0) {
|
|
1125
|
+
return {
|
|
1126
|
+
type: "relative",
|
|
1127
|
+
combinator: token.content,
|
|
1128
|
+
right: nestTokens(right)
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
return {
|
|
1132
|
+
type: "complex",
|
|
1133
|
+
combinator: token.content,
|
|
1134
|
+
left: nestTokens(left),
|
|
1135
|
+
right: nestTokens(right)
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
switch (tokens.length) {
|
|
1140
|
+
case 0:
|
|
1141
|
+
throw new Error("Could not build AST.");
|
|
1142
|
+
case 1:
|
|
1143
|
+
return tokens[0];
|
|
1144
|
+
default:
|
|
1145
|
+
return {
|
|
1146
|
+
type: "compound",
|
|
1147
|
+
list: [...tokens]
|
|
1148
|
+
// clone to avoid pointers messing up the AST
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
function* flatten(node, parent) {
|
|
1153
|
+
switch (node.type) {
|
|
1154
|
+
case "list":
|
|
1155
|
+
for (let child of node.list) {
|
|
1156
|
+
yield* flatten(child, node);
|
|
1157
|
+
}
|
|
1158
|
+
break;
|
|
1159
|
+
case "complex":
|
|
1160
|
+
yield* flatten(node.left, node);
|
|
1161
|
+
yield* flatten(node.right, node);
|
|
1162
|
+
break;
|
|
1163
|
+
case "relative":
|
|
1164
|
+
yield* flatten(node.right, node);
|
|
1165
|
+
break;
|
|
1166
|
+
case "compound":
|
|
1167
|
+
yield* node.list.map((token) => [token, node]);
|
|
1168
|
+
break;
|
|
1169
|
+
default:
|
|
1170
|
+
yield [node, parent];
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
function parse(selector, { recursive = true, list = true } = {}) {
|
|
1174
|
+
const tokens = tokenize(selector);
|
|
1175
|
+
if (!tokens) {
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
const ast = nestTokens(tokens, { list });
|
|
1179
|
+
if (!recursive) {
|
|
1180
|
+
return ast;
|
|
1181
|
+
}
|
|
1182
|
+
for (const [token] of flatten(ast)) {
|
|
1183
|
+
if (token.type !== "pseudo-class" || !token.argument) {
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
if (!RECURSIVE_PSEUDO_CLASSES.has(token.name)) {
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
let argument = token.argument;
|
|
1190
|
+
const childArg = RECURSIVE_PSEUDO_CLASSES_ARGS[token.name];
|
|
1191
|
+
if (childArg) {
|
|
1192
|
+
const match = childArg.exec(argument);
|
|
1193
|
+
if (!match) {
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
Object.assign(token, match.groups);
|
|
1197
|
+
argument = match.groups["subtree"];
|
|
1198
|
+
}
|
|
1199
|
+
if (!argument) {
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
Object.assign(token, {
|
|
1203
|
+
subtree: parse(argument, {
|
|
1204
|
+
recursive: true,
|
|
1205
|
+
list: true
|
|
1206
|
+
})
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
return ast;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// src/core/selectorMatcher.ts
|
|
1213
|
+
var MUSTACHE_KIND_PSEUDO = /* @__PURE__ */ new Set([
|
|
1214
|
+
"m-section",
|
|
1215
|
+
"m-inverted",
|
|
1216
|
+
"m-variable",
|
|
1217
|
+
"m-raw",
|
|
1218
|
+
"m-comment",
|
|
1219
|
+
"m-partial"
|
|
1220
|
+
]);
|
|
1221
|
+
function preprocessMustacheLiterals(raw) {
|
|
1222
|
+
let out = "";
|
|
1223
|
+
let i = 0;
|
|
1224
|
+
const len = raw.length;
|
|
1225
|
+
while (i < len) {
|
|
1226
|
+
const ch = raw[i];
|
|
1227
|
+
if (ch === '"' || ch === "'") {
|
|
1228
|
+
out += ch;
|
|
1229
|
+
i++;
|
|
1230
|
+
while (i < len && raw[i] !== ch) {
|
|
1231
|
+
if (raw[i] === "\\" && i + 1 < len) {
|
|
1232
|
+
out += raw[i] + raw[i + 1];
|
|
1233
|
+
i += 2;
|
|
1234
|
+
} else {
|
|
1235
|
+
out += raw[i];
|
|
1236
|
+
i++;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
if (i < len) {
|
|
1240
|
+
out += raw[i];
|
|
1241
|
+
i++;
|
|
1242
|
+
}
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
if (ch !== "{" || raw[i + 1] !== "{") {
|
|
1246
|
+
out += ch;
|
|
1247
|
+
i++;
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
if (raw[i + 2] === "{") {
|
|
1251
|
+
const end2 = raw.indexOf("}}}", i + 3);
|
|
1252
|
+
if (end2 < 0) return null;
|
|
1253
|
+
const inner = raw.slice(i + 3, end2).trim();
|
|
1254
|
+
if (inner.length === 0) return null;
|
|
1255
|
+
out += `:m-raw(${inner})`;
|
|
1256
|
+
i = end2 + 3;
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
const end = raw.indexOf("}}", i + 2);
|
|
1260
|
+
if (end < 0) return null;
|
|
1261
|
+
const body = raw.slice(i + 2, end);
|
|
1262
|
+
i = end + 2;
|
|
1263
|
+
const sigil = body.trimStart()[0];
|
|
1264
|
+
const content = body.replace(/^\s*[#^!>/]\s*/, "").replace(/^\s+|\s+$/g, "");
|
|
1265
|
+
switch (sigil) {
|
|
1266
|
+
case "#":
|
|
1267
|
+
if (content.length === 0) return null;
|
|
1268
|
+
out += `:m-section(${content})`;
|
|
1269
|
+
break;
|
|
1270
|
+
case "^":
|
|
1271
|
+
if (content.length === 0) return null;
|
|
1272
|
+
out += `:m-inverted(${content})`;
|
|
1273
|
+
break;
|
|
1274
|
+
case "!":
|
|
1275
|
+
if (content.length === 0) return null;
|
|
1276
|
+
out += `:m-comment(${content})`;
|
|
1277
|
+
break;
|
|
1278
|
+
case ">":
|
|
1279
|
+
if (content.length === 0) return null;
|
|
1280
|
+
out += `:m-partial(${content})`;
|
|
1281
|
+
break;
|
|
1282
|
+
case "/":
|
|
1283
|
+
return null;
|
|
1284
|
+
case "=":
|
|
1285
|
+
return null;
|
|
1286
|
+
default: {
|
|
1287
|
+
const path = body.trim();
|
|
1288
|
+
if (path.length === 0) return null;
|
|
1289
|
+
out += `:m-variable(${path})`;
|
|
1290
|
+
break;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return out;
|
|
1295
|
+
}
|
|
1296
|
+
function parseSelector(raw) {
|
|
1297
|
+
if (typeof raw !== "string" || raw.trim() === "") return null;
|
|
1298
|
+
const preprocessed = preprocessMustacheLiterals(raw);
|
|
1299
|
+
if (preprocessed === null) return null;
|
|
1300
|
+
let ast;
|
|
1301
|
+
try {
|
|
1302
|
+
ast = parse(preprocessed);
|
|
1303
|
+
} catch {
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
if (!ast) return null;
|
|
1307
|
+
const tops = ast.type === "list" ? ast.list : [ast];
|
|
1308
|
+
const alts = [];
|
|
1309
|
+
for (const top of tops) {
|
|
1310
|
+
const expanded = expandIs(top);
|
|
1311
|
+
if (expanded === null) return null;
|
|
1312
|
+
for (const exp of expanded) {
|
|
1313
|
+
const segments = [];
|
|
1314
|
+
if (!collectSegments(exp, "descendant", segments)) return null;
|
|
1315
|
+
if (segments.length === 0) return null;
|
|
1316
|
+
alts.push(segments);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return alts.length > 0 ? alts : null;
|
|
1320
|
+
}
|
|
1321
|
+
function expandIs(ast) {
|
|
1322
|
+
switch (ast.type) {
|
|
1323
|
+
case "list": {
|
|
1324
|
+
const out = [];
|
|
1325
|
+
for (const alt of ast.list) {
|
|
1326
|
+
const expanded = expandIs(alt);
|
|
1327
|
+
if (expanded === null) return null;
|
|
1328
|
+
out.push(...expanded);
|
|
1329
|
+
}
|
|
1330
|
+
return out;
|
|
1331
|
+
}
|
|
1332
|
+
case "complex": {
|
|
1333
|
+
const lefts = expandIs(ast.left);
|
|
1334
|
+
if (lefts === null) return null;
|
|
1335
|
+
const rights = expandIs(ast.right);
|
|
1336
|
+
if (rights === null) return null;
|
|
1337
|
+
const out = [];
|
|
1338
|
+
for (const l of lefts) for (const r of rights) {
|
|
1339
|
+
out.push({ ...ast, left: l, right: r });
|
|
1340
|
+
}
|
|
1341
|
+
return out;
|
|
1342
|
+
}
|
|
1343
|
+
case "compound": {
|
|
1344
|
+
if (ast.list.length === 1) {
|
|
1345
|
+
const tok = ast.list[0];
|
|
1346
|
+
if (tok.type === "pseudo-class" && tok.name === "is") {
|
|
1347
|
+
if (!tok.subtree) return null;
|
|
1348
|
+
return expandIs(tok.subtree);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
return expandCompoundWithIs(ast.list);
|
|
1352
|
+
}
|
|
1353
|
+
default:
|
|
1354
|
+
if (ast.type === "pseudo-class" && ast.name === "is") {
|
|
1355
|
+
if (!ast.subtree) return null;
|
|
1356
|
+
return expandIs(ast.subtree);
|
|
1357
|
+
}
|
|
1358
|
+
return [ast];
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
function expandCompoundWithIs(tokens) {
|
|
1362
|
+
let variants = [[]];
|
|
1363
|
+
for (const tok of tokens) {
|
|
1364
|
+
if (tok.type === "pseudo-class" && tok.name === "is") {
|
|
1365
|
+
if (!tok.subtree) return null;
|
|
1366
|
+
const alts = expandIs(tok.subtree);
|
|
1367
|
+
if (alts === null) return null;
|
|
1368
|
+
const next = [];
|
|
1369
|
+
for (const base of variants) {
|
|
1370
|
+
for (const alt of alts) {
|
|
1371
|
+
if (alt.type === "compound") {
|
|
1372
|
+
next.push([...base, ...alt.list]);
|
|
1373
|
+
} else if (alt.type === "complex" || alt.type === "list" || alt.type === "relative") {
|
|
1374
|
+
return null;
|
|
1375
|
+
} else {
|
|
1376
|
+
next.push([...base, alt]);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
variants = next;
|
|
1381
|
+
} else {
|
|
1382
|
+
variants = variants.map((v) => [...v, tok]);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
return variants.map(
|
|
1386
|
+
(list) => list.length === 1 ? list[0] : { type: "compound", list }
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
function collectSegments(ast, combinator, out) {
|
|
1390
|
+
if (ast.type === "complex") {
|
|
1391
|
+
const mapped = mapCombinator(ast.combinator);
|
|
1392
|
+
if (!mapped) return false;
|
|
1393
|
+
return collectSegments(ast.left, "descendant", out) && collectSegments(ast.right, mapped, out);
|
|
1394
|
+
}
|
|
1395
|
+
if (ast.type === "list" || ast.type === "relative") return false;
|
|
1396
|
+
const segment = segmentFromCompound(ast);
|
|
1397
|
+
if (!segment) return false;
|
|
1398
|
+
segment.combinator = combinator;
|
|
1399
|
+
out.push(segment);
|
|
1400
|
+
return true;
|
|
1401
|
+
}
|
|
1402
|
+
function mapCombinator(c) {
|
|
1403
|
+
const trimmed = c.trim();
|
|
1404
|
+
if (trimmed === "") return "descendant";
|
|
1405
|
+
if (trimmed === ">") return "child";
|
|
1406
|
+
if (trimmed === "+") return "adjacent-sibling";
|
|
1407
|
+
if (trimmed === "~") return "general-sibling";
|
|
1408
|
+
return null;
|
|
1409
|
+
}
|
|
1410
|
+
function segmentFromCompound(ast) {
|
|
1411
|
+
const tokens = ast.type === "compound" ? ast.list : [ast];
|
|
1412
|
+
let kind;
|
|
1413
|
+
let name = null;
|
|
1414
|
+
let pathRegex;
|
|
1415
|
+
let rootOnly = false;
|
|
1416
|
+
const attributes = [];
|
|
1417
|
+
const descendantChecks = [];
|
|
1418
|
+
const selfNegations = [];
|
|
1419
|
+
const forbidChange = (requested) => {
|
|
1420
|
+
if (kind === void 0) return false;
|
|
1421
|
+
if (kind === requested) return false;
|
|
1422
|
+
return true;
|
|
1423
|
+
};
|
|
1424
|
+
for (const token of tokens) {
|
|
1425
|
+
switch (token.type) {
|
|
1426
|
+
case "type":
|
|
1427
|
+
if (forbidChange("html")) return null;
|
|
1428
|
+
kind = "html";
|
|
1429
|
+
name = token.name.toLowerCase();
|
|
1430
|
+
break;
|
|
1431
|
+
case "universal":
|
|
1432
|
+
if (forbidChange("html")) return null;
|
|
1433
|
+
kind = "html";
|
|
1434
|
+
name = null;
|
|
1435
|
+
break;
|
|
1436
|
+
case "class":
|
|
1437
|
+
if (forbidChange("html")) return null;
|
|
1438
|
+
kind = "html";
|
|
1439
|
+
attributes.push(classConstraint(token, false));
|
|
1440
|
+
break;
|
|
1441
|
+
case "id":
|
|
1442
|
+
if (forbidChange("html")) return null;
|
|
1443
|
+
kind = "html";
|
|
1444
|
+
attributes.push(idConstraint(token, false));
|
|
1445
|
+
break;
|
|
1446
|
+
case "attribute": {
|
|
1447
|
+
if (forbidChange("html")) return null;
|
|
1448
|
+
if (kind === void 0) kind = "html";
|
|
1449
|
+
const c = attributeConstraint(token, false);
|
|
1450
|
+
if (!c) return null;
|
|
1451
|
+
attributes.push(c);
|
|
1452
|
+
break;
|
|
1453
|
+
}
|
|
1454
|
+
case "pseudo-class": {
|
|
1455
|
+
if (MUSTACHE_KIND_PSEUDO.has(token.name)) {
|
|
1456
|
+
const mustacheKind = mustacheKindFromMarker(token.name);
|
|
1457
|
+
if (mustacheKind === null) return null;
|
|
1458
|
+
if (forbidChange(mustacheKind)) return null;
|
|
1459
|
+
kind = mustacheKind;
|
|
1460
|
+
const glob = parseGlob(token.argument ?? "");
|
|
1461
|
+
name = glob.name;
|
|
1462
|
+
pathRegex = glob.pathRegex;
|
|
1463
|
+
break;
|
|
1464
|
+
}
|
|
1465
|
+
if (token.name === "has") {
|
|
1466
|
+
const sel = subtreeToSelector(token.subtree);
|
|
1467
|
+
if (!sel) return null;
|
|
1468
|
+
descendantChecks.push({ selector: sel, negated: false });
|
|
1469
|
+
break;
|
|
1470
|
+
}
|
|
1471
|
+
if (token.name === "not") {
|
|
1472
|
+
if (!applyNegatedSubtree(token.subtree, attributes, descendantChecks, selfNegations)) return null;
|
|
1473
|
+
break;
|
|
1474
|
+
}
|
|
1475
|
+
if (token.name === "root") {
|
|
1476
|
+
rootOnly = true;
|
|
1477
|
+
if (kind === void 0) kind = "html";
|
|
1478
|
+
break;
|
|
1479
|
+
}
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1482
|
+
default:
|
|
1483
|
+
return null;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
if (kind === void 0) {
|
|
1487
|
+
kind = "html";
|
|
1488
|
+
}
|
|
1489
|
+
if (rootOnly) {
|
|
1490
|
+
if (name !== null || attributes.length > 0 || kind !== "html") return null;
|
|
1491
|
+
}
|
|
1492
|
+
const isHtml = kind === "html";
|
|
1493
|
+
const finalAttrs = isHtml ? attributes : [];
|
|
1494
|
+
return { kind, rootOnly, name, pathRegex, attributes: finalAttrs, descendantChecks, selfNegations, combinator: "descendant" };
|
|
1495
|
+
}
|
|
1496
|
+
function mustacheKindFromMarker(name) {
|
|
1497
|
+
switch (name) {
|
|
1498
|
+
case "m-section":
|
|
1499
|
+
return "section";
|
|
1500
|
+
case "m-inverted":
|
|
1501
|
+
return "inverted";
|
|
1502
|
+
case "m-variable":
|
|
1503
|
+
return "variable";
|
|
1504
|
+
case "m-raw":
|
|
1505
|
+
return "raw";
|
|
1506
|
+
case "m-comment":
|
|
1507
|
+
return "comment";
|
|
1508
|
+
case "m-partial":
|
|
1509
|
+
return "partial";
|
|
1510
|
+
default:
|
|
1511
|
+
return null;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
function parseGlob(arg) {
|
|
1515
|
+
const trimmed = arg.trim();
|
|
1516
|
+
if (trimmed === "" || trimmed === "*") {
|
|
1517
|
+
return { name: null };
|
|
1518
|
+
}
|
|
1519
|
+
if (!trimmed.includes("*")) {
|
|
1520
|
+
return { name: trimmed.toLowerCase() };
|
|
1521
|
+
}
|
|
1522
|
+
const escaped = trimmed.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1523
|
+
const pathRegex = new RegExp(`^${escaped}$`, "i");
|
|
1524
|
+
return { name: trimmed.toLowerCase(), pathRegex };
|
|
1525
|
+
}
|
|
1526
|
+
function attributeConstraint(token, negated) {
|
|
1527
|
+
const name = token.name.toLowerCase();
|
|
1528
|
+
if (token.operator === void 0) {
|
|
1529
|
+
return { name, op: "=", value: void 0, negated };
|
|
1530
|
+
}
|
|
1531
|
+
let op;
|
|
1532
|
+
switch (token.operator) {
|
|
1533
|
+
case "=":
|
|
1534
|
+
op = "=";
|
|
1535
|
+
break;
|
|
1536
|
+
case "^=":
|
|
1537
|
+
op = "^=";
|
|
1538
|
+
break;
|
|
1539
|
+
case "*=":
|
|
1540
|
+
op = "*=";
|
|
1541
|
+
break;
|
|
1542
|
+
case "$=":
|
|
1543
|
+
op = "$=";
|
|
1544
|
+
break;
|
|
1545
|
+
case "~=":
|
|
1546
|
+
op = "~=";
|
|
1547
|
+
break;
|
|
1548
|
+
default:
|
|
1549
|
+
return null;
|
|
1550
|
+
}
|
|
1551
|
+
return { name, op, value: stripQuotes(token.value ?? ""), negated };
|
|
1552
|
+
}
|
|
1553
|
+
function classConstraint(token, negated) {
|
|
1554
|
+
return { name: "class", op: "~=", value: token.name, negated };
|
|
1555
|
+
}
|
|
1556
|
+
function idConstraint(token, negated) {
|
|
1557
|
+
return { name: "id", op: "=", value: token.name, negated };
|
|
1558
|
+
}
|
|
1559
|
+
function applyNegatedSubtree(subtree, attributes, descendantChecks, selfNegations) {
|
|
1560
|
+
if (!subtree) return false;
|
|
1561
|
+
if (subtree.type === "attribute") {
|
|
1562
|
+
const c = attributeConstraint(subtree, true);
|
|
1563
|
+
if (!c) return false;
|
|
1564
|
+
attributes.push(c);
|
|
1565
|
+
return true;
|
|
1566
|
+
}
|
|
1567
|
+
if (subtree.type === "class") {
|
|
1568
|
+
attributes.push(classConstraint(subtree, true));
|
|
1569
|
+
return true;
|
|
1570
|
+
}
|
|
1571
|
+
if (subtree.type === "id") {
|
|
1572
|
+
attributes.push(idConstraint(subtree, true));
|
|
1573
|
+
return true;
|
|
1574
|
+
}
|
|
1575
|
+
if (subtree.type === "pseudo-class" && subtree.name === "has") {
|
|
1576
|
+
const sel2 = subtreeToSelector(subtree.subtree);
|
|
1577
|
+
if (!sel2) return false;
|
|
1578
|
+
descendantChecks.push({ selector: sel2, negated: true });
|
|
1579
|
+
return true;
|
|
1580
|
+
}
|
|
1581
|
+
const sel = subtreeToSelector(subtree);
|
|
1582
|
+
if (sel) {
|
|
1583
|
+
selfNegations.push(sel);
|
|
1584
|
+
return true;
|
|
1585
|
+
}
|
|
1586
|
+
return false;
|
|
1587
|
+
}
|
|
1588
|
+
function subtreeToSelector(subtree) {
|
|
1589
|
+
if (!subtree) return null;
|
|
1590
|
+
const tops = subtree.type === "list" ? subtree.list : [subtree];
|
|
1591
|
+
const alts = [];
|
|
1592
|
+
for (const top of tops) {
|
|
1593
|
+
const segments = [];
|
|
1594
|
+
if (!collectSegments(top, "descendant", segments)) return null;
|
|
1595
|
+
if (segments.length === 0) return null;
|
|
1596
|
+
alts.push(segments);
|
|
1597
|
+
}
|
|
1598
|
+
return alts.length > 0 ? alts : null;
|
|
1599
|
+
}
|
|
1600
|
+
function stripQuotes(raw) {
|
|
1601
|
+
if (raw.length < 2) return raw;
|
|
1602
|
+
const first = raw[0];
|
|
1603
|
+
const last = raw[raw.length - 1];
|
|
1604
|
+
if ((first === '"' || first === "'") && first === last) {
|
|
1605
|
+
return raw.slice(1, -1);
|
|
1606
|
+
}
|
|
1607
|
+
return raw;
|
|
1608
|
+
}
|
|
1609
|
+
function ancestorKindForNode(node) {
|
|
1610
|
+
if (HTML_ELEMENT_TYPES.has(node.type)) return "html";
|
|
1611
|
+
if (node.type === "mustache_section") return "section";
|
|
1612
|
+
if (node.type === "mustache_inverted_section") return "inverted";
|
|
1613
|
+
return null;
|
|
1614
|
+
}
|
|
1615
|
+
function getHtmlAttributes(node) {
|
|
1616
|
+
const startTag = node.children.find(
|
|
1617
|
+
(c) => c.type === "html_start_tag" || c.type === "html_self_closing_tag"
|
|
1618
|
+
);
|
|
1619
|
+
if (!startTag) return [];
|
|
1620
|
+
const attrs = [];
|
|
1621
|
+
for (const child of startTag.children) {
|
|
1622
|
+
if (child.type !== "html_attribute") continue;
|
|
1623
|
+
let attrName = "";
|
|
1624
|
+
let attrValue;
|
|
1625
|
+
for (const part of child.children) {
|
|
1626
|
+
if (part.type === "html_attribute_name") {
|
|
1627
|
+
attrName = part.text.toLowerCase();
|
|
1628
|
+
} else if (part.type === "html_quoted_attribute_value") {
|
|
1629
|
+
attrValue = part.text.replace(/^["']|["']$/g, "");
|
|
1630
|
+
} else if (part.type === "html_attribute_value") {
|
|
1631
|
+
attrValue = part.text;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
if (attrName) attrs.push({ name: attrName, value: attrValue });
|
|
1635
|
+
}
|
|
1636
|
+
return attrs;
|
|
1637
|
+
}
|
|
1638
|
+
function matchesAttributeValue(has, c) {
|
|
1639
|
+
if (has === void 0 || c.value === void 0) return false;
|
|
1640
|
+
const v = c.value;
|
|
1641
|
+
if (v === "") return false;
|
|
1642
|
+
switch (c.op) {
|
|
1643
|
+
case "=":
|
|
1644
|
+
return has === v;
|
|
1645
|
+
case "^=":
|
|
1646
|
+
return has.startsWith(v);
|
|
1647
|
+
case "*=":
|
|
1648
|
+
return has.includes(v);
|
|
1649
|
+
case "$=":
|
|
1650
|
+
return has.endsWith(v);
|
|
1651
|
+
case "~=":
|
|
1652
|
+
return has.split(/\s+/).includes(v);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
function checkAttributes(node, constraints) {
|
|
1656
|
+
if (constraints.length === 0) return true;
|
|
1657
|
+
const nodeAttrs = getHtmlAttributes(node);
|
|
1658
|
+
for (const c of constraints) {
|
|
1659
|
+
const found = nodeAttrs.find((a) => a.name === c.name);
|
|
1660
|
+
if (c.negated) {
|
|
1661
|
+
if (!found) continue;
|
|
1662
|
+
if (c.value === void 0) return false;
|
|
1663
|
+
if (matchesAttributeValue(found.value, c)) return false;
|
|
1664
|
+
continue;
|
|
1665
|
+
}
|
|
1666
|
+
if (c.value === void 0) {
|
|
1667
|
+
if (!found) return false;
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
if (!found || !matchesAttributeValue(found.value, c)) return false;
|
|
1671
|
+
}
|
|
1672
|
+
return true;
|
|
1673
|
+
}
|
|
1674
|
+
function checkDescendants(node, checks) {
|
|
1675
|
+
if (checks.length === 0) return true;
|
|
1676
|
+
for (const check of checks) {
|
|
1677
|
+
const present = hasDescendantMatch(node, check.selector);
|
|
1678
|
+
if (check.negated ? present : !present) return false;
|
|
1679
|
+
}
|
|
1680
|
+
return true;
|
|
1681
|
+
}
|
|
1682
|
+
function hasDescendantMatch(node, selector) {
|
|
1683
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
1684
|
+
if (matchSelector(node.children[i], selector, node.children, i).length > 0) return true;
|
|
1685
|
+
}
|
|
1686
|
+
return false;
|
|
1687
|
+
}
|
|
1688
|
+
function checkSelfNegations(node, negations, rootNode) {
|
|
1689
|
+
for (const sel of negations) {
|
|
1690
|
+
for (const alt of sel) {
|
|
1691
|
+
if (alt.length !== 1) continue;
|
|
1692
|
+
if (nodeMatchesSegment(node, alt[0], rootNode)) return false;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
return true;
|
|
1696
|
+
}
|
|
1697
|
+
function matchesName(actual, segment) {
|
|
1698
|
+
if (segment.name === null) return true;
|
|
1699
|
+
if (actual === null) return false;
|
|
1700
|
+
if (segment.pathRegex) return segment.pathRegex.test(actual);
|
|
1701
|
+
return actual === segment.name;
|
|
1702
|
+
}
|
|
1703
|
+
function nodeMatchesSegment(node, segment, rootNode) {
|
|
1704
|
+
if (segment.rootOnly) {
|
|
1705
|
+
if (node !== rootNode) return false;
|
|
1706
|
+
return checkDescendants(node, segment.descendantChecks) && checkSelfNegations(node, segment.selfNegations, rootNode);
|
|
1707
|
+
}
|
|
1708
|
+
const baseMatches = (() => {
|
|
1709
|
+
switch (segment.kind) {
|
|
1710
|
+
case "html": {
|
|
1711
|
+
if (!HTML_ELEMENT_TYPES.has(node.type)) return false;
|
|
1712
|
+
if (segment.name !== null) {
|
|
1713
|
+
const tagName = getTagName(node)?.toLowerCase();
|
|
1714
|
+
if (tagName !== segment.name) return false;
|
|
1715
|
+
}
|
|
1716
|
+
return checkAttributes(node, segment.attributes) && checkDescendants(node, segment.descendantChecks);
|
|
1717
|
+
}
|
|
1718
|
+
case "section":
|
|
1719
|
+
if (node.type !== "mustache_section") return false;
|
|
1720
|
+
if (!matchesName(getSectionName(node)?.toLowerCase() ?? null, segment)) return false;
|
|
1721
|
+
return checkDescendants(node, segment.descendantChecks);
|
|
1722
|
+
case "inverted":
|
|
1723
|
+
if (node.type !== "mustache_inverted_section") return false;
|
|
1724
|
+
if (!matchesName(getSectionName(node)?.toLowerCase() ?? null, segment)) return false;
|
|
1725
|
+
return checkDescendants(node, segment.descendantChecks);
|
|
1726
|
+
case "variable":
|
|
1727
|
+
if (node.type !== "mustache_interpolation") return false;
|
|
1728
|
+
if (!matchesName(getInterpolationPath(node)?.toLowerCase() ?? null, segment)) return false;
|
|
1729
|
+
return checkDescendants(node, segment.descendantChecks);
|
|
1730
|
+
case "raw":
|
|
1731
|
+
if (node.type !== "mustache_triple") return false;
|
|
1732
|
+
if (!matchesName(getInterpolationPath(node)?.toLowerCase() ?? null, segment)) return false;
|
|
1733
|
+
return checkDescendants(node, segment.descendantChecks);
|
|
1734
|
+
case "comment":
|
|
1735
|
+
if (node.type !== "mustache_comment") return false;
|
|
1736
|
+
if (!matchesName(getCommentContent(node)?.toLowerCase() ?? null, segment)) return false;
|
|
1737
|
+
return checkDescendants(node, segment.descendantChecks);
|
|
1738
|
+
case "partial":
|
|
1739
|
+
if (node.type !== "mustache_partial") return false;
|
|
1740
|
+
if (!matchesName(getPartialName(node)?.toLowerCase() ?? null, segment)) return false;
|
|
1741
|
+
return checkDescendants(node, segment.descendantChecks);
|
|
1742
|
+
}
|
|
1743
|
+
})();
|
|
1744
|
+
if (!baseMatches) return false;
|
|
1745
|
+
return checkSelfNegations(node, segment.selfNegations, rootNode);
|
|
1746
|
+
}
|
|
1747
|
+
function checkPrefix(cursor, segments, segIdx, stepCombinator, rootNode) {
|
|
1748
|
+
if (segIdx < 0) return true;
|
|
1749
|
+
const segment = segments[segIdx];
|
|
1750
|
+
if (stepCombinator === "adjacent-sibling" || stepCombinator === "general-sibling") {
|
|
1751
|
+
for (let i = cursor.indexInSiblings - 1; i >= 0; i--) {
|
|
1752
|
+
const sib = cursor.siblings[i];
|
|
1753
|
+
if (!isMatchableNode(sib)) continue;
|
|
1754
|
+
if (!nodeMatchesSegment(sib, segment, rootNode)) {
|
|
1755
|
+
if (stepCombinator === "adjacent-sibling") return false;
|
|
1756
|
+
continue;
|
|
1757
|
+
}
|
|
1758
|
+
const newCursor = {
|
|
1759
|
+
ancestors: cursor.ancestors,
|
|
1760
|
+
siblings: cursor.siblings,
|
|
1761
|
+
indexInSiblings: i
|
|
1762
|
+
};
|
|
1763
|
+
if (checkPrefix(newCursor, segments, segIdx - 1, segment.combinator, rootNode)) return true;
|
|
1764
|
+
if (stepCombinator === "adjacent-sibling") return false;
|
|
1765
|
+
}
|
|
1766
|
+
return false;
|
|
1767
|
+
}
|
|
1768
|
+
const ancestorKind = ancestorKindForSegment(segment);
|
|
1769
|
+
if (ancestorKind === null) return false;
|
|
1770
|
+
if (stepCombinator === "child") {
|
|
1771
|
+
for (let a = cursor.ancestors.length - 1; a >= 0; a--) {
|
|
1772
|
+
const entry = cursor.ancestors[a];
|
|
1773
|
+
if (entry.kind !== ancestorKind) {
|
|
1774
|
+
if (ancestorKind === "root" && entry.kind === "html") return false;
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1777
|
+
if (!matchesName(entry.name, segment)) return false;
|
|
1778
|
+
if (segment.kind === "html" && !checkAttributes(entry.node, segment.attributes)) return false;
|
|
1779
|
+
if (!checkDescendants(entry.node, segment.descendantChecks)) return false;
|
|
1780
|
+
if (!checkSelfNegations(entry.node, segment.selfNegations, rootNode)) return false;
|
|
1781
|
+
const newCursor = {
|
|
1782
|
+
ancestors: cursor.ancestors.slice(0, a),
|
|
1783
|
+
siblings: entry.siblings,
|
|
1784
|
+
indexInSiblings: entry.indexInSiblings
|
|
1785
|
+
};
|
|
1786
|
+
return checkPrefix(newCursor, segments, segIdx - 1, segment.combinator, rootNode);
|
|
1787
|
+
}
|
|
1788
|
+
return false;
|
|
1789
|
+
}
|
|
1790
|
+
for (let a = cursor.ancestors.length - 1; a >= 0; a--) {
|
|
1791
|
+
const entry = cursor.ancestors[a];
|
|
1792
|
+
if (entry.kind !== ancestorKind) continue;
|
|
1793
|
+
if (!matchesName(entry.name, segment)) continue;
|
|
1794
|
+
if (segment.kind === "html" && !checkAttributes(entry.node, segment.attributes)) continue;
|
|
1795
|
+
if (!checkDescendants(entry.node, segment.descendantChecks)) continue;
|
|
1796
|
+
if (!checkSelfNegations(entry.node, segment.selfNegations, rootNode)) continue;
|
|
1797
|
+
const newCursor = {
|
|
1798
|
+
ancestors: cursor.ancestors.slice(0, a),
|
|
1799
|
+
siblings: entry.siblings,
|
|
1800
|
+
indexInSiblings: entry.indexInSiblings
|
|
1801
|
+
};
|
|
1802
|
+
if (checkPrefix(newCursor, segments, segIdx - 1, segment.combinator, rootNode)) return true;
|
|
1803
|
+
}
|
|
1804
|
+
return false;
|
|
1805
|
+
}
|
|
1806
|
+
function isMatchableNode(node) {
|
|
1807
|
+
return HTML_ELEMENT_TYPES.has(node.type) || node.type === "mustache_section" || node.type === "mustache_inverted_section" || node.type === "mustache_interpolation" || node.type === "mustache_triple" || node.type === "mustache_comment" || node.type === "mustache_partial";
|
|
1808
|
+
}
|
|
1809
|
+
function ancestorKindForSegment(segment) {
|
|
1810
|
+
if (segment.rootOnly) return "root";
|
|
1811
|
+
if (segment.kind === "html") return "html";
|
|
1812
|
+
if (segment.kind === "section") return "section";
|
|
1813
|
+
if (segment.kind === "inverted") return "inverted";
|
|
1814
|
+
return null;
|
|
1815
|
+
}
|
|
1816
|
+
function getReportNode(node, rootNode) {
|
|
1817
|
+
if (HTML_ELEMENT_TYPES.has(node.type)) {
|
|
1818
|
+
const startTag = node.children.find(
|
|
1819
|
+
(c) => c.type === "html_start_tag" || c.type === "html_self_closing_tag"
|
|
1820
|
+
);
|
|
1821
|
+
return startTag ?? node;
|
|
1822
|
+
}
|
|
1823
|
+
if (node.type === "mustache_section" || node.type === "mustache_inverted_section") {
|
|
1824
|
+
const begin = node.children.find(
|
|
1825
|
+
(c) => c.type === "mustache_section_begin" || c.type === "mustache_inverted_section_begin"
|
|
1826
|
+
);
|
|
1827
|
+
return begin ?? node;
|
|
1828
|
+
}
|
|
1829
|
+
if (rootNode && node === rootNode) {
|
|
1830
|
+
return {
|
|
1831
|
+
type: node.type,
|
|
1832
|
+
text: "",
|
|
1833
|
+
startPosition: node.startPosition,
|
|
1834
|
+
endPosition: { row: node.startPosition.row, column: node.startPosition.column + 1 },
|
|
1835
|
+
startIndex: node.startIndex,
|
|
1836
|
+
endIndex: Math.min(node.startIndex + 1, node.endIndex),
|
|
1837
|
+
children: []
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
return node;
|
|
1841
|
+
}
|
|
1842
|
+
function matchAlternative(rootNode, segments, rootSiblings, rootIndexInSiblings) {
|
|
1843
|
+
const results = [];
|
|
1844
|
+
const lastSegment = segments[segments.length - 1];
|
|
1845
|
+
function walk(node, ancestors, siblings, indexInSiblings) {
|
|
1846
|
+
if (nodeMatchesSegment(node, lastSegment, rootNode)) {
|
|
1847
|
+
const cursor = { ancestors, siblings, indexInSiblings };
|
|
1848
|
+
if (segments.length === 1 || checkPrefix(cursor, segments, segments.length - 2, lastSegment.combinator, rootNode)) {
|
|
1849
|
+
results.push(getReportNode(node, rootNode));
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
let newAncestors = ancestors;
|
|
1853
|
+
const ancestorKind = ancestorKindForNode(node);
|
|
1854
|
+
if (ancestorKind !== null) {
|
|
1855
|
+
const name = ancestorKind === "html" ? getTagName(node)?.toLowerCase() : getSectionName(node)?.toLowerCase();
|
|
1856
|
+
if (name) {
|
|
1857
|
+
newAncestors = [...ancestors, { kind: ancestorKind, name, node, siblings, indexInSiblings }];
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
1861
|
+
walk(node.children[i], newAncestors, node.children, i);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
const rootEntry = {
|
|
1865
|
+
kind: "root",
|
|
1866
|
+
name: "",
|
|
1867
|
+
node: rootNode,
|
|
1868
|
+
siblings: rootSiblings,
|
|
1869
|
+
indexInSiblings: rootIndexInSiblings
|
|
1870
|
+
};
|
|
1871
|
+
walk(rootNode, [rootEntry], rootSiblings, rootIndexInSiblings);
|
|
1872
|
+
return results;
|
|
1873
|
+
}
|
|
1874
|
+
function matchSelector(rootNode, selector, siblings = [], indexInSiblings = 0) {
|
|
1875
|
+
const allResults = [];
|
|
1876
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1877
|
+
for (const alt of selector) {
|
|
1878
|
+
for (const node of matchAlternative(rootNode, alt, siblings, indexInSiblings)) {
|
|
1879
|
+
if (!seen.has(node)) {
|
|
1880
|
+
seen.add(node);
|
|
1881
|
+
allResults.push(node);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
return allResults;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// src/core/collectErrors.ts
|
|
1889
|
+
var selectorCache = /* @__PURE__ */ new Map();
|
|
1890
|
+
function parseSelectorCached(raw) {
|
|
1891
|
+
const hit = selectorCache.get(raw);
|
|
1892
|
+
if (hit !== void 0) return hit;
|
|
1893
|
+
const parsed = parseSelector(raw);
|
|
1894
|
+
selectorCache.set(raw, parsed);
|
|
1895
|
+
return parsed;
|
|
1896
|
+
}
|
|
1897
|
+
var ERROR_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
1898
|
+
"ERROR",
|
|
1899
|
+
"mustache_erroneous_section_end",
|
|
1900
|
+
"mustache_erroneous_inverted_section_end"
|
|
1901
|
+
]);
|
|
1902
|
+
function errorMessageForNode(nodeType, node) {
|
|
1903
|
+
if (nodeType === "mustache_erroneous_section_end" || nodeType === "mustache_erroneous_inverted_section_end") {
|
|
1904
|
+
const tagNameNode = node.children.find((c) => c.type === "mustache_erroneous_tag_name");
|
|
1905
|
+
return `Mismatched mustache section: {{/${tagNameNode?.text || "?"}}}`;
|
|
1906
|
+
}
|
|
1907
|
+
if (nodeType === "ERROR") {
|
|
1908
|
+
return "Syntax error";
|
|
1909
|
+
}
|
|
1910
|
+
return `Missing ${nodeType}`;
|
|
1911
|
+
}
|
|
1912
|
+
function resolveRuleConfig(rules, ruleName) {
|
|
1913
|
+
const entry = rules?.[ruleName];
|
|
1914
|
+
let severity;
|
|
1915
|
+
if (entry === void 0) {
|
|
1916
|
+
severity = RULE_DEFAULTS[ruleName] ?? "off";
|
|
1917
|
+
} else if (typeof entry === "string") {
|
|
1918
|
+
severity = entry;
|
|
1919
|
+
} else {
|
|
1920
|
+
severity = entry.severity;
|
|
1921
|
+
}
|
|
1922
|
+
return { severity, entry };
|
|
1923
|
+
}
|
|
1924
|
+
function parseDisableDirective(node, customRuleIds) {
|
|
1925
|
+
if (node.type !== "html_comment" && node.type !== "mustache_comment") return null;
|
|
1926
|
+
let inner = null;
|
|
1927
|
+
if (node.type === "html_comment") {
|
|
1928
|
+
const match = node.text.match(/^<!--([\s\S]*)-->$/);
|
|
1929
|
+
if (match) inner = match[1].trim();
|
|
1930
|
+
} else {
|
|
1931
|
+
const match = node.text.match(/^\{\{!([\s\S]*)\}\}$/);
|
|
1932
|
+
if (match) inner = match[1].trim();
|
|
1933
|
+
}
|
|
1934
|
+
if (!inner) return null;
|
|
1935
|
+
const prefix = "htmlmustache-disable ";
|
|
1936
|
+
if (!inner.startsWith(prefix)) return null;
|
|
1937
|
+
const ruleName = inner.slice(prefix.length).trim();
|
|
1938
|
+
if (KNOWN_RULE_NAMES.has(ruleName)) return ruleName;
|
|
1939
|
+
if (customRuleIds?.has(ruleName)) return ruleName;
|
|
1940
|
+
return null;
|
|
1941
|
+
}
|
|
1942
|
+
function collectDisabledRules(rootNode, customRuleIds) {
|
|
1943
|
+
const disabled = /* @__PURE__ */ new Set();
|
|
1944
|
+
function walk(node) {
|
|
1945
|
+
const rule = parseDisableDirective(node, customRuleIds);
|
|
1946
|
+
if (rule) {
|
|
1947
|
+
disabled.add(rule);
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
for (const child of node.children) walk(child);
|
|
1951
|
+
}
|
|
1952
|
+
walk(rootNode);
|
|
1953
|
+
return disabled;
|
|
1954
|
+
}
|
|
1955
|
+
function collectErrors(tree, rules, customTagNames, customRules) {
|
|
1956
|
+
const errors = [];
|
|
1957
|
+
const cursor = tree.walk();
|
|
1958
|
+
function visit() {
|
|
1959
|
+
const node = cursor.currentNode;
|
|
1960
|
+
const nodeType = cursor.nodeType;
|
|
1961
|
+
if (ERROR_NODE_TYPES.has(nodeType) || cursor.nodeIsMissing) {
|
|
1962
|
+
errors.push({
|
|
1963
|
+
node,
|
|
1964
|
+
message: errorMessageForNode(nodeType, node)
|
|
1965
|
+
});
|
|
1966
|
+
if (nodeType === "ERROR") return;
|
|
1967
|
+
}
|
|
1968
|
+
if (cursor.gotoFirstChild()) {
|
|
1969
|
+
do {
|
|
1970
|
+
visit();
|
|
1971
|
+
} while (cursor.gotoNextSibling());
|
|
1972
|
+
cursor.gotoParent();
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
visit();
|
|
1976
|
+
const balanceErrors = checkHtmlBalance(tree.rootNode);
|
|
1977
|
+
for (const error of balanceErrors) {
|
|
1978
|
+
errors.push({ node: error.node, message: error.message });
|
|
1979
|
+
}
|
|
1980
|
+
const unclosedErrors = checkUnclosedTags(tree.rootNode);
|
|
1981
|
+
for (const error of unclosedErrors) {
|
|
1982
|
+
errors.push({ node: error.node, message: error.message });
|
|
1983
|
+
}
|
|
1984
|
+
const customRuleIds = customRules ? new Set(customRules.map((r) => r.id)) : void 0;
|
|
1985
|
+
const disabledRules = collectDisabledRules(tree.rootNode, customRuleIds);
|
|
1986
|
+
const effectiveRules = { ...rules };
|
|
1987
|
+
for (const rule of disabledRules) {
|
|
1988
|
+
effectiveRules[rule] = "off";
|
|
1989
|
+
}
|
|
1990
|
+
const sourceText = tree.rootNode.text;
|
|
1991
|
+
const ruleChecks = [
|
|
1992
|
+
{ rule: "nestedDuplicateSections", errors: () => checkNestedSameNameSections(tree.rootNode) },
|
|
1993
|
+
{ rule: "unquotedMustacheAttributes", errors: () => checkUnquotedMustacheAttributes(tree.rootNode) },
|
|
1994
|
+
{ rule: "consecutiveDuplicateSections", errors: () => checkConsecutiveSameNameSections(tree.rootNode, sourceText) },
|
|
1995
|
+
{ rule: "selfClosingNonVoidTags", errors: () => checkSelfClosingNonVoidTags(tree.rootNode) },
|
|
1996
|
+
{ rule: "duplicateAttributes", errors: () => checkDuplicateAttributes(tree.rootNode) },
|
|
1997
|
+
{ rule: "unescapedEntities", errors: () => checkUnescapedEntities(tree.rootNode) },
|
|
1998
|
+
{ rule: "preferMustacheComments", errors: () => checkHtmlComments(tree.rootNode) },
|
|
1999
|
+
{ rule: "unrecognizedHtmlTags", errors: () => checkUnrecognizedHtmlTags(tree.rootNode, customTagNames) },
|
|
2000
|
+
{
|
|
2001
|
+
rule: "elementContentTooLong",
|
|
2002
|
+
errors: (entry) => {
|
|
2003
|
+
const elements = (entry && typeof entry === "object" ? entry.elements : void 0) ?? [];
|
|
2004
|
+
return checkElementContentTooLong(tree.rootNode, elements);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
];
|
|
2008
|
+
for (const { rule, errors: getErrors } of ruleChecks) {
|
|
2009
|
+
const { severity, entry } = resolveRuleConfig(effectiveRules, rule);
|
|
2010
|
+
if (severity === "off") continue;
|
|
2011
|
+
for (const error of getErrors(entry)) {
|
|
2012
|
+
errors.push({
|
|
2013
|
+
node: error.node,
|
|
2014
|
+
message: error.message,
|
|
2015
|
+
severity,
|
|
2016
|
+
fix: error.fix,
|
|
2017
|
+
fixDescription: error.fixDescription,
|
|
2018
|
+
ruleName: rule
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
if (customRules) {
|
|
2023
|
+
for (const rule of customRules) {
|
|
2024
|
+
if (disabledRules.has(rule.id)) continue;
|
|
2025
|
+
const severity = rule.severity ?? "error";
|
|
2026
|
+
if (severity === "off") continue;
|
|
2027
|
+
const parsed = parseSelectorCached(rule.selector);
|
|
2028
|
+
if (!parsed) continue;
|
|
2029
|
+
const matches = matchSelector(tree.rootNode, parsed);
|
|
2030
|
+
for (const node of matches) {
|
|
2031
|
+
errors.push({ node, message: rule.message, severity, ruleName: rule.id });
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
return errors.filter(
|
|
2036
|
+
(e) => !(e.message.includes("HTML comment found") && parseDisableDirective(e.node, customRuleIds) !== null)
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// src/core/formatting/printer.ts
|
|
2041
|
+
function print(doc, options) {
|
|
2042
|
+
const output = [];
|
|
2043
|
+
const state = { indentLevel: 0, mode: "break", groupModes: /* @__PURE__ */ new Map() };
|
|
2044
|
+
printDoc(doc, state, output, options);
|
|
2045
|
+
return output.join("");
|
|
2046
|
+
}
|
|
2047
|
+
function currentColumn(output) {
|
|
2048
|
+
let col = 0;
|
|
2049
|
+
for (let i = output.length - 1; i >= 0; i--) {
|
|
2050
|
+
const chunk = output[i];
|
|
2051
|
+
const nlIndex = chunk.lastIndexOf("\n");
|
|
2052
|
+
if (nlIndex !== -1) {
|
|
2053
|
+
col += chunk.length - nlIndex - 1;
|
|
2054
|
+
return col;
|
|
2055
|
+
}
|
|
2056
|
+
col += chunk.length;
|
|
2057
|
+
}
|
|
2058
|
+
return col;
|
|
2059
|
+
}
|
|
2060
|
+
function containsBreakParent(doc) {
|
|
2061
|
+
if (typeof doc === "string") return false;
|
|
2062
|
+
switch (doc.type) {
|
|
2063
|
+
case "breakParent":
|
|
2064
|
+
return true;
|
|
2065
|
+
case "concat":
|
|
2066
|
+
return doc.parts.some(containsBreakParent);
|
|
2067
|
+
case "indent":
|
|
2068
|
+
return containsBreakParent(doc.contents);
|
|
2069
|
+
case "group":
|
|
2070
|
+
return containsBreakParent(doc.contents);
|
|
2071
|
+
case "fill":
|
|
2072
|
+
return doc.parts.some(containsBreakParent);
|
|
2073
|
+
case "ifBreak":
|
|
2074
|
+
return containsBreakParent(doc.breakContents) || containsBreakParent(doc.flatContents);
|
|
2075
|
+
default:
|
|
2076
|
+
return false;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
function printDoc(doc, state, output, options) {
|
|
2080
|
+
if (typeof doc === "string") {
|
|
2081
|
+
output.push(doc);
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
switch (doc.type) {
|
|
2085
|
+
case "concat":
|
|
2086
|
+
for (const part of doc.parts) {
|
|
2087
|
+
printDoc(part, state, output, options);
|
|
2088
|
+
}
|
|
2089
|
+
break;
|
|
2090
|
+
case "indent":
|
|
2091
|
+
state.indentLevel++;
|
|
2092
|
+
printDoc(doc.contents, state, output, options);
|
|
2093
|
+
state.indentLevel--;
|
|
2094
|
+
break;
|
|
2095
|
+
case "hardline":
|
|
2096
|
+
output.push("\n");
|
|
2097
|
+
output.push(makeIndent(state.indentLevel, options));
|
|
2098
|
+
break;
|
|
2099
|
+
case "softline":
|
|
2100
|
+
if (state.mode === "break") {
|
|
2101
|
+
output.push("\n");
|
|
2102
|
+
output.push(makeIndent(state.indentLevel, options));
|
|
2103
|
+
}
|
|
2104
|
+
break;
|
|
2105
|
+
case "line":
|
|
2106
|
+
if (state.mode === "break") {
|
|
2107
|
+
output.push("\n");
|
|
2108
|
+
output.push(makeIndent(state.indentLevel, options));
|
|
2109
|
+
} else {
|
|
2110
|
+
output.push(" ");
|
|
2111
|
+
}
|
|
2112
|
+
break;
|
|
2113
|
+
case "group": {
|
|
2114
|
+
if (doc.break || containsBreakParent(doc.contents)) {
|
|
2115
|
+
const prevMode = state.mode;
|
|
2116
|
+
state.mode = "break";
|
|
2117
|
+
if (doc.id) state.groupModes.set(doc.id, "break");
|
|
2118
|
+
printDoc(doc.contents, state, output, options);
|
|
2119
|
+
state.mode = prevMode;
|
|
2120
|
+
} else {
|
|
2121
|
+
const flatOutput = [];
|
|
2122
|
+
const flatState = { ...state, mode: "flat", groupModes: new Map(state.groupModes) };
|
|
2123
|
+
printDoc(doc.contents, flatState, flatOutput, options);
|
|
2124
|
+
const flatContent = flatOutput.join("");
|
|
2125
|
+
const printWidth = options.printWidth ?? 80;
|
|
2126
|
+
const col = currentColumn(output);
|
|
2127
|
+
if (!flatContent.includes("\n") && col + flatContent.length <= printWidth) {
|
|
2128
|
+
if (doc.id) state.groupModes.set(doc.id, "flat");
|
|
2129
|
+
output.push(flatContent);
|
|
2130
|
+
} else {
|
|
2131
|
+
const prevMode = state.mode;
|
|
2132
|
+
state.mode = "break";
|
|
2133
|
+
if (doc.id) state.groupModes.set(doc.id, "break");
|
|
2134
|
+
printDoc(doc.contents, state, output, options);
|
|
2135
|
+
state.mode = prevMode;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
break;
|
|
2139
|
+
}
|
|
2140
|
+
case "fill":
|
|
2141
|
+
printFill(doc.parts, state, output, options);
|
|
2142
|
+
break;
|
|
2143
|
+
case "ifBreak": {
|
|
2144
|
+
const effectiveMode = doc.groupId ? state.groupModes.get(doc.groupId) ?? state.mode : state.mode;
|
|
2145
|
+
if (effectiveMode === "break") {
|
|
2146
|
+
printDoc(doc.breakContents, state, output, options);
|
|
2147
|
+
} else {
|
|
2148
|
+
printDoc(doc.flatContents, state, output, options);
|
|
2149
|
+
}
|
|
2150
|
+
break;
|
|
2151
|
+
}
|
|
2152
|
+
case "breakParent":
|
|
2153
|
+
state.mode = "break";
|
|
2154
|
+
break;
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
function printFill(parts, state, output, options) {
|
|
2158
|
+
if (parts.length === 0) return;
|
|
2159
|
+
const printWidth = options.printWidth ?? 80;
|
|
2160
|
+
for (let i = 0; i < parts.length; i++) {
|
|
2161
|
+
const content = parts[i];
|
|
2162
|
+
const separator = i + 1 < parts.length ? parts[i + 1] : null;
|
|
2163
|
+
printDoc(content, state, output, options);
|
|
2164
|
+
if (separator === null) break;
|
|
2165
|
+
const nextContent = i + 2 < parts.length ? parts[i + 2] : null;
|
|
2166
|
+
if (nextContent !== null) {
|
|
2167
|
+
const testOutput = [];
|
|
2168
|
+
const flatState = { ...state, mode: "flat" };
|
|
2169
|
+
printDoc(separator, flatState, testOutput, options);
|
|
2170
|
+
printDoc(nextContent, flatState, testOutput, options);
|
|
2171
|
+
const testStr = testOutput.join("");
|
|
2172
|
+
const col = currentColumn(output);
|
|
2173
|
+
if (!testStr.includes("\n") && col + testStr.length <= printWidth) {
|
|
2174
|
+
const sepOutput = [];
|
|
2175
|
+
printDoc(separator, flatState, sepOutput, options);
|
|
2176
|
+
output.push(sepOutput.join(""));
|
|
2177
|
+
} else {
|
|
2178
|
+
printDoc(separator, { ...state, mode: "break" }, output, options);
|
|
2179
|
+
}
|
|
2180
|
+
} else {
|
|
2181
|
+
printDoc(separator, state, output, options);
|
|
2182
|
+
}
|
|
2183
|
+
i++;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
function makeIndent(level, options) {
|
|
2187
|
+
return options.indentUnit.repeat(level);
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// src/core/formatting/ir.ts
|
|
2191
|
+
var hardline = { type: "hardline" };
|
|
2192
|
+
var softline = { type: "softline" };
|
|
2193
|
+
var line = { type: "line" };
|
|
2194
|
+
var empty = "";
|
|
2195
|
+
function text(value) {
|
|
2196
|
+
return value;
|
|
2197
|
+
}
|
|
2198
|
+
function concat(parts) {
|
|
2199
|
+
const flattened = [];
|
|
2200
|
+
for (const part of parts) {
|
|
2201
|
+
if (part === "") continue;
|
|
2202
|
+
if (typeof part === "object" && part.type === "concat") {
|
|
2203
|
+
flattened.push(...part.parts);
|
|
2204
|
+
} else {
|
|
2205
|
+
flattened.push(part);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
if (flattened.length === 0) return "";
|
|
2209
|
+
if (flattened.length === 1) return flattened[0];
|
|
2210
|
+
return { type: "concat", parts: flattened };
|
|
2211
|
+
}
|
|
2212
|
+
function indent(contents) {
|
|
2213
|
+
if (contents === "") return "";
|
|
2214
|
+
return { type: "indent", contents };
|
|
2215
|
+
}
|
|
2216
|
+
function indentN(contents, n) {
|
|
2217
|
+
if (n <= 0 || contents === "") return contents;
|
|
2218
|
+
let result = contents;
|
|
2219
|
+
for (let i = 0; i < n; i++) {
|
|
2220
|
+
result = indent(result);
|
|
2221
|
+
}
|
|
2222
|
+
return result;
|
|
2223
|
+
}
|
|
2224
|
+
function group(contents, options) {
|
|
2225
|
+
if (contents === "") return "";
|
|
2226
|
+
const shouldBreak = options?.shouldBreak;
|
|
2227
|
+
return { type: "group", contents, break: shouldBreak || void 0, id: options?.id };
|
|
2228
|
+
}
|
|
2229
|
+
function ifBreak(breakContents, flatContents, options) {
|
|
2230
|
+
return { type: "ifBreak", breakContents, flatContents, groupId: options?.groupId };
|
|
2231
|
+
}
|
|
2232
|
+
function fill(parts) {
|
|
2233
|
+
const filtered = parts.filter((p) => p !== "");
|
|
2234
|
+
if (filtered.length === 0) return "";
|
|
2235
|
+
if (filtered.length === 1) return filtered[0];
|
|
2236
|
+
return { type: "fill", parts: filtered };
|
|
2237
|
+
}
|
|
2238
|
+
function isLine(doc) {
|
|
2239
|
+
return typeof doc === "object" && doc.type === "line";
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
// src/core/formatting/utils.ts
|
|
2243
|
+
function normalizeText(text2) {
|
|
2244
|
+
return text2.split("\n").map((line2) => line2.replace(/[ \t]+/g, " ").trim()).filter((line2, i, arr) => line2 || i > 0 && i < arr.length - 1).join("\n");
|
|
2245
|
+
}
|
|
2246
|
+
function getVisibleChildren(node) {
|
|
2247
|
+
const children = [];
|
|
2248
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2249
|
+
const child = node.child(i);
|
|
2250
|
+
if (child && !child.type.startsWith("_")) {
|
|
2251
|
+
children.push(child);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
return children;
|
|
2255
|
+
}
|
|
2256
|
+
function normalizeMustacheWhitespace(raw, addSpaces) {
|
|
2257
|
+
const space = addSpaces ? " " : "";
|
|
2258
|
+
const tripleMatch = raw.match(/^\{\{\{([\s\S]*)\}\}\}$/);
|
|
2259
|
+
if (tripleMatch) {
|
|
2260
|
+
const inner = tripleMatch[1].trim();
|
|
2261
|
+
return `{{{${space}${inner}${space}}}}`;
|
|
2262
|
+
}
|
|
2263
|
+
const prefixedMatch = raw.match(/^\{\{([#/^!>])([\s\S]*)\}\}$/);
|
|
2264
|
+
if (prefixedMatch) {
|
|
2265
|
+
const prefix = prefixedMatch[1];
|
|
2266
|
+
const inner = prefixedMatch[2];
|
|
2267
|
+
if (prefix === "!" && inner.includes("\n")) {
|
|
2268
|
+
const lines = inner.split("\n");
|
|
2269
|
+
const first = lines[0].trimStart();
|
|
2270
|
+
const last = lines[lines.length - 1].trimEnd();
|
|
2271
|
+
if (lines.length === 1) {
|
|
2272
|
+
return `{{${prefix} ${first} }}`;
|
|
2273
|
+
}
|
|
2274
|
+
const middle = lines.slice(1, -1);
|
|
2275
|
+
return `{{${prefix} ${first}
|
|
2276
|
+
${middle.join("\n")}
|
|
2277
|
+
${last} }}`;
|
|
2278
|
+
}
|
|
2279
|
+
const trimmed = inner.trim();
|
|
2280
|
+
const s = prefix === "!" ? " " : space;
|
|
2281
|
+
return `{{${prefix}${s}${trimmed}${s}}}`;
|
|
2282
|
+
}
|
|
2283
|
+
const plainMatch = raw.match(/^\{\{([\s\S]*)\}\}$/);
|
|
2284
|
+
if (plainMatch) {
|
|
2285
|
+
const inner = plainMatch[1].trim();
|
|
2286
|
+
return `{{${space}${inner}${space}}}`;
|
|
2287
|
+
}
|
|
2288
|
+
return raw;
|
|
2289
|
+
}
|
|
2290
|
+
function normalizeMustacheWhitespaceAll(raw, addSpaces) {
|
|
2291
|
+
return raw.replace(/\{\{\{[\s\S]*?\}\}\}|\{\{[\s\S]*?\}\}/g, (match) => {
|
|
2292
|
+
return normalizeMustacheWhitespace(match, addSpaces);
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
function getIgnoreDirective(node) {
|
|
2296
|
+
if (node.type !== "html_comment" && node.type !== "mustache_comment") {
|
|
2297
|
+
return null;
|
|
2298
|
+
}
|
|
2299
|
+
let inner = null;
|
|
2300
|
+
if (node.type === "html_comment") {
|
|
2301
|
+
const match = node.text.match(/^<!--([\s\S]*)-->$/);
|
|
2302
|
+
if (match) {
|
|
2303
|
+
inner = match[1].trim();
|
|
2304
|
+
}
|
|
2305
|
+
} else {
|
|
2306
|
+
const match = node.text.match(/^\{\{!([\s\S]*)\}\}$/);
|
|
2307
|
+
if (match) {
|
|
2308
|
+
inner = match[1].trim();
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
if (!inner) return null;
|
|
2312
|
+
if (inner === "htmlmustache-ignore") return "ignore";
|
|
2313
|
+
if (inner === "htmlmustache-ignore-start") return "ignore-start";
|
|
2314
|
+
if (inner === "htmlmustache-ignore-end") return "ignore-end";
|
|
2315
|
+
return null;
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
// src/core/customCodeTags.ts
|
|
2319
|
+
function isCodeTag(config) {
|
|
2320
|
+
return !!(config.languageAttribute || config.languageDefault);
|
|
2321
|
+
}
|
|
2322
|
+
function getAttributeValue(node, attrName) {
|
|
2323
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2324
|
+
const child = node.child(i);
|
|
2325
|
+
if (child?.type === "html_start_tag") {
|
|
2326
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
2327
|
+
const attr = child.child(j);
|
|
2328
|
+
if (attr?.type === "html_attribute") {
|
|
2329
|
+
let name = "";
|
|
2330
|
+
let value = "";
|
|
2331
|
+
for (let k = 0; k < attr.childCount; k++) {
|
|
2332
|
+
const part = attr.child(k);
|
|
2333
|
+
if (part?.type === "html_attribute_name") name = part.text.toLowerCase();
|
|
2334
|
+
if (part?.type === "html_quoted_attribute_value") value = part.text.replace(/^["']|["']$/g, "");
|
|
2335
|
+
if (part?.type === "html_attribute_value") value = part.text;
|
|
2336
|
+
}
|
|
2337
|
+
if (name === attrName.toLowerCase()) {
|
|
2338
|
+
return value;
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
return null;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
// src/core/formatting/classifier.ts
|
|
2348
|
+
var EMPTY_MAP = /* @__PURE__ */ new Map();
|
|
2349
|
+
var CSS_DISPLAY_MAP = {
|
|
2350
|
+
// Block elements
|
|
2351
|
+
address: "block",
|
|
2352
|
+
article: "block",
|
|
2353
|
+
aside: "block",
|
|
2354
|
+
blockquote: "block",
|
|
2355
|
+
body: "block",
|
|
2356
|
+
center: "block",
|
|
2357
|
+
dd: "block",
|
|
2358
|
+
details: "block",
|
|
2359
|
+
dialog: "block",
|
|
2360
|
+
dir: "block",
|
|
2361
|
+
div: "block",
|
|
2362
|
+
dl: "block",
|
|
2363
|
+
dt: "block",
|
|
2364
|
+
fieldset: "block",
|
|
2365
|
+
figcaption: "block",
|
|
2366
|
+
figure: "block",
|
|
2367
|
+
footer: "block",
|
|
2368
|
+
form: "block",
|
|
2369
|
+
h1: "block",
|
|
2370
|
+
h2: "block",
|
|
2371
|
+
h3: "block",
|
|
2372
|
+
h4: "block",
|
|
2373
|
+
h5: "block",
|
|
2374
|
+
h6: "block",
|
|
2375
|
+
header: "block",
|
|
2376
|
+
hgroup: "block",
|
|
2377
|
+
hr: "block",
|
|
2378
|
+
html: "block",
|
|
2379
|
+
legend: "block",
|
|
2380
|
+
listing: "block",
|
|
2381
|
+
main: "block",
|
|
2382
|
+
menu: "block",
|
|
2383
|
+
nav: "block",
|
|
2384
|
+
ol: "block",
|
|
2385
|
+
p: "block",
|
|
2386
|
+
plaintext: "block",
|
|
2387
|
+
pre: "block",
|
|
2388
|
+
search: "block",
|
|
2389
|
+
section: "block",
|
|
2390
|
+
summary: "block",
|
|
2391
|
+
ul: "block",
|
|
2392
|
+
xmp: "block",
|
|
2393
|
+
// List items
|
|
2394
|
+
li: "list-item",
|
|
2395
|
+
// Table elements
|
|
2396
|
+
table: "table",
|
|
2397
|
+
caption: "table-caption",
|
|
2398
|
+
colgroup: "table-column-group",
|
|
2399
|
+
col: "table-column",
|
|
2400
|
+
thead: "table-header-group",
|
|
2401
|
+
tbody: "table-row-group",
|
|
2402
|
+
tfoot: "table-footer-group",
|
|
2403
|
+
tr: "table-row",
|
|
2404
|
+
td: "table-cell",
|
|
2405
|
+
th: "table-cell",
|
|
2406
|
+
// Inline-block elements
|
|
2407
|
+
button: "inline-block",
|
|
2408
|
+
img: "inline-block",
|
|
2409
|
+
input: "inline-block",
|
|
2410
|
+
select: "inline-block",
|
|
2411
|
+
textarea: "inline-block",
|
|
2412
|
+
video: "inline-block",
|
|
2413
|
+
audio: "inline-block",
|
|
2414
|
+
canvas: "inline-block",
|
|
2415
|
+
embed: "inline-block",
|
|
2416
|
+
iframe: "inline-block",
|
|
2417
|
+
object: "inline-block",
|
|
2418
|
+
// None
|
|
2419
|
+
head: "none",
|
|
2420
|
+
link: "none",
|
|
2421
|
+
meta: "none",
|
|
2422
|
+
script: "none",
|
|
2423
|
+
style: "none",
|
|
2424
|
+
title: "none",
|
|
2425
|
+
template: "none",
|
|
2426
|
+
// Ruby
|
|
2427
|
+
ruby: "ruby",
|
|
2428
|
+
rb: "ruby-base",
|
|
2429
|
+
rt: "ruby-text",
|
|
2430
|
+
rp: "none"
|
|
2431
|
+
};
|
|
2432
|
+
var PRESERVE_CONTENT_ELEMENTS = /* @__PURE__ */ new Set([
|
|
2433
|
+
"pre",
|
|
2434
|
+
"code",
|
|
2435
|
+
"textarea",
|
|
2436
|
+
"script",
|
|
2437
|
+
"style"
|
|
2438
|
+
]);
|
|
2439
|
+
function getCSSDisplay(node, customTags = EMPTY_MAP) {
|
|
2440
|
+
const type = node.type;
|
|
2441
|
+
if (type === "html_element") {
|
|
2442
|
+
const tagName = getTagName(node);
|
|
2443
|
+
if (tagName) {
|
|
2444
|
+
const lower = tagName.toLowerCase();
|
|
2445
|
+
const config = customTags.get(lower);
|
|
2446
|
+
if (config) {
|
|
2447
|
+
if (config.display) return config.display;
|
|
2448
|
+
if (isCodeTag(config)) return "block";
|
|
2449
|
+
return "inline-block";
|
|
2450
|
+
}
|
|
2451
|
+
return CSS_DISPLAY_MAP[lower] ?? "inline";
|
|
2452
|
+
}
|
|
2453
|
+
return "block";
|
|
2454
|
+
}
|
|
2455
|
+
if (isRawContentElement(node)) {
|
|
2456
|
+
return "block";
|
|
2457
|
+
}
|
|
2458
|
+
if (isMustacheSection(node)) {
|
|
2459
|
+
return hasBlockContent(node, customTags) ? "block" : "inline";
|
|
2460
|
+
}
|
|
2461
|
+
return "inline";
|
|
2462
|
+
}
|
|
2463
|
+
function isWhitespaceInsensitive(display) {
|
|
2464
|
+
switch (display) {
|
|
2465
|
+
case "block":
|
|
2466
|
+
case "list-item":
|
|
2467
|
+
case "table":
|
|
2468
|
+
case "table-row":
|
|
2469
|
+
case "table-row-group":
|
|
2470
|
+
case "table-header-group":
|
|
2471
|
+
case "table-footer-group":
|
|
2472
|
+
case "table-column":
|
|
2473
|
+
case "table-column-group":
|
|
2474
|
+
case "table-caption":
|
|
2475
|
+
case "table-cell":
|
|
2476
|
+
case "none":
|
|
2477
|
+
return true;
|
|
2478
|
+
default:
|
|
2479
|
+
return false;
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
function isBlockLevel(node, customTags = EMPTY_MAP) {
|
|
2483
|
+
const type = node.type;
|
|
2484
|
+
if (isMustacheSection(node)) {
|
|
2485
|
+
return hasBlockContent(node, customTags);
|
|
2486
|
+
}
|
|
2487
|
+
if (type === "html_element") {
|
|
2488
|
+
const display = getCSSDisplay(node, customTags);
|
|
2489
|
+
return isWhitespaceInsensitive(display);
|
|
2490
|
+
}
|
|
2491
|
+
if (isRawContentElement(node)) {
|
|
2492
|
+
return true;
|
|
2493
|
+
}
|
|
2494
|
+
return false;
|
|
2495
|
+
}
|
|
2496
|
+
function shouldPreserveContent(node, customTags = EMPTY_MAP) {
|
|
2497
|
+
const type = node.type;
|
|
2498
|
+
if (isRawContentElement(node)) {
|
|
2499
|
+
return true;
|
|
2500
|
+
}
|
|
2501
|
+
if (type === "html_element") {
|
|
2502
|
+
const tagName = getTagName(node);
|
|
2503
|
+
if (!tagName) return false;
|
|
2504
|
+
const lower = tagName.toLowerCase();
|
|
2505
|
+
if (PRESERVE_CONTENT_ELEMENTS.has(lower)) return true;
|
|
2506
|
+
const config = customTags.get(lower);
|
|
2507
|
+
if (config && isCodeTag(config)) return true;
|
|
2508
|
+
}
|
|
2509
|
+
return false;
|
|
2510
|
+
}
|
|
2511
|
+
function hasBlockContent(sectionNode, customTags = EMPTY_MAP) {
|
|
2512
|
+
const contentNodes = getContentNodes(sectionNode);
|
|
2513
|
+
if (hasImplicitEndTags(contentNodes)) {
|
|
2514
|
+
return true;
|
|
2515
|
+
}
|
|
2516
|
+
for (const node of contentNodes) {
|
|
2517
|
+
if (isBlockLevelContent(node, customTags)) {
|
|
2518
|
+
return true;
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
return false;
|
|
2522
|
+
}
|
|
2523
|
+
function isBlockLevelContent(node, customTags = EMPTY_MAP) {
|
|
2524
|
+
const type = node.type;
|
|
2525
|
+
if (type === "html_element") {
|
|
2526
|
+
return true;
|
|
2527
|
+
}
|
|
2528
|
+
if (isRawContentElement(node)) {
|
|
2529
|
+
return true;
|
|
2530
|
+
}
|
|
2531
|
+
if (isMustacheSection(node)) {
|
|
2532
|
+
return hasBlockContent(node, customTags);
|
|
2533
|
+
}
|
|
2534
|
+
return false;
|
|
2535
|
+
}
|
|
2536
|
+
function getContentNodes(sectionNode) {
|
|
2537
|
+
const isInverted = sectionNode.type === "mustache_inverted_section";
|
|
2538
|
+
const beginType = isInverted ? "mustache_inverted_section_begin" : "mustache_section_begin";
|
|
2539
|
+
const endType = isInverted ? "mustache_inverted_section_end" : "mustache_section_end";
|
|
2540
|
+
const contentNodes = [];
|
|
2541
|
+
for (let i = 0; i < sectionNode.childCount; i++) {
|
|
2542
|
+
const child = sectionNode.child(i);
|
|
2543
|
+
if (!child) continue;
|
|
2544
|
+
if (child.type !== beginType && child.type !== endType && child.type !== "mustache_erroneous_section_end" && child.type !== "mustache_erroneous_inverted_section_end" && !child.type.startsWith("_")) {
|
|
2545
|
+
contentNodes.push(child);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
return contentNodes;
|
|
2549
|
+
}
|
|
2550
|
+
function hasImplicitEndTags(nodes) {
|
|
2551
|
+
for (const node of nodes) {
|
|
2552
|
+
if (hasImplicitEndTagsRecursive(node)) {
|
|
2553
|
+
return true;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
return false;
|
|
2557
|
+
}
|
|
2558
|
+
function hasImplicitEndTagsRecursive(node) {
|
|
2559
|
+
if (node.type === "html_element") {
|
|
2560
|
+
let hasStartTag = false;
|
|
2561
|
+
let hasEndTag = false;
|
|
2562
|
+
let hasContentChildren = false;
|
|
2563
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2564
|
+
const child = node.child(i);
|
|
2565
|
+
if (!child) continue;
|
|
2566
|
+
if (child.type === "html_start_tag") hasStartTag = true;
|
|
2567
|
+
else if (child.type === "html_end_tag") hasEndTag = true;
|
|
2568
|
+
else if (child.type === "html_forced_end_tag") return true;
|
|
2569
|
+
else if (!child.type.startsWith("_")) hasContentChildren = true;
|
|
2570
|
+
}
|
|
2571
|
+
if (hasStartTag && !hasEndTag && hasContentChildren) return true;
|
|
2572
|
+
}
|
|
2573
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2574
|
+
const child = node.child(i);
|
|
2575
|
+
if (child && hasImplicitEndTagsRecursive(child)) {
|
|
2576
|
+
return true;
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
return false;
|
|
2580
|
+
}
|
|
2581
|
+
function isInlineContentNode(node) {
|
|
2582
|
+
if (node.type === "text") return node.text.trim().length > 0;
|
|
2583
|
+
return node.type === "mustache_interpolation" || node.type === "mustache_triple" || node.type === "mustache_partial";
|
|
2584
|
+
}
|
|
2585
|
+
function isInTextFlow(node, index, nodes) {
|
|
2586
|
+
if (index > 0) {
|
|
2587
|
+
const prev = nodes[index - 1];
|
|
2588
|
+
if (prev.type === "text" && prev.text.trim().length > 0) {
|
|
2589
|
+
return true;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
if (index < nodes.length - 1) {
|
|
2593
|
+
const next = nodes[index + 1];
|
|
2594
|
+
if (next.type === "text" && next.text.trim().length > 0) {
|
|
2595
|
+
return true;
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
return false;
|
|
2599
|
+
}
|
|
2600
|
+
function hasAdjacentInlineContent(index, nodes) {
|
|
2601
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
2602
|
+
const n = nodes[i];
|
|
2603
|
+
if (n.type === "text" && n.text.trim().length === 0) continue;
|
|
2604
|
+
if (isInlineContentNode(n)) return true;
|
|
2605
|
+
break;
|
|
2606
|
+
}
|
|
2607
|
+
for (let i = index + 1; i < nodes.length; i++) {
|
|
2608
|
+
const n = nodes[i];
|
|
2609
|
+
if (n.type === "text" && n.text.trim().length === 0) continue;
|
|
2610
|
+
if (isInlineContentNode(n)) return true;
|
|
2611
|
+
break;
|
|
2612
|
+
}
|
|
2613
|
+
return false;
|
|
2614
|
+
}
|
|
2615
|
+
function shouldHtmlElementStayInline(node, index, nodes, customTags = EMPTY_MAP) {
|
|
2616
|
+
if (node.type !== "html_element") {
|
|
2617
|
+
return false;
|
|
2618
|
+
}
|
|
2619
|
+
if (isWhitespaceInsensitive(getCSSDisplay(node, customTags))) {
|
|
2620
|
+
return false;
|
|
2621
|
+
}
|
|
2622
|
+
if (isInTextFlow(node, index, nodes)) {
|
|
2623
|
+
return true;
|
|
2624
|
+
}
|
|
2625
|
+
if (hasAdjacentInlineContent(index, nodes)) {
|
|
2626
|
+
return true;
|
|
2627
|
+
}
|
|
2628
|
+
if (index > 0) {
|
|
2629
|
+
const prev = nodes[index - 1];
|
|
2630
|
+
if (prev.type === "html_element" && isInTextFlow(prev, index - 1, nodes)) {
|
|
2631
|
+
return true;
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
if (index < nodes.length - 1) {
|
|
2635
|
+
const next = nodes[index + 1];
|
|
2636
|
+
if (next.type === "html_element" && isInTextFlow(next, index + 1, nodes)) {
|
|
2637
|
+
return true;
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
return false;
|
|
2641
|
+
}
|
|
2642
|
+
function shouldTreatAsBlock(node, index, nodes, customTags = EMPTY_MAP) {
|
|
2643
|
+
const isHtmlEl = isHtmlElementType(node);
|
|
2644
|
+
const isMustacheSec = isMustacheSection(node);
|
|
2645
|
+
if (node.type === "html_erroneous_end_tag") return true;
|
|
2646
|
+
return isHtmlEl && !shouldHtmlElementStayInline(node, index, nodes, customTags) || isMustacheSec && !isInTextFlow(node, index, nodes) || isBlockLevel(node, customTags) && !isInTextFlow(node, index, nodes);
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
// src/core/formatting/formatters.ts
|
|
2650
|
+
function isAttributeTruthy(value) {
|
|
2651
|
+
if (value === null || value === "" || value === "false" || value === "0") {
|
|
2652
|
+
return false;
|
|
2653
|
+
}
|
|
2654
|
+
return true;
|
|
2655
|
+
}
|
|
2656
|
+
function dedentContent(rawContent) {
|
|
2657
|
+
const lines = rawContent.split("\n");
|
|
2658
|
+
while (lines.length > 0 && lines[0].trim() === "") {
|
|
2659
|
+
lines.shift();
|
|
2660
|
+
}
|
|
2661
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
2662
|
+
lines.pop();
|
|
2663
|
+
}
|
|
2664
|
+
if (lines.length === 0) return "";
|
|
2665
|
+
let minIndent = Infinity;
|
|
2666
|
+
for (const l of lines) {
|
|
2667
|
+
if (l.trim() === "") continue;
|
|
2668
|
+
const match = l.match(/^(\s*)/);
|
|
2669
|
+
if (match && match[1].length < minIndent) {
|
|
2670
|
+
minIndent = match[1].length;
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
if (minIndent === Infinity) minIndent = 0;
|
|
2674
|
+
return lines.map((l) => l.trim() === "" ? "" : l.slice(minIndent)).join("\n");
|
|
2675
|
+
}
|
|
2676
|
+
function resolveIndentMode(node, config) {
|
|
2677
|
+
const mode = config.indent ?? "never";
|
|
2678
|
+
if (mode === "never") return false;
|
|
2679
|
+
if (mode === "always") return true;
|
|
2680
|
+
if (!config.indentAttribute) return false;
|
|
2681
|
+
const value = getAttributeValue(node, config.indentAttribute);
|
|
2682
|
+
return isAttributeTruthy(value);
|
|
2683
|
+
}
|
|
2684
|
+
function getTagNameFromStartTag(startTag) {
|
|
2685
|
+
for (let i = 0; i < startTag.childCount; i++) {
|
|
2686
|
+
const child = startTag.child(i);
|
|
2687
|
+
if (child?.type === "html_tag_name") return child.text.toLowerCase();
|
|
2688
|
+
}
|
|
2689
|
+
return null;
|
|
2690
|
+
}
|
|
2691
|
+
function mustacheText(raw, context) {
|
|
2692
|
+
if (context.mustacheSpaces !== void 0) {
|
|
2693
|
+
return normalizeMustacheWhitespace(raw, context.mustacheSpaces);
|
|
2694
|
+
}
|
|
2695
|
+
return raw;
|
|
2696
|
+
}
|
|
2697
|
+
function formatDocument(node, context) {
|
|
2698
|
+
const children = getVisibleChildren(node);
|
|
2699
|
+
const content = formatBlockChildren(children, context);
|
|
2700
|
+
return concat([content, hardline]);
|
|
2701
|
+
}
|
|
2702
|
+
function formatNode(node, context, forceInline = false) {
|
|
2703
|
+
const type = node.type;
|
|
2704
|
+
switch (type) {
|
|
2705
|
+
case "document":
|
|
2706
|
+
return formatDocument(node, context);
|
|
2707
|
+
case "html_element":
|
|
2708
|
+
return formatHtmlElement(node, context, forceInline);
|
|
2709
|
+
case "html_script_element":
|
|
2710
|
+
case "html_style_element":
|
|
2711
|
+
case "html_raw_element":
|
|
2712
|
+
return formatScriptStyleElement(node, context);
|
|
2713
|
+
case "mustache_section":
|
|
2714
|
+
case "mustache_inverted_section":
|
|
2715
|
+
if (forceInline) {
|
|
2716
|
+
if (context.mustacheSpaces !== void 0) {
|
|
2717
|
+
return text(normalizeMustacheWhitespaceAll(node.text, context.mustacheSpaces));
|
|
2718
|
+
}
|
|
2719
|
+
return text(node.text);
|
|
2720
|
+
}
|
|
2721
|
+
return formatMustacheSection(node, context);
|
|
2722
|
+
case "mustache_interpolation":
|
|
2723
|
+
case "mustache_triple":
|
|
2724
|
+
case "mustache_partial":
|
|
2725
|
+
case "mustache_comment":
|
|
2726
|
+
return text(mustacheText(node.text, context));
|
|
2727
|
+
case "html_comment":
|
|
2728
|
+
case "html_doctype":
|
|
2729
|
+
case "html_entity":
|
|
2730
|
+
case "html_erroneous_end_tag":
|
|
2731
|
+
return text(node.text);
|
|
2732
|
+
case "text":
|
|
2733
|
+
return formatText(node);
|
|
2734
|
+
default:
|
|
2735
|
+
return text(node.text);
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
function formatText(node) {
|
|
2739
|
+
return text(normalizeText(node.text));
|
|
2740
|
+
}
|
|
2741
|
+
function formatHtmlElement(node, context, forceInline = false) {
|
|
2742
|
+
const tags = context.customTags;
|
|
2743
|
+
const display = getCSSDisplay(node, tags);
|
|
2744
|
+
const isBlock = isWhitespaceInsensitive(display);
|
|
2745
|
+
const preserveContent = shouldPreserveContent(node, tags);
|
|
2746
|
+
const selfClosing = node.childCount === 1 && node.child(0)?.type === "html_self_closing_tag";
|
|
2747
|
+
if (selfClosing) {
|
|
2748
|
+
const tag = node.child(0);
|
|
2749
|
+
return formatStartTag(tag, context);
|
|
2750
|
+
}
|
|
2751
|
+
let startTag = null;
|
|
2752
|
+
let endTag = null;
|
|
2753
|
+
let hasRealEndTag = false;
|
|
2754
|
+
const contentNodes = [];
|
|
2755
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2756
|
+
const child = node.child(i);
|
|
2757
|
+
if (!child) continue;
|
|
2758
|
+
if (child.type === "html_start_tag") {
|
|
2759
|
+
startTag = child;
|
|
2760
|
+
} else if (child.type === "html_end_tag") {
|
|
2761
|
+
endTag = child;
|
|
2762
|
+
hasRealEndTag = true;
|
|
2763
|
+
} else if (child.type === "html_forced_end_tag") {
|
|
2764
|
+
endTag = child;
|
|
2765
|
+
} else if (!child.type.startsWith("_")) {
|
|
2766
|
+
contentNodes.push(child);
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
const parts = [];
|
|
2770
|
+
if (startTag) {
|
|
2771
|
+
parts.push(formatStartTag(startTag, context));
|
|
2772
|
+
}
|
|
2773
|
+
const hasHtmlElementChildren = contentNodes.some(
|
|
2774
|
+
(child) => child.type === "html_element" || isRawContentElement(child) || isBlockLevel(child, tags)
|
|
2775
|
+
);
|
|
2776
|
+
if (preserveContent) {
|
|
2777
|
+
const tagNameLower = startTag ? getTagNameFromStartTag(startTag) : null;
|
|
2778
|
+
const tagConfig = tagNameLower ? context.customTags?.get(tagNameLower) : void 0;
|
|
2779
|
+
const shouldIndent = tagConfig ? resolveIndentMode(node, tagConfig) : false;
|
|
2780
|
+
if (shouldIndent && startTag && endTag) {
|
|
2781
|
+
const rawContent = context.document.getText().slice(
|
|
2782
|
+
startTag.endIndex,
|
|
2783
|
+
endTag.startIndex
|
|
2784
|
+
);
|
|
2785
|
+
const dedented = dedentContent(rawContent);
|
|
2786
|
+
if (dedented.length > 0) {
|
|
2787
|
+
const contentLines = dedented.split("\n");
|
|
2788
|
+
const lineDocs = [];
|
|
2789
|
+
for (let j = 0; j < contentLines.length; j++) {
|
|
2790
|
+
if (j > 0) {
|
|
2791
|
+
if (contentLines[j] === "") {
|
|
2792
|
+
lineDocs.push("\n");
|
|
2793
|
+
} else {
|
|
2794
|
+
lineDocs.push(hardline);
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
if (contentLines[j] !== "") {
|
|
2798
|
+
lineDocs.push(text(contentLines[j]));
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
parts.push(indent(concat([hardline, ...lineDocs])));
|
|
2802
|
+
parts.push(hardline);
|
|
2803
|
+
}
|
|
2804
|
+
} else if (startTag && endTag) {
|
|
2805
|
+
const rawContent = context.document.getText().slice(
|
|
2806
|
+
startTag.endIndex,
|
|
2807
|
+
endTag.startIndex
|
|
2808
|
+
);
|
|
2809
|
+
const trailingMatch = isBlock ? rawContent.match(/\n[\t ]*$/) : null;
|
|
2810
|
+
if (trailingMatch) {
|
|
2811
|
+
parts.push(text(rawContent.slice(0, -trailingMatch[0].length)));
|
|
2812
|
+
parts.push(hardline);
|
|
2813
|
+
} else {
|
|
2814
|
+
parts.push(text(rawContent));
|
|
2815
|
+
}
|
|
2816
|
+
} else {
|
|
2817
|
+
for (const child of contentNodes) {
|
|
2818
|
+
parts.push(text(child.text));
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
} else if (!isBlock && (!hasHtmlElementChildren || forceInline && display !== "inline-block" && !contentNodes.some(
|
|
2822
|
+
(child) => isRawContentElement(child) || isBlockLevel(child, tags)
|
|
2823
|
+
))) {
|
|
2824
|
+
if (!forceInline && startTag && startTagHasAttributes(startTag)) {
|
|
2825
|
+
const formattedContent = formatBlockChildren(contentNodes, context);
|
|
2826
|
+
if (hasDocContent(formattedContent)) {
|
|
2827
|
+
const bareStartTag = formatStartTag(startTag, context, true);
|
|
2828
|
+
const outerParts = [
|
|
2829
|
+
group(bareStartTag),
|
|
2830
|
+
indent(concat([softline, formattedContent]))
|
|
2831
|
+
];
|
|
2832
|
+
if (hasRealEndTag) {
|
|
2833
|
+
outerParts.push(softline);
|
|
2834
|
+
}
|
|
2835
|
+
if (endTag) {
|
|
2836
|
+
outerParts.push(formatEndTag(endTag));
|
|
2837
|
+
}
|
|
2838
|
+
return group(concat(outerParts));
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
let prevEnd = startTag ? startTag.endIndex : -1;
|
|
2842
|
+
for (const child of contentNodes) {
|
|
2843
|
+
if (prevEnd >= 0 && child.startIndex > prevEnd) {
|
|
2844
|
+
const gap = context.document.getText().slice(prevEnd, child.startIndex);
|
|
2845
|
+
if (/\s/.test(gap)) {
|
|
2846
|
+
parts.push(text(" "));
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
parts.push(formatNode(child, context, forceInline));
|
|
2850
|
+
prevEnd = child.endIndex;
|
|
2851
|
+
}
|
|
2852
|
+
} else {
|
|
2853
|
+
const formattedContent = formatBlockChildren(contentNodes, context);
|
|
2854
|
+
const hasContent = hasDocContent(formattedContent);
|
|
2855
|
+
if (hasContent) {
|
|
2856
|
+
const hasBlockChildren = contentNodes.some((child, i) => {
|
|
2857
|
+
if (!shouldTreatAsBlock(child, i, contentNodes, tags)) {
|
|
2858
|
+
return false;
|
|
2859
|
+
}
|
|
2860
|
+
const childDisplay = getCSSDisplay(child, tags);
|
|
2861
|
+
return isWhitespaceInsensitive(childDisplay) || isRawContentElement(child);
|
|
2862
|
+
});
|
|
2863
|
+
if (isBlock && !hasBlockChildren) {
|
|
2864
|
+
const hasAttrs = startTag && startTagHasAttributes(startTag);
|
|
2865
|
+
if (hasAttrs && startTag) {
|
|
2866
|
+
const bareStartTag = formatStartTag(startTag, context, true);
|
|
2867
|
+
const outerParts = [
|
|
2868
|
+
group(bareStartTag),
|
|
2869
|
+
indent(concat([softline, formattedContent]))
|
|
2870
|
+
];
|
|
2871
|
+
if (hasRealEndTag) {
|
|
2872
|
+
outerParts.push(softline);
|
|
2873
|
+
}
|
|
2874
|
+
if (endTag) {
|
|
2875
|
+
outerParts.push(formatEndTag(endTag));
|
|
2876
|
+
}
|
|
2877
|
+
return group(concat(outerParts));
|
|
2878
|
+
}
|
|
2879
|
+
const doc = group(
|
|
2880
|
+
concat([
|
|
2881
|
+
indent(concat([softline, formattedContent])),
|
|
2882
|
+
softline
|
|
2883
|
+
])
|
|
2884
|
+
);
|
|
2885
|
+
parts.push(doc);
|
|
2886
|
+
if (!hasRealEndTag && endTag) {
|
|
2887
|
+
parts.pop();
|
|
2888
|
+
parts.push(
|
|
2889
|
+
group(
|
|
2890
|
+
concat([
|
|
2891
|
+
indent(concat([softline, formattedContent]))
|
|
2892
|
+
])
|
|
2893
|
+
)
|
|
2894
|
+
);
|
|
2895
|
+
}
|
|
2896
|
+
} else {
|
|
2897
|
+
parts.push(indent(concat([hardline, formattedContent])));
|
|
2898
|
+
if (hasRealEndTag) {
|
|
2899
|
+
parts.push(hardline);
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
} else if (contentNodes.length === 0 && hasRealEndTag) {
|
|
2903
|
+
parts.push(hardline);
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
if (endTag) {
|
|
2907
|
+
parts.push(formatEndTag(endTag));
|
|
2908
|
+
}
|
|
2909
|
+
return concat(parts);
|
|
2910
|
+
}
|
|
2911
|
+
function formatScriptStyleElement(node, context) {
|
|
2912
|
+
const parts = [];
|
|
2913
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2914
|
+
const child = node.child(i);
|
|
2915
|
+
if (!child) continue;
|
|
2916
|
+
if (child.type === "html_start_tag") {
|
|
2917
|
+
parts.push(formatStartTag(child, context));
|
|
2918
|
+
} else if (child.type === "html_end_tag") {
|
|
2919
|
+
parts.push(formatEndTag(child));
|
|
2920
|
+
} else if (child.type === "html_raw_text") {
|
|
2921
|
+
const formatted = context.embeddedFormatted?.get(child.startIndex);
|
|
2922
|
+
if (formatted !== void 0) {
|
|
2923
|
+
const trimmed = formatted.replace(/^\n+/, "").replace(/\n+$/, "");
|
|
2924
|
+
if (trimmed.length === 0) {
|
|
2925
|
+
} else {
|
|
2926
|
+
const lines = trimmed.split("\n");
|
|
2927
|
+
const lineDocs = [];
|
|
2928
|
+
for (let j = 0; j < lines.length; j++) {
|
|
2929
|
+
if (j > 0) {
|
|
2930
|
+
lineDocs.push(hardline);
|
|
2931
|
+
}
|
|
2932
|
+
lineDocs.push(text(lines[j]));
|
|
2933
|
+
}
|
|
2934
|
+
parts.push(indent(concat([hardline, ...lineDocs])));
|
|
2935
|
+
parts.push(hardline);
|
|
2936
|
+
}
|
|
2937
|
+
} else {
|
|
2938
|
+
if (node.type === "html_raw_element") {
|
|
2939
|
+
const startTagNode = node.child(0);
|
|
2940
|
+
const tagNameLower = startTagNode?.type === "html_start_tag" ? getTagNameFromStartTag(startTagNode) : null;
|
|
2941
|
+
const tagConfig = tagNameLower ? context.customTags?.get(tagNameLower) : void 0;
|
|
2942
|
+
if (tagConfig && resolveIndentMode(node, tagConfig)) {
|
|
2943
|
+
const dedented = dedentContent(child.text);
|
|
2944
|
+
if (dedented.length > 0) {
|
|
2945
|
+
const contentLines = dedented.split("\n");
|
|
2946
|
+
const lineDocs = [];
|
|
2947
|
+
for (let j = 0; j < contentLines.length; j++) {
|
|
2948
|
+
if (j > 0) {
|
|
2949
|
+
if (contentLines[j] === "") {
|
|
2950
|
+
lineDocs.push("\n");
|
|
2951
|
+
} else {
|
|
2952
|
+
lineDocs.push(hardline);
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
if (contentLines[j] !== "") {
|
|
2956
|
+
lineDocs.push(text(contentLines[j]));
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
parts.push(indent(concat([hardline, ...lineDocs])));
|
|
2960
|
+
parts.push(hardline);
|
|
2961
|
+
}
|
|
2962
|
+
} else {
|
|
2963
|
+
parts.push(text(child.text));
|
|
2964
|
+
}
|
|
2965
|
+
} else {
|
|
2966
|
+
const dedented = dedentContent(child.text);
|
|
2967
|
+
if (dedented.length > 0) {
|
|
2968
|
+
const contentLines = dedented.split("\n");
|
|
2969
|
+
const lineDocs = [];
|
|
2970
|
+
for (let j = 0; j < contentLines.length; j++) {
|
|
2971
|
+
if (j > 0) {
|
|
2972
|
+
if (contentLines[j] === "") {
|
|
2973
|
+
lineDocs.push("\n");
|
|
2974
|
+
} else {
|
|
2975
|
+
lineDocs.push(hardline);
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
if (contentLines[j] !== "") {
|
|
2979
|
+
lineDocs.push(text(contentLines[j]));
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
parts.push(indent(concat([hardline, ...lineDocs])));
|
|
2983
|
+
parts.push(hardline);
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
return concat(parts);
|
|
2990
|
+
}
|
|
2991
|
+
function formatMustacheSection(node, context) {
|
|
2992
|
+
const isInverted = node.type === "mustache_inverted_section";
|
|
2993
|
+
const beginType = isInverted ? "mustache_inverted_section_begin" : "mustache_section_begin";
|
|
2994
|
+
const endType = isInverted ? "mustache_inverted_section_end" : "mustache_section_end";
|
|
2995
|
+
let beginNode = null;
|
|
2996
|
+
let endNode = null;
|
|
2997
|
+
const contentNodes = [];
|
|
2998
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2999
|
+
const child = node.child(i);
|
|
3000
|
+
if (!child) continue;
|
|
3001
|
+
if (child.type === beginType) {
|
|
3002
|
+
beginNode = child;
|
|
3003
|
+
} else if (child.type === endType || child.type === "mustache_erroneous_section_end" || child.type === "mustache_erroneous_inverted_section_end") {
|
|
3004
|
+
endNode = child;
|
|
3005
|
+
} else if (!child.type.startsWith("_")) {
|
|
3006
|
+
contentNodes.push(child);
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
const parts = [];
|
|
3010
|
+
if (beginNode) {
|
|
3011
|
+
parts.push(text(mustacheText(beginNode.text, context)));
|
|
3012
|
+
}
|
|
3013
|
+
const hasImplicit = hasImplicitEndTags(contentNodes);
|
|
3014
|
+
const erroneousCount = contentNodes.filter((n) => n.type === "html_erroneous_end_tag").length;
|
|
3015
|
+
const hasStaircase = !hasImplicit && erroneousCount > 0;
|
|
3016
|
+
if (hasStaircase) {
|
|
3017
|
+
let virtualDepth = erroneousCount - 1;
|
|
3018
|
+
const groupNodes = [];
|
|
3019
|
+
let lastNodeEnd = -1;
|
|
3020
|
+
let pendingBlankLine = false;
|
|
3021
|
+
let groupBlankLine = false;
|
|
3022
|
+
const emitGroup = () => {
|
|
3023
|
+
if (groupNodes.length === 0) return;
|
|
3024
|
+
const formatted = formatBlockChildren(groupNodes, context);
|
|
3025
|
+
if (hasDocContent(formatted)) {
|
|
3026
|
+
if (groupBlankLine) parts.push("\n");
|
|
3027
|
+
const depth = Math.max(0, virtualDepth + 1);
|
|
3028
|
+
parts.push(depth > 0 ? indentN(concat([hardline, formatted]), depth) : concat([hardline, formatted]));
|
|
3029
|
+
}
|
|
3030
|
+
groupNodes.length = 0;
|
|
3031
|
+
groupBlankLine = false;
|
|
3032
|
+
};
|
|
3033
|
+
for (const node2 of contentNodes) {
|
|
3034
|
+
if (lastNodeEnd >= 0 && node2.startIndex > lastNodeEnd) {
|
|
3035
|
+
const gap = context.document.getText().slice(lastNodeEnd, node2.startIndex);
|
|
3036
|
+
if ((gap.match(/\n/g) || []).length >= 2) {
|
|
3037
|
+
pendingBlankLine = true;
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
if (node2.type === "html_erroneous_end_tag") {
|
|
3041
|
+
emitGroup();
|
|
3042
|
+
if (pendingBlankLine) parts.push("\n");
|
|
3043
|
+
pendingBlankLine = false;
|
|
3044
|
+
const formatted = formatNode(node2, context);
|
|
3045
|
+
const depth = Math.max(0, virtualDepth);
|
|
3046
|
+
parts.push(depth > 0 ? indentN(concat([hardline, formatted]), depth) : concat([hardline, formatted]));
|
|
3047
|
+
virtualDepth--;
|
|
3048
|
+
} else {
|
|
3049
|
+
if (groupNodes.length === 0) {
|
|
3050
|
+
groupBlankLine = pendingBlankLine;
|
|
3051
|
+
pendingBlankLine = false;
|
|
3052
|
+
}
|
|
3053
|
+
groupNodes.push(node2);
|
|
3054
|
+
}
|
|
3055
|
+
lastNodeEnd = node2.endIndex;
|
|
3056
|
+
}
|
|
3057
|
+
emitGroup();
|
|
3058
|
+
parts.push(hardline);
|
|
3059
|
+
} else {
|
|
3060
|
+
const formattedContent = formatBlockChildren(contentNodes, context);
|
|
3061
|
+
const hasContent = hasDocContent(formattedContent);
|
|
3062
|
+
if (hasContent) {
|
|
3063
|
+
if (hasImplicit) {
|
|
3064
|
+
parts.push(hardline);
|
|
3065
|
+
parts.push(formattedContent);
|
|
3066
|
+
parts.push(hardline);
|
|
3067
|
+
} else {
|
|
3068
|
+
const hasBlockChildren = contentNodes.some((child, i) => {
|
|
3069
|
+
if (!shouldTreatAsBlock(child, i, contentNodes, context.customTags)) {
|
|
3070
|
+
return false;
|
|
3071
|
+
}
|
|
3072
|
+
const childDisplay = getCSSDisplay(child, context.customTags);
|
|
3073
|
+
return isWhitespaceInsensitive(childDisplay) || isRawContentElement(child);
|
|
3074
|
+
});
|
|
3075
|
+
if (!hasBlockChildren) {
|
|
3076
|
+
parts.push(indent(concat([softline, formattedContent])));
|
|
3077
|
+
parts.push(softline);
|
|
3078
|
+
} else {
|
|
3079
|
+
parts.push(indent(concat([hardline, formattedContent])));
|
|
3080
|
+
parts.push(hardline);
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
if (endNode) {
|
|
3086
|
+
parts.push(text(mustacheText(endNode.text, context)));
|
|
3087
|
+
}
|
|
3088
|
+
return group(concat(parts));
|
|
3089
|
+
}
|
|
3090
|
+
function startTagHasAttributes(startTag) {
|
|
3091
|
+
for (let i = 0; i < startTag.childCount; i++) {
|
|
3092
|
+
const child = startTag.child(i);
|
|
3093
|
+
if (!child) continue;
|
|
3094
|
+
if (child.type === "html_attribute" || child.type === "mustache_attribute" || child.type === "mustache_interpolation" || child.type === "mustache_triple") {
|
|
3095
|
+
return true;
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
return false;
|
|
3099
|
+
}
|
|
3100
|
+
function formatStartTag(node, context, bare = false) {
|
|
3101
|
+
let tagNameText = "";
|
|
3102
|
+
const attrs = [];
|
|
3103
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
3104
|
+
const child = node.child(i);
|
|
3105
|
+
if (!child) continue;
|
|
3106
|
+
if (child.type === "html_tag_name") {
|
|
3107
|
+
tagNameText = child.text;
|
|
3108
|
+
} else if (child.type === "html_attribute") {
|
|
3109
|
+
attrs.push(formatAttribute(child, context));
|
|
3110
|
+
} else if (child.type === "mustache_attribute") {
|
|
3111
|
+
if (context?.mustacheSpaces !== void 0) {
|
|
3112
|
+
attrs.push(text(normalizeMustacheWhitespaceAll(child.text, context.mustacheSpaces)));
|
|
3113
|
+
} else {
|
|
3114
|
+
attrs.push(text(child.text));
|
|
3115
|
+
}
|
|
3116
|
+
} else if (child.type === "mustache_interpolation" || child.type === "mustache_triple") {
|
|
3117
|
+
attrs.push(text(context ? mustacheText(child.text, context) : child.text));
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
const isSelfClosing = node.type === "html_self_closing_tag";
|
|
3121
|
+
const closingBracket = isSelfClosing ? " />" : ">";
|
|
3122
|
+
if (attrs.length === 0) {
|
|
3123
|
+
return text("<" + tagNameText + closingBracket);
|
|
3124
|
+
}
|
|
3125
|
+
const attrParts = [];
|
|
3126
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
3127
|
+
if (i > 0) {
|
|
3128
|
+
attrParts.push(line);
|
|
3129
|
+
}
|
|
3130
|
+
attrParts.push(attrs[i]);
|
|
3131
|
+
}
|
|
3132
|
+
const breakClosingBracket = isSelfClosing ? "/>" : ">";
|
|
3133
|
+
const inner = concat([
|
|
3134
|
+
text("<"),
|
|
3135
|
+
text(tagNameText),
|
|
3136
|
+
indent(concat([line, concat(attrParts)])),
|
|
3137
|
+
ifBreak(concat([hardline, text(breakClosingBracket)]), text(closingBracket))
|
|
3138
|
+
]);
|
|
3139
|
+
return bare ? inner : group(inner);
|
|
3140
|
+
}
|
|
3141
|
+
function formatEndTag(node) {
|
|
3142
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
3143
|
+
const child = node.child(i);
|
|
3144
|
+
if (child && child.type === "html_tag_name") {
|
|
3145
|
+
return text("</" + child.text + ">");
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
return text(node.text);
|
|
3149
|
+
}
|
|
3150
|
+
function formatAttribute(node, context) {
|
|
3151
|
+
const parts = [];
|
|
3152
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
3153
|
+
const child = node.child(i);
|
|
3154
|
+
if (!child) continue;
|
|
3155
|
+
if (child.type === "html_attribute_name") {
|
|
3156
|
+
parts.push(text(child.text));
|
|
3157
|
+
} else if (child.type === "html_attribute_value") {
|
|
3158
|
+
parts.push(text("="));
|
|
3159
|
+
parts.push(text(child.text));
|
|
3160
|
+
} else if (child.type === "html_quoted_attribute_value") {
|
|
3161
|
+
parts.push(text("="));
|
|
3162
|
+
if (context?.mustacheSpaces !== void 0) {
|
|
3163
|
+
parts.push(text(normalizeMustacheWhitespaceAll(child.text, context.mustacheSpaces)));
|
|
3164
|
+
} else {
|
|
3165
|
+
parts.push(text(child.text));
|
|
3166
|
+
}
|
|
3167
|
+
} else if (child.type === "mustache_interpolation") {
|
|
3168
|
+
parts.push(text("="));
|
|
3169
|
+
parts.push(text(context ? mustacheText(child.text, context) : child.text));
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
return concat(parts);
|
|
3173
|
+
}
|
|
3174
|
+
function textWords(str) {
|
|
3175
|
+
const words = str.split(/\s+/).filter((w) => w.length > 0);
|
|
3176
|
+
if (words.length === 0) return [];
|
|
3177
|
+
const parts = [words[0]];
|
|
3178
|
+
for (let i = 1; i < words.length; i++) {
|
|
3179
|
+
parts.push(line);
|
|
3180
|
+
parts.push(words[i]);
|
|
3181
|
+
}
|
|
3182
|
+
return parts;
|
|
3183
|
+
}
|
|
3184
|
+
function collapseDelimitedRegions(parts, delimiters) {
|
|
3185
|
+
if (delimiters.length === 0) return parts;
|
|
3186
|
+
const sorted = [...delimiters].sort(
|
|
3187
|
+
(a, b) => Math.max(b.start.length, b.end.length) - Math.max(a.start.length, a.end.length)
|
|
3188
|
+
);
|
|
3189
|
+
const result = [...parts];
|
|
3190
|
+
let activeDelimiter = null;
|
|
3191
|
+
for (let i = 0; i < result.length; i++) {
|
|
3192
|
+
const part = result[i];
|
|
3193
|
+
if (typeof part === "string") {
|
|
3194
|
+
if (activeDelimiter === null) {
|
|
3195
|
+
for (const delim of sorted) {
|
|
3196
|
+
const startIdx = part.indexOf(delim.start);
|
|
3197
|
+
if (startIdx >= 0) {
|
|
3198
|
+
const afterOpen = startIdx + delim.start.length;
|
|
3199
|
+
const closeIdx = part.indexOf(delim.end, afterOpen);
|
|
3200
|
+
if (closeIdx >= 0) {
|
|
3201
|
+
continue;
|
|
3202
|
+
}
|
|
3203
|
+
activeDelimiter = delim;
|
|
3204
|
+
break;
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
} else {
|
|
3208
|
+
if (part.includes(activeDelimiter.end)) {
|
|
3209
|
+
activeDelimiter = null;
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
} else if (activeDelimiter !== null && isLine(part)) {
|
|
3213
|
+
result[i] = " ";
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
return result;
|
|
3217
|
+
}
|
|
3218
|
+
function inlineContentToFill(parts) {
|
|
3219
|
+
if (parts.length === 0) return empty;
|
|
3220
|
+
if (parts.length === 1) return parts[0];
|
|
3221
|
+
const fillParts = [];
|
|
3222
|
+
for (const item of parts) {
|
|
3223
|
+
if (isLine(item)) {
|
|
3224
|
+
if (fillParts.length > 0 && !isLine(fillParts[fillParts.length - 1])) {
|
|
3225
|
+
fillParts.push(item);
|
|
3226
|
+
}
|
|
3227
|
+
} else {
|
|
3228
|
+
const lastIdx = fillParts.length - 1;
|
|
3229
|
+
if (lastIdx >= 0 && !isLine(fillParts[lastIdx])) {
|
|
3230
|
+
fillParts[lastIdx] = concat([fillParts[lastIdx], item]);
|
|
3231
|
+
} else if (typeof item === "string" && /^[,.:;!?)\]]/.test(item) && lastIdx >= 0 && isLine(fillParts[lastIdx])) {
|
|
3232
|
+
fillParts.pop();
|
|
3233
|
+
if (fillParts.length > 0) {
|
|
3234
|
+
fillParts[fillParts.length - 1] = concat([
|
|
3235
|
+
fillParts[fillParts.length - 1],
|
|
3236
|
+
item
|
|
3237
|
+
]);
|
|
3238
|
+
} else {
|
|
3239
|
+
fillParts.push(item);
|
|
3240
|
+
}
|
|
3241
|
+
} else {
|
|
3242
|
+
fillParts.push(item);
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
if (fillParts.length > 0 && isLine(fillParts[fillParts.length - 1])) {
|
|
3247
|
+
fillParts.pop();
|
|
3248
|
+
}
|
|
3249
|
+
return fill(fillParts);
|
|
3250
|
+
}
|
|
3251
|
+
function formatBlockChildren(nodes, context) {
|
|
3252
|
+
const lines = [];
|
|
3253
|
+
let currentLine = [];
|
|
3254
|
+
let lastNodeEnd = -1;
|
|
3255
|
+
let pendingBlankLine = false;
|
|
3256
|
+
let blankLineBeforeCurrentLine = false;
|
|
3257
|
+
let ignoreNext = false;
|
|
3258
|
+
let inIgnoreRegion = false;
|
|
3259
|
+
let ignoreRegionStartIndex = -1;
|
|
3260
|
+
const noBreakDelims = context.noBreakDelimiters;
|
|
3261
|
+
function flushCurrentLine() {
|
|
3262
|
+
const parts2 = noBreakDelims ? collapseDelimitedRegions(currentLine, noBreakDelims) : currentLine;
|
|
3263
|
+
return inlineContentToFill(parts2);
|
|
3264
|
+
}
|
|
3265
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
3266
|
+
const node = nodes[i];
|
|
3267
|
+
if (lastNodeEnd >= 0 && node.startIndex > lastNodeEnd && !inIgnoreRegion) {
|
|
3268
|
+
const gap = context.document.getText().slice(lastNodeEnd, node.startIndex);
|
|
3269
|
+
const newlineCount = (gap.match(/\n/g) || []).length;
|
|
3270
|
+
if (newlineCount >= 2) {
|
|
3271
|
+
pendingBlankLine = true;
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
const directive = getIgnoreDirective(node);
|
|
3275
|
+
if (directive === "ignore-end" && inIgnoreRegion) {
|
|
3276
|
+
if (currentLine.length > 0) {
|
|
3277
|
+
const lineContent = trimDoc(flushCurrentLine());
|
|
3278
|
+
if (hasDocContent(lineContent)) {
|
|
3279
|
+
lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
|
|
3280
|
+
}
|
|
3281
|
+
currentLine = [];
|
|
3282
|
+
blankLineBeforeCurrentLine = false;
|
|
3283
|
+
}
|
|
3284
|
+
const rawText = context.document.getText().slice(ignoreRegionStartIndex, node.startIndex).replace(/^\n/, "").replace(/\n$/, "");
|
|
3285
|
+
if (rawText.length > 0) {
|
|
3286
|
+
lines.push({ doc: text(rawText), blankLineBefore: false, rawLine: true });
|
|
3287
|
+
}
|
|
3288
|
+
const commentText = node.type === "mustache_comment" ? mustacheText(node.text, context) : node.text;
|
|
3289
|
+
lines.push({ doc: text(commentText), blankLineBefore: false, rawLine: true });
|
|
3290
|
+
inIgnoreRegion = false;
|
|
3291
|
+
ignoreRegionStartIndex = -1;
|
|
3292
|
+
lastNodeEnd = node.endIndex;
|
|
3293
|
+
continue;
|
|
3294
|
+
}
|
|
3295
|
+
if (inIgnoreRegion) {
|
|
3296
|
+
lastNodeEnd = node.endIndex;
|
|
3297
|
+
continue;
|
|
3298
|
+
}
|
|
3299
|
+
if (directive === "ignore-start") {
|
|
3300
|
+
if (currentLine.length > 0) {
|
|
3301
|
+
const lineContent = trimDoc(flushCurrentLine());
|
|
3302
|
+
if (hasDocContent(lineContent)) {
|
|
3303
|
+
lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
|
|
3304
|
+
}
|
|
3305
|
+
currentLine = [];
|
|
3306
|
+
blankLineBeforeCurrentLine = false;
|
|
3307
|
+
}
|
|
3308
|
+
const commentText = node.type === "mustache_comment" ? mustacheText(node.text, context) : node.text;
|
|
3309
|
+
lines.push({ doc: text(commentText), blankLineBefore: pendingBlankLine });
|
|
3310
|
+
pendingBlankLine = false;
|
|
3311
|
+
inIgnoreRegion = true;
|
|
3312
|
+
ignoreRegionStartIndex = node.endIndex;
|
|
3313
|
+
lastNodeEnd = node.endIndex;
|
|
3314
|
+
continue;
|
|
3315
|
+
}
|
|
3316
|
+
if (directive === "ignore") {
|
|
3317
|
+
if (currentLine.length > 0) {
|
|
3318
|
+
const lineContent = trimDoc(flushCurrentLine());
|
|
3319
|
+
if (hasDocContent(lineContent)) {
|
|
3320
|
+
lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
|
|
3321
|
+
}
|
|
3322
|
+
currentLine = [];
|
|
3323
|
+
blankLineBeforeCurrentLine = false;
|
|
3324
|
+
}
|
|
3325
|
+
const commentText = node.type === "mustache_comment" ? mustacheText(node.text, context) : node.text;
|
|
3326
|
+
lines.push({ doc: text(commentText), blankLineBefore: pendingBlankLine });
|
|
3327
|
+
pendingBlankLine = false;
|
|
3328
|
+
ignoreNext = true;
|
|
3329
|
+
lastNodeEnd = node.endIndex;
|
|
3330
|
+
continue;
|
|
3331
|
+
}
|
|
3332
|
+
if (ignoreNext) {
|
|
3333
|
+
lines.push({ doc: text(node.text), blankLineBefore: pendingBlankLine });
|
|
3334
|
+
pendingBlankLine = false;
|
|
3335
|
+
ignoreNext = false;
|
|
3336
|
+
lastNodeEnd = node.endIndex;
|
|
3337
|
+
continue;
|
|
3338
|
+
}
|
|
3339
|
+
const treatAsBlock = shouldTreatAsBlock(node, i, nodes, context.customTags);
|
|
3340
|
+
if (lastNodeEnd >= 0 && node.startIndex > lastNodeEnd) {
|
|
3341
|
+
const prevNode = nodes[i - 1];
|
|
3342
|
+
const prevTreatAsBlock = shouldTreatAsBlock(prevNode, i - 1, nodes, context.customTags);
|
|
3343
|
+
if (!prevTreatAsBlock && !treatAsBlock) {
|
|
3344
|
+
const gap = context.document.getText().slice(lastNodeEnd, node.startIndex);
|
|
3345
|
+
if (/\s/.test(gap)) {
|
|
3346
|
+
currentLine.push(line);
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
if (treatAsBlock) {
|
|
3351
|
+
if (currentLine.length > 0) {
|
|
3352
|
+
const lineContent = trimDoc(flushCurrentLine());
|
|
3353
|
+
if (hasDocContent(lineContent)) {
|
|
3354
|
+
lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
|
|
3355
|
+
}
|
|
3356
|
+
currentLine = [];
|
|
3357
|
+
blankLineBeforeCurrentLine = false;
|
|
3358
|
+
}
|
|
3359
|
+
lines.push({ doc: formatNode(node, context), blankLineBefore: pendingBlankLine });
|
|
3360
|
+
pendingBlankLine = false;
|
|
3361
|
+
} else if (node.type === "html_comment" || node.type === "mustache_comment") {
|
|
3362
|
+
const isMultiline = node.startPosition.row !== node.endPosition.row;
|
|
3363
|
+
const isOnOwnLine = i > 0 && node.startPosition.row > nodes[i - 1].endPosition.row;
|
|
3364
|
+
if (isMultiline || isOnOwnLine) {
|
|
3365
|
+
if (currentLine.length > 0) {
|
|
3366
|
+
const lineContent = trimDoc(flushCurrentLine());
|
|
3367
|
+
if (hasDocContent(lineContent)) {
|
|
3368
|
+
lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
|
|
3369
|
+
}
|
|
3370
|
+
currentLine = [];
|
|
3371
|
+
blankLineBeforeCurrentLine = false;
|
|
3372
|
+
}
|
|
3373
|
+
const commentText = node.type === "mustache_comment" ? mustacheText(node.text, context) : node.text;
|
|
3374
|
+
lines.push({ doc: text(commentText), blankLineBefore: pendingBlankLine });
|
|
3375
|
+
pendingBlankLine = false;
|
|
3376
|
+
} else {
|
|
3377
|
+
if (currentLine.length === 0) {
|
|
3378
|
+
blankLineBeforeCurrentLine = pendingBlankLine;
|
|
3379
|
+
pendingBlankLine = false;
|
|
3380
|
+
}
|
|
3381
|
+
const commentText = node.type === "mustache_comment" ? mustacheText(node.text, context) : node.text;
|
|
3382
|
+
currentLine.push(text(commentText));
|
|
3383
|
+
}
|
|
3384
|
+
} else {
|
|
3385
|
+
if (currentLine.length === 0) {
|
|
3386
|
+
blankLineBeforeCurrentLine = pendingBlankLine;
|
|
3387
|
+
pendingBlankLine = false;
|
|
3388
|
+
}
|
|
3389
|
+
const forceInline = isInTextFlow(node, i, nodes);
|
|
3390
|
+
const formatted = formatNode(node, context, forceInline);
|
|
3391
|
+
if (typeof formatted === "string" && formatted.includes("\n")) {
|
|
3392
|
+
const contentLines = formatted.split("\n");
|
|
3393
|
+
const isTextNode = node.type === "text";
|
|
3394
|
+
if (isTextNode) {
|
|
3395
|
+
for (let j = 0; j < contentLines.length; j++) {
|
|
3396
|
+
const trimmed = contentLines[j].trim();
|
|
3397
|
+
if (!trimmed) {
|
|
3398
|
+
if (currentLine.length > 0) {
|
|
3399
|
+
const lineContent = trimDoc(flushCurrentLine());
|
|
3400
|
+
if (hasDocContent(lineContent)) {
|
|
3401
|
+
lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
|
|
3402
|
+
blankLineBeforeCurrentLine = false;
|
|
3403
|
+
}
|
|
3404
|
+
currentLine = [];
|
|
3405
|
+
}
|
|
3406
|
+
pendingBlankLine = true;
|
|
3407
|
+
} else {
|
|
3408
|
+
if (currentLine.length === 0) {
|
|
3409
|
+
blankLineBeforeCurrentLine = pendingBlankLine;
|
|
3410
|
+
pendingBlankLine = false;
|
|
3411
|
+
}
|
|
3412
|
+
if (j > 0 && currentLine.length > 0) {
|
|
3413
|
+
currentLine.push(line);
|
|
3414
|
+
}
|
|
3415
|
+
currentLine.push(...textWords(trimmed));
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
} else {
|
|
3419
|
+
const firstTrimmed = contentLines[0].trim();
|
|
3420
|
+
if (firstTrimmed) {
|
|
3421
|
+
currentLine.push(firstTrimmed);
|
|
3422
|
+
}
|
|
3423
|
+
if (currentLine.length > 0) {
|
|
3424
|
+
const lineContent = trimDoc(flushCurrentLine());
|
|
3425
|
+
if (hasDocContent(lineContent)) {
|
|
3426
|
+
lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
|
|
3427
|
+
blankLineBeforeCurrentLine = pendingBlankLine;
|
|
3428
|
+
pendingBlankLine = false;
|
|
3429
|
+
}
|
|
3430
|
+
currentLine = [];
|
|
3431
|
+
}
|
|
3432
|
+
let sawBlankLine = false;
|
|
3433
|
+
for (let j = 1; j < contentLines.length - 1; j++) {
|
|
3434
|
+
const trimmed = contentLines[j].trim();
|
|
3435
|
+
if (trimmed) {
|
|
3436
|
+
lines.push({ doc: text(trimmed), blankLineBefore: blankLineBeforeCurrentLine || sawBlankLine });
|
|
3437
|
+
blankLineBeforeCurrentLine = false;
|
|
3438
|
+
sawBlankLine = false;
|
|
3439
|
+
} else {
|
|
3440
|
+
sawBlankLine = true;
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
if (contentLines.length > 1) {
|
|
3444
|
+
const lastTrimmed = contentLines[contentLines.length - 1].trim();
|
|
3445
|
+
if (lastTrimmed) {
|
|
3446
|
+
blankLineBeforeCurrentLine = sawBlankLine;
|
|
3447
|
+
sawBlankLine = false;
|
|
3448
|
+
currentLine = [lastTrimmed];
|
|
3449
|
+
}
|
|
3450
|
+
if (sawBlankLine) {
|
|
3451
|
+
pendingBlankLine = true;
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
} else {
|
|
3456
|
+
if (node.type === "text" && typeof formatted === "string") {
|
|
3457
|
+
const words = textWords(formatted);
|
|
3458
|
+
if (words.length > 0) {
|
|
3459
|
+
currentLine.push(...words);
|
|
3460
|
+
} else if (node.text.trim() === "" && currentLine.length > 0) {
|
|
3461
|
+
currentLine.push(line);
|
|
3462
|
+
}
|
|
3463
|
+
} else {
|
|
3464
|
+
currentLine.push(formatted);
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
if (node.type === "html_element" && currentLine.length > 0) {
|
|
3469
|
+
const tagName = getTagName(node);
|
|
3470
|
+
if (tagName?.toLowerCase() === "br") {
|
|
3471
|
+
const lineContent = trimDoc(flushCurrentLine());
|
|
3472
|
+
if (hasDocContent(lineContent)) {
|
|
3473
|
+
lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
|
|
3474
|
+
blankLineBeforeCurrentLine = false;
|
|
3475
|
+
}
|
|
3476
|
+
currentLine = [];
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
lastNodeEnd = node.endIndex;
|
|
3480
|
+
}
|
|
3481
|
+
if (inIgnoreRegion && nodes.length > 0) {
|
|
3482
|
+
const lastNode = nodes[nodes.length - 1];
|
|
3483
|
+
const rawText = context.document.getText().slice(ignoreRegionStartIndex, lastNode.endIndex).replace(/^\n/, "");
|
|
3484
|
+
if (rawText.length > 0) {
|
|
3485
|
+
lines.push({ doc: text(rawText), blankLineBefore: false, rawLine: true });
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
if (currentLine.length > 0) {
|
|
3489
|
+
const lineContent = trimDoc(flushCurrentLine());
|
|
3490
|
+
if (hasDocContent(lineContent)) {
|
|
3491
|
+
lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
if (lines.length === 0) {
|
|
3495
|
+
return empty;
|
|
3496
|
+
}
|
|
3497
|
+
const parts = [];
|
|
3498
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3499
|
+
if (i > 0) {
|
|
3500
|
+
if (lines[i].blankLineBefore) {
|
|
3501
|
+
parts.push("\n");
|
|
3502
|
+
}
|
|
3503
|
+
if (lines[i].rawLine) {
|
|
3504
|
+
parts.push("\n");
|
|
3505
|
+
} else {
|
|
3506
|
+
parts.push(hardline);
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
parts.push(lines[i].doc);
|
|
3510
|
+
}
|
|
3511
|
+
return concat(parts);
|
|
3512
|
+
}
|
|
3513
|
+
function hasDocContent(doc) {
|
|
3514
|
+
if (typeof doc === "string") {
|
|
3515
|
+
return doc.trim().length > 0;
|
|
3516
|
+
}
|
|
3517
|
+
if (doc.type === "concat") {
|
|
3518
|
+
return doc.parts.some(hasDocContent);
|
|
3519
|
+
}
|
|
3520
|
+
if (doc.type === "indent") {
|
|
3521
|
+
return hasDocContent(doc.contents);
|
|
3522
|
+
}
|
|
3523
|
+
if (doc.type === "group") {
|
|
3524
|
+
return hasDocContent(doc.contents);
|
|
3525
|
+
}
|
|
3526
|
+
if (doc.type === "fill") {
|
|
3527
|
+
return doc.parts.some(hasDocContent);
|
|
3528
|
+
}
|
|
3529
|
+
if (doc.type === "ifBreak") {
|
|
3530
|
+
return hasDocContent(doc.breakContents) || hasDocContent(doc.flatContents);
|
|
3531
|
+
}
|
|
3532
|
+
return false;
|
|
3533
|
+
}
|
|
3534
|
+
function trimDoc(doc) {
|
|
3535
|
+
if (typeof doc === "string") {
|
|
3536
|
+
return doc.trim();
|
|
3537
|
+
}
|
|
3538
|
+
return doc;
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
// src/core/formatting/mergeOptions.ts
|
|
3542
|
+
function mergeOptions(lspOptions, configFile, overrides) {
|
|
3543
|
+
let tabSize = lspOptions.tabSize;
|
|
3544
|
+
if (configFile?.indentSize !== void 0) tabSize = configFile.indentSize;
|
|
3545
|
+
if (overrides?.tabSize !== void 0) tabSize = overrides.tabSize;
|
|
3546
|
+
const insertSpaces = overrides?.insertSpaces ?? lspOptions.insertSpaces;
|
|
3547
|
+
return { tabSize, insertSpaces };
|
|
3548
|
+
}
|
|
3549
|
+
function createIndentUnit(options) {
|
|
3550
|
+
return options.insertSpaces ? " ".repeat(options.tabSize) : " ";
|
|
3551
|
+
}
|
|
3552
|
+
|
|
3553
|
+
// src/core/formatting/index.ts
|
|
3554
|
+
function buildCustomTagMap(customTags) {
|
|
3555
|
+
if (!customTags || customTags.length === 0) return void 0;
|
|
3556
|
+
const map = /* @__PURE__ */ new Map();
|
|
3557
|
+
for (const config of customTags) {
|
|
3558
|
+
map.set(config.name.toLowerCase(), config);
|
|
3559
|
+
}
|
|
3560
|
+
return map;
|
|
3561
|
+
}
|
|
3562
|
+
function formatDocument2(tree, document, options, params = {}) {
|
|
3563
|
+
const { printWidth = 80, embeddedFormatted, mustacheSpaces, noBreakDelimiters } = params;
|
|
3564
|
+
const indentUnit = createIndentUnit(options);
|
|
3565
|
+
if (tree.rootNode.hasError) return [];
|
|
3566
|
+
const customTagMap = buildCustomTagMap(params.customTags);
|
|
3567
|
+
const context = {
|
|
3568
|
+
document,
|
|
3569
|
+
customTags: customTagMap,
|
|
3570
|
+
embeddedFormatted,
|
|
3571
|
+
mustacheSpaces,
|
|
3572
|
+
noBreakDelimiters
|
|
3573
|
+
};
|
|
3574
|
+
const doc = formatDocument(tree.rootNode, context);
|
|
3575
|
+
const formatted = print(doc, { indentUnit, printWidth });
|
|
3576
|
+
const fullRange = {
|
|
3577
|
+
start: { line: 0, character: 0 },
|
|
3578
|
+
end: document.positionAt(document.getText().length)
|
|
3579
|
+
};
|
|
3580
|
+
return [{ range: fullRange, newText: formatted }];
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
// src/core/embeddedRegions.ts
|
|
3584
|
+
function getEmbeddedLanguageId(node) {
|
|
3585
|
+
if (node.type === "html_style_element") {
|
|
3586
|
+
return "css";
|
|
3587
|
+
}
|
|
3588
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
3589
|
+
const child = node.child(i);
|
|
3590
|
+
if (child?.type === "html_start_tag") {
|
|
3591
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
3592
|
+
const attr = child.child(j);
|
|
3593
|
+
if (attr?.type === "html_attribute") {
|
|
3594
|
+
let name = "";
|
|
3595
|
+
let value = "";
|
|
3596
|
+
for (let k = 0; k < attr.childCount; k++) {
|
|
3597
|
+
const part = attr.child(k);
|
|
3598
|
+
if (part?.type === "html_attribute_name") name = part.text.toLowerCase();
|
|
3599
|
+
if (part?.type === "html_quoted_attribute_value") value = part.text.replace(/^["']|["']$/g, "").toLowerCase();
|
|
3600
|
+
if (part?.type === "html_attribute_value") value = part.text.toLowerCase();
|
|
3601
|
+
}
|
|
3602
|
+
if (name === "type" && (value === "text/typescript" || value === "ts")) {
|
|
3603
|
+
return "typescript";
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
return "javascript";
|
|
3610
|
+
}
|
|
3611
|
+
function collectEmbeddedRegions(rootNode) {
|
|
3612
|
+
const regions = [];
|
|
3613
|
+
const walk = (node) => {
|
|
3614
|
+
if (node.type === "html_script_element" || node.type === "html_style_element") {
|
|
3615
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
3616
|
+
const child = node.child(i);
|
|
3617
|
+
if (child?.type === "html_raw_text") {
|
|
3618
|
+
regions.push({
|
|
3619
|
+
startIndex: child.startIndex,
|
|
3620
|
+
content: child.text,
|
|
3621
|
+
languageId: getEmbeddedLanguageId(node)
|
|
3622
|
+
});
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
return;
|
|
3626
|
+
}
|
|
3627
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
3628
|
+
const child = node.child(i);
|
|
3629
|
+
if (child) walk(child);
|
|
3630
|
+
}
|
|
3631
|
+
};
|
|
3632
|
+
walk(rootNode);
|
|
3633
|
+
return regions;
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
// src/core/formatting/embedded.ts
|
|
3637
|
+
var LANGUAGE_TO_PRETTIER_PARSER = {
|
|
3638
|
+
javascript: "babel",
|
|
3639
|
+
typescript: "typescript",
|
|
3640
|
+
css: "css"
|
|
3641
|
+
};
|
|
3642
|
+
async function formatEmbeddedRegions(rootNode, options, prettier) {
|
|
3643
|
+
const result = /* @__PURE__ */ new Map();
|
|
3644
|
+
if (!prettier) return result;
|
|
3645
|
+
const regions = collectEmbeddedRegions(rootNode);
|
|
3646
|
+
if (regions.length === 0) return result;
|
|
3647
|
+
await Promise.all(
|
|
3648
|
+
regions.map(async (region) => {
|
|
3649
|
+
const parser = LANGUAGE_TO_PRETTIER_PARSER[region.languageId];
|
|
3650
|
+
if (!parser) return;
|
|
3651
|
+
try {
|
|
3652
|
+
const formatted = await prettier.format(region.content, {
|
|
3653
|
+
parser,
|
|
3654
|
+
tabWidth: options.tabSize,
|
|
3655
|
+
useTabs: !options.insertSpaces
|
|
3656
|
+
});
|
|
3657
|
+
result.set(region.startIndex, formatted);
|
|
3658
|
+
} catch {
|
|
3659
|
+
}
|
|
3660
|
+
})
|
|
3661
|
+
);
|
|
3662
|
+
return result;
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3665
|
+
// src/core/diagnostic.ts
|
|
3666
|
+
function toFix(r) {
|
|
3667
|
+
return { range: [r.startIndex, r.endIndex], newText: r.newText };
|
|
3668
|
+
}
|
|
3669
|
+
function toDiagnostic(err) {
|
|
3670
|
+
const { node } = err;
|
|
3671
|
+
return {
|
|
3672
|
+
line: node.startPosition.row + 1,
|
|
3673
|
+
column: node.startPosition.column + 1,
|
|
3674
|
+
endLine: node.endPosition.row + 1,
|
|
3675
|
+
endColumn: node.endPosition.column + 1,
|
|
3676
|
+
message: err.message,
|
|
3677
|
+
severity: err.severity ?? "error",
|
|
3678
|
+
ruleName: err.ruleName,
|
|
3679
|
+
fix: err.fix && err.fix.length > 0 ? err.fix.map(toFix) : void 0,
|
|
3680
|
+
fixDescription: err.fixDescription
|
|
3681
|
+
};
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
// src/core/grammar.ts
|
|
3685
|
+
var GRAMMAR_WASM_FILENAME = "tree-sitter-htmlmustache.wasm";
|
|
3686
|
+
|
|
3687
|
+
// src/browser/index.ts
|
|
3688
|
+
var DEFAULT_CONFIG = { rules: RULE_DEFAULTS };
|
|
3689
|
+
var DEFAULT_FORMATTING_OPTIONS = { tabSize: 2, insertSpaces: true };
|
|
3690
|
+
function toLocateFile(locateWasm) {
|
|
3691
|
+
return typeof locateWasm === "function" ? (name) => locateWasm(name) : void 0;
|
|
3692
|
+
}
|
|
3693
|
+
function resolveGrammarUrl(locateWasm) {
|
|
3694
|
+
return typeof locateWasm === "string" ? locateWasm : locateWasm(GRAMMAR_WASM_FILENAME);
|
|
3695
|
+
}
|
|
3696
|
+
async function createLinter(opts) {
|
|
3697
|
+
const { locateWasm, prettier: factoryPrettier } = opts;
|
|
3698
|
+
const locateFile = toLocateFile(locateWasm);
|
|
3699
|
+
await Parser.init(locateFile ? { locateFile } : void 0);
|
|
3700
|
+
const parser = new Parser();
|
|
3701
|
+
const language = await Language.load(resolveGrammarUrl(locateWasm));
|
|
3702
|
+
parser.setLanguage(language);
|
|
3703
|
+
return {
|
|
3704
|
+
lint(source, config) {
|
|
3705
|
+
const tree = parser.parse(source);
|
|
3706
|
+
if (!tree) throw new Error("Failed to parse document");
|
|
3707
|
+
try {
|
|
3708
|
+
const customTagNames = config?.customTags?.map((t) => t.name);
|
|
3709
|
+
const errors = collectErrors(
|
|
3710
|
+
tree,
|
|
3711
|
+
config?.rules,
|
|
3712
|
+
customTagNames,
|
|
3713
|
+
config?.customRules
|
|
3714
|
+
);
|
|
3715
|
+
return errors.map(toDiagnostic);
|
|
3716
|
+
} finally {
|
|
3717
|
+
tree.delete();
|
|
3718
|
+
}
|
|
3719
|
+
},
|
|
3720
|
+
async format(source, config, callOpts) {
|
|
3721
|
+
const prettier = callOpts?.prettier ?? factoryPrettier;
|
|
3722
|
+
const options = mergeOptions(DEFAULT_FORMATTING_OPTIONS, config ?? null);
|
|
3723
|
+
const tree = parser.parse(source);
|
|
3724
|
+
if (!tree) throw new Error("Failed to parse document");
|
|
3725
|
+
try {
|
|
3726
|
+
const embeddedFormatted = await formatEmbeddedRegions(tree.rootNode, options, prettier);
|
|
3727
|
+
const document = TextDocument.create("file:///input", "htmlmustache", 1, source);
|
|
3728
|
+
const edits = formatDocument2(tree, document, options, {
|
|
3729
|
+
customTags: config?.customTags,
|
|
3730
|
+
printWidth: config?.printWidth,
|
|
3731
|
+
mustacheSpaces: config?.mustacheSpaces,
|
|
3732
|
+
noBreakDelimiters: config?.noBreakDelimiters,
|
|
3733
|
+
embeddedFormatted: embeddedFormatted.size > 0 ? embeddedFormatted : void 0
|
|
3734
|
+
});
|
|
3735
|
+
return edits.length === 0 ? source : edits[0].newText;
|
|
3736
|
+
} finally {
|
|
3737
|
+
tree.delete();
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
};
|
|
3741
|
+
}
|
|
3742
|
+
export {
|
|
3743
|
+
DEFAULT_CONFIG,
|
|
3744
|
+
createLinter
|
|
3745
|
+
};
|
|
3746
|
+
//# sourceMappingURL=index.mjs.map
|