@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.
Files changed (3) hide show
  1. package/README.md +12 -0
  2. package/cli/out/main.js +225 -10
  3. 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 collectErrors(tree, rules) {
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(rules, rule);
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
- if (isMultiline) {
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reteps/tree-sitter-htmlmustache",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "HTML with Mustache/Handlebars template syntax grammar for tree-sitter",
5
5
  "repository": {
6
6
  "type": "git",