@reteps/tree-sitter-htmlmustache 0.4.2 → 0.5.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.
- package/README.md +35 -0
- package/cli/out/main.js +128 -36
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -212,6 +212,41 @@ Create a `.htmlmustache.jsonc` file in your project root to configure formatting
|
|
|
212
212
|
}
|
|
213
213
|
```
|
|
214
214
|
|
|
215
|
+
### Lint Rules
|
|
216
|
+
|
|
217
|
+
The following checks are always enabled and report as errors:
|
|
218
|
+
|
|
219
|
+
- **Syntax errors** — invalid or unparseable template syntax
|
|
220
|
+
- **Missing tokens** — e.g. a missing closing `>`
|
|
221
|
+
- **Mismatched mustache sections** — `{{/wrong}}` closing a different section than was opened
|
|
222
|
+
- **Mismatched HTML tags** — closing tags that don't match their opening tag, including across mustache branches
|
|
223
|
+
- **Unclosed HTML tags** — non-void elements that are never closed
|
|
224
|
+
|
|
225
|
+
Additionally, the following rules are configurable. Set their severities (`"error"`, `"warning"`, or `"off"`) in the `rules` object:
|
|
226
|
+
|
|
227
|
+
```jsonc
|
|
228
|
+
{
|
|
229
|
+
"rules": {
|
|
230
|
+
"consecutiveDuplicateSections": "off",
|
|
231
|
+
"preferMustacheComments": "warning"
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
<!-- RULES_TABLE_START -->
|
|
237
|
+
|
|
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
|
+
|
|
248
|
+
<!-- RULES_TABLE_END -->
|
|
249
|
+
|
|
215
250
|
### EditorConfig
|
|
216
251
|
|
|
217
252
|
Both the CLI and VS Code extension respect your `.editorconfig` file for indentation settings (`indent_style`, `indent_size`). EditorConfig values override `.htmlmustache.jsonc` for indentation, and CLI flags override everything.
|
package/cli/out/main.js
CHANGED
|
@@ -544,6 +544,51 @@ function parseDocument(source) {
|
|
|
544
544
|
// lsp/server/src/configFile.ts
|
|
545
545
|
var fs = __toESM(require("fs"));
|
|
546
546
|
var path2 = __toESM(require("path"));
|
|
547
|
+
|
|
548
|
+
// lsp/server/src/ruleMetadata.ts
|
|
549
|
+
var RULES = [
|
|
550
|
+
{
|
|
551
|
+
name: "nestedDuplicateSections",
|
|
552
|
+
defaultSeverity: "error",
|
|
553
|
+
description: "Flags `{{#name}}` nested inside another `{{#name}}` with the same name"
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
name: "unquotedMustacheAttributes",
|
|
557
|
+
defaultSeverity: "error",
|
|
558
|
+
description: "Requires quotes around mustache expressions used as attribute values"
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
name: "consecutiveDuplicateSections",
|
|
562
|
+
defaultSeverity: "warning",
|
|
563
|
+
description: "Warns when adjacent same-name sections can be merged"
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
name: "selfClosingNonVoidTags",
|
|
567
|
+
defaultSeverity: "error",
|
|
568
|
+
description: "Disallows self-closing syntax on non-void HTML elements (e.g. `<div/>`)"
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
name: "duplicateAttributes",
|
|
572
|
+
defaultSeverity: "error",
|
|
573
|
+
description: "Detects duplicate HTML attributes on the same element"
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
name: "unescapedEntities",
|
|
577
|
+
defaultSeverity: "warning",
|
|
578
|
+
description: "Flags unescaped `&` and `>` characters in text content"
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
name: "preferMustacheComments",
|
|
582
|
+
defaultSeverity: "off",
|
|
583
|
+
description: "Suggests replacing HTML comments with mustache comments"
|
|
584
|
+
}
|
|
585
|
+
];
|
|
586
|
+
var KNOWN_RULE_NAMES = new Set(RULES.map((r) => r.name));
|
|
587
|
+
var RULE_DEFAULTS = Object.fromEntries(
|
|
588
|
+
RULES.map((r) => [r.name, r.defaultSeverity])
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
// lsp/server/src/configFile.ts
|
|
547
592
|
var VALID_CSS_DISPLAY_VALUES = /* @__PURE__ */ new Set([
|
|
548
593
|
"block",
|
|
549
594
|
"inline",
|
|
@@ -563,6 +608,7 @@ var VALID_CSS_DISPLAY_VALUES = /* @__PURE__ */ new Set([
|
|
|
563
608
|
"ruby-text",
|
|
564
609
|
"none"
|
|
565
610
|
]);
|
|
611
|
+
var VALID_RULE_SEVERITIES = /* @__PURE__ */ new Set(["error", "warning", "off"]);
|
|
566
612
|
var CONFIG_FILENAME = ".htmlmustache.jsonc";
|
|
567
613
|
function parseJsonc(text2) {
|
|
568
614
|
let result = "";
|
|
@@ -682,6 +728,18 @@ function validateConfig(raw) {
|
|
|
682
728
|
}
|
|
683
729
|
config.customTags = Array.from(mergedMap.values());
|
|
684
730
|
}
|
|
731
|
+
if (obj.rules && typeof obj.rules === "object" && !Array.isArray(obj.rules)) {
|
|
732
|
+
const rawRules = obj.rules;
|
|
733
|
+
const rules = {};
|
|
734
|
+
let hasRules = false;
|
|
735
|
+
for (const [key, value] of Object.entries(rawRules)) {
|
|
736
|
+
if (KNOWN_RULE_NAMES.has(key) && typeof value === "string" && VALID_RULE_SEVERITIES.has(value)) {
|
|
737
|
+
rules[key] = value;
|
|
738
|
+
hasRules = true;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (hasRules) config.rules = rules;
|
|
742
|
+
}
|
|
685
743
|
return config;
|
|
686
744
|
}
|
|
687
745
|
function loadConfigFileForPath(filePath) {
|
|
@@ -1274,6 +1332,35 @@ function checkUnescapedEntities(rootNode) {
|
|
|
1274
1332
|
visit(rootNode);
|
|
1275
1333
|
return errors;
|
|
1276
1334
|
}
|
|
1335
|
+
function checkHtmlComments(rootNode) {
|
|
1336
|
+
const errors = [];
|
|
1337
|
+
function visit(node) {
|
|
1338
|
+
if (node.type === "html_comment") {
|
|
1339
|
+
const raw = node.text;
|
|
1340
|
+
let content = raw;
|
|
1341
|
+
if (content.startsWith("<!--")) content = content.slice(4);
|
|
1342
|
+
if (content.endsWith("-->")) content = content.slice(0, -3);
|
|
1343
|
+
content = content.trim();
|
|
1344
|
+
errors.push({
|
|
1345
|
+
node,
|
|
1346
|
+
message: `HTML comment found \u2014 use mustache comment {{! ... }} instead`,
|
|
1347
|
+
severity: "warning",
|
|
1348
|
+
fix: [{
|
|
1349
|
+
startIndex: node.startIndex,
|
|
1350
|
+
endIndex: node.endIndex,
|
|
1351
|
+
newText: `{{! ${content} }}`
|
|
1352
|
+
}],
|
|
1353
|
+
fixDescription: "Replace HTML comment with mustache comment"
|
|
1354
|
+
});
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
for (const child of node.children) {
|
|
1358
|
+
visit(child);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
visit(rootNode);
|
|
1362
|
+
return errors;
|
|
1363
|
+
}
|
|
1277
1364
|
function checkDuplicateAttributes(rootNode) {
|
|
1278
1365
|
const errors = [];
|
|
1279
1366
|
function visit(node) {
|
|
@@ -1335,7 +1422,10 @@ function errorMessageForNode(nodeType, node) {
|
|
|
1335
1422
|
}
|
|
1336
1423
|
return `Missing ${nodeType}`;
|
|
1337
1424
|
}
|
|
1338
|
-
function
|
|
1425
|
+
function resolveRuleSeverity(rules, ruleName) {
|
|
1426
|
+
return rules?.[ruleName] ?? RULE_DEFAULTS[ruleName] ?? "off";
|
|
1427
|
+
}
|
|
1428
|
+
function collectErrors(tree, rules) {
|
|
1339
1429
|
const errors = [];
|
|
1340
1430
|
const cursor = tree.walk();
|
|
1341
1431
|
function visit() {
|
|
@@ -1365,29 +1455,34 @@ function collectErrors(tree) {
|
|
|
1365
1455
|
errors.push({ node: error.node, message: error.message });
|
|
1366
1456
|
}
|
|
1367
1457
|
const sourceText = tree.rootNode.text;
|
|
1368
|
-
const
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1458
|
+
const ruleChecks = [
|
|
1459
|
+
{ rule: "nestedDuplicateSections", errors: () => checkNestedSameNameSections(tree.rootNode) },
|
|
1460
|
+
{ rule: "unquotedMustacheAttributes", errors: () => checkUnquotedMustacheAttributes(tree.rootNode) },
|
|
1461
|
+
{ rule: "consecutiveDuplicateSections", errors: () => checkConsecutiveSameNameSections(tree.rootNode, sourceText) },
|
|
1462
|
+
{ rule: "selfClosingNonVoidTags", errors: () => checkSelfClosingNonVoidTags(tree.rootNode) },
|
|
1463
|
+
{ rule: "duplicateAttributes", errors: () => checkDuplicateAttributes(tree.rootNode) },
|
|
1464
|
+
{ rule: "unescapedEntities", errors: () => checkUnescapedEntities(tree.rootNode) },
|
|
1465
|
+
{ rule: "preferMustacheComments", errors: () => checkHtmlComments(tree.rootNode) }
|
|
1375
1466
|
];
|
|
1376
|
-
for (const
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1467
|
+
for (const { rule, errors: getErrors } of ruleChecks) {
|
|
1468
|
+
const severity = resolveRuleSeverity(rules, rule);
|
|
1469
|
+
if (severity === "off") continue;
|
|
1470
|
+
for (const error of getErrors()) {
|
|
1471
|
+
errors.push({
|
|
1472
|
+
node: error.node,
|
|
1473
|
+
message: error.message,
|
|
1474
|
+
severity,
|
|
1475
|
+
fix: error.fix,
|
|
1476
|
+
fixDescription: error.fixDescription
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1384
1479
|
}
|
|
1385
1480
|
return errors;
|
|
1386
1481
|
}
|
|
1387
1482
|
|
|
1388
1483
|
// cli/src/check.ts
|
|
1389
|
-
function collectErrors2(tree, file) {
|
|
1390
|
-
const errors = collectErrors(tree);
|
|
1484
|
+
function collectErrors2(tree, file, rules) {
|
|
1485
|
+
const errors = collectErrors(tree, rules);
|
|
1391
1486
|
return errors.map((error) => ({
|
|
1392
1487
|
file,
|
|
1393
1488
|
line: error.node.startPosition.row + 1,
|
|
@@ -1570,12 +1665,13 @@ async function run(args) {
|
|
|
1570
1665
|
let filesWithErrors = 0;
|
|
1571
1666
|
const cwd = process.cwd();
|
|
1572
1667
|
const errorOutput = [];
|
|
1668
|
+
const rules = config?.rules;
|
|
1573
1669
|
for (const file of files) {
|
|
1574
1670
|
const displayPath = import_node_path.default.relative(cwd, file) || file;
|
|
1575
1671
|
let source = import_node_fs.default.readFileSync(file, "utf-8");
|
|
1576
1672
|
if (fixMode) {
|
|
1577
1673
|
const tree2 = parseDocument(source);
|
|
1578
|
-
const errors2 = collectErrors2(tree2, displayPath);
|
|
1674
|
+
const errors2 = collectErrors2(tree2, displayPath, rules);
|
|
1579
1675
|
const fixed = applyFixes(source, errors2);
|
|
1580
1676
|
if (fixed !== source) {
|
|
1581
1677
|
import_node_fs.default.writeFileSync(file, fixed, "utf-8");
|
|
@@ -1583,7 +1679,7 @@ async function run(args) {
|
|
|
1583
1679
|
}
|
|
1584
1680
|
}
|
|
1585
1681
|
const tree = parseDocument(source);
|
|
1586
|
-
const errors = collectErrors2(tree, displayPath);
|
|
1682
|
+
const errors = collectErrors2(tree, displayPath, rules);
|
|
1587
1683
|
const fileErrors = errors.filter((e) => e.severity !== "warning");
|
|
1588
1684
|
const fileWarnings = errors.filter((e) => e.severity === "warning");
|
|
1589
1685
|
if (errors.length > 0) {
|
|
@@ -2764,25 +2860,21 @@ function formatScriptStyleElement(node, context) {
|
|
|
2764
2860
|
const dedented = dedentContent(child.text);
|
|
2765
2861
|
if (dedented.length > 0) {
|
|
2766
2862
|
const contentLines = dedented.split("\n");
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
lineDocs.push("\n");
|
|
2775
|
-
} else {
|
|
2776
|
-
lineDocs.push(hardline);
|
|
2777
|
-
}
|
|
2778
|
-
}
|
|
2779
|
-
if (contentLines[j] !== "") {
|
|
2780
|
-
lineDocs.push(text(contentLines[j]));
|
|
2863
|
+
const lineDocs = [];
|
|
2864
|
+
for (let j = 0; j < contentLines.length; j++) {
|
|
2865
|
+
if (j > 0) {
|
|
2866
|
+
if (contentLines[j] === "") {
|
|
2867
|
+
lineDocs.push("\n");
|
|
2868
|
+
} else {
|
|
2869
|
+
lineDocs.push(hardline);
|
|
2781
2870
|
}
|
|
2782
2871
|
}
|
|
2783
|
-
|
|
2784
|
-
|
|
2872
|
+
if (contentLines[j] !== "") {
|
|
2873
|
+
lineDocs.push(text(contentLines[j]));
|
|
2874
|
+
}
|
|
2785
2875
|
}
|
|
2876
|
+
parts.push(indent(concat([hardline, ...lineDocs])));
|
|
2877
|
+
parts.push(hardline);
|
|
2786
2878
|
}
|
|
2787
2879
|
}
|
|
2788
2880
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reteps/tree-sitter-htmlmustache",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "HTML with Mustache/Handlebars template syntax grammar for tree-sitter",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
"prestart": "tree-sitter build --wasm",
|
|
90
90
|
"start": "tree-sitter playground",
|
|
91
91
|
"test": "tree-sitter test && node --test bindings/node/*_test.js",
|
|
92
|
-
"test:cli": "vitest run --config cli/vitest.config.ts"
|
|
92
|
+
"test:cli": "vitest run --config cli/vitest.config.ts",
|
|
93
|
+
"generate:rule-docs": "tsx scripts/generate-rule-docs.ts"
|
|
93
94
|
}
|
|
94
95
|
}
|