@reteps/tree-sitter-htmlmustache 0.0.38 → 0.0.40
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 +12 -9
- package/cli/out/main.js +234 -9
- package/grammar.js +4 -2
- package/package.json +10 -4
- package/src/grammar.json +10 -2
- package/src/node-types.json +4 -0
- package/src/parser.c +3696 -3571
- package/tree-sitter-htmlmustache.wasm +0 -0
package/README.md
CHANGED
|
@@ -145,14 +145,17 @@ Place a comment immediately before the element to preserve its original formatti
|
|
|
145
145
|
|
|
146
146
|
```html
|
|
147
147
|
<!-- htmlmustache-ignore -->
|
|
148
|
-
<div
|
|
149
|
-
manually formatted
|
|
150
|
-
</div>
|
|
148
|
+
<div class="a" id="b">manually formatted</div>
|
|
151
149
|
```
|
|
152
150
|
|
|
153
151
|
```html
|
|
154
152
|
{{! htmlmustache-ignore }}
|
|
155
|
-
<table
|
|
153
|
+
<table>
|
|
154
|
+
<tr>
|
|
155
|
+
<td>compact</td>
|
|
156
|
+
<td>table</td>
|
|
157
|
+
</tr>
|
|
158
|
+
</table>
|
|
156
159
|
```
|
|
157
160
|
|
|
158
161
|
Only the immediately following sibling node is ignored. Subsequent nodes are formatted normally.
|
|
@@ -163,15 +166,15 @@ Wrap a region in start/end comments to preserve everything between them:
|
|
|
163
166
|
|
|
164
167
|
```html
|
|
165
168
|
<!-- htmlmustache-ignore-start -->
|
|
166
|
-
<div
|
|
167
|
-
<p>
|
|
169
|
+
<div class="a">content</div>
|
|
170
|
+
<p>kept as-is</p>
|
|
168
171
|
<!-- htmlmustache-ignore-end -->
|
|
169
172
|
```
|
|
170
173
|
|
|
171
174
|
```html
|
|
172
|
-
{{! htmlmustache-ignore-start }}
|
|
173
|
-
|
|
174
|
-
{{! htmlmustache-ignore-end }}
|
|
175
|
+
{{! htmlmustache-ignore-start }} {{#items}}
|
|
176
|
+
<li>{{name}}</li>
|
|
177
|
+
{{/items}} {{! htmlmustache-ignore-end }}
|
|
175
178
|
```
|
|
176
179
|
|
|
177
180
|
If `ignore-start` has no matching `ignore-end`, all remaining siblings in the current scope are preserved as raw text.
|
package/cli/out/main.js
CHANGED
|
@@ -1715,16 +1715,229 @@ function loadConfigFileForPath(filePath) {
|
|
|
1715
1715
|
}
|
|
1716
1716
|
}
|
|
1717
1717
|
|
|
1718
|
+
// lsp/server/src/htmlBalanceChecker.ts
|
|
1719
|
+
function getTagName(element) {
|
|
1720
|
+
const startTag = element.children.find((c) => c.type === "html_start_tag");
|
|
1721
|
+
if (!startTag) return null;
|
|
1722
|
+
const tagNameNode = startTag.children.find((c) => c.type === "html_tag_name");
|
|
1723
|
+
return tagNameNode?.text?.toLowerCase() ?? null;
|
|
1724
|
+
}
|
|
1725
|
+
function getErroneousEndTagName(node) {
|
|
1726
|
+
const nameNode = node.children.find((c) => c.type === "html_erroneous_end_tag_name");
|
|
1727
|
+
return nameNode?.text?.toLowerCase() ?? null;
|
|
1728
|
+
}
|
|
1729
|
+
function getSectionName(node) {
|
|
1730
|
+
const beginNode = node.children.find(
|
|
1731
|
+
(c) => c.type === "mustache_section_begin" || c.type === "mustache_inverted_section_begin"
|
|
1732
|
+
);
|
|
1733
|
+
if (!beginNode) return null;
|
|
1734
|
+
const tagNameNode = beginNode.children.find((c) => c.type === "mustache_tag_name");
|
|
1735
|
+
return tagNameNode?.text ?? null;
|
|
1736
|
+
}
|
|
1737
|
+
function hasForcedEndTag(element) {
|
|
1738
|
+
return element.children.some((c) => c.type === "html_forced_end_tag");
|
|
1739
|
+
}
|
|
1740
|
+
function extractFromNodes(nodes) {
|
|
1741
|
+
const items = [];
|
|
1742
|
+
for (const node of nodes) {
|
|
1743
|
+
items.push(...extractFromNode(node));
|
|
1744
|
+
}
|
|
1745
|
+
return items;
|
|
1746
|
+
}
|
|
1747
|
+
function extractFromNode(node) {
|
|
1748
|
+
if (node.type === "html_element") {
|
|
1749
|
+
const contentChildren = node.children.filter(
|
|
1750
|
+
(c) => c.type !== "html_start_tag" && c.type !== "html_end_tag" && c.type !== "html_forced_end_tag"
|
|
1751
|
+
);
|
|
1752
|
+
if (hasForcedEndTag(node)) {
|
|
1753
|
+
const tagName = getTagName(node);
|
|
1754
|
+
const items = [];
|
|
1755
|
+
if (tagName) {
|
|
1756
|
+
const startTag = node.children.find((c) => c.type === "html_start_tag");
|
|
1757
|
+
items.push({ type: "open", tagName, node: startTag ?? node });
|
|
1758
|
+
}
|
|
1759
|
+
items.push(...extractFromNodes(contentChildren));
|
|
1760
|
+
return items;
|
|
1761
|
+
}
|
|
1762
|
+
return extractFromNodes(contentChildren);
|
|
1763
|
+
}
|
|
1764
|
+
if (node.type === "html_self_closing_tag") {
|
|
1765
|
+
return [];
|
|
1766
|
+
}
|
|
1767
|
+
if (node.type === "html_erroneous_end_tag") {
|
|
1768
|
+
const tagName = getErroneousEndTagName(node);
|
|
1769
|
+
if (tagName) {
|
|
1770
|
+
return [{ type: "close", tagName, node }];
|
|
1771
|
+
}
|
|
1772
|
+
return [];
|
|
1773
|
+
}
|
|
1774
|
+
if (node.type === "mustache_section") {
|
|
1775
|
+
const sectionName = getSectionName(node);
|
|
1776
|
+
if (sectionName) {
|
|
1777
|
+
const contentChildren = node.children.filter(
|
|
1778
|
+
(c) => c.type !== "mustache_section_begin" && c.type !== "mustache_section_end" && c.type !== "mustache_erroneous_section_end"
|
|
1779
|
+
);
|
|
1780
|
+
return [
|
|
1781
|
+
{
|
|
1782
|
+
type: "fork",
|
|
1783
|
+
sectionName,
|
|
1784
|
+
truthy: extractFromNodes(contentChildren),
|
|
1785
|
+
falsy: []
|
|
1786
|
+
}
|
|
1787
|
+
];
|
|
1788
|
+
}
|
|
1789
|
+
return [];
|
|
1790
|
+
}
|
|
1791
|
+
if (node.type === "mustache_inverted_section") {
|
|
1792
|
+
const sectionName = getSectionName(node);
|
|
1793
|
+
if (sectionName) {
|
|
1794
|
+
const contentChildren = node.children.filter(
|
|
1795
|
+
(c) => c.type !== "mustache_inverted_section_begin" && c.type !== "mustache_inverted_section_end" && c.type !== "mustache_erroneous_inverted_section_end"
|
|
1796
|
+
);
|
|
1797
|
+
return [
|
|
1798
|
+
{
|
|
1799
|
+
type: "fork",
|
|
1800
|
+
sectionName,
|
|
1801
|
+
truthy: [],
|
|
1802
|
+
falsy: extractFromNodes(contentChildren)
|
|
1803
|
+
}
|
|
1804
|
+
];
|
|
1805
|
+
}
|
|
1806
|
+
return [];
|
|
1807
|
+
}
|
|
1808
|
+
return extractFromNodes(node.children);
|
|
1809
|
+
}
|
|
1810
|
+
function mergeAdjacentForks(items) {
|
|
1811
|
+
if (items.length === 0) return items;
|
|
1812
|
+
const result = [];
|
|
1813
|
+
let i = 0;
|
|
1814
|
+
while (i < items.length) {
|
|
1815
|
+
const item = items[i];
|
|
1816
|
+
if (item.type !== "fork") {
|
|
1817
|
+
result.push(item);
|
|
1818
|
+
i++;
|
|
1819
|
+
continue;
|
|
1820
|
+
}
|
|
1821
|
+
const truthy = [...item.truthy];
|
|
1822
|
+
const falsy = [...item.falsy];
|
|
1823
|
+
let j = i + 1;
|
|
1824
|
+
while (j < items.length) {
|
|
1825
|
+
const next = items[j];
|
|
1826
|
+
if (next.type !== "fork" || next.sectionName !== item.sectionName) break;
|
|
1827
|
+
truthy.push(...next.truthy);
|
|
1828
|
+
falsy.push(...next.falsy);
|
|
1829
|
+
j++;
|
|
1830
|
+
}
|
|
1831
|
+
result.push({
|
|
1832
|
+
type: "fork",
|
|
1833
|
+
sectionName: item.sectionName,
|
|
1834
|
+
truthy: mergeAdjacentForks(truthy),
|
|
1835
|
+
falsy: mergeAdjacentForks(falsy)
|
|
1836
|
+
});
|
|
1837
|
+
i = j;
|
|
1838
|
+
}
|
|
1839
|
+
return result;
|
|
1840
|
+
}
|
|
1841
|
+
function collectSectionNames(items) {
|
|
1842
|
+
const names = /* @__PURE__ */ new Set();
|
|
1843
|
+
for (const item of items) {
|
|
1844
|
+
if (item.type === "fork") {
|
|
1845
|
+
names.add(item.sectionName);
|
|
1846
|
+
for (const name of collectSectionNames(item.truthy)) names.add(name);
|
|
1847
|
+
for (const name of collectSectionNames(item.falsy)) names.add(name);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
return names;
|
|
1851
|
+
}
|
|
1852
|
+
function flattenPath(items, assignment) {
|
|
1853
|
+
const events = [];
|
|
1854
|
+
for (const item of items) {
|
|
1855
|
+
if (item.type === "fork") {
|
|
1856
|
+
const value = assignment.get(item.sectionName) ?? true;
|
|
1857
|
+
const branch = value ? item.truthy : item.falsy;
|
|
1858
|
+
events.push(...flattenPath(branch, assignment));
|
|
1859
|
+
} else {
|
|
1860
|
+
events.push(item);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
return events;
|
|
1864
|
+
}
|
|
1865
|
+
function formatCondition(assignment) {
|
|
1866
|
+
if (assignment.size === 0) return "";
|
|
1867
|
+
const parts = [];
|
|
1868
|
+
for (const [name, value] of assignment) {
|
|
1869
|
+
parts.push(`${name} is ${value ? "truthy" : "falsy"}`);
|
|
1870
|
+
}
|
|
1871
|
+
return ` (when ${parts.join(", ")})`;
|
|
1872
|
+
}
|
|
1873
|
+
function validateBalance(events, condition) {
|
|
1874
|
+
const errors = [];
|
|
1875
|
+
const stack = [];
|
|
1876
|
+
for (const event of events) {
|
|
1877
|
+
if (event.type === "open") {
|
|
1878
|
+
stack.push(event);
|
|
1879
|
+
} else {
|
|
1880
|
+
if (stack.length === 0) {
|
|
1881
|
+
errors.push({
|
|
1882
|
+
node: event.node,
|
|
1883
|
+
message: `Mismatched HTML end tag: </${event.tagName}>${condition}`
|
|
1884
|
+
});
|
|
1885
|
+
} else {
|
|
1886
|
+
const top = stack[stack.length - 1];
|
|
1887
|
+
if (top.tagName !== event.tagName) {
|
|
1888
|
+
errors.push({
|
|
1889
|
+
node: event.node,
|
|
1890
|
+
message: `Mismatched HTML end tag: </${event.tagName}>${condition}`
|
|
1891
|
+
});
|
|
1892
|
+
} else {
|
|
1893
|
+
stack.pop();
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
for (const event of stack) {
|
|
1899
|
+
errors.push({
|
|
1900
|
+
node: event.node,
|
|
1901
|
+
message: `Unclosed HTML tag: <${event.tagName}>${condition}`
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
return errors;
|
|
1905
|
+
}
|
|
1906
|
+
var MAX_SECTION_NAMES = 15;
|
|
1907
|
+
function checkHtmlBalance(rootNode) {
|
|
1908
|
+
const rawItems = extractFromNode(rootNode);
|
|
1909
|
+
const items = mergeAdjacentForks(rawItems);
|
|
1910
|
+
const sectionNames = [...collectSectionNames(items)];
|
|
1911
|
+
if (sectionNames.length > MAX_SECTION_NAMES) {
|
|
1912
|
+
return [];
|
|
1913
|
+
}
|
|
1914
|
+
const allErrors = [];
|
|
1915
|
+
const errorNodes = /* @__PURE__ */ new Set();
|
|
1916
|
+
const totalPaths = 1 << sectionNames.length;
|
|
1917
|
+
for (let mask = 0; mask < totalPaths; mask++) {
|
|
1918
|
+
const assignment = /* @__PURE__ */ new Map();
|
|
1919
|
+
for (let i = 0; i < sectionNames.length; i++) {
|
|
1920
|
+
assignment.set(sectionNames[i], (mask & 1 << i) !== 0);
|
|
1921
|
+
}
|
|
1922
|
+
const events = flattenPath(items, assignment);
|
|
1923
|
+
const condition = formatCondition(assignment);
|
|
1924
|
+
const pathErrors = validateBalance(events, condition);
|
|
1925
|
+
for (const error of pathErrors) {
|
|
1926
|
+
if (!errorNodes.has(error.node)) {
|
|
1927
|
+
errorNodes.add(error.node);
|
|
1928
|
+
allErrors.push(error);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
return allErrors;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1718
1935
|
// cli/src/check.ts
|
|
1719
1936
|
function errorMessageForNode(nodeType, node) {
|
|
1720
1937
|
if (nodeType === "mustache_erroneous_section_end" || nodeType === "mustache_erroneous_inverted_section_end") {
|
|
1721
1938
|
const tagNameNode = node.children.find((c) => c.type === "mustache_erroneous_tag_name");
|
|
1722
1939
|
return `Mismatched mustache section: {{/${tagNameNode?.text || "?"}}}`;
|
|
1723
1940
|
}
|
|
1724
|
-
if (nodeType === "html_erroneous_end_tag") {
|
|
1725
|
-
const tagNameNode = node.children.find((c) => c.type === "html_erroneous_end_tag_name");
|
|
1726
|
-
return `Mismatched HTML end tag: </${tagNameNode?.text || "?"}>`;
|
|
1727
|
-
}
|
|
1728
1941
|
if (nodeType === "ERROR") {
|
|
1729
1942
|
return "Syntax error";
|
|
1730
1943
|
}
|
|
@@ -1733,8 +1946,7 @@ function errorMessageForNode(nodeType, node) {
|
|
|
1733
1946
|
var ERROR_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
1734
1947
|
"ERROR",
|
|
1735
1948
|
"mustache_erroneous_section_end",
|
|
1736
|
-
"mustache_erroneous_inverted_section_end"
|
|
1737
|
-
"html_erroneous_end_tag"
|
|
1949
|
+
"mustache_erroneous_inverted_section_end"
|
|
1738
1950
|
]);
|
|
1739
1951
|
function collectErrors(tree, file) {
|
|
1740
1952
|
const errors = [];
|
|
@@ -1762,6 +1974,19 @@ function collectErrors(tree, file) {
|
|
|
1762
1974
|
}
|
|
1763
1975
|
}
|
|
1764
1976
|
visit();
|
|
1977
|
+
const rootNode = tree.rootNode;
|
|
1978
|
+
const balanceErrors = checkHtmlBalance(rootNode);
|
|
1979
|
+
for (const error of balanceErrors) {
|
|
1980
|
+
errors.push({
|
|
1981
|
+
file,
|
|
1982
|
+
line: error.node.startPosition.row + 1,
|
|
1983
|
+
column: error.node.startPosition.column + 1,
|
|
1984
|
+
endLine: error.node.endPosition.row + 1,
|
|
1985
|
+
endColumn: error.node.endPosition.column + 1,
|
|
1986
|
+
message: error.message,
|
|
1987
|
+
nodeText: error.node.text
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1765
1990
|
return errors;
|
|
1766
1991
|
}
|
|
1767
1992
|
function formatError(error, source) {
|
|
@@ -2343,7 +2568,7 @@ function isLine(doc) {
|
|
|
2343
2568
|
}
|
|
2344
2569
|
|
|
2345
2570
|
// lsp/server/src/formatting/utils.ts
|
|
2346
|
-
function
|
|
2571
|
+
function getTagName2(node) {
|
|
2347
2572
|
for (let i = 0; i < node.childCount; i++) {
|
|
2348
2573
|
const child = node.child(i);
|
|
2349
2574
|
if (!child) continue;
|
|
@@ -2531,7 +2756,7 @@ var PRESERVE_CONTENT_ELEMENTS = /* @__PURE__ */ new Set([
|
|
|
2531
2756
|
function getCSSDisplay(node) {
|
|
2532
2757
|
const type = node.type;
|
|
2533
2758
|
if (type === "html_element") {
|
|
2534
|
-
const tagName =
|
|
2759
|
+
const tagName = getTagName2(node);
|
|
2535
2760
|
if (tagName) {
|
|
2536
2761
|
if (customCodeTags.has(tagName.toLowerCase())) {
|
|
2537
2762
|
return "block";
|
|
@@ -2587,7 +2812,7 @@ function shouldPreserveContent(node) {
|
|
|
2587
2812
|
return true;
|
|
2588
2813
|
}
|
|
2589
2814
|
if (type === "html_element") {
|
|
2590
|
-
const tagName =
|
|
2815
|
+
const tagName = getTagName2(node);
|
|
2591
2816
|
if (!tagName) return false;
|
|
2592
2817
|
const lower = tagName.toLowerCase();
|
|
2593
2818
|
return PRESERVE_CONTENT_ELEMENTS.has(lower) || customCodeTags.has(lower);
|
package/grammar.js
CHANGED
|
@@ -281,12 +281,12 @@ module.exports = grammar({
|
|
|
281
281
|
_attribute_value_no_double_quote: ($) =>
|
|
282
282
|
choice(
|
|
283
283
|
$._mustache_node,
|
|
284
|
-
alias($.
|
|
284
|
+
alias($._html_attribute_value_no_double_quote, $.text),
|
|
285
285
|
),
|
|
286
286
|
_attribute_value_no_single_quote: ($) =>
|
|
287
287
|
choice(
|
|
288
288
|
$._mustache_node,
|
|
289
|
-
alias($.
|
|
289
|
+
alias($._html_attribute_value_no_single_quote, $.text),
|
|
290
290
|
),
|
|
291
291
|
_mustache_section_no_single_quote: ($) =>
|
|
292
292
|
seq(
|
|
@@ -370,6 +370,7 @@ module.exports = grammar({
|
|
|
370
370
|
),
|
|
371
371
|
_mustache_node_no_single_quote: ($) =>
|
|
372
372
|
choice(
|
|
373
|
+
$.mustache_triple,
|
|
373
374
|
$.mustache_interpolation,
|
|
374
375
|
alias($._mustache_comment_no_single_quote, $.mustache_comment),
|
|
375
376
|
alias($._mustache_partial_no_single_quote, $.mustache_partial),
|
|
@@ -381,6 +382,7 @@ module.exports = grammar({
|
|
|
381
382
|
),
|
|
382
383
|
_mustache_node_no_double_quote: ($) =>
|
|
383
384
|
choice(
|
|
385
|
+
$.mustache_triple,
|
|
384
386
|
$.mustache_interpolation,
|
|
385
387
|
alias($._mustache_comment_no_double_quote, $.mustache_comment),
|
|
386
388
|
alias($._mustache_partial_no_double_quote, $.mustache_partial),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reteps/tree-sitter-htmlmustache",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.40",
|
|
4
4
|
"description": "HTML with Mustache/Handlebars template syntax grammar for tree-sitter",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -46,9 +46,15 @@
|
|
|
46
46
|
"tree-sitter": "^0.25.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependenciesMeta": {
|
|
49
|
-
"node-addon-api": {
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
"node-addon-api": {
|
|
50
|
+
"optional": true
|
|
51
|
+
},
|
|
52
|
+
"node-gyp-build": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"tree-sitter": {
|
|
56
|
+
"optional": true
|
|
57
|
+
}
|
|
52
58
|
},
|
|
53
59
|
"devDependencies": {
|
|
54
60
|
"@eslint/js": "^9.39.2",
|
package/src/grammar.json
CHANGED
|
@@ -942,7 +942,7 @@
|
|
|
942
942
|
"type": "ALIAS",
|
|
943
943
|
"content": {
|
|
944
944
|
"type": "SYMBOL",
|
|
945
|
-
"name": "
|
|
945
|
+
"name": "_html_attribute_value_no_double_quote"
|
|
946
946
|
},
|
|
947
947
|
"named": true,
|
|
948
948
|
"value": "text"
|
|
@@ -960,7 +960,7 @@
|
|
|
960
960
|
"type": "ALIAS",
|
|
961
961
|
"content": {
|
|
962
962
|
"type": "SYMBOL",
|
|
963
|
-
"name": "
|
|
963
|
+
"name": "_html_attribute_value_no_single_quote"
|
|
964
964
|
},
|
|
965
965
|
"named": true,
|
|
966
966
|
"value": "text"
|
|
@@ -1158,6 +1158,10 @@
|
|
|
1158
1158
|
"_mustache_node_no_single_quote": {
|
|
1159
1159
|
"type": "CHOICE",
|
|
1160
1160
|
"members": [
|
|
1161
|
+
{
|
|
1162
|
+
"type": "SYMBOL",
|
|
1163
|
+
"name": "mustache_triple"
|
|
1164
|
+
},
|
|
1161
1165
|
{
|
|
1162
1166
|
"type": "SYMBOL",
|
|
1163
1167
|
"name": "mustache_interpolation"
|
|
@@ -1203,6 +1207,10 @@
|
|
|
1203
1207
|
"_mustache_node_no_double_quote": {
|
|
1204
1208
|
"type": "CHOICE",
|
|
1205
1209
|
"members": [
|
|
1210
|
+
{
|
|
1211
|
+
"type": "SYMBOL",
|
|
1212
|
+
"name": "mustache_triple"
|
|
1213
|
+
},
|
|
1206
1214
|
{
|
|
1207
1215
|
"type": "SYMBOL",
|
|
1208
1216
|
"name": "mustache_interpolation"
|