@reteps/tree-sitter-htmlmustache 0.5.2 → 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 +349 -25
  2. package/package.json +1 -1
package/cli/out/main.js CHANGED
@@ -708,9 +708,12 @@ function validateConfig(raw) {
708
708
  config.mustacheSpaces = obj.mustacheSpaces;
709
709
  }
710
710
  if (Array.isArray(obj.noBreakDelimiters)) {
711
- const items = obj.noBreakDelimiters.filter(
712
- (s) => typeof s === "string" && s.length > 0
713
- );
711
+ const items = [];
712
+ for (const entry of obj.noBreakDelimiters) {
713
+ if (entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.start === "string" && entry.start !== "" && typeof entry.end === "string" && entry.end !== "") {
714
+ items.push({ start: entry.start, end: entry.end });
715
+ }
716
+ }
714
717
  if (items.length > 0) config.noBreakDelimiters = items;
715
718
  }
716
719
  if (Array.isArray(obj.include)) {
@@ -745,6 +748,22 @@ function validateConfig(raw) {
745
748
  }
746
749
  if (hasRules) config.rules = rules;
747
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
+ }
748
767
  return config;
749
768
  }
750
769
  function loadConfigFileForPath(filePath) {
@@ -918,18 +937,29 @@ function mergeAdjacentForks(items) {
918
937
  }
919
938
  return result;
920
939
  }
921
- function hasTagEvents(items) {
940
+ function isBranchBalanced(items) {
941
+ const stack = [];
922
942
  for (const item of items) {
923
- if (item.type !== "fork") return true;
924
- 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
+ }
925
955
  }
926
- return false;
956
+ return stack.length === 0;
927
957
  }
928
958
  function collectSectionNames(items) {
929
959
  const names = /* @__PURE__ */ new Set();
930
960
  for (const item of items) {
931
961
  if (item.type === "fork") {
932
- if (hasTagEvents(item.truthy) || hasTagEvents(item.falsy)) {
962
+ if (!isBranchBalanced(item.truthy) || !isBranchBalanced(item.falsy)) {
933
963
  names.add(item.sectionName);
934
964
  }
935
965
  for (const name of collectSectionNames(item.truthy)) names.add(name);
@@ -1570,6 +1600,281 @@ function checkDuplicateAttributes(rootNode) {
1570
1600
  return errors;
1571
1601
  }
1572
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
+
1573
1878
  // lsp/server/src/collectErrors.ts
1574
1879
  var ERROR_NODE_TYPES = /* @__PURE__ */ new Set([
1575
1880
  "ERROR",
@@ -1589,7 +1894,7 @@ function errorMessageForNode(nodeType, node) {
1589
1894
  function resolveRuleSeverity(rules, ruleName) {
1590
1895
  return rules?.[ruleName] ?? RULE_DEFAULTS[ruleName] ?? "off";
1591
1896
  }
1592
- function parseDisableDirective(node) {
1897
+ function parseDisableDirective(node, customRuleIds) {
1593
1898
  if (node.type !== "html_comment" && node.type !== "mustache_comment") return null;
1594
1899
  let inner = null;
1595
1900
  if (node.type === "html_comment") {
@@ -1603,12 +1908,14 @@ function parseDisableDirective(node) {
1603
1908
  const prefix = "htmlmustache-disable ";
1604
1909
  if (!inner.startsWith(prefix)) return null;
1605
1910
  const ruleName = inner.slice(prefix.length).trim();
1606
- 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;
1607
1914
  }
1608
- function collectDisabledRules(rootNode) {
1915
+ function collectDisabledRules(rootNode, customRuleIds) {
1609
1916
  const disabled = /* @__PURE__ */ new Set();
1610
1917
  function walk(node) {
1611
- const rule = parseDisableDirective(node);
1918
+ const rule = parseDisableDirective(node, customRuleIds);
1612
1919
  if (rule) {
1613
1920
  disabled.add(rule);
1614
1921
  return;
@@ -1618,7 +1925,7 @@ function collectDisabledRules(rootNode) {
1618
1925
  walk(rootNode);
1619
1926
  return disabled;
1620
1927
  }
1621
- function collectErrors(tree, rules, customTagNames) {
1928
+ function collectErrors(tree, rules, customTagNames, customRules) {
1622
1929
  const errors = [];
1623
1930
  const cursor = tree.walk();
1624
1931
  function visit() {
@@ -1647,7 +1954,8 @@ function collectErrors(tree, rules, customTagNames) {
1647
1954
  for (const error of unclosedErrors) {
1648
1955
  errors.push({ node: error.node, message: error.message });
1649
1956
  }
1650
- 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);
1651
1959
  const effectiveRules = { ...rules };
1652
1960
  for (const rule of disabledRules) {
1653
1961
  effectiveRules[rule] = "off";
@@ -1676,14 +1984,27 @@ function collectErrors(tree, rules, customTagNames) {
1676
1984
  });
1677
1985
  }
1678
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
+ }
1679
2000
  return errors.filter(
1680
- (e) => !(e.message.includes("HTML comment found") && parseDisableDirective(e.node) !== null)
2001
+ (e) => !(e.message.includes("HTML comment found") && parseDisableDirective(e.node, customRuleIds) !== null)
1681
2002
  );
1682
2003
  }
1683
2004
 
1684
2005
  // cli/src/check.ts
1685
- function collectErrors2(tree, file, rules, customTagNames) {
1686
- const errors = collectErrors(tree, rules, customTagNames);
2006
+ function collectErrors2(tree, file, rules, customTagNames, customRules) {
2007
+ const errors = collectErrors(tree, rules, customTagNames, customRules);
1687
2008
  return errors.map((error) => ({
1688
2009
  file,
1689
2010
  line: error.node.startPosition.row + 1,
@@ -1868,12 +2189,13 @@ async function run(args) {
1868
2189
  const errorOutput = [];
1869
2190
  const rules = config?.rules;
1870
2191
  const customTagNames = config?.customTags?.map((t) => t.name);
2192
+ const customRules = config?.customRules;
1871
2193
  for (const file of files) {
1872
2194
  const displayPath = import_node_path.default.relative(cwd, file) || file;
1873
2195
  let source = import_node_fs.default.readFileSync(file, "utf-8");
1874
2196
  if (fixMode) {
1875
2197
  const tree2 = parseDocument(source);
1876
- const errors2 = collectErrors2(tree2, displayPath, rules, customTagNames);
2198
+ const errors2 = collectErrors2(tree2, displayPath, rules, customTagNames, customRules);
1877
2199
  const fixed = applyFixes(source, errors2);
1878
2200
  if (fixed !== source) {
1879
2201
  import_node_fs.default.writeFileSync(file, fixed, "utf-8");
@@ -1881,7 +2203,7 @@ async function run(args) {
1881
2203
  }
1882
2204
  }
1883
2205
  const tree = parseDocument(source);
1884
- const errors = collectErrors2(tree, displayPath, rules, customTagNames);
2206
+ const errors = collectErrors2(tree, displayPath, rules, customTagNames, customRules);
1885
2207
  const fileErrors = errors.filter((e) => e.severity !== "warning");
1886
2208
  const fileWarnings = errors.filter((e) => e.severity === "warning");
1887
2209
  if (errors.length > 0) {
@@ -3280,7 +3602,9 @@ function textWords(str) {
3280
3602
  }
3281
3603
  function collapseDelimitedRegions(parts, delimiters) {
3282
3604
  if (delimiters.length === 0) return parts;
3283
- const sorted = [...delimiters].sort((a, b) => b.length - a.length);
3605
+ const sorted = [...delimiters].sort(
3606
+ (a, b) => Math.max(b.start.length, b.end.length) - Math.max(a.start.length, a.end.length)
3607
+ );
3284
3608
  const result = [...parts];
3285
3609
  let activeDelimiter = null;
3286
3610
  for (let i = 0; i < result.length; i++) {
@@ -3288,10 +3612,10 @@ function collapseDelimitedRegions(parts, delimiters) {
3288
3612
  if (typeof part === "string") {
3289
3613
  if (activeDelimiter === null) {
3290
3614
  for (const delim of sorted) {
3291
- const delimIdx = part.indexOf(delim);
3292
- if (delimIdx >= 0) {
3293
- const afterOpen = delimIdx + delim.length;
3294
- const closeIdx = part.indexOf(delim, afterOpen);
3615
+ const startIdx = part.indexOf(delim.start);
3616
+ if (startIdx >= 0) {
3617
+ const afterOpen = startIdx + delim.start.length;
3618
+ const closeIdx = part.indexOf(delim.end, afterOpen);
3295
3619
  if (closeIdx >= 0) {
3296
3620
  continue;
3297
3621
  }
@@ -3300,7 +3624,7 @@ function collapseDelimitedRegions(parts, delimiters) {
3300
3624
  }
3301
3625
  }
3302
3626
  } else {
3303
- if (part.includes(activeDelimiter)) {
3627
+ if (part.includes(activeDelimiter.end)) {
3304
3628
  activeDelimiter = null;
3305
3629
  }
3306
3630
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reteps/tree-sitter-htmlmustache",
3
- "version": "0.5.2",
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",