@reteps/tree-sitter-htmlmustache 0.6.0 → 0.7.0

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.
Files changed (2) hide show
  1. package/cli/out/main.js +335 -16
  2. package/package.json +1 -1
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 hasTagEvents(items) {
940
+ function isBranchBalanced(items) {
941
+ const stack = [];
925
942
  for (const item of items) {
926
- if (item.type !== "fork") return true;
927
- if (hasTagEvents(item.truthy) || hasTagEvents(item.falsy)) return true;
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 false;
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 (hasTagEvents(item.truthy) || hasTagEvents(item.falsy)) {
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
- return KNOWN_RULE_NAMES.has(ruleName) ? ruleName : null;
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 disabledRules = collectDisabledRules(tree.rootNode);
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";
@@ -1679,14 +1984,27 @@ function collectErrors(tree, rules, customTagNames) {
1679
1984
  });
1680
1985
  }
1681
1986
  }
1987
+ if (customRules) {
1988
+ for (const rule of customRules) {
1989
+ if (disabledRules.has(rule.id)) continue;
1990
+ const severity = rule.severity ?? "error";
1991
+ if (severity === "off") continue;
1992
+ const parsed = parseSelector(rule.selector);
1993
+ if (!parsed) continue;
1994
+ const matches = matchSelector(tree.rootNode, parsed);
1995
+ for (const node of matches) {
1996
+ errors.push({ node, message: rule.message, severity });
1997
+ }
1998
+ }
1999
+ }
1682
2000
  return errors.filter(
1683
- (e) => !(e.message.includes("HTML comment found") && parseDisableDirective(e.node) !== null)
2001
+ (e) => !(e.message.includes("HTML comment found") && parseDisableDirective(e.node, customRuleIds) !== null)
1684
2002
  );
1685
2003
  }
1686
2004
 
1687
2005
  // cli/src/check.ts
1688
- function collectErrors2(tree, file, rules, customTagNames) {
1689
- const errors = collectErrors(tree, rules, customTagNames);
2006
+ function collectErrors2(tree, file, rules, customTagNames, customRules) {
2007
+ const errors = collectErrors(tree, rules, customTagNames, customRules);
1690
2008
  return errors.map((error) => ({
1691
2009
  file,
1692
2010
  line: error.node.startPosition.row + 1,
@@ -1871,12 +2189,13 @@ async function run(args) {
1871
2189
  const errorOutput = [];
1872
2190
  const rules = config?.rules;
1873
2191
  const customTagNames = config?.customTags?.map((t) => t.name);
2192
+ const customRules = config?.customRules;
1874
2193
  for (const file of files) {
1875
2194
  const displayPath = import_node_path.default.relative(cwd, file) || file;
1876
2195
  let source = import_node_fs.default.readFileSync(file, "utf-8");
1877
2196
  if (fixMode) {
1878
2197
  const tree2 = parseDocument(source);
1879
- const errors2 = collectErrors2(tree2, displayPath, rules, customTagNames);
2198
+ const errors2 = collectErrors2(tree2, displayPath, rules, customTagNames, customRules);
1880
2199
  const fixed = applyFixes(source, errors2);
1881
2200
  if (fixed !== source) {
1882
2201
  import_node_fs.default.writeFileSync(file, fixed, "utf-8");
@@ -1884,7 +2203,7 @@ async function run(args) {
1884
2203
  }
1885
2204
  }
1886
2205
  const tree = parseDocument(source);
1887
- const errors = collectErrors2(tree, displayPath, rules, customTagNames);
2206
+ const errors = collectErrors2(tree, displayPath, rules, customTagNames, customRules);
1888
2207
  const fileErrors = errors.filter((e) => e.severity !== "warning");
1889
2208
  const fileWarnings = errors.filter((e) => e.severity === "warning");
1890
2209
  if (errors.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reteps/tree-sitter-htmlmustache",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "HTML with Mustache/Handlebars template syntax grammar for tree-sitter",
5
5
  "repository": {
6
6
  "type": "git",