@reteps/tree-sitter-htmlmustache 0.4.0 → 0.4.1

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);
679
+ }
680
+ for (const tag of parsedCustomTags) {
681
+ mergedMap.set(tag.name.toLowerCase(), tag);
641
682
  }
642
- if (tags.length > 0) config.customCodeTags = tags;
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,37 @@ 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;
2747
- }
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);
2754
- } else {
2755
- parts.push(indent(concat([hardline, formattedContent])));
2816
+ const isStaircase = !hasImplicit && contentNodes.length > 0 && contentNodes.every((n) => n.type === "html_erroneous_end_tag");
2817
+ if (isStaircase) {
2818
+ const E = contentNodes.length;
2819
+ for (let i = 0; i < E; i++) {
2820
+ const formatted = formatNode(contentNodes[i], context);
2821
+ parts.push(indentN(concat([hardline, formatted]), E - i));
2822
+ }
2823
+ parts.push(hardline);
2824
+ } else {
2825
+ const formattedContent = formatBlockChildren(contentNodes, context);
2826
+ const hasContent = hasDocContent(formattedContent);
2827
+ if (hasContent) {
2828
+ if (hasImplicit) {
2756
2829
  parts.push(hardline);
2830
+ parts.push(formattedContent);
2831
+ parts.push(hardline);
2832
+ } else {
2833
+ const hasBlockChildren = contentNodes.some((child, i) => {
2834
+ if (!shouldTreatAsBlock(child, i, contentNodes, context.customTags)) {
2835
+ return false;
2836
+ }
2837
+ const childDisplay = getCSSDisplay(child, context.customTags);
2838
+ return isWhitespaceInsensitive(childDisplay) || isRawContentElement(child);
2839
+ });
2840
+ if (!hasBlockChildren) {
2841
+ parts.push(indent(concat([softline, formattedContent])));
2842
+ parts.push(softline);
2843
+ } else {
2844
+ parts.push(indent(concat([hardline, formattedContent])));
2845
+ parts.push(hardline);
2846
+ }
2757
2847
  }
2758
2848
  }
2759
2849
  }
@@ -2762,7 +2852,17 @@ function formatMustacheSection(node, context) {
2762
2852
  }
2763
2853
  return group(concat(parts));
2764
2854
  }
2765
- function formatStartTag(node, context) {
2855
+ function startTagHasAttributes(startTag) {
2856
+ for (let i = 0; i < startTag.childCount; i++) {
2857
+ const child = startTag.child(i);
2858
+ if (!child) continue;
2859
+ if (child.type === "html_attribute" || child.type === "mustache_attribute" || child.type === "mustache_interpolation" || child.type === "mustache_triple") {
2860
+ return true;
2861
+ }
2862
+ }
2863
+ return false;
2864
+ }
2865
+ function formatStartTag(node, context, bare = false) {
2766
2866
  let tagNameText = "";
2767
2867
  const attrs = [];
2768
2868
  for (let i = 0; i < node.childCount; i++) {
@@ -2795,14 +2895,13 @@ function formatStartTag(node, context) {
2795
2895
  attrParts.push(attrs[i]);
2796
2896
  }
2797
2897
  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
- );
2898
+ const inner = concat([
2899
+ text("<"),
2900
+ text(tagNameText),
2901
+ indent(concat([line, concat(attrParts)])),
2902
+ ifBreak(concat([hardline, text(breakClosingBracket)]), text(closingBracket))
2903
+ ]);
2904
+ return bare ? inner : group(inner);
2806
2905
  }
2807
2906
  function formatEndTag(node) {
2808
2907
  for (let i = 0; i < node.childCount; i++) {
@@ -2847,6 +2946,38 @@ function textWords(str) {
2847
2946
  }
2848
2947
  return parts;
2849
2948
  }
2949
+ function collapseDelimitedRegions(parts, delimiters) {
2950
+ if (delimiters.length === 0) return parts;
2951
+ const sorted = [...delimiters].sort((a, b) => b.length - a.length);
2952
+ const result = [...parts];
2953
+ let activeDelimiter = null;
2954
+ for (let i = 0; i < result.length; i++) {
2955
+ const part = result[i];
2956
+ if (typeof part === "string") {
2957
+ if (activeDelimiter === null) {
2958
+ for (const delim of sorted) {
2959
+ const delimIdx = part.indexOf(delim);
2960
+ if (delimIdx >= 0) {
2961
+ const afterOpen = delimIdx + delim.length;
2962
+ const closeIdx = part.indexOf(delim, afterOpen);
2963
+ if (closeIdx >= 0) {
2964
+ continue;
2965
+ }
2966
+ activeDelimiter = delim;
2967
+ break;
2968
+ }
2969
+ }
2970
+ } else {
2971
+ if (part.includes(activeDelimiter)) {
2972
+ activeDelimiter = null;
2973
+ }
2974
+ }
2975
+ } else if (activeDelimiter !== null && isLine(part)) {
2976
+ result[i] = " ";
2977
+ }
2978
+ }
2979
+ return result;
2980
+ }
2850
2981
  function inlineContentToFill(parts) {
2851
2982
  if (parts.length === 0) return empty;
2852
2983
  if (parts.length === 1) return parts[0];
@@ -2889,6 +3020,11 @@ function formatBlockChildren(nodes, context) {
2889
3020
  let ignoreNext = false;
2890
3021
  let inIgnoreRegion = false;
2891
3022
  let ignoreRegionStartIndex = -1;
3023
+ const noBreakDelims = context.noBreakDelimiters;
3024
+ function flushCurrentLine() {
3025
+ const parts2 = noBreakDelims ? collapseDelimitedRegions(currentLine, noBreakDelims) : currentLine;
3026
+ return inlineContentToFill(parts2);
3027
+ }
2892
3028
  for (let i = 0; i < nodes.length; i++) {
2893
3029
  const node = nodes[i];
2894
3030
  if (lastNodeEnd >= 0 && node.startIndex > lastNodeEnd && !inIgnoreRegion) {
@@ -2901,7 +3037,7 @@ function formatBlockChildren(nodes, context) {
2901
3037
  const directive = getIgnoreDirective(node);
2902
3038
  if (directive === "ignore-end" && inIgnoreRegion) {
2903
3039
  if (currentLine.length > 0) {
2904
- const lineContent = trimDoc(inlineContentToFill(currentLine));
3040
+ const lineContent = trimDoc(flushCurrentLine());
2905
3041
  if (hasDocContent(lineContent)) {
2906
3042
  lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
2907
3043
  }
@@ -2925,7 +3061,7 @@ function formatBlockChildren(nodes, context) {
2925
3061
  }
2926
3062
  if (directive === "ignore-start") {
2927
3063
  if (currentLine.length > 0) {
2928
- const lineContent = trimDoc(inlineContentToFill(currentLine));
3064
+ const lineContent = trimDoc(flushCurrentLine());
2929
3065
  if (hasDocContent(lineContent)) {
2930
3066
  lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
2931
3067
  }
@@ -2942,7 +3078,7 @@ function formatBlockChildren(nodes, context) {
2942
3078
  }
2943
3079
  if (directive === "ignore") {
2944
3080
  if (currentLine.length > 0) {
2945
- const lineContent = trimDoc(inlineContentToFill(currentLine));
3081
+ const lineContent = trimDoc(flushCurrentLine());
2946
3082
  if (hasDocContent(lineContent)) {
2947
3083
  lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
2948
3084
  }
@@ -2963,10 +3099,10 @@ function formatBlockChildren(nodes, context) {
2963
3099
  lastNodeEnd = node.endIndex;
2964
3100
  continue;
2965
3101
  }
2966
- const treatAsBlock = shouldTreatAsBlock(node, i, nodes, context.customCodeTags);
3102
+ const treatAsBlock = shouldTreatAsBlock(node, i, nodes, context.customTags);
2967
3103
  if (lastNodeEnd >= 0 && node.startIndex > lastNodeEnd) {
2968
3104
  const prevNode = nodes[i - 1];
2969
- const prevTreatAsBlock = shouldTreatAsBlock(prevNode, i - 1, nodes, context.customCodeTags);
3105
+ const prevTreatAsBlock = shouldTreatAsBlock(prevNode, i - 1, nodes, context.customTags);
2970
3106
  if (!prevTreatAsBlock && !treatAsBlock) {
2971
3107
  const gap = context.document.getText().slice(lastNodeEnd, node.startIndex);
2972
3108
  if (/\s/.test(gap)) {
@@ -2976,7 +3112,7 @@ function formatBlockChildren(nodes, context) {
2976
3112
  }
2977
3113
  if (treatAsBlock) {
2978
3114
  if (currentLine.length > 0) {
2979
- const lineContent = trimDoc(inlineContentToFill(currentLine));
3115
+ const lineContent = trimDoc(flushCurrentLine());
2980
3116
  if (hasDocContent(lineContent)) {
2981
3117
  lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
2982
3118
  }
@@ -2989,7 +3125,7 @@ function formatBlockChildren(nodes, context) {
2989
3125
  const isMultiline = node.startPosition.row !== node.endPosition.row;
2990
3126
  if (isMultiline) {
2991
3127
  if (currentLine.length > 0) {
2992
- const lineContent = trimDoc(inlineContentToFill(currentLine));
3128
+ const lineContent = trimDoc(flushCurrentLine());
2993
3129
  if (hasDocContent(lineContent)) {
2994
3130
  lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
2995
3131
  }
@@ -3022,7 +3158,7 @@ function formatBlockChildren(nodes, context) {
3022
3158
  const trimmed = contentLines[j].trim();
3023
3159
  if (!trimmed) {
3024
3160
  if (currentLine.length > 0) {
3025
- const lineContent = trimDoc(inlineContentToFill(currentLine));
3161
+ const lineContent = trimDoc(flushCurrentLine());
3026
3162
  if (hasDocContent(lineContent)) {
3027
3163
  lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
3028
3164
  blankLineBeforeCurrentLine = false;
@@ -3047,7 +3183,7 @@ function formatBlockChildren(nodes, context) {
3047
3183
  currentLine.push(firstTrimmed);
3048
3184
  }
3049
3185
  if (currentLine.length > 0) {
3050
- const lineContent = trimDoc(inlineContentToFill(currentLine));
3186
+ const lineContent = trimDoc(flushCurrentLine());
3051
3187
  if (hasDocContent(lineContent)) {
3052
3188
  lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
3053
3189
  blankLineBeforeCurrentLine = pendingBlankLine;
@@ -3101,7 +3237,7 @@ function formatBlockChildren(nodes, context) {
3101
3237
  }
3102
3238
  }
3103
3239
  if (currentLine.length > 0) {
3104
- const lineContent = trimDoc(inlineContentToFill(currentLine));
3240
+ const lineContent = trimDoc(flushCurrentLine());
3105
3241
  if (hasDocContent(lineContent)) {
3106
3242
  lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
3107
3243
  }
@@ -3193,21 +3329,28 @@ function createIndentUnit(options) {
3193
3329
  }
3194
3330
 
3195
3331
  // lsp/server/src/formatting/index.ts
3332
+ function buildCustomTagMap(customTags) {
3333
+ if (!customTags || customTags.length === 0) return void 0;
3334
+ const map = /* @__PURE__ */ new Map();
3335
+ for (const config of customTags) {
3336
+ map.set(config.name.toLowerCase(), config);
3337
+ }
3338
+ return map;
3339
+ }
3196
3340
  function formatDocument2(tree, document, options, params = {}) {
3197
- const { customCodeTags, printWidth = 80, embeddedFormatted, mustacheSpaces, customCodeTagConfigs, configFile } = params;
3341
+ const { printWidth = 80, embeddedFormatted, mustacheSpaces, noBreakDelimiters, configFile } = params;
3198
3342
  const mergedOptions = mergeOptions(options, document.uri, configFile);
3199
3343
  const indentUnit = createIndentUnit(mergedOptions);
3200
3344
  if (tree.rootNode.hasError) {
3201
3345
  return [];
3202
3346
  }
3203
- const configMap = buildConfigMap(customCodeTagConfigs);
3204
- const customCodeTagSet = customCodeTags ? new Set(customCodeTags.map((t) => t.toLowerCase())) : void 0;
3347
+ const customTagMap = buildCustomTagMap(params.customTags);
3205
3348
  const context = {
3206
3349
  document,
3207
- customCodeTags: customCodeTagSet,
3208
- customCodeTagConfigs: configMap,
3350
+ customTags: customTagMap,
3209
3351
  embeddedFormatted,
3210
- mustacheSpaces
3352
+ mustacheSpaces,
3353
+ noBreakDelimiters
3211
3354
  };
3212
3355
  const doc = formatDocument(tree.rootNode, context);
3213
3356
  const formatted = print(doc, { indentUnit, printWidth });
@@ -3217,14 +3360,6 @@ function formatDocument2(tree, document, options, params = {}) {
3217
3360
  };
3218
3361
  return [{ range: fullRange, newText: formatted }];
3219
3362
  }
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
3363
 
3229
3364
  // cli/src/format.ts
3230
3365
  var USAGE2 = `Usage: htmlmustache format [options] [patterns...]
@@ -3303,17 +3438,16 @@ function resolveSettings(flags, filePath) {
3303
3438
  let insertSpaces = true;
3304
3439
  let printWidth = 80;
3305
3440
  let mustacheSpaces = false;
3306
- let customCodeTags;
3307
- let customCodeTagConfigs;
3441
+ let customTags;
3308
3442
  const configFile = filePath ? loadConfigFileForPath(filePath) : null;
3443
+ let noBreakDelimiters;
3309
3444
  if (configFile) {
3310
3445
  if (configFile.indentSize !== void 0) tabSize = configFile.indentSize;
3311
3446
  if (configFile.printWidth !== void 0) printWidth = configFile.printWidth;
3312
3447
  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;
3448
+ if (configFile.noBreakDelimiters) noBreakDelimiters = configFile.noBreakDelimiters;
3449
+ if (configFile.customTags && configFile.customTags.length > 0) {
3450
+ customTags = configFile.customTags;
3317
3451
  }
3318
3452
  }
3319
3453
  if (filePath) {
@@ -3329,8 +3463,8 @@ function resolveSettings(flags, filePath) {
3329
3463
  options: { tabSize, insertSpaces },
3330
3464
  printWidth,
3331
3465
  mustacheSpaces,
3332
- customCodeTags,
3333
- customCodeTagConfigs,
3466
+ noBreakDelimiters,
3467
+ customTags,
3334
3468
  configFile
3335
3469
  };
3336
3470
  }
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.1",
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": [