@reteps/tree-sitter-htmlmustache 0.0.35 → 0.0.37

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
@@ -135,6 +135,47 @@ echo '<div><p>hi</p></div>' | htmlmustache format --stdin
135
135
  | `--print-width N` | Max line width (default: 80) |
136
136
  | `--mustache-spaces` | Add spaces inside mustache delimiters |
137
137
 
138
+ ## Format Ignore
139
+
140
+ Skip formatting for specific regions using ignore directives. Both HTML and Mustache comment forms are supported.
141
+
142
+ ### Ignore Next Node
143
+
144
+ Place a comment immediately before the element to preserve its original formatting:
145
+
146
+ ```html
147
+ <!-- htmlmustache-ignore -->
148
+ <div class="a" id="b" >
149
+ manually formatted
150
+ </div>
151
+ ```
152
+
153
+ ```html
154
+ {{! htmlmustache-ignore }}
155
+ <table><tr><td>compact</td><td>table</td></tr></table>
156
+ ```
157
+
158
+ Only the immediately following sibling node is ignored. Subsequent nodes are formatted normally.
159
+
160
+ ### Ignore Region
161
+
162
+ Wrap a region in start/end comments to preserve everything between them:
163
+
164
+ ```html
165
+ <!-- htmlmustache-ignore-start -->
166
+ <div class="a" >content</div>
167
+ <p> kept as-is </p>
168
+ <!-- htmlmustache-ignore-end -->
169
+ ```
170
+
171
+ ```html
172
+ {{! htmlmustache-ignore-start }}
173
+ {{#items}}<li>{{name}}</li>{{/items}}
174
+ {{! htmlmustache-ignore-end }}
175
+ ```
176
+
177
+ If `ignore-start` has no matching `ignore-end`, all remaining siblings in the current scope are preserved as raw text.
178
+
138
179
  ## Configuration
139
180
 
140
181
  ### `.htmlmustache.jsonc`
package/cli/out/main.js CHANGED
@@ -2387,15 +2387,16 @@ function normalizeMustacheWhitespace(raw, addSpaces) {
2387
2387
  const first = lines[0].trimStart();
2388
2388
  const last = lines[lines.length - 1].trimEnd();
2389
2389
  if (lines.length === 1) {
2390
- return `{{${prefix}${space}${first}${space}}}`;
2390
+ return `{{${prefix} ${first} }}`;
2391
2391
  }
2392
2392
  const middle = lines.slice(1, -1);
2393
- return `{{${prefix}${space}${first}
2393
+ return `{{${prefix} ${first}
2394
2394
  ${middle.join("\n")}
2395
- ${last}${space}}}`;
2395
+ ${last} }}`;
2396
2396
  }
2397
2397
  const trimmed = inner.trim();
2398
- return `{{${prefix}${space}${trimmed}${space}}}`;
2398
+ const s = prefix === "!" ? " " : space;
2399
+ return `{{${prefix}${s}${trimmed}${s}}}`;
2399
2400
  }
2400
2401
  const plainMatch = raw.match(/^\{\{([\s\S]*)\}\}$/);
2401
2402
  if (plainMatch) {
@@ -2409,6 +2410,28 @@ function normalizeMustacheWhitespaceAll(raw, addSpaces) {
2409
2410
  return normalizeMustacheWhitespace(match, addSpaces);
2410
2411
  });
2411
2412
  }
2413
+ function getIgnoreDirective(node) {
2414
+ if (node.type !== "html_comment" && node.type !== "mustache_comment") {
2415
+ return null;
2416
+ }
2417
+ let inner = null;
2418
+ if (node.type === "html_comment") {
2419
+ const match = node.text.match(/^<!--([\s\S]*)-->$/);
2420
+ if (match) {
2421
+ inner = match[1].trim();
2422
+ }
2423
+ } else {
2424
+ const match = node.text.match(/^\{\{!([\s\S]*)\}\}$/);
2425
+ if (match) {
2426
+ inner = match[1].trim();
2427
+ }
2428
+ }
2429
+ if (!inner) return null;
2430
+ if (inner === "htmlmustache-ignore") return "ignore";
2431
+ if (inner === "htmlmustache-ignore-start") return "ignore-start";
2432
+ if (inner === "htmlmustache-ignore-end") return "ignore-end";
2433
+ return null;
2434
+ }
2412
2435
 
2413
2436
  // lsp/server/src/formatting/classifier.ts
2414
2437
  var customCodeTags = /* @__PURE__ */ new Set();
@@ -2622,14 +2645,16 @@ function hasImplicitEndTagsRecursive(node) {
2622
2645
  if (node.type === "html_element") {
2623
2646
  let hasStartTag = false;
2624
2647
  let hasEndTag = false;
2648
+ let hasContentChildren = false;
2625
2649
  for (let i = 0; i < node.childCount; i++) {
2626
2650
  const child = node.child(i);
2627
2651
  if (!child) continue;
2628
2652
  if (child.type === "html_start_tag") hasStartTag = true;
2629
- if (child.type === "html_end_tag") hasEndTag = true;
2630
- if (child.type === "html_forced_end_tag") return true;
2653
+ else if (child.type === "html_end_tag") hasEndTag = true;
2654
+ else if (child.type === "html_forced_end_tag") return true;
2655
+ else if (!child.type.startsWith("_")) hasContentChildren = true;
2631
2656
  }
2632
- if (hasStartTag && !hasEndTag) return true;
2657
+ if (hasStartTag && !hasEndTag && hasContentChildren) return true;
2633
2658
  }
2634
2659
  for (let i = 0; i < node.childCount; i++) {
2635
2660
  const child = node.child(i);
@@ -2900,8 +2925,16 @@ function formatHtmlElement(node, context) {
2900
2925
  }
2901
2926
  }
2902
2927
  } else if (!isBlock && !hasHtmlElementChildren) {
2928
+ let prevEnd = startTag ? startTag.endIndex : -1;
2903
2929
  for (const child of contentNodes) {
2930
+ if (prevEnd >= 0 && child.startIndex > prevEnd) {
2931
+ const gap = context.document.getText().slice(prevEnd, child.startIndex);
2932
+ if (/\s/.test(gap)) {
2933
+ parts.push(text(" "));
2934
+ }
2935
+ }
2904
2936
  parts.push(formatNode(child, context));
2937
+ prevEnd = child.endIndex;
2905
2938
  }
2906
2939
  } else {
2907
2940
  const formattedContent = formatBlockChildren(contentNodes, context);
@@ -3002,7 +3035,29 @@ function formatScriptStyleElement(node, context) {
3002
3035
  parts.push(text(child.text));
3003
3036
  }
3004
3037
  } else {
3005
- parts.push(text(child.text));
3038
+ const dedented = dedentContent(child.text);
3039
+ if (dedented.length > 0) {
3040
+ const contentLines = dedented.split("\n");
3041
+ if (contentLines.length === 1) {
3042
+ parts.push(text(contentLines[0]));
3043
+ } else {
3044
+ const lineDocs = [];
3045
+ for (let j = 0; j < contentLines.length; j++) {
3046
+ if (j > 0) {
3047
+ if (contentLines[j] === "") {
3048
+ lineDocs.push("\n");
3049
+ } else {
3050
+ lineDocs.push(hardline);
3051
+ }
3052
+ }
3053
+ if (contentLines[j] !== "") {
3054
+ lineDocs.push(text(contentLines[j]));
3055
+ }
3056
+ }
3057
+ parts.push(indent(concat([hardline, ...lineDocs])));
3058
+ parts.push(hardline);
3059
+ }
3060
+ }
3006
3061
  }
3007
3062
  }
3008
3063
  }
@@ -3184,18 +3239,89 @@ function formatBlockChildren(nodes, context) {
3184
3239
  let lastNodeEnd = -1;
3185
3240
  let pendingBlankLine = false;
3186
3241
  let blankLineBeforeCurrentLine = false;
3242
+ let ignoreNext = false;
3243
+ let inIgnoreRegion = false;
3244
+ let ignoreRegionStartIndex = -1;
3187
3245
  for (let i = 0; i < nodes.length; i++) {
3188
3246
  const node = nodes[i];
3189
- const treatAsBlock = shouldTreatAsBlock(node, i, nodes);
3190
- if (lastNodeEnd >= 0 && node.startIndex > lastNodeEnd) {
3247
+ if (lastNodeEnd >= 0 && node.startIndex > lastNodeEnd && !inIgnoreRegion) {
3191
3248
  const gap = context.document.getText().slice(lastNodeEnd, node.startIndex);
3192
- const prevNode = nodes[i - 1];
3193
- const prevTreatAsBlock = shouldTreatAsBlock(prevNode, i - 1, nodes);
3194
3249
  const newlineCount = (gap.match(/\n/g) || []).length;
3195
3250
  if (newlineCount >= 2) {
3196
3251
  pendingBlankLine = true;
3197
3252
  }
3253
+ }
3254
+ const directive = getIgnoreDirective(node);
3255
+ if (directive === "ignore-end" && inIgnoreRegion) {
3256
+ if (currentLine.length > 0) {
3257
+ const lineContent = trimDoc(inlineContentToFill(currentLine));
3258
+ if (hasDocContent(lineContent)) {
3259
+ lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
3260
+ }
3261
+ currentLine = [];
3262
+ blankLineBeforeCurrentLine = false;
3263
+ }
3264
+ const rawText = context.document.getText().slice(ignoreRegionStartIndex, node.startIndex).replace(/^\n/, "").replace(/\n$/, "");
3265
+ if (rawText.length > 0) {
3266
+ lines.push({ doc: text(rawText), blankLineBefore: false });
3267
+ }
3268
+ const commentText = node.type === "mustache_comment" ? mustacheText(node.text, context) : node.text;
3269
+ lines.push({ doc: text(commentText), blankLineBefore: false });
3270
+ inIgnoreRegion = false;
3271
+ ignoreRegionStartIndex = -1;
3272
+ lastNodeEnd = node.endIndex;
3273
+ continue;
3274
+ }
3275
+ if (inIgnoreRegion) {
3276
+ lastNodeEnd = node.endIndex;
3277
+ continue;
3278
+ }
3279
+ if (directive === "ignore-start") {
3280
+ if (currentLine.length > 0) {
3281
+ const lineContent = trimDoc(inlineContentToFill(currentLine));
3282
+ if (hasDocContent(lineContent)) {
3283
+ lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
3284
+ }
3285
+ currentLine = [];
3286
+ blankLineBeforeCurrentLine = false;
3287
+ }
3288
+ const commentText = node.type === "mustache_comment" ? mustacheText(node.text, context) : node.text;
3289
+ lines.push({ doc: text(commentText), blankLineBefore: pendingBlankLine });
3290
+ pendingBlankLine = false;
3291
+ inIgnoreRegion = true;
3292
+ ignoreRegionStartIndex = node.endIndex;
3293
+ lastNodeEnd = node.endIndex;
3294
+ continue;
3295
+ }
3296
+ if (directive === "ignore") {
3297
+ if (currentLine.length > 0) {
3298
+ const lineContent = trimDoc(inlineContentToFill(currentLine));
3299
+ if (hasDocContent(lineContent)) {
3300
+ lines.push({ doc: lineContent, blankLineBefore: blankLineBeforeCurrentLine });
3301
+ }
3302
+ currentLine = [];
3303
+ blankLineBeforeCurrentLine = false;
3304
+ }
3305
+ const commentText = node.type === "mustache_comment" ? mustacheText(node.text, context) : node.text;
3306
+ lines.push({ doc: text(commentText), blankLineBefore: pendingBlankLine });
3307
+ pendingBlankLine = false;
3308
+ ignoreNext = true;
3309
+ lastNodeEnd = node.endIndex;
3310
+ continue;
3311
+ }
3312
+ if (ignoreNext) {
3313
+ lines.push({ doc: text(node.text), blankLineBefore: pendingBlankLine });
3314
+ pendingBlankLine = false;
3315
+ ignoreNext = false;
3316
+ lastNodeEnd = node.endIndex;
3317
+ continue;
3318
+ }
3319
+ const treatAsBlock = shouldTreatAsBlock(node, i, nodes);
3320
+ if (lastNodeEnd >= 0 && node.startIndex > lastNodeEnd) {
3321
+ const prevNode = nodes[i - 1];
3322
+ const prevTreatAsBlock = shouldTreatAsBlock(prevNode, i - 1, nodes);
3198
3323
  if (!prevTreatAsBlock && !treatAsBlock) {
3324
+ const gap = context.document.getText().slice(lastNodeEnd, node.startIndex);
3199
3325
  if (/\s/.test(gap)) {
3200
3326
  currentLine.push(line);
3201
3327
  }
@@ -3315,6 +3441,13 @@ function formatBlockChildren(nodes, context) {
3315
3441
  }
3316
3442
  lastNodeEnd = node.endIndex;
3317
3443
  }
3444
+ if (inIgnoreRegion && nodes.length > 0) {
3445
+ const lastNode = nodes[nodes.length - 1];
3446
+ const rawText = context.document.getText().slice(ignoreRegionStartIndex, lastNode.endIndex).replace(/^\n/, "");
3447
+ if (rawText.length > 0) {
3448
+ lines.push({ doc: text(rawText), blankLineBefore: false });
3449
+ }
3450
+ }
3318
3451
  if (currentLine.length > 0) {
3319
3452
  const lineContent = trimDoc(inlineContentToFill(currentLine));
3320
3453
  if (hasDocContent(lineContent)) {
@@ -3411,6 +3544,9 @@ function formatDocument2(tree, document, options, params = {}) {
3411
3544
  if (customCodeTags2) {
3412
3545
  setCustomCodeTags(customCodeTags2);
3413
3546
  }
3547
+ if (tree.rootNode.hasError) {
3548
+ return [];
3549
+ }
3414
3550
  const configMap = buildConfigMap(customCodeTagConfigs);
3415
3551
  const context = {
3416
3552
  document,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reteps/tree-sitter-htmlmustache",
3
- "version": "0.0.35",
3
+ "version": "0.0.37",
4
4
  "description": "HTML with Mustache/Handlebars template syntax grammar for tree-sitter",
5
5
  "repository": {
6
6
  "type": "git",
@@ -40,11 +40,16 @@
40
40
  "vscode-languageserver-textdocument": "^1.0.12",
41
41
  "web-tree-sitter": "^0.25.0"
42
42
  },
43
- "optionalDependencies": {
43
+ "peerDependencies": {
44
44
  "node-addon-api": "^8.2.2",
45
45
  "node-gyp-build": "^4.8.2",
46
46
  "tree-sitter": "^0.25.0"
47
47
  },
48
+ "peerDependenciesMeta": {
49
+ "node-addon-api": { "optional": true },
50
+ "node-gyp-build": { "optional": true },
51
+ "tree-sitter": { "optional": true }
52
+ },
48
53
  "devDependencies": {
49
54
  "@eslint/js": "^9.39.2",
50
55
  "@types/node": "^22.19.11",
package/src/scanner.c CHANGED
@@ -516,6 +516,12 @@ static bool scan_mustache_end_tag_html_implicit_end_tag(Scanner *scanner, TSLexe
516
516
  if (scanner->mustache_tags.size > 0) {
517
517
  MustacheTag *current_mustache_tag = array_back(&scanner->mustache_tags);
518
518
  if (scanner->tags.size > current_mustache_tag->html_tag_stack_size) {
519
+ Tag *top_tag = array_back(&scanner->tags);
520
+ // Void elements don't cross mustache boundaries — let the normal
521
+ // implicit end handler close them instead of forcing closure.
522
+ if (tag_is_void(top_tag)) {
523
+ return false;
524
+ }
519
525
  pop_html_tag(scanner);
520
526
  lexer->result_symbol = MUSTACHE_END_TAG_HTML_IMPLICIT_END_TAG;
521
527
  return true;
Binary file