@reteps/tree-sitter-htmlmustache 0.4.0 → 0.4.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/cli/out/main.js CHANGED
@@ -544,6 +544,25 @@ 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
+ var VALID_CSS_DISPLAY_VALUES = /* @__PURE__ */ new Set([
548
+ "block",
549
+ "inline",
550
+ "inline-block",
551
+ "table-row",
552
+ "table-cell",
553
+ "table",
554
+ "table-row-group",
555
+ "table-header-group",
556
+ "table-footer-group",
557
+ "table-column",
558
+ "table-column-group",
559
+ "table-caption",
560
+ "list-item",
561
+ "ruby",
562
+ "ruby-base",
563
+ "ruby-text",
564
+ "none"
565
+ ]);
547
566
  var CONFIG_FILENAME = ".htmlmustache.jsonc";
548
567
  function parseJsonc(text2) {
549
568
  let result = "";
@@ -599,6 +618,31 @@ function findConfigFile(startDir) {
599
618
  }
600
619
  }
601
620
  var VALID_INDENT_MODES = /* @__PURE__ */ new Set(["never", "always", "attribute"]);
621
+ function parseCustomTagArray(arr) {
622
+ if (!Array.isArray(arr)) return [];
623
+ const tags = [];
624
+ for (const entry of arr) {
625
+ if (entry && typeof entry === "object" && "name" in entry) {
626
+ const e = entry;
627
+ if (typeof e.name !== "string" || e.name.length === 0) continue;
628
+ const tag = { name: e.name };
629
+ if (typeof e.display === "string" && VALID_CSS_DISPLAY_VALUES.has(e.display)) {
630
+ tag.display = e.display;
631
+ }
632
+ if (typeof e.languageAttribute === "string") tag.languageAttribute = e.languageAttribute;
633
+ if (e.languageMap && typeof e.languageMap === "object" && !Array.isArray(e.languageMap)) {
634
+ tag.languageMap = e.languageMap;
635
+ }
636
+ if (typeof e.languageDefault === "string") tag.languageDefault = e.languageDefault;
637
+ if (typeof e.indent === "string" && VALID_INDENT_MODES.has(e.indent)) {
638
+ tag.indent = e.indent;
639
+ }
640
+ if (typeof e.indentAttribute === "string") tag.indentAttribute = e.indentAttribute;
641
+ tags.push(tag);
642
+ }
643
+ }
644
+ return tags;
645
+ }
602
646
  function validateConfig(raw) {
603
647
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
604
648
  const obj = raw;
@@ -612,6 +656,12 @@ function validateConfig(raw) {
612
656
  if (typeof obj.mustacheSpaces === "boolean") {
613
657
  config.mustacheSpaces = obj.mustacheSpaces;
614
658
  }
659
+ if (Array.isArray(obj.noBreakDelimiters)) {
660
+ const items = obj.noBreakDelimiters.filter(
661
+ (s) => typeof s === "string" && s.length > 0
662
+ );
663
+ if (items.length > 0) config.noBreakDelimiters = items;
664
+ }
615
665
  if (Array.isArray(obj.include)) {
616
666
  const items = obj.include.filter((s) => typeof s === "string" && s.length > 0);
617
667
  if (items.length > 0) config.include = items;
@@ -620,26 +670,17 @@ function validateConfig(raw) {
620
670
  const items = obj.exclude.filter((s) => typeof s === "string" && s.length > 0);
621
671
  if (items.length > 0) config.exclude = items;
622
672
  }
623
- if (Array.isArray(obj.customCodeTags)) {
624
- const tags = [];
625
- for (const entry of obj.customCodeTags) {
626
- if (entry && typeof entry === "object" && "name" in entry) {
627
- const e = entry;
628
- if (typeof e.name !== "string" || e.name.length === 0) continue;
629
- const tag = { name: e.name };
630
- if (typeof e.languageAttribute === "string") tag.languageAttribute = e.languageAttribute;
631
- if (e.languageMap && typeof e.languageMap === "object" && !Array.isArray(e.languageMap)) {
632
- tag.languageMap = e.languageMap;
633
- }
634
- if (typeof e.languageDefault === "string") tag.languageDefault = e.languageDefault;
635
- if (typeof e.indent === "string" && VALID_INDENT_MODES.has(e.indent)) {
636
- tag.indent = e.indent;
637
- }
638
- if (typeof e.indentAttribute === "string") tag.indentAttribute = e.indentAttribute;
639
- tags.push(tag);
640
- }
673
+ const parsedCodeTags = parseCustomTagArray(obj.customCodeTags);
674
+ const parsedCustomTags = parseCustomTagArray(obj.customTags);
675
+ if (parsedCodeTags.length > 0 || parsedCustomTags.length > 0) {
676
+ const mergedMap = /* @__PURE__ */ new Map();
677
+ for (const tag of parsedCodeTags) {
678
+ mergedMap.set(tag.name.toLowerCase(), tag);
641
679
  }
642
- if (tags.length > 0) config.customCodeTags = tags;
680
+ for (const tag of parsedCustomTags) {
681
+ mergedMap.set(tag.name.toLowerCase(), tag);
682
+ }
683
+ config.customTags = Array.from(mergedMap.values());
643
684
  }
644
685
  return config;
645
686
  }
@@ -1798,7 +1839,7 @@ function getWellformedEdit(textEdit) {
1798
1839
  // lsp/server/src/formatting/printer.ts
1799
1840
  function print(doc, options) {
1800
1841
  const output = [];
1801
- const state = { indentLevel: 0, mode: "break" };
1842
+ const state = { indentLevel: 0, mode: "break", groupModes: /* @__PURE__ */ new Map() };
1802
1843
  printDoc(doc, state, output, options);
1803
1844
  return output.join("");
1804
1845
  }
@@ -1872,20 +1913,23 @@ function printDoc(doc, state, output, options) {
1872
1913
  if (doc.break || containsBreakParent(doc.contents)) {
1873
1914
  const prevMode = state.mode;
1874
1915
  state.mode = "break";
1916
+ if (doc.id) state.groupModes.set(doc.id, "break");
1875
1917
  printDoc(doc.contents, state, output, options);
1876
1918
  state.mode = prevMode;
1877
1919
  } else {
1878
1920
  const flatOutput = [];
1879
- const flatState = { ...state, mode: "flat" };
1921
+ const flatState = { ...state, mode: "flat", groupModes: new Map(state.groupModes) };
1880
1922
  printDoc(doc.contents, flatState, flatOutput, options);
1881
1923
  const flatContent = flatOutput.join("");
1882
1924
  const printWidth = options.printWidth ?? 80;
1883
1925
  const col = currentColumn(output);
1884
1926
  if (!flatContent.includes("\n") && col + flatContent.length <= printWidth) {
1927
+ if (doc.id) state.groupModes.set(doc.id, "flat");
1885
1928
  output.push(flatContent);
1886
1929
  } else {
1887
1930
  const prevMode = state.mode;
1888
1931
  state.mode = "break";
1932
+ if (doc.id) state.groupModes.set(doc.id, "break");
1889
1933
  printDoc(doc.contents, state, output, options);
1890
1934
  state.mode = prevMode;
1891
1935
  }
@@ -1895,13 +1939,15 @@ function printDoc(doc, state, output, options) {
1895
1939
  case "fill":
1896
1940
  printFill(doc.parts, state, output, options);
1897
1941
  break;
1898
- case "ifBreak":
1899
- if (state.mode === "break") {
1942
+ case "ifBreak": {
1943
+ const effectiveMode = doc.groupId ? state.groupModes.get(doc.groupId) ?? state.mode : state.mode;
1944
+ if (effectiveMode === "break") {
1900
1945
  printDoc(doc.breakContents, state, output, options);
1901
1946
  } else {
1902
1947
  printDoc(doc.flatContents, state, output, options);
1903
1948
  }
1904
1949
  break;
1950
+ }
1905
1951
  case "breakParent":
1906
1952
  state.mode = "break";
1907
1953
  break;
@@ -1966,12 +2012,21 @@ function indent(contents) {
1966
2012
  if (contents === "") return "";
1967
2013
  return { type: "indent", contents };
1968
2014
  }
1969
- function group(contents, shouldBreak = false) {
2015
+ function indentN(contents, n) {
2016
+ if (n <= 0 || contents === "") return contents;
2017
+ let result = contents;
2018
+ for (let i = 0; i < n; i++) {
2019
+ result = indent(result);
2020
+ }
2021
+ return result;
2022
+ }
2023
+ function group(contents, options) {
1970
2024
  if (contents === "") return "";
1971
- return { type: "group", contents, break: shouldBreak || void 0 };
2025
+ const shouldBreak = options?.shouldBreak;
2026
+ return { type: "group", contents, break: shouldBreak || void 0, id: options?.id };
1972
2027
  }
1973
- function ifBreak(breakContents, flatContents) {
1974
- return { type: "ifBreak", breakContents, flatContents };
2028
+ function ifBreak(breakContents, flatContents, options) {
2029
+ return { type: "ifBreak", breakContents, flatContents, groupId: options?.groupId };
1975
2030
  }
1976
2031
  function fill(parts) {
1977
2032
  const filtered = parts.filter((p) => p !== "");
@@ -2059,8 +2114,37 @@ function getIgnoreDirective(node) {
2059
2114
  return null;
2060
2115
  }
2061
2116
 
2117
+ // lsp/server/src/customCodeTags.ts
2118
+ function isCodeTag(config) {
2119
+ return !!(config.languageAttribute || config.languageDefault);
2120
+ }
2121
+ function getAttributeValue(node, attrName) {
2122
+ for (let i = 0; i < node.childCount; i++) {
2123
+ const child = node.child(i);
2124
+ if (child?.type === "html_start_tag") {
2125
+ for (let j = 0; j < child.childCount; j++) {
2126
+ const attr = child.child(j);
2127
+ if (attr?.type === "html_attribute") {
2128
+ let name = "";
2129
+ let value = "";
2130
+ for (let k = 0; k < attr.childCount; k++) {
2131
+ const part = attr.child(k);
2132
+ if (part?.type === "html_attribute_name") name = part.text.toLowerCase();
2133
+ if (part?.type === "html_quoted_attribute_value") value = part.text.replace(/^["']|["']$/g, "");
2134
+ if (part?.type === "html_attribute_value") value = part.text;
2135
+ }
2136
+ if (name === attrName.toLowerCase()) {
2137
+ return value;
2138
+ }
2139
+ }
2140
+ }
2141
+ }
2142
+ }
2143
+ return null;
2144
+ }
2145
+
2062
2146
  // lsp/server/src/formatting/classifier.ts
2063
- var EMPTY_SET = /* @__PURE__ */ new Set();
2147
+ var EMPTY_MAP = /* @__PURE__ */ new Map();
2064
2148
  var CSS_DISPLAY_MAP = {
2065
2149
  // Block elements
2066
2150
  address: "block",
@@ -2151,15 +2235,18 @@ var PRESERVE_CONTENT_ELEMENTS = /* @__PURE__ */ new Set([
2151
2235
  "script",
2152
2236
  "style"
2153
2237
  ]);
2154
- function getCSSDisplay(node, customCodeTags = EMPTY_SET) {
2238
+ function getCSSDisplay(node, customTags = EMPTY_MAP) {
2155
2239
  const type = node.type;
2156
2240
  if (type === "html_element") {
2157
2241
  const tagName = getTagName(node);
2158
2242
  if (tagName) {
2159
- if (customCodeTags.has(tagName.toLowerCase())) {
2160
- return "block";
2243
+ const lower = tagName.toLowerCase();
2244
+ const config = customTags.get(lower);
2245
+ if (config) {
2246
+ if (config.display) return config.display;
2247
+ if (isCodeTag(config)) return "block";
2161
2248
  }
2162
- return CSS_DISPLAY_MAP[tagName.toLowerCase()] ?? "inline";
2249
+ return CSS_DISPLAY_MAP[lower] ?? "inline";
2163
2250
  }
2164
2251
  return "block";
2165
2252
  }
@@ -2167,7 +2254,7 @@ function getCSSDisplay(node, customCodeTags = EMPTY_SET) {
2167
2254
  return "block";
2168
2255
  }
2169
2256
  if (isMustacheSection(node)) {
2170
- return hasBlockContent(node, customCodeTags) ? "block" : "inline";
2257
+ return hasBlockContent(node, customTags) ? "block" : "inline";
2171
2258
  }
2172
2259
  return "inline";
2173
2260
  }
@@ -2190,13 +2277,13 @@ function isWhitespaceInsensitive(display) {
2190
2277
  return false;
2191
2278
  }
2192
2279
  }
2193
- function isBlockLevel(node, customCodeTags = EMPTY_SET) {
2280
+ function isBlockLevel(node, customTags = EMPTY_MAP) {
2194
2281
  const type = node.type;
2195
2282
  if (isMustacheSection(node)) {
2196
- return hasBlockContent(node, customCodeTags);
2283
+ return hasBlockContent(node, customTags);
2197
2284
  }
2198
2285
  if (type === "html_element") {
2199
- const display = getCSSDisplay(node, customCodeTags);
2286
+ const display = getCSSDisplay(node, customTags);
2200
2287
  return isWhitespaceInsensitive(display);
2201
2288
  }
2202
2289
  if (isRawContentElement(node)) {
@@ -2204,7 +2291,7 @@ function isBlockLevel(node, customCodeTags = EMPTY_SET) {
2204
2291
  }
2205
2292
  return false;
2206
2293
  }
2207
- function shouldPreserveContent(node, customCodeTags = EMPTY_SET) {
2294
+ function shouldPreserveContent(node, customTags = EMPTY_MAP) {
2208
2295
  const type = node.type;
2209
2296
  if (isRawContentElement(node)) {
2210
2297
  return true;
@@ -2213,23 +2300,25 @@ function shouldPreserveContent(node, customCodeTags = EMPTY_SET) {
2213
2300
  const tagName = getTagName(node);
2214
2301
  if (!tagName) return false;
2215
2302
  const lower = tagName.toLowerCase();
2216
- return PRESERVE_CONTENT_ELEMENTS.has(lower) || customCodeTags.has(lower);
2303
+ if (PRESERVE_CONTENT_ELEMENTS.has(lower)) return true;
2304
+ const config = customTags.get(lower);
2305
+ if (config && isCodeTag(config)) return true;
2217
2306
  }
2218
2307
  return false;
2219
2308
  }
2220
- function hasBlockContent(sectionNode, customCodeTags = EMPTY_SET) {
2309
+ function hasBlockContent(sectionNode, customTags = EMPTY_MAP) {
2221
2310
  const contentNodes = getContentNodes(sectionNode);
2222
2311
  if (hasImplicitEndTags(contentNodes)) {
2223
2312
  return true;
2224
2313
  }
2225
2314
  for (const node of contentNodes) {
2226
- if (isBlockLevelContent(node, customCodeTags)) {
2315
+ if (isBlockLevelContent(node, customTags)) {
2227
2316
  return true;
2228
2317
  }
2229
2318
  }
2230
2319
  return false;
2231
2320
  }
2232
- function isBlockLevelContent(node, customCodeTags = EMPTY_SET) {
2321
+ function isBlockLevelContent(node, customTags = EMPTY_MAP) {
2233
2322
  const type = node.type;
2234
2323
  if (type === "html_element") {
2235
2324
  return true;
@@ -2238,7 +2327,7 @@ function isBlockLevelContent(node, customCodeTags = EMPTY_SET) {
2238
2327
  return true;
2239
2328
  }
2240
2329
  if (isMustacheSection(node)) {
2241
- return hasBlockContent(node, customCodeTags);
2330
+ return hasBlockContent(node, customTags);
2242
2331
  }
2243
2332
  return false;
2244
2333
  }
@@ -2321,10 +2410,13 @@ function hasAdjacentInlineContent(index, nodes) {
2321
2410
  }
2322
2411
  return false;
2323
2412
  }
2324
- function shouldHtmlElementStayInline(node, index, nodes) {
2413
+ function shouldHtmlElementStayInline(node, index, nodes, customTags = EMPTY_MAP) {
2325
2414
  if (node.type !== "html_element") {
2326
2415
  return false;
2327
2416
  }
2417
+ if (isWhitespaceInsensitive(getCSSDisplay(node, customTags))) {
2418
+ return false;
2419
+ }
2328
2420
  if (isInTextFlow(node, index, nodes)) {
2329
2421
  return true;
2330
2422
  }
@@ -2345,57 +2437,11 @@ function shouldHtmlElementStayInline(node, index, nodes) {
2345
2437
  }
2346
2438
  return false;
2347
2439
  }
2348
- function shouldTreatAsBlock(node, index, nodes, customCodeTags = EMPTY_SET) {
2440
+ function shouldTreatAsBlock(node, index, nodes, customTags = EMPTY_MAP) {
2349
2441
  const isHtmlEl = isHtmlElementType(node);
2350
2442
  const isMustacheSec = isMustacheSection(node);
2351
- return isHtmlEl && !shouldHtmlElementStayInline(node, index, nodes) || isMustacheSec && !isInTextFlow(node, index, nodes) || isBlockLevel(node, customCodeTags) && !isInTextFlow(node, index, nodes);
2352
- }
2353
-
2354
- // lsp/server/src/customCodeTags.ts
2355
- var VALID_INDENT_MODES2 = /* @__PURE__ */ new Set(["never", "always", "attribute"]);
2356
- function parseCustomCodeTagSettings(tags) {
2357
- const tagNames = [];
2358
- const configs = [];
2359
- for (const tag of tags) {
2360
- if (tag && typeof tag === "object" && "name" in tag && typeof tag.name === "string") {
2361
- const t = tag;
2362
- const config = { name: t.name };
2363
- if (typeof t.languageAttribute === "string") config.languageAttribute = t.languageAttribute;
2364
- if (t.languageMap && typeof t.languageMap === "object") config.languageMap = t.languageMap;
2365
- if (typeof t.languageDefault === "string") config.languageDefault = t.languageDefault;
2366
- if (typeof t.indent === "string" && VALID_INDENT_MODES2.has(t.indent)) {
2367
- config.indent = t.indent;
2368
- }
2369
- if (typeof t.indentAttribute === "string") config.indentAttribute = t.indentAttribute;
2370
- tagNames.push(config.name);
2371
- configs.push(config);
2372
- }
2373
- }
2374
- return { tagNames, configs };
2375
- }
2376
- function getAttributeValue(node, attrName) {
2377
- for (let i = 0; i < node.childCount; i++) {
2378
- const child = node.child(i);
2379
- if (child?.type === "html_start_tag") {
2380
- for (let j = 0; j < child.childCount; j++) {
2381
- const attr = child.child(j);
2382
- if (attr?.type === "html_attribute") {
2383
- let name = "";
2384
- let value = "";
2385
- for (let k = 0; k < attr.childCount; k++) {
2386
- const part = attr.child(k);
2387
- if (part?.type === "html_attribute_name") name = part.text.toLowerCase();
2388
- if (part?.type === "html_quoted_attribute_value") value = part.text.replace(/^["']|["']$/g, "");
2389
- if (part?.type === "html_attribute_value") value = part.text;
2390
- }
2391
- if (name === attrName.toLowerCase()) {
2392
- return value;
2393
- }
2394
- }
2395
- }
2396
- }
2397
- }
2398
- return null;
2443
+ if (node.type === "html_erroneous_end_tag") return true;
2444
+ return isHtmlEl && !shouldHtmlElementStayInline(node, index, nodes, customTags) || isMustacheSec && !isInTextFlow(node, index, nodes) || isBlockLevel(node, customTags) && !isInTextFlow(node, index, nodes);
2399
2445
  }
2400
2446
 
2401
2447
  // lsp/server/src/formatting/formatters.ts
@@ -2457,7 +2503,7 @@ function formatNode(node, context, forceInline = false) {
2457
2503
  case "document":
2458
2504
  return formatDocument(node, context);
2459
2505
  case "html_element":
2460
- return formatHtmlElement(node, context);
2506
+ return formatHtmlElement(node, context, forceInline);
2461
2507
  case "html_script_element":
2462
2508
  case "html_style_element":
2463
2509
  case "html_raw_element":
@@ -2490,8 +2536,8 @@ function formatNode(node, context, forceInline = false) {
2490
2536
  function formatText(node) {
2491
2537
  return text(normalizeText(node.text));
2492
2538
  }
2493
- function formatHtmlElement(node, context) {
2494
- const tags = context.customCodeTags;
2539
+ function formatHtmlElement(node, context, forceInline = false) {
2540
+ const tags = context.customTags;
2495
2541
  const display = getCSSDisplay(node, tags);
2496
2542
  const isBlock = isWhitespaceInsensitive(display);
2497
2543
  const preserveContent = shouldPreserveContent(node, tags);
@@ -2527,7 +2573,7 @@ function formatHtmlElement(node, context) {
2527
2573
  );
2528
2574
  if (preserveContent) {
2529
2575
  const tagNameLower = startTag ? getTagNameFromStartTag(startTag) : null;
2530
- const tagConfig = tagNameLower ? context.customCodeTagConfigs?.get(tagNameLower) : void 0;
2576
+ const tagConfig = tagNameLower ? context.customTags?.get(tagNameLower) : void 0;
2531
2577
  const shouldIndent = tagConfig ? resolveIndentMode(node, tagConfig) : false;
2532
2578
  if (shouldIndent && startTag && endTag) {
2533
2579
  const rawContent = context.document.getText().slice(
@@ -2570,7 +2616,26 @@ function formatHtmlElement(node, context) {
2570
2616
  parts.push(text(child.text));
2571
2617
  }
2572
2618
  }
2573
- } else if (!isBlock && !hasHtmlElementChildren) {
2619
+ } else if (!isBlock && (!hasHtmlElementChildren || forceInline && !contentNodes.some(
2620
+ (child) => isRawContentElement(child) || isBlockLevel(child, tags)
2621
+ ))) {
2622
+ if (!forceInline && startTag && startTagHasAttributes(startTag)) {
2623
+ const formattedContent = formatBlockChildren(contentNodes, context);
2624
+ if (hasDocContent(formattedContent)) {
2625
+ const bareStartTag = formatStartTag(startTag, context, true);
2626
+ const outerParts = [
2627
+ group(bareStartTag),
2628
+ indent(concat([softline, formattedContent]))
2629
+ ];
2630
+ if (hasRealEndTag) {
2631
+ outerParts.push(softline);
2632
+ }
2633
+ if (endTag) {
2634
+ outerParts.push(formatEndTag(endTag));
2635
+ }
2636
+ return group(concat(outerParts));
2637
+ }
2638
+ }
2574
2639
  let prevEnd = startTag ? startTag.endIndex : -1;
2575
2640
  for (const child of contentNodes) {
2576
2641
  if (prevEnd >= 0 && child.startIndex > prevEnd) {
@@ -2579,7 +2644,7 @@ function formatHtmlElement(node, context) {
2579
2644
  parts.push(text(" "));
2580
2645
  }
2581
2646
  }
2582
- parts.push(formatNode(child, context));
2647
+ parts.push(formatNode(child, context, forceInline));
2583
2648
  prevEnd = child.endIndex;
2584
2649
  }
2585
2650
  } else {
@@ -2594,6 +2659,21 @@ function formatHtmlElement(node, context) {
2594
2659
  return isWhitespaceInsensitive(childDisplay) || isRawContentElement(child);
2595
2660
  });
2596
2661
  if (isBlock && !hasBlockChildren) {
2662
+ const hasAttrs = startTag && startTagHasAttributes(startTag);
2663
+ if (hasAttrs && startTag) {
2664
+ const bareStartTag = formatStartTag(startTag, context, true);
2665
+ const outerParts = [
2666
+ group(bareStartTag),
2667
+ indent(concat([softline, formattedContent]))
2668
+ ];
2669
+ if (hasRealEndTag) {
2670
+ outerParts.push(softline);
2671
+ }
2672
+ if (endTag) {
2673
+ outerParts.push(formatEndTag(endTag));
2674
+ }
2675
+ return group(concat(outerParts));
2676
+ }
2597
2677
  const doc = group(
2598
2678
  concat([
2599
2679
  indent(concat([softline, formattedContent])),
@@ -2656,7 +2736,7 @@ function formatScriptStyleElement(node, context) {
2656
2736
  if (node.type === "html_raw_element") {
2657
2737
  const startTagNode = node.child(0);
2658
2738
  const tagNameLower = startTagNode?.type === "html_start_tag" ? getTagNameFromStartTag(startTagNode) : null;
2659
- const tagConfig = tagNameLower ? context.customCodeTagConfigs?.get(tagNameLower) : void 0;
2739
+ const tagConfig = tagNameLower ? context.customTags?.get(tagNameLower) : void 0;
2660
2740
  if (tagConfig && resolveIndentMode(node, tagConfig)) {
2661
2741
  const dedented = dedentContent(child.text);
2662
2742
  if (dedented.length > 0) {
@@ -2733,27 +2813,74 @@ function formatMustacheSection(node, context) {
2733
2813
  parts.push(text(mustacheText(beginNode.text, context)));
2734
2814
  }
2735
2815
  const hasImplicit = hasImplicitEndTags(contentNodes);
2736
- const formattedContent = formatBlockChildren(contentNodes, context);
2737
- const hasContent = hasDocContent(formattedContent);
2738
- if (hasContent) {
2739
- if (hasImplicit) {
2740
- parts.push(hardline);
2741
- parts.push(formattedContent);
2742
- parts.push(hardline);
2743
- } else {
2744
- const hasBlockChildren = contentNodes.some((child, i) => {
2745
- if (!shouldTreatAsBlock(child, i, contentNodes, context.customCodeTags)) {
2746
- return false;
2816
+ const erroneousCount = contentNodes.filter((n) => n.type === "html_erroneous_end_tag").length;
2817
+ const hasStaircase = !hasImplicit && erroneousCount > 0;
2818
+ if (hasStaircase) {
2819
+ let virtualDepth = erroneousCount - 1;
2820
+ const groupNodes = [];
2821
+ let lastNodeEnd = -1;
2822
+ let pendingBlankLine = false;
2823
+ let groupBlankLine = false;
2824
+ const emitGroup = () => {
2825
+ if (groupNodes.length === 0) return;
2826
+ const formatted = formatBlockChildren(groupNodes, context);
2827
+ if (hasDocContent(formatted)) {
2828
+ if (groupBlankLine) parts.push("\n");
2829
+ const depth = Math.max(0, virtualDepth + 1);
2830
+ parts.push(depth > 0 ? indentN(concat([hardline, formatted]), depth) : concat([hardline, formatted]));
2831
+ }
2832
+ groupNodes.length = 0;
2833
+ groupBlankLine = false;
2834
+ };
2835
+ for (const node2 of contentNodes) {
2836
+ if (lastNodeEnd >= 0 && node2.startIndex > lastNodeEnd) {
2837
+ const gap = context.document.getText().slice(lastNodeEnd, node2.startIndex);
2838
+ if ((gap.match(/\n/g) || []).length >= 2) {
2839
+ pendingBlankLine = true;
2747
2840
  }
2748
- const childDisplay = getCSSDisplay(child, context.customCodeTags);
2749
- return isWhitespaceInsensitive(childDisplay) || isRawContentElement(child);
2750
- });
2751
- if (!hasBlockChildren) {
2752
- parts.push(indent(concat([softline, formattedContent])));
2753
- parts.push(softline);
2841
+ }
2842
+ if (node2.type === "html_erroneous_end_tag") {
2843
+ emitGroup();
2844
+ if (pendingBlankLine) parts.push("\n");
2845
+ pendingBlankLine = false;
2846
+ const formatted = formatNode(node2, context);
2847
+ const depth = Math.max(0, virtualDepth);
2848
+ parts.push(depth > 0 ? indentN(concat([hardline, formatted]), depth) : concat([hardline, formatted]));
2849
+ virtualDepth--;
2754
2850
  } else {
2755
- parts.push(indent(concat([hardline, formattedContent])));
2851
+ if (groupNodes.length === 0) {
2852
+ groupBlankLine = pendingBlankLine;
2853
+ pendingBlankLine = false;
2854
+ }
2855
+ groupNodes.push(node2);
2856
+ }
2857
+ lastNodeEnd = node2.endIndex;
2858
+ }
2859
+ emitGroup();
2860
+ parts.push(hardline);
2861
+ } else {
2862
+ const formattedContent = formatBlockChildren(contentNodes, context);
2863
+ const hasContent = hasDocContent(formattedContent);
2864
+ if (hasContent) {
2865
+ if (hasImplicit) {
2866
+ parts.push(hardline);
2867
+ parts.push(formattedContent);
2756
2868
  parts.push(hardline);
2869
+ } else {
2870
+ const hasBlockChildren = contentNodes.some((child, i) => {
2871
+ if (!shouldTreatAsBlock(child, i, contentNodes, context.customTags)) {
2872
+ return false;
2873
+ }
2874
+ const childDisplay = getCSSDisplay(child, context.customTags);
2875
+ return isWhitespaceInsensitive(childDisplay) || isRawContentElement(child);
2876
+ });
2877
+ if (!hasBlockChildren) {
2878
+ parts.push(indent(concat([softline, formattedContent])));
2879
+ parts.push(softline);
2880
+ } else {
2881
+ parts.push(indent(concat([hardline, formattedContent])));
2882
+ parts.push(hardline);
2883
+ }
2757
2884
  }
2758
2885
  }
2759
2886
  }
@@ -2762,7 +2889,17 @@ function formatMustacheSection(node, context) {
2762
2889
  }
2763
2890
  return group(concat(parts));
2764
2891
  }
2765
- function formatStartTag(node, context) {
2892
+ function startTagHasAttributes(startTag) {
2893
+ for (let i = 0; i < startTag.childCount; i++) {
2894
+ const child = startTag.child(i);
2895
+ if (!child) continue;
2896
+ if (child.type === "html_attribute" || child.type === "mustache_attribute" || child.type === "mustache_interpolation" || child.type === "mustache_triple") {
2897
+ return true;
2898
+ }
2899
+ }
2900
+ return false;
2901
+ }
2902
+ function formatStartTag(node, context, bare = false) {
2766
2903
  let tagNameText = "";
2767
2904
  const attrs = [];
2768
2905
  for (let i = 0; i < node.childCount; i++) {
@@ -2795,14 +2932,13 @@ function formatStartTag(node, context) {
2795
2932
  attrParts.push(attrs[i]);
2796
2933
  }
2797
2934
  const breakClosingBracket = isSelfClosing ? "/>" : ">";
2798
- return group(
2799
- concat([
2800
- text("<"),
2801
- text(tagNameText),
2802
- indent(concat([line, concat(attrParts)])),
2803
- ifBreak(concat([hardline, text(breakClosingBracket)]), text(closingBracket))
2804
- ])
2805
- );
2935
+ const inner = concat([
2936
+ text("<"),
2937
+ text(tagNameText),
2938
+ indent(concat([line, concat(attrParts)])),
2939
+ ifBreak(concat([hardline, text(breakClosingBracket)]), text(closingBracket))
2940
+ ]);
2941
+ return bare ? inner : group(inner);
2806
2942
  }
2807
2943
  function formatEndTag(node) {
2808
2944
  for (let i = 0; i < node.childCount; i++) {
@@ -2847,6 +2983,38 @@ function textWords(str) {
2847
2983
  }
2848
2984
  return parts;
2849
2985
  }
2986
+ function collapseDelimitedRegions(parts, delimiters) {
2987
+ if (delimiters.length === 0) return parts;
2988
+ const sorted = [...delimiters].sort((a, b) => b.length - a.length);
2989
+ const result = [...parts];
2990
+ let activeDelimiter = null;
2991
+ for (let i = 0; i < result.length; i++) {
2992
+ const part = result[i];
2993
+ if (typeof part === "string") {
2994
+ if (activeDelimiter === null) {
2995
+ for (const delim of sorted) {
2996
+ const delimIdx = part.indexOf(delim);
2997
+ if (delimIdx >= 0) {
2998
+ const afterOpen = delimIdx + delim.length;
2999
+ const closeIdx = part.indexOf(delim, afterOpen);
3000
+ if (closeIdx >= 0) {
3001
+ continue;
3002
+ }
3003
+ activeDelimiter = delim;
3004
+ break;
3005
+ }
3006
+ }
3007
+ } else {
3008
+ if (part.includes(activeDelimiter)) {
3009
+ activeDelimiter = null;
3010
+ }
3011
+ }
3012
+ } else if (activeDelimiter !== null && isLine(part)) {
3013
+ result[i] = " ";
3014
+ }
3015
+ }
3016
+ return result;
3017
+ }
2850
3018
  function inlineContentToFill(parts) {
2851
3019
  if (parts.length === 0) return empty;
2852
3020
  if (parts.length === 1) return parts[0];
@@ -2889,6 +3057,11 @@ function formatBlockChildren(nodes, context) {
2889
3057
  let ignoreNext = false;
2890
3058
  let inIgnoreRegion = false;
2891
3059
  let ignoreRegionStartIndex = -1;
3060
+ const noBreakDelims = context.noBreakDelimiters;
3061
+ function flushCurrentLine() {
3062
+ const parts2 = noBreakDelims ? collapseDelimitedRegions(currentLine, noBreakDelims) : currentLine;
3063
+ return inlineContentToFill(parts2);
3064
+ }
2892
3065
  for (let i = 0; i < nodes.length; i++) {
2893
3066
  const node = nodes[i];
2894
3067
  if (lastNodeEnd >= 0 && node.startIndex > lastNodeEnd && !inIgnoreRegion) {
@@ -2901,7 +3074,7 @@ function formatBlockChildren(nodes, context) {
2901
3074
  const directive = getIgnoreDirective(node);
2902
3075
  if (directive === "ignore-end" && inIgnoreRegion) {
2903
3076
  if (currentLine.length > 0) {
2904
- const lineContent = trimDoc(inlineContentToFill(currentLine));
3077
+ const lineContent = trimDoc(flushCurrentLine());
2905
3078
  if (hasDocContent(lineContent)) {
2906
3079
  lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
2907
3080
  }
@@ -2925,7 +3098,7 @@ function formatBlockChildren(nodes, context) {
2925
3098
  }
2926
3099
  if (directive === "ignore-start") {
2927
3100
  if (currentLine.length > 0) {
2928
- const lineContent = trimDoc(inlineContentToFill(currentLine));
3101
+ const lineContent = trimDoc(flushCurrentLine());
2929
3102
  if (hasDocContent(lineContent)) {
2930
3103
  lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
2931
3104
  }
@@ -2942,7 +3115,7 @@ function formatBlockChildren(nodes, context) {
2942
3115
  }
2943
3116
  if (directive === "ignore") {
2944
3117
  if (currentLine.length > 0) {
2945
- const lineContent = trimDoc(inlineContentToFill(currentLine));
3118
+ const lineContent = trimDoc(flushCurrentLine());
2946
3119
  if (hasDocContent(lineContent)) {
2947
3120
  lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
2948
3121
  }
@@ -2963,10 +3136,10 @@ function formatBlockChildren(nodes, context) {
2963
3136
  lastNodeEnd = node.endIndex;
2964
3137
  continue;
2965
3138
  }
2966
- const treatAsBlock = shouldTreatAsBlock(node, i, nodes, context.customCodeTags);
3139
+ const treatAsBlock = shouldTreatAsBlock(node, i, nodes, context.customTags);
2967
3140
  if (lastNodeEnd >= 0 && node.startIndex > lastNodeEnd) {
2968
3141
  const prevNode = nodes[i - 1];
2969
- const prevTreatAsBlock = shouldTreatAsBlock(prevNode, i - 1, nodes, context.customCodeTags);
3142
+ const prevTreatAsBlock = shouldTreatAsBlock(prevNode, i - 1, nodes, context.customTags);
2970
3143
  if (!prevTreatAsBlock && !treatAsBlock) {
2971
3144
  const gap = context.document.getText().slice(lastNodeEnd, node.startIndex);
2972
3145
  if (/\s/.test(gap)) {
@@ -2976,7 +3149,7 @@ function formatBlockChildren(nodes, context) {
2976
3149
  }
2977
3150
  if (treatAsBlock) {
2978
3151
  if (currentLine.length > 0) {
2979
- const lineContent = trimDoc(inlineContentToFill(currentLine));
3152
+ const lineContent = trimDoc(flushCurrentLine());
2980
3153
  if (hasDocContent(lineContent)) {
2981
3154
  lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
2982
3155
  }
@@ -2989,7 +3162,7 @@ function formatBlockChildren(nodes, context) {
2989
3162
  const isMultiline = node.startPosition.row !== node.endPosition.row;
2990
3163
  if (isMultiline) {
2991
3164
  if (currentLine.length > 0) {
2992
- const lineContent = trimDoc(inlineContentToFill(currentLine));
3165
+ const lineContent = trimDoc(flushCurrentLine());
2993
3166
  if (hasDocContent(lineContent)) {
2994
3167
  lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
2995
3168
  }
@@ -3022,7 +3195,7 @@ function formatBlockChildren(nodes, context) {
3022
3195
  const trimmed = contentLines[j].trim();
3023
3196
  if (!trimmed) {
3024
3197
  if (currentLine.length > 0) {
3025
- const lineContent = trimDoc(inlineContentToFill(currentLine));
3198
+ const lineContent = trimDoc(flushCurrentLine());
3026
3199
  if (hasDocContent(lineContent)) {
3027
3200
  lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
3028
3201
  blankLineBeforeCurrentLine = false;
@@ -3047,7 +3220,7 @@ function formatBlockChildren(nodes, context) {
3047
3220
  currentLine.push(firstTrimmed);
3048
3221
  }
3049
3222
  if (currentLine.length > 0) {
3050
- const lineContent = trimDoc(inlineContentToFill(currentLine));
3223
+ const lineContent = trimDoc(flushCurrentLine());
3051
3224
  if (hasDocContent(lineContent)) {
3052
3225
  lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
3053
3226
  blankLineBeforeCurrentLine = pendingBlankLine;
@@ -3101,7 +3274,7 @@ function formatBlockChildren(nodes, context) {
3101
3274
  }
3102
3275
  }
3103
3276
  if (currentLine.length > 0) {
3104
- const lineContent = trimDoc(inlineContentToFill(currentLine));
3277
+ const lineContent = trimDoc(flushCurrentLine());
3105
3278
  if (hasDocContent(lineContent)) {
3106
3279
  lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
3107
3280
  }
@@ -3193,21 +3366,28 @@ function createIndentUnit(options) {
3193
3366
  }
3194
3367
 
3195
3368
  // lsp/server/src/formatting/index.ts
3369
+ function buildCustomTagMap(customTags) {
3370
+ if (!customTags || customTags.length === 0) return void 0;
3371
+ const map = /* @__PURE__ */ new Map();
3372
+ for (const config of customTags) {
3373
+ map.set(config.name.toLowerCase(), config);
3374
+ }
3375
+ return map;
3376
+ }
3196
3377
  function formatDocument2(tree, document, options, params = {}) {
3197
- const { customCodeTags, printWidth = 80, embeddedFormatted, mustacheSpaces, customCodeTagConfigs, configFile } = params;
3378
+ const { printWidth = 80, embeddedFormatted, mustacheSpaces, noBreakDelimiters, configFile } = params;
3198
3379
  const mergedOptions = mergeOptions(options, document.uri, configFile);
3199
3380
  const indentUnit = createIndentUnit(mergedOptions);
3200
3381
  if (tree.rootNode.hasError) {
3201
3382
  return [];
3202
3383
  }
3203
- const configMap = buildConfigMap(customCodeTagConfigs);
3204
- const customCodeTagSet = customCodeTags ? new Set(customCodeTags.map((t) => t.toLowerCase())) : void 0;
3384
+ const customTagMap = buildCustomTagMap(params.customTags);
3205
3385
  const context = {
3206
3386
  document,
3207
- customCodeTags: customCodeTagSet,
3208
- customCodeTagConfigs: configMap,
3387
+ customTags: customTagMap,
3209
3388
  embeddedFormatted,
3210
- mustacheSpaces
3389
+ mustacheSpaces,
3390
+ noBreakDelimiters
3211
3391
  };
3212
3392
  const doc = formatDocument(tree.rootNode, context);
3213
3393
  const formatted = print(doc, { indentUnit, printWidth });
@@ -3217,14 +3397,6 @@ function formatDocument2(tree, document, options, params = {}) {
3217
3397
  };
3218
3398
  return [{ range: fullRange, newText: formatted }];
3219
3399
  }
3220
- function buildConfigMap(configs) {
3221
- if (!configs || configs.length === 0) return void 0;
3222
- const map = /* @__PURE__ */ new Map();
3223
- for (const config of configs) {
3224
- map.set(config.name.toLowerCase(), config);
3225
- }
3226
- return map;
3227
- }
3228
3400
 
3229
3401
  // cli/src/format.ts
3230
3402
  var USAGE2 = `Usage: htmlmustache format [options] [patterns...]
@@ -3303,17 +3475,16 @@ function resolveSettings(flags, filePath) {
3303
3475
  let insertSpaces = true;
3304
3476
  let printWidth = 80;
3305
3477
  let mustacheSpaces = false;
3306
- let customCodeTags;
3307
- let customCodeTagConfigs;
3478
+ let customTags;
3308
3479
  const configFile = filePath ? loadConfigFileForPath(filePath) : null;
3480
+ let noBreakDelimiters;
3309
3481
  if (configFile) {
3310
3482
  if (configFile.indentSize !== void 0) tabSize = configFile.indentSize;
3311
3483
  if (configFile.printWidth !== void 0) printWidth = configFile.printWidth;
3312
3484
  if (configFile.mustacheSpaces !== void 0) mustacheSpaces = configFile.mustacheSpaces;
3313
- if (configFile.customCodeTags && configFile.customCodeTags.length > 0) {
3314
- const parsed = parseCustomCodeTagSettings(configFile.customCodeTags);
3315
- customCodeTags = parsed.tagNames;
3316
- customCodeTagConfigs = parsed.configs;
3485
+ if (configFile.noBreakDelimiters) noBreakDelimiters = configFile.noBreakDelimiters;
3486
+ if (configFile.customTags && configFile.customTags.length > 0) {
3487
+ customTags = configFile.customTags;
3317
3488
  }
3318
3489
  }
3319
3490
  if (filePath) {
@@ -3329,8 +3500,8 @@ function resolveSettings(flags, filePath) {
3329
3500
  options: { tabSize, insertSpaces },
3330
3501
  printWidth,
3331
3502
  mustacheSpaces,
3332
- customCodeTags,
3333
- customCodeTagConfigs,
3503
+ noBreakDelimiters,
3504
+ customTags,
3334
3505
  configFile
3335
3506
  };
3336
3507
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reteps/tree-sitter-htmlmustache",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "HTML with Mustache/Handlebars template syntax grammar for tree-sitter",
5
5
  "repository": {
6
6
  "type": "git",
package/tree-sitter.json CHANGED
@@ -13,7 +13,7 @@
13
13
  }
14
14
  ],
15
15
  "metadata": {
16
- "version": "0.4.0",
16
+ "version": "0.4.1",
17
17
  "license": "MIT",
18
18
  "description": "HTML with Mustache/Handlebars template syntax grammar for tree-sitter",
19
19
  "authors": [