@reteps/tree-sitter-htmlmustache 0.8.0 → 0.8.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 +48 -32
- package/cli/out/main.js +35 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -294,41 +294,57 @@ Each custom rule requires an `id`, `selector`, and `message`. The `severity` def
|
|
|
294
294
|
|
|
295
295
|
**Selector syntax:**
|
|
296
296
|
|
|
297
|
-
| Selector | Matches
|
|
298
|
-
| ----------------------------------------- |
|
|
299
|
-
| `div` | HTML elements by tag name
|
|
300
|
-
| `*` | Any HTML element
|
|
301
|
-
| `#main` | ID (shorthand for `[id="main"]`)
|
|
302
|
-
| `.panel` | Class (shorthand for `[class~="panel"]`)
|
|
303
|
-
| `div span` | Descendant (span anywhere inside div)
|
|
304
|
-
| `div > span` | Direct child
|
|
305
|
-
| `[style]` | Attribute presence
|
|
306
|
-
| `input[type=hidden]` | Attribute value (exact)
|
|
307
|
-
| `[src^="prefix/"]` | Attribute starts with
|
|
308
|
-
| `[href*="substring"]` | Attribute contains
|
|
309
|
-
| `[src$=".png"]` | Attribute ends with
|
|
310
|
-
| `[class~="warning"]` | Attribute contains whitespace-token
|
|
311
|
-
| `img:not([alt])` | Negated attribute / class / id
|
|
312
|
-
| `{{foo}}` | Escaped variable `{{foo}}`
|
|
313
|
-
| `{{data.foo}}` | Variable with a dotted path
|
|
314
|
-
| `{{{foo}}}` | Triple / unescaped variable
|
|
315
|
-
| `{{options.*}}` | Variable path prefix match
|
|
316
|
-
| `{{*.deprecated}}` | Variable path suffix match
|
|
317
|
-
| `{{*}}` | Any escaped variable
|
|
318
|
-
| `{{{*}}}` | Any triple
|
|
319
|
-
| `{{#items}}` | Section `{{#items}}...{{/items}}`
|
|
320
|
-
| `{{^items}}` | Inverted section `{{^items}}...{{/items}}`
|
|
321
|
-
| `{{#items}} > li` | Direct child inside a section
|
|
322
|
-
| `{{!TODO}}` | Comment with exact content
|
|
323
|
-
| `{{!*TODO*}}` | Comment containing "TODO"
|
|
324
|
-
| `{{>header}}` | Partial invocation
|
|
325
|
-
| `{{>legacy_*}}` | Partial name prefix
|
|
326
|
-
| `pl-multiple-choice:has({{foo}})` | Element containing a given variable
|
|
327
|
-
| `pl-multiple-choice:not(:has(pl-answer))` | Element missing a required descendant
|
|
328
|
-
| `div, span` | Comma-separated alternatives
|
|
297
|
+
| Selector | Matches |
|
|
298
|
+
| ----------------------------------------- | ------------------------------------------ |
|
|
299
|
+
| `div` | HTML elements by tag name |
|
|
300
|
+
| `*` | Any HTML element |
|
|
301
|
+
| `#main` | ID (shorthand for `[id="main"]`) |
|
|
302
|
+
| `.panel` | Class (shorthand for `[class~="panel"]`) |
|
|
303
|
+
| `div span` | Descendant (span anywhere inside div) |
|
|
304
|
+
| `div > span` | Direct child |
|
|
305
|
+
| `[style]` | Attribute presence |
|
|
306
|
+
| `input[type=hidden]` | Attribute value (exact) |
|
|
307
|
+
| `[src^="prefix/"]` | Attribute starts with |
|
|
308
|
+
| `[href*="substring"]` | Attribute contains |
|
|
309
|
+
| `[src$=".png"]` | Attribute ends with |
|
|
310
|
+
| `[class~="warning"]` | Attribute contains whitespace-token |
|
|
311
|
+
| `img:not([alt])` | Negated attribute / class / id |
|
|
312
|
+
| `{{foo}}` | Escaped variable `{{foo}}` |
|
|
313
|
+
| `{{data.foo}}` | Variable with a dotted path |
|
|
314
|
+
| `{{{foo}}}` | Triple / unescaped variable |
|
|
315
|
+
| `{{options.*}}` | Variable path prefix match |
|
|
316
|
+
| `{{*.deprecated}}` | Variable path suffix match |
|
|
317
|
+
| `{{*}}` | Any escaped variable |
|
|
318
|
+
| `{{{*}}}` | Any triple |
|
|
319
|
+
| `{{#items}}` | Section `{{#items}}...{{/items}}` |
|
|
320
|
+
| `{{^items}}` | Inverted section `{{^items}}...{{/items}}` |
|
|
321
|
+
| `{{#items}} > li` | Direct child inside a section |
|
|
322
|
+
| `{{!TODO}}` | Comment with exact content |
|
|
323
|
+
| `{{!*TODO*}}` | Comment containing "TODO" |
|
|
324
|
+
| `{{>header}}` | Partial invocation |
|
|
325
|
+
| `{{>legacy_*}}` | Partial name prefix |
|
|
326
|
+
| `pl-multiple-choice:has({{foo}})` | Element containing a given variable |
|
|
327
|
+
| `pl-multiple-choice:not(:has(pl-answer))` | Element missing a required descendant |
|
|
328
|
+
| `div, span` | Comma-separated alternatives |
|
|
329
|
+
| `:root` | The document root (the whole parse tree) |
|
|
330
|
+
| `:root:has(pl-answer-panel)` | Document contains a descendant anywhere |
|
|
331
|
+
| `:root:not(:has(pl-answer-panel))` | Document is missing a descendant anywhere |
|
|
332
|
+
| `:root > section` | Top-level element (direct child of root) |
|
|
329
333
|
|
|
330
334
|
The `>` (child) combinator is kind-transparent: `div > span` matches even if a Mustache section sits between them (e.g. `<div>{{#show}}<span>{{/show}}</div>`), and `{{#a}} > {{#b}}` matches across intervening HTML elements. `{{#foo}}` matches only positive sections — to target inverted sections use `{{^foo}}`.
|
|
331
335
|
|
|
336
|
+
**Document-scoped conditional rules.** Use `:root` with `:has(...)` / `:not(:has(...))` to express rules that depend on the overall document. Chained `:has(...)` acts as AND, so you can combine "contains X" and "missing Y" in one selector:
|
|
337
|
+
|
|
338
|
+
```jsonc
|
|
339
|
+
{
|
|
340
|
+
"id": "question-needs-answer-panel",
|
|
341
|
+
"selector": ":root:has(pl-question-panel):not(:has(pl-answer-panel))",
|
|
342
|
+
"message": "A question-panel document must also declare a pl-answer-panel.",
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
When `:root` matches, the diagnostic is reported at the start of the document (row 0, column 0) so the squiggle doesn't span the whole file. `:root` here is the tree-sitter fragment root, so it works on partial templates and fragments — unlike browser CSS, which anchors `:root` on `<html>`. Inside `:has(...)`, `:root` refers to the element being has-checked, not the document.
|
|
347
|
+
|
|
332
348
|
### Disabling Lint Rules
|
|
333
349
|
|
|
334
350
|
Disable a lint rule for an entire file with an inline comment:
|
package/cli/out/main.js
CHANGED
|
@@ -1963,6 +1963,7 @@ function segmentFromCompound(ast) {
|
|
|
1963
1963
|
let kind;
|
|
1964
1964
|
let name = null;
|
|
1965
1965
|
let pathRegex;
|
|
1966
|
+
let rootOnly = false;
|
|
1966
1967
|
const attributes = [];
|
|
1967
1968
|
const descendantChecks = [];
|
|
1968
1969
|
const forbidChange = (requested) => {
|
|
@@ -2021,6 +2022,11 @@ function segmentFromCompound(ast) {
|
|
|
2021
2022
|
if (!applyNegatedSubtree(token.subtree, attributes, descendantChecks)) return null;
|
|
2022
2023
|
break;
|
|
2023
2024
|
}
|
|
2025
|
+
if (token.name === "root") {
|
|
2026
|
+
rootOnly = true;
|
|
2027
|
+
if (kind === void 0) kind = "html";
|
|
2028
|
+
break;
|
|
2029
|
+
}
|
|
2024
2030
|
return null;
|
|
2025
2031
|
}
|
|
2026
2032
|
default:
|
|
@@ -2030,9 +2036,12 @@ function segmentFromCompound(ast) {
|
|
|
2030
2036
|
if (kind === void 0) {
|
|
2031
2037
|
kind = "html";
|
|
2032
2038
|
}
|
|
2039
|
+
if (rootOnly) {
|
|
2040
|
+
if (name !== null || attributes.length > 0 || kind !== "html") return null;
|
|
2041
|
+
}
|
|
2033
2042
|
const isHtml = kind === "html";
|
|
2034
2043
|
const finalAttrs = isHtml ? attributes : [];
|
|
2035
|
-
return { kind, name, pathRegex, attributes: finalAttrs, descendantChecks, combinator: "descendant" };
|
|
2044
|
+
return { kind, rootOnly, name, pathRegex, attributes: finalAttrs, descendantChecks, combinator: "descendant" };
|
|
2036
2045
|
}
|
|
2037
2046
|
function mustacheKindFromMarker(name) {
|
|
2038
2047
|
switch (name) {
|
|
@@ -2227,7 +2236,11 @@ function matchesName(actual, segment) {
|
|
|
2227
2236
|
if (segment.pathRegex) return segment.pathRegex.test(actual);
|
|
2228
2237
|
return actual === segment.name;
|
|
2229
2238
|
}
|
|
2230
|
-
function nodeMatchesSegment(node, segment) {
|
|
2239
|
+
function nodeMatchesSegment(node, segment, rootNode) {
|
|
2240
|
+
if (segment.rootOnly) {
|
|
2241
|
+
if (node !== rootNode) return false;
|
|
2242
|
+
return checkDescendants(node, segment.descendantChecks);
|
|
2243
|
+
}
|
|
2231
2244
|
switch (segment.kind) {
|
|
2232
2245
|
case "html": {
|
|
2233
2246
|
if (!HTML_ELEMENT_TYPES.has(node.type)) return false;
|
|
@@ -2271,7 +2284,10 @@ function checkAncestors(ancestors, segments, segIdx, childCombinator) {
|
|
|
2271
2284
|
if (childCombinator === "child") {
|
|
2272
2285
|
for (let a2 = ancestors.length - 1; a2 >= 0; a2--) {
|
|
2273
2286
|
const entry = ancestors[a2];
|
|
2274
|
-
if (entry.kind !== ancestorKind)
|
|
2287
|
+
if (entry.kind !== ancestorKind) {
|
|
2288
|
+
if (ancestorKind === "root" && entry.kind === "html") return false;
|
|
2289
|
+
continue;
|
|
2290
|
+
}
|
|
2275
2291
|
if (!matchesName(entry.name, segment)) return false;
|
|
2276
2292
|
if (segment.kind === "html" && !checkAttributes(entry.node, segment.attributes)) return false;
|
|
2277
2293
|
if (!checkDescendants(entry.node, segment.descendantChecks)) return false;
|
|
@@ -2292,12 +2308,13 @@ function checkAncestors(ancestors, segments, segIdx, childCombinator) {
|
|
|
2292
2308
|
return false;
|
|
2293
2309
|
}
|
|
2294
2310
|
function ancestorKindForSegment(segment) {
|
|
2311
|
+
if (segment.rootOnly) return "root";
|
|
2295
2312
|
if (segment.kind === "html") return "html";
|
|
2296
2313
|
if (segment.kind === "section") return "section";
|
|
2297
2314
|
if (segment.kind === "inverted") return "inverted";
|
|
2298
2315
|
return null;
|
|
2299
2316
|
}
|
|
2300
|
-
function getReportNode(node) {
|
|
2317
|
+
function getReportNode(node, rootNode) {
|
|
2301
2318
|
if (HTML_ELEMENT_TYPES.has(node.type)) {
|
|
2302
2319
|
const startTag = node.children.find(
|
|
2303
2320
|
(c2) => c2.type === "html_start_tag" || c2.type === "html_self_closing_tag"
|
|
@@ -2310,15 +2327,26 @@ function getReportNode(node) {
|
|
|
2310
2327
|
);
|
|
2311
2328
|
return begin ?? node;
|
|
2312
2329
|
}
|
|
2330
|
+
if (rootNode && node === rootNode) {
|
|
2331
|
+
return {
|
|
2332
|
+
type: node.type,
|
|
2333
|
+
text: "",
|
|
2334
|
+
startPosition: node.startPosition,
|
|
2335
|
+
endPosition: { row: node.startPosition.row, column: node.startPosition.column + 1 },
|
|
2336
|
+
startIndex: node.startIndex,
|
|
2337
|
+
endIndex: Math.min(node.startIndex + 1, node.endIndex),
|
|
2338
|
+
children: []
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2313
2341
|
return node;
|
|
2314
2342
|
}
|
|
2315
2343
|
function matchAlternative(rootNode, segments) {
|
|
2316
2344
|
const results = [];
|
|
2317
2345
|
const lastSegment = segments[segments.length - 1];
|
|
2318
2346
|
function walk(node, ancestors) {
|
|
2319
|
-
if (nodeMatchesSegment(node, lastSegment)) {
|
|
2347
|
+
if (nodeMatchesSegment(node, lastSegment, rootNode)) {
|
|
2320
2348
|
if (segments.length === 1 || checkAncestors(ancestors, segments, segments.length - 2, lastSegment.combinator)) {
|
|
2321
|
-
results.push(getReportNode(node));
|
|
2349
|
+
results.push(getReportNode(node, rootNode));
|
|
2322
2350
|
}
|
|
2323
2351
|
}
|
|
2324
2352
|
let newAncestors = ancestors;
|
|
@@ -2331,7 +2359,7 @@ function matchAlternative(rootNode, segments) {
|
|
|
2331
2359
|
}
|
|
2332
2360
|
for (const child of node.children) walk(child, newAncestors);
|
|
2333
2361
|
}
|
|
2334
|
-
walk(rootNode, []);
|
|
2362
|
+
walk(rootNode, [{ kind: "root", name: "", node: rootNode }]);
|
|
2335
2363
|
return results;
|
|
2336
2364
|
}
|
|
2337
2365
|
function matchSelector(rootNode, selector) {
|