@reteps/tree-sitter-htmlmustache 0.5.0 → 0.5.2
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 -0
- package/cli/out/main.js +225 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -244,9 +244,21 @@ Additionally, the following rules are configurable. Set their severities (`"erro
|
|
|
244
244
|
| `duplicateAttributes` | `error` | Detects duplicate HTML attributes on the same element |
|
|
245
245
|
| `unescapedEntities` | `warning` | Flags unescaped `&` and `>` characters in text content |
|
|
246
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 |
|
|
247
248
|
|
|
248
249
|
<!-- RULES_TABLE_END -->
|
|
249
250
|
|
|
251
|
+
### Disabling Lint Rules
|
|
252
|
+
|
|
253
|
+
Disable a configurable lint rule for an entire file with an inline comment:
|
|
254
|
+
|
|
255
|
+
```html
|
|
256
|
+
<!-- htmlmustache-disable preferMustacheComments -->
|
|
257
|
+
{{! htmlmustache-disable selfClosingNonVoidTags }}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The comment can appear anywhere in the file. Only configurable rules (listed above) can be disabled. Use multiple comments to disable multiple rules.
|
|
261
|
+
|
|
250
262
|
### EditorConfig
|
|
251
263
|
|
|
252
264
|
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
|
@@ -581,6 +581,11 @@ var RULES = [
|
|
|
581
581
|
name: "preferMustacheComments",
|
|
582
582
|
defaultSeverity: "off",
|
|
583
583
|
description: "Suggests replacing HTML comments with mustache comments"
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
name: "unrecognizedHtmlTags",
|
|
587
|
+
defaultSeverity: "error",
|
|
588
|
+
description: "Flags HTML tags that are not standard HTML elements or valid custom elements"
|
|
584
589
|
}
|
|
585
590
|
];
|
|
586
591
|
var KNOWN_RULE_NAMES = new Set(RULES.map((r) => r.name));
|
|
@@ -1361,6 +1366,165 @@ function checkHtmlComments(rootNode) {
|
|
|
1361
1366
|
visit(rootNode);
|
|
1362
1367
|
return errors;
|
|
1363
1368
|
}
|
|
1369
|
+
var KNOWN_HTML_TAGS = /* @__PURE__ */ new Set([
|
|
1370
|
+
// Void elements
|
|
1371
|
+
"area",
|
|
1372
|
+
"base",
|
|
1373
|
+
"basefont",
|
|
1374
|
+
"bgsound",
|
|
1375
|
+
"br",
|
|
1376
|
+
"col",
|
|
1377
|
+
"command",
|
|
1378
|
+
"embed",
|
|
1379
|
+
"frame",
|
|
1380
|
+
"hr",
|
|
1381
|
+
"image",
|
|
1382
|
+
"img",
|
|
1383
|
+
"input",
|
|
1384
|
+
"isindex",
|
|
1385
|
+
"keygen",
|
|
1386
|
+
"link",
|
|
1387
|
+
"menuitem",
|
|
1388
|
+
"meta",
|
|
1389
|
+
"nextid",
|
|
1390
|
+
"param",
|
|
1391
|
+
"source",
|
|
1392
|
+
"track",
|
|
1393
|
+
"wbr",
|
|
1394
|
+
// Non-void elements
|
|
1395
|
+
"a",
|
|
1396
|
+
"abbr",
|
|
1397
|
+
"address",
|
|
1398
|
+
"article",
|
|
1399
|
+
"aside",
|
|
1400
|
+
"audio",
|
|
1401
|
+
"b",
|
|
1402
|
+
"bdi",
|
|
1403
|
+
"bdo",
|
|
1404
|
+
"blockquote",
|
|
1405
|
+
"body",
|
|
1406
|
+
"button",
|
|
1407
|
+
"canvas",
|
|
1408
|
+
"caption",
|
|
1409
|
+
"cite",
|
|
1410
|
+
"code",
|
|
1411
|
+
"colgroup",
|
|
1412
|
+
"data",
|
|
1413
|
+
"datalist",
|
|
1414
|
+
"dd",
|
|
1415
|
+
"del",
|
|
1416
|
+
"details",
|
|
1417
|
+
"dfn",
|
|
1418
|
+
"dialog",
|
|
1419
|
+
"div",
|
|
1420
|
+
"dl",
|
|
1421
|
+
"dt",
|
|
1422
|
+
"em",
|
|
1423
|
+
"fieldset",
|
|
1424
|
+
"figcaption",
|
|
1425
|
+
"figure",
|
|
1426
|
+
"footer",
|
|
1427
|
+
"form",
|
|
1428
|
+
"h1",
|
|
1429
|
+
"h2",
|
|
1430
|
+
"h3",
|
|
1431
|
+
"h4",
|
|
1432
|
+
"h5",
|
|
1433
|
+
"h6",
|
|
1434
|
+
"head",
|
|
1435
|
+
"header",
|
|
1436
|
+
"hgroup",
|
|
1437
|
+
"html",
|
|
1438
|
+
"i",
|
|
1439
|
+
"iframe",
|
|
1440
|
+
"ins",
|
|
1441
|
+
"kbd",
|
|
1442
|
+
"label",
|
|
1443
|
+
"legend",
|
|
1444
|
+
"li",
|
|
1445
|
+
"main",
|
|
1446
|
+
"map",
|
|
1447
|
+
"mark",
|
|
1448
|
+
"math",
|
|
1449
|
+
"menu",
|
|
1450
|
+
"meter",
|
|
1451
|
+
"nav",
|
|
1452
|
+
"noscript",
|
|
1453
|
+
"object",
|
|
1454
|
+
"ol",
|
|
1455
|
+
"optgroup",
|
|
1456
|
+
"option",
|
|
1457
|
+
"output",
|
|
1458
|
+
"p",
|
|
1459
|
+
"picture",
|
|
1460
|
+
"pre",
|
|
1461
|
+
"progress",
|
|
1462
|
+
"q",
|
|
1463
|
+
"rb",
|
|
1464
|
+
"rp",
|
|
1465
|
+
"rt",
|
|
1466
|
+
"rtc",
|
|
1467
|
+
"ruby",
|
|
1468
|
+
"s",
|
|
1469
|
+
"samp",
|
|
1470
|
+
"script",
|
|
1471
|
+
"search",
|
|
1472
|
+
"section",
|
|
1473
|
+
"select",
|
|
1474
|
+
"slot",
|
|
1475
|
+
"small",
|
|
1476
|
+
"span",
|
|
1477
|
+
"strong",
|
|
1478
|
+
"style",
|
|
1479
|
+
"sub",
|
|
1480
|
+
"summary",
|
|
1481
|
+
"sup",
|
|
1482
|
+
"svg",
|
|
1483
|
+
"table",
|
|
1484
|
+
"tbody",
|
|
1485
|
+
"td",
|
|
1486
|
+
"template",
|
|
1487
|
+
"textarea",
|
|
1488
|
+
"tfoot",
|
|
1489
|
+
"th",
|
|
1490
|
+
"thead",
|
|
1491
|
+
"time",
|
|
1492
|
+
"title",
|
|
1493
|
+
"tr",
|
|
1494
|
+
"u",
|
|
1495
|
+
"ul",
|
|
1496
|
+
"var",
|
|
1497
|
+
"video"
|
|
1498
|
+
]);
|
|
1499
|
+
function checkUnrecognizedHtmlTags(rootNode, customTagNames) {
|
|
1500
|
+
const errors = [];
|
|
1501
|
+
const customSet = customTagNames ? new Set(customTagNames.map((n) => n.toLowerCase())) : void 0;
|
|
1502
|
+
function visit(node) {
|
|
1503
|
+
if (node.type === "html_element" || node.type === "html_self_closing_tag") {
|
|
1504
|
+
const tagNameNode = node.type === "html_self_closing_tag" ? node.children.find((c) => c.type === "html_tag_name") : node.children.find((c) => c.type === "html_start_tag")?.children.find((c) => c.type === "html_tag_name");
|
|
1505
|
+
const tagName = tagNameNode?.text.toLowerCase();
|
|
1506
|
+
if (tagName === "svg" || tagName === "math") return;
|
|
1507
|
+
}
|
|
1508
|
+
if (node.type === "html_start_tag" || node.type === "html_self_closing_tag") {
|
|
1509
|
+
const tagNameNode = node.children.find((c) => c.type === "html_tag_name");
|
|
1510
|
+
if (tagNameNode) {
|
|
1511
|
+
const tagName = tagNameNode.text.toLowerCase();
|
|
1512
|
+
if (!KNOWN_HTML_TAGS.has(tagName) && !customSet?.has(tagName)) {
|
|
1513
|
+
errors.push({
|
|
1514
|
+
node: tagNameNode,
|
|
1515
|
+
message: `Unrecognized HTML tag: <${tagNameNode.text}>`
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
for (const child of node.children) {
|
|
1522
|
+
visit(child);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
visit(rootNode);
|
|
1526
|
+
return errors;
|
|
1527
|
+
}
|
|
1364
1528
|
function checkDuplicateAttributes(rootNode) {
|
|
1365
1529
|
const errors = [];
|
|
1366
1530
|
function visit(node) {
|
|
@@ -1425,7 +1589,36 @@ function errorMessageForNode(nodeType, node) {
|
|
|
1425
1589
|
function resolveRuleSeverity(rules, ruleName) {
|
|
1426
1590
|
return rules?.[ruleName] ?? RULE_DEFAULTS[ruleName] ?? "off";
|
|
1427
1591
|
}
|
|
1428
|
-
function
|
|
1592
|
+
function parseDisableDirective(node) {
|
|
1593
|
+
if (node.type !== "html_comment" && node.type !== "mustache_comment") return null;
|
|
1594
|
+
let inner = null;
|
|
1595
|
+
if (node.type === "html_comment") {
|
|
1596
|
+
const match = node.text.match(/^<!--([\s\S]*)-->$/);
|
|
1597
|
+
if (match) inner = match[1].trim();
|
|
1598
|
+
} else {
|
|
1599
|
+
const match = node.text.match(/^\{\{!([\s\S]*)\}\}$/);
|
|
1600
|
+
if (match) inner = match[1].trim();
|
|
1601
|
+
}
|
|
1602
|
+
if (!inner) return null;
|
|
1603
|
+
const prefix = "htmlmustache-disable ";
|
|
1604
|
+
if (!inner.startsWith(prefix)) return null;
|
|
1605
|
+
const ruleName = inner.slice(prefix.length).trim();
|
|
1606
|
+
return KNOWN_RULE_NAMES.has(ruleName) ? ruleName : null;
|
|
1607
|
+
}
|
|
1608
|
+
function collectDisabledRules(rootNode) {
|
|
1609
|
+
const disabled = /* @__PURE__ */ new Set();
|
|
1610
|
+
function walk(node) {
|
|
1611
|
+
const rule = parseDisableDirective(node);
|
|
1612
|
+
if (rule) {
|
|
1613
|
+
disabled.add(rule);
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
for (const child of node.children) walk(child);
|
|
1617
|
+
}
|
|
1618
|
+
walk(rootNode);
|
|
1619
|
+
return disabled;
|
|
1620
|
+
}
|
|
1621
|
+
function collectErrors(tree, rules, customTagNames) {
|
|
1429
1622
|
const errors = [];
|
|
1430
1623
|
const cursor = tree.walk();
|
|
1431
1624
|
function visit() {
|
|
@@ -1454,6 +1647,11 @@ function collectErrors(tree, rules) {
|
|
|
1454
1647
|
for (const error of unclosedErrors) {
|
|
1455
1648
|
errors.push({ node: error.node, message: error.message });
|
|
1456
1649
|
}
|
|
1650
|
+
const disabledRules = collectDisabledRules(tree.rootNode);
|
|
1651
|
+
const effectiveRules = { ...rules };
|
|
1652
|
+
for (const rule of disabledRules) {
|
|
1653
|
+
effectiveRules[rule] = "off";
|
|
1654
|
+
}
|
|
1457
1655
|
const sourceText = tree.rootNode.text;
|
|
1458
1656
|
const ruleChecks = [
|
|
1459
1657
|
{ rule: "nestedDuplicateSections", errors: () => checkNestedSameNameSections(tree.rootNode) },
|
|
@@ -1462,10 +1660,11 @@ function collectErrors(tree, rules) {
|
|
|
1462
1660
|
{ rule: "selfClosingNonVoidTags", errors: () => checkSelfClosingNonVoidTags(tree.rootNode) },
|
|
1463
1661
|
{ rule: "duplicateAttributes", errors: () => checkDuplicateAttributes(tree.rootNode) },
|
|
1464
1662
|
{ rule: "unescapedEntities", errors: () => checkUnescapedEntities(tree.rootNode) },
|
|
1465
|
-
{ rule: "preferMustacheComments", errors: () => checkHtmlComments(tree.rootNode) }
|
|
1663
|
+
{ rule: "preferMustacheComments", errors: () => checkHtmlComments(tree.rootNode) },
|
|
1664
|
+
{ rule: "unrecognizedHtmlTags", errors: () => checkUnrecognizedHtmlTags(tree.rootNode, customTagNames) }
|
|
1466
1665
|
];
|
|
1467
1666
|
for (const { rule, errors: getErrors } of ruleChecks) {
|
|
1468
|
-
const severity = resolveRuleSeverity(
|
|
1667
|
+
const severity = resolveRuleSeverity(effectiveRules, rule);
|
|
1469
1668
|
if (severity === "off") continue;
|
|
1470
1669
|
for (const error of getErrors()) {
|
|
1471
1670
|
errors.push({
|
|
@@ -1477,12 +1676,14 @@ function collectErrors(tree, rules) {
|
|
|
1477
1676
|
});
|
|
1478
1677
|
}
|
|
1479
1678
|
}
|
|
1480
|
-
return errors
|
|
1679
|
+
return errors.filter(
|
|
1680
|
+
(e) => !(e.message.includes("HTML comment found") && parseDisableDirective(e.node) !== null)
|
|
1681
|
+
);
|
|
1481
1682
|
}
|
|
1482
1683
|
|
|
1483
1684
|
// cli/src/check.ts
|
|
1484
|
-
function collectErrors2(tree, file, rules) {
|
|
1485
|
-
const errors = collectErrors(tree, rules);
|
|
1685
|
+
function collectErrors2(tree, file, rules, customTagNames) {
|
|
1686
|
+
const errors = collectErrors(tree, rules, customTagNames);
|
|
1486
1687
|
return errors.map((error) => ({
|
|
1487
1688
|
file,
|
|
1488
1689
|
line: error.node.startPosition.row + 1,
|
|
@@ -1666,12 +1867,13 @@ async function run(args) {
|
|
|
1666
1867
|
const cwd = process.cwd();
|
|
1667
1868
|
const errorOutput = [];
|
|
1668
1869
|
const rules = config?.rules;
|
|
1870
|
+
const customTagNames = config?.customTags?.map((t) => t.name);
|
|
1669
1871
|
for (const file of files) {
|
|
1670
1872
|
const displayPath = import_node_path.default.relative(cwd, file) || file;
|
|
1671
1873
|
let source = import_node_fs.default.readFileSync(file, "utf-8");
|
|
1672
1874
|
if (fixMode) {
|
|
1673
1875
|
const tree2 = parseDocument(source);
|
|
1674
|
-
const errors2 = collectErrors2(tree2, displayPath, rules);
|
|
1876
|
+
const errors2 = collectErrors2(tree2, displayPath, rules, customTagNames);
|
|
1675
1877
|
const fixed = applyFixes(source, errors2);
|
|
1676
1878
|
if (fixed !== source) {
|
|
1677
1879
|
import_node_fs.default.writeFileSync(file, fixed, "utf-8");
|
|
@@ -1679,7 +1881,7 @@ async function run(args) {
|
|
|
1679
1881
|
}
|
|
1680
1882
|
}
|
|
1681
1883
|
const tree = parseDocument(source);
|
|
1682
|
-
const errors = collectErrors2(tree, displayPath, rules);
|
|
1884
|
+
const errors = collectErrors2(tree, displayPath, rules, customTagNames);
|
|
1683
1885
|
const fileErrors = errors.filter((e) => e.severity !== "warning");
|
|
1684
1886
|
const fileWarnings = errors.filter((e) => e.severity === "warning");
|
|
1685
1887
|
if (errors.length > 0) {
|
|
@@ -2341,6 +2543,7 @@ function getCSSDisplay(node, customTags = EMPTY_MAP) {
|
|
|
2341
2543
|
if (config) {
|
|
2342
2544
|
if (config.display) return config.display;
|
|
2343
2545
|
if (isCodeTag(config)) return "block";
|
|
2546
|
+
return "inline-block";
|
|
2344
2547
|
}
|
|
2345
2548
|
return CSS_DISPLAY_MAP[lower] ?? "inline";
|
|
2346
2549
|
}
|
|
@@ -2712,7 +2915,7 @@ function formatHtmlElement(node, context, forceInline = false) {
|
|
|
2712
2915
|
parts.push(text(child.text));
|
|
2713
2916
|
}
|
|
2714
2917
|
}
|
|
2715
|
-
} else if (!isBlock && (!hasHtmlElementChildren || forceInline && !contentNodes.some(
|
|
2918
|
+
} else if (!isBlock && (!hasHtmlElementChildren || forceInline && display !== "inline-block" && !contentNodes.some(
|
|
2716
2919
|
(child) => isRawContentElement(child) || isBlockLevel(child, tags)
|
|
2717
2920
|
))) {
|
|
2718
2921
|
if (!forceInline && startTag && startTagHasAttributes(startTag)) {
|
|
@@ -3252,7 +3455,8 @@ function formatBlockChildren(nodes, context) {
|
|
|
3252
3455
|
pendingBlankLine = false;
|
|
3253
3456
|
} else if (node.type === "html_comment" || node.type === "mustache_comment") {
|
|
3254
3457
|
const isMultiline = node.startPosition.row !== node.endPosition.row;
|
|
3255
|
-
|
|
3458
|
+
const isOnOwnLine = i > 0 && node.startPosition.row > nodes[i - 1].endPosition.row;
|
|
3459
|
+
if (isMultiline || isOnOwnLine) {
|
|
3256
3460
|
if (currentLine.length > 0) {
|
|
3257
3461
|
const lineContent = trimDoc(flushCurrentLine());
|
|
3258
3462
|
if (hasDocContent(lineContent)) {
|
|
@@ -3356,6 +3560,17 @@ function formatBlockChildren(nodes, context) {
|
|
|
3356
3560
|
}
|
|
3357
3561
|
}
|
|
3358
3562
|
}
|
|
3563
|
+
if (node.type === "html_element" && currentLine.length > 0) {
|
|
3564
|
+
const tagName = getTagName(node);
|
|
3565
|
+
if (tagName?.toLowerCase() === "br") {
|
|
3566
|
+
const lineContent = trimDoc(flushCurrentLine());
|
|
3567
|
+
if (hasDocContent(lineContent)) {
|
|
3568
|
+
lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
|
|
3569
|
+
blankLineBeforeCurrentLine = false;
|
|
3570
|
+
}
|
|
3571
|
+
currentLine = [];
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3359
3574
|
lastNodeEnd = node.endIndex;
|
|
3360
3575
|
}
|
|
3361
3576
|
if (inIgnoreRegion && nodes.length > 0) {
|