@reteps/tree-sitter-htmlmustache 0.6.0 → 0.7.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 +65 -14
- package/cli/out/main.js +337 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -228,36 +228,87 @@ Additionally, the following rules are configurable. Set their severities (`"erro
|
|
|
228
228
|
{
|
|
229
229
|
"rules": {
|
|
230
230
|
"consecutiveDuplicateSections": "off",
|
|
231
|
-
"preferMustacheComments": "warning"
|
|
232
|
-
}
|
|
231
|
+
"preferMustacheComments": "warning",
|
|
232
|
+
},
|
|
233
233
|
}
|
|
234
234
|
```
|
|
235
235
|
|
|
236
236
|
<!-- RULES_TABLE_START -->
|
|
237
237
|
|
|
238
|
-
| Rule
|
|
239
|
-
|
|
|
240
|
-
| `nestedDuplicateSections`
|
|
241
|
-
| `unquotedMustacheAttributes`
|
|
242
|
-
| `consecutiveDuplicateSections` | `warning` | Warns when adjacent same-name sections can be merged
|
|
243
|
-
| `selfClosingNonVoidTags`
|
|
244
|
-
| `duplicateAttributes`
|
|
245
|
-
| `unescapedEntities`
|
|
246
|
-
| `preferMustacheComments`
|
|
247
|
-
| `unrecognizedHtmlTags`
|
|
238
|
+
| Rule | Default | Description |
|
|
239
|
+
| ------------------------------ | --------- | ---------------------------------------------------------------------------- |
|
|
240
|
+
| `nestedDuplicateSections` | `error` | Flags `{{#name}}` nested inside another `{{#name}}` with the same name |
|
|
241
|
+
| `unquotedMustacheAttributes` | `error` | Requires quotes around mustache expressions used as attribute values |
|
|
242
|
+
| `consecutiveDuplicateSections` | `warning` | Warns when adjacent same-name sections can be merged |
|
|
243
|
+
| `selfClosingNonVoidTags` | `error` | Disallows self-closing syntax on non-void HTML elements (e.g. `<div/>`) |
|
|
244
|
+
| `duplicateAttributes` | `error` | Detects duplicate HTML attributes on the same element |
|
|
245
|
+
| `unescapedEntities` | `warning` | Flags unescaped `&` and `>` characters in text content |
|
|
246
|
+
| `preferMustacheComments` | `off` | Suggests replacing HTML comments with mustache comments |
|
|
247
|
+
| `unrecognizedHtmlTags` | `error` | Flags HTML tags that are not standard HTML elements or valid custom elements |
|
|
248
248
|
|
|
249
249
|
<!-- RULES_TABLE_END -->
|
|
250
250
|
|
|
251
|
+
### Custom Rules
|
|
252
|
+
|
|
253
|
+
Define project-specific lint rules using CSS-like selectors to match HTML elements and Mustache sections:
|
|
254
|
+
|
|
255
|
+
```jsonc
|
|
256
|
+
{
|
|
257
|
+
"customRules": [
|
|
258
|
+
{
|
|
259
|
+
"id": "no-font",
|
|
260
|
+
"selector": "font",
|
|
261
|
+
"message": "The <font> tag is deprecated. Use CSS instead.",
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"id": "no-inline-styles",
|
|
265
|
+
"selector": "[style]",
|
|
266
|
+
"message": "Avoid inline styles",
|
|
267
|
+
"severity": "warning",
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"id": "images-need-alt",
|
|
271
|
+
"selector": "img:not([alt])",
|
|
272
|
+
"message": "Images must have alt text for accessibility",
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"id": "no-hidden-inputs-in-list",
|
|
276
|
+
"selector": "#items > input[type=hidden]",
|
|
277
|
+
"message": "Hidden inputs inside {{#items}} sections are usually a mistake",
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Each custom rule requires an `id`, `selector`, and `message`. The `severity` defaults to `"error"` but can be set to `"warning"` or `"off"`.
|
|
284
|
+
|
|
285
|
+
**Selector syntax:**
|
|
286
|
+
|
|
287
|
+
| Selector | Matches |
|
|
288
|
+
| -------------------- | ---------------------------------------- |
|
|
289
|
+
| `div` | HTML elements by tag name |
|
|
290
|
+
| `#items` | Mustache sections by name (`{{#items}}`) |
|
|
291
|
+
| `*` | Any HTML element |
|
|
292
|
+
| `#` | Any Mustache section |
|
|
293
|
+
| `div span` | Descendant (span anywhere inside div) |
|
|
294
|
+
| `div > span` | Direct child (span directly inside div) |
|
|
295
|
+
| `[style]` | Attribute presence |
|
|
296
|
+
| `input[type=hidden]` | Attribute value |
|
|
297
|
+
| `img:not([alt])` | Negated attribute |
|
|
298
|
+
| `div, span` | Comma-separated alternatives |
|
|
299
|
+
|
|
300
|
+
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.
|
|
301
|
+
|
|
251
302
|
### Disabling Lint Rules
|
|
252
303
|
|
|
253
|
-
Disable a
|
|
304
|
+
Disable a lint rule for an entire file with an inline comment:
|
|
254
305
|
|
|
255
306
|
```html
|
|
256
307
|
<!-- htmlmustache-disable preferMustacheComments -->
|
|
257
308
|
{{! htmlmustache-disable selfClosingNonVoidTags }}
|
|
258
309
|
```
|
|
259
310
|
|
|
260
|
-
The comment can appear anywhere in the file.
|
|
311
|
+
The comment can appear anywhere in the file. Both built-in and custom rules can be disabled by name/id. Use multiple comments to disable multiple rules.
|
|
261
312
|
|
|
262
313
|
### EditorConfig
|
|
263
314
|
|
package/cli/out/main.js
CHANGED
|
@@ -748,6 +748,22 @@ function validateConfig(raw) {
|
|
|
748
748
|
}
|
|
749
749
|
if (hasRules) config.rules = rules;
|
|
750
750
|
}
|
|
751
|
+
if (Array.isArray(obj.customRules)) {
|
|
752
|
+
const rules = [];
|
|
753
|
+
for (const entry of obj.customRules) {
|
|
754
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
755
|
+
const e = entry;
|
|
756
|
+
if (typeof e.id !== "string" || e.id.length === 0) continue;
|
|
757
|
+
if (typeof e.selector !== "string" || e.selector.length === 0) continue;
|
|
758
|
+
if (typeof e.message !== "string" || e.message.length === 0) continue;
|
|
759
|
+
const rule = { id: e.id, selector: e.selector, message: e.message };
|
|
760
|
+
if (typeof e.severity === "string" && VALID_RULE_SEVERITIES.has(e.severity)) {
|
|
761
|
+
rule.severity = e.severity;
|
|
762
|
+
}
|
|
763
|
+
rules.push(rule);
|
|
764
|
+
}
|
|
765
|
+
if (rules.length > 0) config.customRules = rules;
|
|
766
|
+
}
|
|
751
767
|
return config;
|
|
752
768
|
}
|
|
753
769
|
function loadConfigFileForPath(filePath) {
|
|
@@ -921,18 +937,29 @@ function mergeAdjacentForks(items) {
|
|
|
921
937
|
}
|
|
922
938
|
return result;
|
|
923
939
|
}
|
|
924
|
-
function
|
|
940
|
+
function isBranchBalanced(items) {
|
|
941
|
+
const stack = [];
|
|
925
942
|
for (const item of items) {
|
|
926
|
-
if (item.type
|
|
927
|
-
|
|
943
|
+
if (item.type === "fork") {
|
|
944
|
+
if (!isBranchBalanced(item.truthy) || !isBranchBalanced(item.falsy)) {
|
|
945
|
+
return false;
|
|
946
|
+
}
|
|
947
|
+
} else if (item.type === "open") {
|
|
948
|
+
stack.push(item.tagName);
|
|
949
|
+
} else {
|
|
950
|
+
if (stack.length === 0 || stack[stack.length - 1] !== item.tagName) {
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
stack.pop();
|
|
954
|
+
}
|
|
928
955
|
}
|
|
929
|
-
return
|
|
956
|
+
return stack.length === 0;
|
|
930
957
|
}
|
|
931
958
|
function collectSectionNames(items) {
|
|
932
959
|
const names = /* @__PURE__ */ new Set();
|
|
933
960
|
for (const item of items) {
|
|
934
961
|
if (item.type === "fork") {
|
|
935
|
-
if (
|
|
962
|
+
if (!isBranchBalanced(item.truthy) || !isBranchBalanced(item.falsy)) {
|
|
936
963
|
names.add(item.sectionName);
|
|
937
964
|
}
|
|
938
965
|
for (const name of collectSectionNames(item.truthy)) names.add(name);
|
|
@@ -1573,6 +1600,281 @@ function checkDuplicateAttributes(rootNode) {
|
|
|
1573
1600
|
return errors;
|
|
1574
1601
|
}
|
|
1575
1602
|
|
|
1603
|
+
// lsp/server/src/selectorMatcher.ts
|
|
1604
|
+
function isNameChar(ch) {
|
|
1605
|
+
return /[a-zA-Z0-9\-_]/.test(ch);
|
|
1606
|
+
}
|
|
1607
|
+
function parseAttributes(raw, pos) {
|
|
1608
|
+
const attrs = [];
|
|
1609
|
+
while (pos.i < raw.length) {
|
|
1610
|
+
if (raw[pos.i] === ":") {
|
|
1611
|
+
if (raw.slice(pos.i, pos.i + 6).toLowerCase() !== ":not([") return attrs;
|
|
1612
|
+
pos.i += 6;
|
|
1613
|
+
const attr = parseOneAttribute(raw, pos, true);
|
|
1614
|
+
if (!attr) return attrs;
|
|
1615
|
+
if (raw[pos.i] !== "]" || raw[pos.i + 1] !== ")") return attrs;
|
|
1616
|
+
pos.i += 2;
|
|
1617
|
+
attrs.push(attr);
|
|
1618
|
+
} else if (raw[pos.i] === "[") {
|
|
1619
|
+
pos.i++;
|
|
1620
|
+
const attr = parseOneAttribute(raw, pos, false);
|
|
1621
|
+
if (!attr) return attrs;
|
|
1622
|
+
if (raw[pos.i] !== "]") return attrs;
|
|
1623
|
+
pos.i++;
|
|
1624
|
+
attrs.push(attr);
|
|
1625
|
+
} else {
|
|
1626
|
+
break;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
return attrs;
|
|
1630
|
+
}
|
|
1631
|
+
function parseOneAttribute(raw, pos, negated) {
|
|
1632
|
+
let name = "";
|
|
1633
|
+
while (pos.i < raw.length && isNameChar(raw[pos.i])) {
|
|
1634
|
+
name += raw[pos.i];
|
|
1635
|
+
pos.i++;
|
|
1636
|
+
}
|
|
1637
|
+
if (name.length === 0) return null;
|
|
1638
|
+
let value;
|
|
1639
|
+
if (raw[pos.i] === "=") {
|
|
1640
|
+
pos.i++;
|
|
1641
|
+
value = "";
|
|
1642
|
+
if (raw[pos.i] === '"' || raw[pos.i] === "'") {
|
|
1643
|
+
const quote = raw[pos.i];
|
|
1644
|
+
pos.i++;
|
|
1645
|
+
while (pos.i < raw.length && raw[pos.i] !== quote) {
|
|
1646
|
+
value += raw[pos.i];
|
|
1647
|
+
pos.i++;
|
|
1648
|
+
}
|
|
1649
|
+
if (pos.i < raw.length) pos.i++;
|
|
1650
|
+
} else {
|
|
1651
|
+
while (pos.i < raw.length && raw[pos.i] !== "]") {
|
|
1652
|
+
value += raw[pos.i];
|
|
1653
|
+
pos.i++;
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
return { name: name.toLowerCase(), value, negated };
|
|
1658
|
+
}
|
|
1659
|
+
function parseSelector(raw) {
|
|
1660
|
+
const trimmed = raw.trim();
|
|
1661
|
+
if (trimmed.length === 0) return null;
|
|
1662
|
+
const parts = trimmed.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1663
|
+
if (parts.length === 0) return null;
|
|
1664
|
+
const alternatives = [];
|
|
1665
|
+
for (const part of parts) {
|
|
1666
|
+
const alt = parseSingleSelector(part);
|
|
1667
|
+
if (!alt) return null;
|
|
1668
|
+
alternatives.push(alt);
|
|
1669
|
+
}
|
|
1670
|
+
return { alternatives };
|
|
1671
|
+
}
|
|
1672
|
+
function parseSingleSelector(raw) {
|
|
1673
|
+
const segments = [];
|
|
1674
|
+
let i = 0;
|
|
1675
|
+
let nextCombinator = "descendant";
|
|
1676
|
+
while (i < raw.length) {
|
|
1677
|
+
while (i < raw.length && raw[i] === " ") i++;
|
|
1678
|
+
if (i >= raw.length) break;
|
|
1679
|
+
if (raw[i] === ">") {
|
|
1680
|
+
if (segments.length === 0) return null;
|
|
1681
|
+
nextCombinator = "child";
|
|
1682
|
+
i++;
|
|
1683
|
+
while (i < raw.length && raw[i] === " ") i++;
|
|
1684
|
+
if (i >= raw.length) return null;
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
const pos = { i };
|
|
1688
|
+
const segment = parseOneSegment(raw, pos);
|
|
1689
|
+
if (!segment) return null;
|
|
1690
|
+
i = pos.i;
|
|
1691
|
+
segment.combinator = nextCombinator;
|
|
1692
|
+
nextCombinator = "descendant";
|
|
1693
|
+
segments.push(segment);
|
|
1694
|
+
}
|
|
1695
|
+
if (segments.length === 0) return null;
|
|
1696
|
+
return { segments };
|
|
1697
|
+
}
|
|
1698
|
+
function parseOneSegment(raw, pos) {
|
|
1699
|
+
let kind;
|
|
1700
|
+
let name;
|
|
1701
|
+
if (raw[pos.i] === "#") {
|
|
1702
|
+
kind = "mustache";
|
|
1703
|
+
pos.i++;
|
|
1704
|
+
name = "";
|
|
1705
|
+
while (pos.i < raw.length && isNameChar(raw[pos.i])) {
|
|
1706
|
+
name += raw[pos.i];
|
|
1707
|
+
pos.i++;
|
|
1708
|
+
}
|
|
1709
|
+
if (name.length === 0) name = null;
|
|
1710
|
+
else name = name.toLowerCase();
|
|
1711
|
+
return { kind, name, attributes: [], combinator: "descendant" };
|
|
1712
|
+
}
|
|
1713
|
+
if (raw[pos.i] === "*") {
|
|
1714
|
+
kind = "html";
|
|
1715
|
+
name = null;
|
|
1716
|
+
pos.i++;
|
|
1717
|
+
const attrs2 = parseAttributes(raw, pos);
|
|
1718
|
+
return { kind, name, attributes: attrs2, combinator: "descendant" };
|
|
1719
|
+
}
|
|
1720
|
+
if (raw[pos.i] === "[" || raw[pos.i] === ":") {
|
|
1721
|
+
kind = "html";
|
|
1722
|
+
name = null;
|
|
1723
|
+
const attrs2 = parseAttributes(raw, pos);
|
|
1724
|
+
if (attrs2.length === 0) return null;
|
|
1725
|
+
return { kind, name, attributes: attrs2, combinator: "descendant" };
|
|
1726
|
+
}
|
|
1727
|
+
if (!isNameChar(raw[pos.i])) return null;
|
|
1728
|
+
kind = "html";
|
|
1729
|
+
name = "";
|
|
1730
|
+
while (pos.i < raw.length && isNameChar(raw[pos.i])) {
|
|
1731
|
+
name += raw[pos.i];
|
|
1732
|
+
pos.i++;
|
|
1733
|
+
}
|
|
1734
|
+
name = name.toLowerCase();
|
|
1735
|
+
const attrs = parseAttributes(raw, pos);
|
|
1736
|
+
return { kind, name, attributes: attrs, combinator: "descendant" };
|
|
1737
|
+
}
|
|
1738
|
+
function getNodeAttributes(node) {
|
|
1739
|
+
const startTag = node.children.find(
|
|
1740
|
+
(c) => c.type === "html_start_tag" || c.type === "html_self_closing_tag"
|
|
1741
|
+
);
|
|
1742
|
+
if (!startTag) return [];
|
|
1743
|
+
const attrs = [];
|
|
1744
|
+
for (const child of startTag.children) {
|
|
1745
|
+
if (child.type === "html_attribute") {
|
|
1746
|
+
let attrName = "";
|
|
1747
|
+
let attrValue;
|
|
1748
|
+
for (const part of child.children) {
|
|
1749
|
+
if (part.type === "html_attribute_name") {
|
|
1750
|
+
attrName = part.text.toLowerCase();
|
|
1751
|
+
} else if (part.type === "html_quoted_attribute_value") {
|
|
1752
|
+
attrValue = part.text.replace(/^["']|["']$/g, "");
|
|
1753
|
+
} else if (part.type === "html_attribute_value") {
|
|
1754
|
+
attrValue = part.text;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
if (attrName) attrs.push({ name: attrName, value: attrValue });
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
return attrs;
|
|
1761
|
+
}
|
|
1762
|
+
function checkAttributes(node, constraints) {
|
|
1763
|
+
if (constraints.length === 0) return true;
|
|
1764
|
+
const nodeAttrs = getNodeAttributes(node);
|
|
1765
|
+
for (const constraint of constraints) {
|
|
1766
|
+
const found = nodeAttrs.find((a) => a.name === constraint.name);
|
|
1767
|
+
if (constraint.negated) {
|
|
1768
|
+
if (found) return false;
|
|
1769
|
+
} else if (constraint.value !== void 0) {
|
|
1770
|
+
if (!found || found.value !== constraint.value) return false;
|
|
1771
|
+
} else {
|
|
1772
|
+
if (!found) return false;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
return true;
|
|
1776
|
+
}
|
|
1777
|
+
function nodeMatchesSegment(node, segment) {
|
|
1778
|
+
if (segment.kind === "html") {
|
|
1779
|
+
if (!HTML_ELEMENT_TYPES.has(node.type)) return false;
|
|
1780
|
+
if (segment.name !== null) {
|
|
1781
|
+
const tagName = getTagName(node)?.toLowerCase();
|
|
1782
|
+
if (tagName !== segment.name) return false;
|
|
1783
|
+
}
|
|
1784
|
+
return checkAttributes(node, segment.attributes);
|
|
1785
|
+
}
|
|
1786
|
+
if (!MUSTACHE_SECTION_TYPES.has(node.type)) return false;
|
|
1787
|
+
if (segment.name !== null) {
|
|
1788
|
+
const sectionName = getSectionName(node)?.toLowerCase();
|
|
1789
|
+
if (sectionName !== segment.name) return false;
|
|
1790
|
+
}
|
|
1791
|
+
return true;
|
|
1792
|
+
}
|
|
1793
|
+
function checkAncestors(ancestors, segments, segIdx, childCombinator) {
|
|
1794
|
+
if (segIdx < 0) return true;
|
|
1795
|
+
const segment = segments[segIdx];
|
|
1796
|
+
if (childCombinator === "child") {
|
|
1797
|
+
for (let a = ancestors.length - 1; a >= 0; a--) {
|
|
1798
|
+
const entry = ancestors[a];
|
|
1799
|
+
if (entry.kind !== segment.kind) continue;
|
|
1800
|
+
if (segment.name !== null && entry.name !== segment.name) return false;
|
|
1801
|
+
if (segment.kind === "html" && segment.attributes.length > 0) {
|
|
1802
|
+
if (!checkAttributes(entry.node, segment.attributes)) return false;
|
|
1803
|
+
}
|
|
1804
|
+
return checkAncestors(ancestors.slice(0, a), segments, segIdx - 1, segment.combinator);
|
|
1805
|
+
}
|
|
1806
|
+
return false;
|
|
1807
|
+
}
|
|
1808
|
+
for (let a = ancestors.length - 1; a >= 0; a--) {
|
|
1809
|
+
const entry = ancestors[a];
|
|
1810
|
+
if (entry.kind !== segment.kind) continue;
|
|
1811
|
+
if (segment.name !== null && entry.name !== segment.name) continue;
|
|
1812
|
+
if (segment.kind === "html" && segment.attributes.length > 0) {
|
|
1813
|
+
if (!checkAttributes(entry.node, segment.attributes)) continue;
|
|
1814
|
+
}
|
|
1815
|
+
if (checkAncestors(ancestors.slice(0, a), segments, segIdx - 1, segment.combinator)) {
|
|
1816
|
+
return true;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
return false;
|
|
1820
|
+
}
|
|
1821
|
+
function getReportNode(node) {
|
|
1822
|
+
if (HTML_ELEMENT_TYPES.has(node.type)) {
|
|
1823
|
+
const startTag = node.children.find(
|
|
1824
|
+
(c) => c.type === "html_start_tag" || c.type === "html_self_closing_tag"
|
|
1825
|
+
);
|
|
1826
|
+
return startTag ?? node;
|
|
1827
|
+
}
|
|
1828
|
+
if (MUSTACHE_SECTION_TYPES.has(node.type)) {
|
|
1829
|
+
const begin = node.children.find(
|
|
1830
|
+
(c) => c.type === "mustache_section_begin" || c.type === "mustache_inverted_section_begin"
|
|
1831
|
+
);
|
|
1832
|
+
return begin ?? node;
|
|
1833
|
+
}
|
|
1834
|
+
return node;
|
|
1835
|
+
}
|
|
1836
|
+
function matchAlternative(rootNode, alt) {
|
|
1837
|
+
const results = [];
|
|
1838
|
+
const lastSegment = alt.segments[alt.segments.length - 1];
|
|
1839
|
+
function walk(node, ancestors) {
|
|
1840
|
+
if (nodeMatchesSegment(node, lastSegment)) {
|
|
1841
|
+
if (alt.segments.length === 1 || checkAncestors(ancestors, alt.segments, alt.segments.length - 2, lastSegment.combinator)) {
|
|
1842
|
+
results.push(getReportNode(node));
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
let newAncestors = ancestors;
|
|
1846
|
+
if (HTML_ELEMENT_TYPES.has(node.type)) {
|
|
1847
|
+
const tagName = getTagName(node)?.toLowerCase();
|
|
1848
|
+
if (tagName) {
|
|
1849
|
+
newAncestors = [...ancestors, { kind: "html", name: tagName, node }];
|
|
1850
|
+
}
|
|
1851
|
+
} else if (MUSTACHE_SECTION_TYPES.has(node.type)) {
|
|
1852
|
+
const sectionName = getSectionName(node)?.toLowerCase();
|
|
1853
|
+
if (sectionName) {
|
|
1854
|
+
newAncestors = [...ancestors, { kind: "mustache", name: sectionName, node }];
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
for (const child of node.children) {
|
|
1858
|
+
walk(child, newAncestors);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
walk(rootNode, []);
|
|
1862
|
+
return results;
|
|
1863
|
+
}
|
|
1864
|
+
function matchSelector(rootNode, selector) {
|
|
1865
|
+
const allResults = [];
|
|
1866
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1867
|
+
for (const alt of selector.alternatives) {
|
|
1868
|
+
for (const node of matchAlternative(rootNode, alt)) {
|
|
1869
|
+
if (!seen.has(node)) {
|
|
1870
|
+
seen.add(node);
|
|
1871
|
+
allResults.push(node);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
return allResults;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1576
1878
|
// lsp/server/src/collectErrors.ts
|
|
1577
1879
|
var ERROR_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
1578
1880
|
"ERROR",
|
|
@@ -1592,7 +1894,7 @@ function errorMessageForNode(nodeType, node) {
|
|
|
1592
1894
|
function resolveRuleSeverity(rules, ruleName) {
|
|
1593
1895
|
return rules?.[ruleName] ?? RULE_DEFAULTS[ruleName] ?? "off";
|
|
1594
1896
|
}
|
|
1595
|
-
function parseDisableDirective(node) {
|
|
1897
|
+
function parseDisableDirective(node, customRuleIds) {
|
|
1596
1898
|
if (node.type !== "html_comment" && node.type !== "mustache_comment") return null;
|
|
1597
1899
|
let inner = null;
|
|
1598
1900
|
if (node.type === "html_comment") {
|
|
@@ -1606,12 +1908,14 @@ function parseDisableDirective(node) {
|
|
|
1606
1908
|
const prefix = "htmlmustache-disable ";
|
|
1607
1909
|
if (!inner.startsWith(prefix)) return null;
|
|
1608
1910
|
const ruleName = inner.slice(prefix.length).trim();
|
|
1609
|
-
|
|
1911
|
+
if (KNOWN_RULE_NAMES.has(ruleName)) return ruleName;
|
|
1912
|
+
if (customRuleIds?.has(ruleName)) return ruleName;
|
|
1913
|
+
return null;
|
|
1610
1914
|
}
|
|
1611
|
-
function collectDisabledRules(rootNode) {
|
|
1915
|
+
function collectDisabledRules(rootNode, customRuleIds) {
|
|
1612
1916
|
const disabled = /* @__PURE__ */ new Set();
|
|
1613
1917
|
function walk(node) {
|
|
1614
|
-
const rule = parseDisableDirective(node);
|
|
1918
|
+
const rule = parseDisableDirective(node, customRuleIds);
|
|
1615
1919
|
if (rule) {
|
|
1616
1920
|
disabled.add(rule);
|
|
1617
1921
|
return;
|
|
@@ -1621,7 +1925,7 @@ function collectDisabledRules(rootNode) {
|
|
|
1621
1925
|
walk(rootNode);
|
|
1622
1926
|
return disabled;
|
|
1623
1927
|
}
|
|
1624
|
-
function collectErrors(tree, rules, customTagNames) {
|
|
1928
|
+
function collectErrors(tree, rules, customTagNames, customRules) {
|
|
1625
1929
|
const errors = [];
|
|
1626
1930
|
const cursor = tree.walk();
|
|
1627
1931
|
function visit() {
|
|
@@ -1650,7 +1954,8 @@ function collectErrors(tree, rules, customTagNames) {
|
|
|
1650
1954
|
for (const error of unclosedErrors) {
|
|
1651
1955
|
errors.push({ node: error.node, message: error.message });
|
|
1652
1956
|
}
|
|
1653
|
-
const
|
|
1957
|
+
const customRuleIds = customRules ? new Set(customRules.map((r) => r.id)) : void 0;
|
|
1958
|
+
const disabledRules = collectDisabledRules(tree.rootNode, customRuleIds);
|
|
1654
1959
|
const effectiveRules = { ...rules };
|
|
1655
1960
|
for (const rule of disabledRules) {
|
|
1656
1961
|
effectiveRules[rule] = "off";
|
|
@@ -1675,18 +1980,32 @@ function collectErrors(tree, rules, customTagNames) {
|
|
|
1675
1980
|
message: error.message,
|
|
1676
1981
|
severity,
|
|
1677
1982
|
fix: error.fix,
|
|
1678
|
-
fixDescription: error.fixDescription
|
|
1983
|
+
fixDescription: error.fixDescription,
|
|
1984
|
+
ruleName: rule
|
|
1679
1985
|
});
|
|
1680
1986
|
}
|
|
1681
1987
|
}
|
|
1988
|
+
if (customRules) {
|
|
1989
|
+
for (const rule of customRules) {
|
|
1990
|
+
if (disabledRules.has(rule.id)) continue;
|
|
1991
|
+
const severity = rule.severity ?? "error";
|
|
1992
|
+
if (severity === "off") continue;
|
|
1993
|
+
const parsed = parseSelector(rule.selector);
|
|
1994
|
+
if (!parsed) continue;
|
|
1995
|
+
const matches = matchSelector(tree.rootNode, parsed);
|
|
1996
|
+
for (const node of matches) {
|
|
1997
|
+
errors.push({ node, message: rule.message, severity, ruleName: rule.id });
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
1682
2001
|
return errors.filter(
|
|
1683
|
-
(e) => !(e.message.includes("HTML comment found") && parseDisableDirective(e.node) !== null)
|
|
2002
|
+
(e) => !(e.message.includes("HTML comment found") && parseDisableDirective(e.node, customRuleIds) !== null)
|
|
1684
2003
|
);
|
|
1685
2004
|
}
|
|
1686
2005
|
|
|
1687
2006
|
// cli/src/check.ts
|
|
1688
|
-
function collectErrors2(tree, file, rules, customTagNames) {
|
|
1689
|
-
const errors = collectErrors(tree, rules, customTagNames);
|
|
2007
|
+
function collectErrors2(tree, file, rules, customTagNames, customRules) {
|
|
2008
|
+
const errors = collectErrors(tree, rules, customTagNames, customRules);
|
|
1690
2009
|
return errors.map((error) => ({
|
|
1691
2010
|
file,
|
|
1692
2011
|
line: error.node.startPosition.row + 1,
|
|
@@ -1871,12 +2190,13 @@ async function run(args) {
|
|
|
1871
2190
|
const errorOutput = [];
|
|
1872
2191
|
const rules = config?.rules;
|
|
1873
2192
|
const customTagNames = config?.customTags?.map((t) => t.name);
|
|
2193
|
+
const customRules = config?.customRules;
|
|
1874
2194
|
for (const file of files) {
|
|
1875
2195
|
const displayPath = import_node_path.default.relative(cwd, file) || file;
|
|
1876
2196
|
let source = import_node_fs.default.readFileSync(file, "utf-8");
|
|
1877
2197
|
if (fixMode) {
|
|
1878
2198
|
const tree2 = parseDocument(source);
|
|
1879
|
-
const errors2 = collectErrors2(tree2, displayPath, rules, customTagNames);
|
|
2199
|
+
const errors2 = collectErrors2(tree2, displayPath, rules, customTagNames, customRules);
|
|
1880
2200
|
const fixed = applyFixes(source, errors2);
|
|
1881
2201
|
if (fixed !== source) {
|
|
1882
2202
|
import_node_fs.default.writeFileSync(file, fixed, "utf-8");
|
|
@@ -1884,7 +2204,7 @@ async function run(args) {
|
|
|
1884
2204
|
}
|
|
1885
2205
|
}
|
|
1886
2206
|
const tree = parseDocument(source);
|
|
1887
|
-
const errors = collectErrors2(tree, displayPath, rules, customTagNames);
|
|
2207
|
+
const errors = collectErrors2(tree, displayPath, rules, customTagNames, customRules);
|
|
1888
2208
|
const fileErrors = errors.filter((e) => e.severity !== "warning");
|
|
1889
2209
|
const fileWarnings = errors.filter((e) => e.severity === "warning");
|
|
1890
2210
|
if (errors.length > 0) {
|