@reteps/tree-sitter-htmlmustache 0.5.1 → 0.6.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 CHANGED
@@ -244,6 +244,7 @@ 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
 
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));
@@ -703,9 +708,12 @@ function validateConfig(raw) {
703
708
  config.mustacheSpaces = obj.mustacheSpaces;
704
709
  }
705
710
  if (Array.isArray(obj.noBreakDelimiters)) {
706
- const items = obj.noBreakDelimiters.filter(
707
- (s) => typeof s === "string" && s.length > 0
708
- );
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
+ }
709
717
  if (items.length > 0) config.noBreakDelimiters = items;
710
718
  }
711
719
  if (Array.isArray(obj.include)) {
@@ -1361,6 +1369,165 @@ function checkHtmlComments(rootNode) {
1361
1369
  visit(rootNode);
1362
1370
  return errors;
1363
1371
  }
1372
+ var KNOWN_HTML_TAGS = /* @__PURE__ */ new Set([
1373
+ // Void elements
1374
+ "area",
1375
+ "base",
1376
+ "basefont",
1377
+ "bgsound",
1378
+ "br",
1379
+ "col",
1380
+ "command",
1381
+ "embed",
1382
+ "frame",
1383
+ "hr",
1384
+ "image",
1385
+ "img",
1386
+ "input",
1387
+ "isindex",
1388
+ "keygen",
1389
+ "link",
1390
+ "menuitem",
1391
+ "meta",
1392
+ "nextid",
1393
+ "param",
1394
+ "source",
1395
+ "track",
1396
+ "wbr",
1397
+ // Non-void elements
1398
+ "a",
1399
+ "abbr",
1400
+ "address",
1401
+ "article",
1402
+ "aside",
1403
+ "audio",
1404
+ "b",
1405
+ "bdi",
1406
+ "bdo",
1407
+ "blockquote",
1408
+ "body",
1409
+ "button",
1410
+ "canvas",
1411
+ "caption",
1412
+ "cite",
1413
+ "code",
1414
+ "colgroup",
1415
+ "data",
1416
+ "datalist",
1417
+ "dd",
1418
+ "del",
1419
+ "details",
1420
+ "dfn",
1421
+ "dialog",
1422
+ "div",
1423
+ "dl",
1424
+ "dt",
1425
+ "em",
1426
+ "fieldset",
1427
+ "figcaption",
1428
+ "figure",
1429
+ "footer",
1430
+ "form",
1431
+ "h1",
1432
+ "h2",
1433
+ "h3",
1434
+ "h4",
1435
+ "h5",
1436
+ "h6",
1437
+ "head",
1438
+ "header",
1439
+ "hgroup",
1440
+ "html",
1441
+ "i",
1442
+ "iframe",
1443
+ "ins",
1444
+ "kbd",
1445
+ "label",
1446
+ "legend",
1447
+ "li",
1448
+ "main",
1449
+ "map",
1450
+ "mark",
1451
+ "math",
1452
+ "menu",
1453
+ "meter",
1454
+ "nav",
1455
+ "noscript",
1456
+ "object",
1457
+ "ol",
1458
+ "optgroup",
1459
+ "option",
1460
+ "output",
1461
+ "p",
1462
+ "picture",
1463
+ "pre",
1464
+ "progress",
1465
+ "q",
1466
+ "rb",
1467
+ "rp",
1468
+ "rt",
1469
+ "rtc",
1470
+ "ruby",
1471
+ "s",
1472
+ "samp",
1473
+ "script",
1474
+ "search",
1475
+ "section",
1476
+ "select",
1477
+ "slot",
1478
+ "small",
1479
+ "span",
1480
+ "strong",
1481
+ "style",
1482
+ "sub",
1483
+ "summary",
1484
+ "sup",
1485
+ "svg",
1486
+ "table",
1487
+ "tbody",
1488
+ "td",
1489
+ "template",
1490
+ "textarea",
1491
+ "tfoot",
1492
+ "th",
1493
+ "thead",
1494
+ "time",
1495
+ "title",
1496
+ "tr",
1497
+ "u",
1498
+ "ul",
1499
+ "var",
1500
+ "video"
1501
+ ]);
1502
+ function checkUnrecognizedHtmlTags(rootNode, customTagNames) {
1503
+ const errors = [];
1504
+ const customSet = customTagNames ? new Set(customTagNames.map((n) => n.toLowerCase())) : void 0;
1505
+ function visit(node) {
1506
+ if (node.type === "html_element" || node.type === "html_self_closing_tag") {
1507
+ 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");
1508
+ const tagName = tagNameNode?.text.toLowerCase();
1509
+ if (tagName === "svg" || tagName === "math") return;
1510
+ }
1511
+ if (node.type === "html_start_tag" || node.type === "html_self_closing_tag") {
1512
+ const tagNameNode = node.children.find((c) => c.type === "html_tag_name");
1513
+ if (tagNameNode) {
1514
+ const tagName = tagNameNode.text.toLowerCase();
1515
+ if (!KNOWN_HTML_TAGS.has(tagName) && !customSet?.has(tagName)) {
1516
+ errors.push({
1517
+ node: tagNameNode,
1518
+ message: `Unrecognized HTML tag: <${tagNameNode.text}>`
1519
+ });
1520
+ }
1521
+ }
1522
+ return;
1523
+ }
1524
+ for (const child of node.children) {
1525
+ visit(child);
1526
+ }
1527
+ }
1528
+ visit(rootNode);
1529
+ return errors;
1530
+ }
1364
1531
  function checkDuplicateAttributes(rootNode) {
1365
1532
  const errors = [];
1366
1533
  function visit(node) {
@@ -1454,7 +1621,7 @@ function collectDisabledRules(rootNode) {
1454
1621
  walk(rootNode);
1455
1622
  return disabled;
1456
1623
  }
1457
- function collectErrors(tree, rules) {
1624
+ function collectErrors(tree, rules, customTagNames) {
1458
1625
  const errors = [];
1459
1626
  const cursor = tree.walk();
1460
1627
  function visit() {
@@ -1496,7 +1663,8 @@ function collectErrors(tree, rules) {
1496
1663
  { rule: "selfClosingNonVoidTags", errors: () => checkSelfClosingNonVoidTags(tree.rootNode) },
1497
1664
  { rule: "duplicateAttributes", errors: () => checkDuplicateAttributes(tree.rootNode) },
1498
1665
  { rule: "unescapedEntities", errors: () => checkUnescapedEntities(tree.rootNode) },
1499
- { rule: "preferMustacheComments", errors: () => checkHtmlComments(tree.rootNode) }
1666
+ { rule: "preferMustacheComments", errors: () => checkHtmlComments(tree.rootNode) },
1667
+ { rule: "unrecognizedHtmlTags", errors: () => checkUnrecognizedHtmlTags(tree.rootNode, customTagNames) }
1500
1668
  ];
1501
1669
  for (const { rule, errors: getErrors } of ruleChecks) {
1502
1670
  const severity = resolveRuleSeverity(effectiveRules, rule);
@@ -1517,8 +1685,8 @@ function collectErrors(tree, rules) {
1517
1685
  }
1518
1686
 
1519
1687
  // cli/src/check.ts
1520
- function collectErrors2(tree, file, rules) {
1521
- const errors = collectErrors(tree, rules);
1688
+ function collectErrors2(tree, file, rules, customTagNames) {
1689
+ const errors = collectErrors(tree, rules, customTagNames);
1522
1690
  return errors.map((error) => ({
1523
1691
  file,
1524
1692
  line: error.node.startPosition.row + 1,
@@ -1702,12 +1870,13 @@ async function run(args) {
1702
1870
  const cwd = process.cwd();
1703
1871
  const errorOutput = [];
1704
1872
  const rules = config?.rules;
1873
+ const customTagNames = config?.customTags?.map((t) => t.name);
1705
1874
  for (const file of files) {
1706
1875
  const displayPath = import_node_path.default.relative(cwd, file) || file;
1707
1876
  let source = import_node_fs.default.readFileSync(file, "utf-8");
1708
1877
  if (fixMode) {
1709
1878
  const tree2 = parseDocument(source);
1710
- const errors2 = collectErrors2(tree2, displayPath, rules);
1879
+ const errors2 = collectErrors2(tree2, displayPath, rules, customTagNames);
1711
1880
  const fixed = applyFixes(source, errors2);
1712
1881
  if (fixed !== source) {
1713
1882
  import_node_fs.default.writeFileSync(file, fixed, "utf-8");
@@ -1715,7 +1884,7 @@ async function run(args) {
1715
1884
  }
1716
1885
  }
1717
1886
  const tree = parseDocument(source);
1718
- const errors = collectErrors2(tree, displayPath, rules);
1887
+ const errors = collectErrors2(tree, displayPath, rules, customTagNames);
1719
1888
  const fileErrors = errors.filter((e) => e.severity !== "warning");
1720
1889
  const fileWarnings = errors.filter((e) => e.severity === "warning");
1721
1890
  if (errors.length > 0) {
@@ -2377,6 +2546,7 @@ function getCSSDisplay(node, customTags = EMPTY_MAP) {
2377
2546
  if (config) {
2378
2547
  if (config.display) return config.display;
2379
2548
  if (isCodeTag(config)) return "block";
2549
+ return "inline-block";
2380
2550
  }
2381
2551
  return CSS_DISPLAY_MAP[lower] ?? "inline";
2382
2552
  }
@@ -2748,7 +2918,7 @@ function formatHtmlElement(node, context, forceInline = false) {
2748
2918
  parts.push(text(child.text));
2749
2919
  }
2750
2920
  }
2751
- } else if (!isBlock && (!hasHtmlElementChildren || forceInline && !contentNodes.some(
2921
+ } else if (!isBlock && (!hasHtmlElementChildren || forceInline && display !== "inline-block" && !contentNodes.some(
2752
2922
  (child) => isRawContentElement(child) || isBlockLevel(child, tags)
2753
2923
  ))) {
2754
2924
  if (!forceInline && startTag && startTagHasAttributes(startTag)) {
@@ -3113,7 +3283,9 @@ function textWords(str) {
3113
3283
  }
3114
3284
  function collapseDelimitedRegions(parts, delimiters) {
3115
3285
  if (delimiters.length === 0) return parts;
3116
- const sorted = [...delimiters].sort((a, b) => b.length - a.length);
3286
+ const sorted = [...delimiters].sort(
3287
+ (a, b) => Math.max(b.start.length, b.end.length) - Math.max(a.start.length, a.end.length)
3288
+ );
3117
3289
  const result = [...parts];
3118
3290
  let activeDelimiter = null;
3119
3291
  for (let i = 0; i < result.length; i++) {
@@ -3121,10 +3293,10 @@ function collapseDelimitedRegions(parts, delimiters) {
3121
3293
  if (typeof part === "string") {
3122
3294
  if (activeDelimiter === null) {
3123
3295
  for (const delim of sorted) {
3124
- const delimIdx = part.indexOf(delim);
3125
- if (delimIdx >= 0) {
3126
- const afterOpen = delimIdx + delim.length;
3127
- const closeIdx = part.indexOf(delim, afterOpen);
3296
+ const startIdx = part.indexOf(delim.start);
3297
+ if (startIdx >= 0) {
3298
+ const afterOpen = startIdx + delim.start.length;
3299
+ const closeIdx = part.indexOf(delim.end, afterOpen);
3128
3300
  if (closeIdx >= 0) {
3129
3301
  continue;
3130
3302
  }
@@ -3133,7 +3305,7 @@ function collapseDelimitedRegions(parts, delimiters) {
3133
3305
  }
3134
3306
  }
3135
3307
  } else {
3136
- if (part.includes(activeDelimiter)) {
3308
+ if (part.includes(activeDelimiter.end)) {
3137
3309
  activeDelimiter = null;
3138
3310
  }
3139
3311
  }
@@ -3393,6 +3565,17 @@ function formatBlockChildren(nodes, context) {
3393
3565
  }
3394
3566
  }
3395
3567
  }
3568
+ if (node.type === "html_element" && currentLine.length > 0) {
3569
+ const tagName = getTagName(node);
3570
+ if (tagName?.toLowerCase() === "br") {
3571
+ const lineContent = trimDoc(flushCurrentLine());
3572
+ if (hasDocContent(lineContent)) {
3573
+ lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
3574
+ blankLineBeforeCurrentLine = false;
3575
+ }
3576
+ currentLine = [];
3577
+ }
3578
+ }
3396
3579
  lastNodeEnd = node.endIndex;
3397
3580
  }
3398
3581
  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.1",
3
+ "version": "0.6.0",
4
4
  "description": "HTML with Mustache/Handlebars template syntax grammar for tree-sitter",
5
5
  "repository": {
6
6
  "type": "git",