@reteps/tree-sitter-htmlmustache 0.8.1 → 0.9.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.
Files changed (67) hide show
  1. package/README.md +1 -1
  2. package/browser/out/browser/index.d.ts +43 -0
  3. package/browser/out/browser/index.d.ts.map +1 -0
  4. package/browser/out/browser/index.mjs +3612 -0
  5. package/browser/out/browser/index.mjs.map +7 -0
  6. package/browser/out/core/collectErrors.d.ts +36 -0
  7. package/browser/out/core/collectErrors.d.ts.map +1 -0
  8. package/browser/out/core/configSchema.d.ts +63 -0
  9. package/browser/out/core/configSchema.d.ts.map +1 -0
  10. package/browser/out/core/customCodeTags.d.ts +34 -0
  11. package/browser/out/core/customCodeTags.d.ts.map +1 -0
  12. package/browser/out/core/diagnostic.d.ts +24 -0
  13. package/browser/out/core/diagnostic.d.ts.map +1 -0
  14. package/browser/out/core/embeddedRegions.d.ts +12 -0
  15. package/browser/out/core/embeddedRegions.d.ts.map +1 -0
  16. package/browser/out/core/formatting/classifier.d.ts +68 -0
  17. package/browser/out/core/formatting/classifier.d.ts.map +1 -0
  18. package/browser/out/core/formatting/embedded.d.ts +19 -0
  19. package/browser/out/core/formatting/embedded.d.ts.map +1 -0
  20. package/browser/out/core/formatting/formatters.d.ts +85 -0
  21. package/browser/out/core/formatting/formatters.d.ts.map +1 -0
  22. package/browser/out/core/formatting/index.d.ts +44 -0
  23. package/browser/out/core/formatting/index.d.ts.map +1 -0
  24. package/browser/out/core/formatting/ir.d.ts +100 -0
  25. package/browser/out/core/formatting/ir.d.ts.map +1 -0
  26. package/browser/out/core/formatting/mergeOptions.d.ts +18 -0
  27. package/browser/out/core/formatting/mergeOptions.d.ts.map +1 -0
  28. package/browser/out/core/formatting/printer.d.ts +18 -0
  29. package/browser/out/core/formatting/printer.d.ts.map +1 -0
  30. package/browser/out/core/formatting/utils.d.ts +39 -0
  31. package/browser/out/core/formatting/utils.d.ts.map +1 -0
  32. package/browser/out/core/grammar.d.ts +3 -0
  33. package/browser/out/core/grammar.d.ts.map +1 -0
  34. package/browser/out/core/htmlBalanceChecker.d.ts +23 -0
  35. package/browser/out/core/htmlBalanceChecker.d.ts.map +1 -0
  36. package/browser/out/core/mustacheChecks.d.ts +24 -0
  37. package/browser/out/core/mustacheChecks.d.ts.map +1 -0
  38. package/browser/out/core/nodeHelpers.d.ts +54 -0
  39. package/browser/out/core/nodeHelpers.d.ts.map +1 -0
  40. package/browser/out/core/ruleMetadata.d.ts +12 -0
  41. package/browser/out/core/ruleMetadata.d.ts.map +1 -0
  42. package/browser/out/core/selectorMatcher.d.ts +74 -0
  43. package/browser/out/core/selectorMatcher.d.ts.map +1 -0
  44. package/cli/out/main.js +133 -115
  45. package/package.json +21 -3
  46. package/src/browser/browser.test.ts +207 -0
  47. package/src/browser/index.ts +128 -0
  48. package/src/browser/tsconfig.json +18 -0
  49. package/src/core/collectErrors.ts +233 -0
  50. package/src/core/configSchema.ts +273 -0
  51. package/src/core/customCodeTags.ts +159 -0
  52. package/src/core/diagnostic.ts +45 -0
  53. package/src/core/embeddedRegions.ts +70 -0
  54. package/src/core/formatting/classifier.ts +549 -0
  55. package/src/core/formatting/embedded.ts +56 -0
  56. package/src/core/formatting/formatters.ts +1272 -0
  57. package/src/core/formatting/index.ts +185 -0
  58. package/src/core/formatting/ir.ts +202 -0
  59. package/src/core/formatting/mergeOptions.ts +34 -0
  60. package/src/core/formatting/printer.ts +242 -0
  61. package/src/core/formatting/utils.ts +193 -0
  62. package/src/core/grammar.ts +2 -0
  63. package/src/core/htmlBalanceChecker.ts +382 -0
  64. package/src/core/mustacheChecks.ts +504 -0
  65. package/src/core/nodeHelpers.ts +126 -0
  66. package/src/core/ruleMetadata.ts +63 -0
  67. package/src/core/selectorMatcher.ts +719 -0
@@ -0,0 +1,549 @@
1
+ /**
2
+ * Node classifier - determines how to format different node types.
3
+ *
4
+ * This module uses CSS display values to classify HTML elements, matching
5
+ * Prettier's approach to whitespace sensitivity in HTML formatting.
6
+ */
7
+
8
+ import type { Node as SyntaxNode } from 'web-tree-sitter';
9
+ import { getTagName } from './utils.js';
10
+ import { isMustacheSection, isRawContentElement, isHtmlElementType } from '../nodeHelpers.js';
11
+ import type { CustomCodeTagConfig } from '../customCodeTags.js';
12
+ import { isCodeTag } from '../customCodeTags.js';
13
+
14
+ const EMPTY_MAP: Map<string, CustomCodeTagConfig> = new Map();
15
+
16
+ export type CSSDisplay =
17
+ | 'block'
18
+ | 'inline'
19
+ | 'inline-block'
20
+ | 'table-row'
21
+ | 'table-cell'
22
+ | 'table'
23
+ | 'table-row-group'
24
+ | 'table-header-group'
25
+ | 'table-footer-group'
26
+ | 'table-column'
27
+ | 'table-column-group'
28
+ | 'table-caption'
29
+ | 'list-item'
30
+ | 'ruby'
31
+ | 'ruby-base'
32
+ | 'ruby-text'
33
+ | 'none';
34
+
35
+ /**
36
+ * Default CSS display values for HTML elements, matching browser defaults.
37
+ * Elements not in this map default to 'inline'.
38
+ */
39
+ const CSS_DISPLAY_MAP: Record<string, CSSDisplay> = {
40
+ // Block elements
41
+ address: 'block',
42
+ article: 'block',
43
+ aside: 'block',
44
+ blockquote: 'block',
45
+ body: 'block',
46
+ center: 'block',
47
+ dd: 'block',
48
+ details: 'block',
49
+ dialog: 'block',
50
+ dir: 'block',
51
+ div: 'block',
52
+ dl: 'block',
53
+ dt: 'block',
54
+ fieldset: 'block',
55
+ figcaption: 'block',
56
+ figure: 'block',
57
+ footer: 'block',
58
+ form: 'block',
59
+ h1: 'block',
60
+ h2: 'block',
61
+ h3: 'block',
62
+ h4: 'block',
63
+ h5: 'block',
64
+ h6: 'block',
65
+ header: 'block',
66
+ hgroup: 'block',
67
+ hr: 'block',
68
+ html: 'block',
69
+ legend: 'block',
70
+ listing: 'block',
71
+ main: 'block',
72
+ menu: 'block',
73
+ nav: 'block',
74
+ ol: 'block',
75
+ p: 'block',
76
+ plaintext: 'block',
77
+ pre: 'block',
78
+ search: 'block',
79
+ section: 'block',
80
+ summary: 'block',
81
+ ul: 'block',
82
+ xmp: 'block',
83
+
84
+ // List items
85
+ li: 'list-item',
86
+
87
+ // Table elements
88
+ table: 'table',
89
+ caption: 'table-caption',
90
+ colgroup: 'table-column-group',
91
+ col: 'table-column',
92
+ thead: 'table-header-group',
93
+ tbody: 'table-row-group',
94
+ tfoot: 'table-footer-group',
95
+ tr: 'table-row',
96
+ td: 'table-cell',
97
+ th: 'table-cell',
98
+
99
+ // Inline-block elements
100
+ button: 'inline-block',
101
+ img: 'inline-block',
102
+ input: 'inline-block',
103
+ select: 'inline-block',
104
+ textarea: 'inline-block',
105
+ video: 'inline-block',
106
+ audio: 'inline-block',
107
+ canvas: 'inline-block',
108
+ embed: 'inline-block',
109
+ iframe: 'inline-block',
110
+ object: 'inline-block',
111
+
112
+ // None
113
+ head: 'none',
114
+ link: 'none',
115
+ meta: 'none',
116
+ script: 'none',
117
+ style: 'none',
118
+ title: 'none',
119
+ template: 'none',
120
+
121
+ // Ruby
122
+ ruby: 'ruby',
123
+ rb: 'ruby-base',
124
+ rt: 'ruby-text',
125
+ rp: 'none',
126
+ };
127
+
128
+ // HTML inline elements that should not cause line breaks
129
+ export const INLINE_ELEMENTS = new Set([
130
+ 'a',
131
+ 'abbr',
132
+ 'acronym',
133
+ 'b',
134
+ 'bdo',
135
+ 'big',
136
+ 'br',
137
+ 'button',
138
+ 'cite',
139
+ 'code',
140
+ 'dfn',
141
+ 'em',
142
+ 'i',
143
+ 'img',
144
+ 'input',
145
+ 'kbd',
146
+ 'label',
147
+ 'map',
148
+ 'object',
149
+ 'output',
150
+ 'q',
151
+ 'samp',
152
+ 'script',
153
+ 'select',
154
+ 'small',
155
+ 'span',
156
+ 'strong',
157
+ 'sub',
158
+ 'sup',
159
+ 'textarea',
160
+ 'time',
161
+ 'tt',
162
+ 'u',
163
+ 'var',
164
+ 'wbr',
165
+ ]);
166
+
167
+ // Elements whose content should be preserved as-is
168
+ export const PRESERVE_CONTENT_ELEMENTS = new Set([
169
+ 'pre',
170
+ 'code',
171
+ 'textarea',
172
+ 'script',
173
+ 'style',
174
+ ]);
175
+
176
+ /**
177
+ * Get the CSS display value for a node.
178
+ */
179
+ export function getCSSDisplay(node: SyntaxNode, customTags: Map<string, CustomCodeTagConfig> = EMPTY_MAP): CSSDisplay {
180
+ const type = node.type;
181
+
182
+ if (type === 'html_element') {
183
+ const tagName = getTagName(node);
184
+ if (tagName) {
185
+ const lower = tagName.toLowerCase();
186
+ const config = customTags.get(lower);
187
+ if (config) {
188
+ // Explicit display takes priority
189
+ if (config.display) return config.display;
190
+ // Code tags default to block
191
+ if (isCodeTag(config)) return 'block';
192
+ // Custom tags default to inline-block (inline externally, block internally)
193
+ return 'inline-block';
194
+ }
195
+ return CSS_DISPLAY_MAP[lower] ?? 'inline';
196
+ }
197
+ return 'block'; // Unknown elements default to block
198
+ }
199
+
200
+ if (isRawContentElement(node)) {
201
+ return 'block';
202
+ }
203
+
204
+ if (isMustacheSection(node)) {
205
+ return hasBlockContent(node, customTags) ? 'block' : 'inline';
206
+ }
207
+
208
+ // Text, interpolation, comments, etc. are inline
209
+ return 'inline';
210
+ }
211
+
212
+ /**
213
+ * Check if a display value means the element is whitespace-insensitive
214
+ * (i.e., we can freely add/remove whitespace around it).
215
+ */
216
+ export function isWhitespaceInsensitive(display: CSSDisplay): boolean {
217
+ switch (display) {
218
+ case 'block':
219
+ case 'list-item':
220
+ case 'table':
221
+ case 'table-row':
222
+ case 'table-row-group':
223
+ case 'table-header-group':
224
+ case 'table-footer-group':
225
+ case 'table-column':
226
+ case 'table-column-group':
227
+ case 'table-caption':
228
+ case 'table-cell':
229
+ case 'none':
230
+ return true;
231
+ default:
232
+ return false;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Check if a node represents a block-level element that should cause indentation.
238
+ * Delegates to getCSSDisplay for classification.
239
+ */
240
+ export function isBlockLevel(node: SyntaxNode, customTags: Map<string, CustomCodeTagConfig> = EMPTY_MAP): boolean {
241
+ const type = node.type;
242
+
243
+ // Mustache sections are block-level only if they contain block-level content
244
+ if (isMustacheSection(node)) {
245
+ return hasBlockContent(node, customTags);
246
+ }
247
+
248
+ // HTML elements: check CSS display
249
+ if (type === 'html_element') {
250
+ const display = getCSSDisplay(node, customTags);
251
+ return isWhitespaceInsensitive(display);
252
+ }
253
+
254
+ // Script, style, and raw elements are block-level
255
+ if (isRawContentElement(node)) {
256
+ return true;
257
+ }
258
+
259
+ return false;
260
+ }
261
+
262
+ /**
263
+ * Check if an HTML element is an inline element.
264
+ */
265
+ export function isInlineElement(node: SyntaxNode, customTags: Map<string, CustomCodeTagConfig> = EMPTY_MAP): boolean {
266
+ if (node.type !== 'html_element') {
267
+ return false;
268
+ }
269
+ const display = getCSSDisplay(node, customTags);
270
+ return !isWhitespaceInsensitive(display);
271
+ }
272
+
273
+ /**
274
+ * Check if element content should be preserved as-is.
275
+ */
276
+ export function shouldPreserveContent(node: SyntaxNode, customTags: Map<string, CustomCodeTagConfig> = EMPTY_MAP): boolean {
277
+ const type = node.type;
278
+
279
+ if (isRawContentElement(node)) {
280
+ return true;
281
+ }
282
+
283
+ if (type === 'html_element') {
284
+ const tagName = getTagName(node);
285
+ if (!tagName) return false;
286
+ const lower = tagName.toLowerCase();
287
+ if (PRESERVE_CONTENT_ELEMENTS.has(lower)) return true;
288
+ // Only code tags (with language config) get preserved content, not display-only custom tags
289
+ const config = customTags.get(lower);
290
+ if (config && isCodeTag(config)) return true;
291
+ }
292
+
293
+ return false;
294
+ }
295
+
296
+ /**
297
+ * Check if a mustache section contains any block-level content.
298
+ * A section has block content if:
299
+ * - It contains block-level HTML elements, OR
300
+ * - It contains any HTML elements with implicit end tags (HTML crossing boundaries)
301
+ */
302
+ export function hasBlockContent(sectionNode: SyntaxNode, customTags: Map<string, CustomCodeTagConfig> = EMPTY_MAP): boolean {
303
+ const contentNodes = getContentNodes(sectionNode);
304
+
305
+ // Check for implicit end tags first - this makes the section block-level
306
+ if (hasImplicitEndTags(contentNodes)) {
307
+ return true;
308
+ }
309
+
310
+ // Check for block-level elements
311
+ for (const node of contentNodes) {
312
+ if (isBlockLevelContent(node, customTags)) {
313
+ return true;
314
+ }
315
+ }
316
+ return false;
317
+ }
318
+
319
+ /**
320
+ * Check if a node is block-level content (for determining mustache section treatment).
321
+ */
322
+ export function isBlockLevelContent(node: SyntaxNode, customTags: Map<string, CustomCodeTagConfig> = EMPTY_MAP): boolean {
323
+ const type = node.type;
324
+
325
+ // Any HTML element is considered block-level content for mustache sections
326
+ // This ensures {{#section}}<span>...</span>{{/section}} gets formatted as a block
327
+ if (type === 'html_element') {
328
+ return true;
329
+ }
330
+
331
+ // Script/style/raw are block-level
332
+ if (isRawContentElement(node)) {
333
+ return true;
334
+ }
335
+
336
+ // Nested mustache sections - recurse
337
+ if (isMustacheSection(node)) {
338
+ return hasBlockContent(node, customTags);
339
+ }
340
+
341
+ // Text, interpolation, comments, etc. are inline
342
+ return false;
343
+ }
344
+
345
+ /**
346
+ * Get the content nodes from a mustache section (excluding begin/end tags).
347
+ */
348
+ export function getContentNodes(sectionNode: SyntaxNode): SyntaxNode[] {
349
+ const isInverted = sectionNode.type === 'mustache_inverted_section';
350
+ const beginType = isInverted
351
+ ? 'mustache_inverted_section_begin'
352
+ : 'mustache_section_begin';
353
+ const endType = isInverted
354
+ ? 'mustache_inverted_section_end'
355
+ : 'mustache_section_end';
356
+ const contentNodes: SyntaxNode[] = [];
357
+
358
+ for (let i = 0; i < sectionNode.childCount; i++) {
359
+ const child = sectionNode.child(i);
360
+ if (!child) continue;
361
+ if (
362
+ child.type !== beginType &&
363
+ child.type !== endType &&
364
+ child.type !== 'mustache_erroneous_section_end' &&
365
+ child.type !== 'mustache_erroneous_inverted_section_end' &&
366
+ !child.type.startsWith('_')
367
+ ) {
368
+ contentNodes.push(child);
369
+ }
370
+ }
371
+ return contentNodes;
372
+ }
373
+
374
+ /**
375
+ * Check if any HTML elements in the given nodes have implicit end tags
376
+ * (forced closed by mustache section end rather than explicit </tag>).
377
+ */
378
+ export function hasImplicitEndTags(nodes: SyntaxNode[]): boolean {
379
+ for (const node of nodes) {
380
+ if (hasImplicitEndTagsRecursive(node)) {
381
+ return true;
382
+ }
383
+ }
384
+ return false;
385
+ }
386
+
387
+ function hasImplicitEndTagsRecursive(node: SyntaxNode): boolean {
388
+ // Check if this HTML element has a forced/implicit end tag
389
+ if (node.type === 'html_element') {
390
+ let hasStartTag = false;
391
+ let hasEndTag = false;
392
+ let hasContentChildren = false;
393
+ for (let i = 0; i < node.childCount; i++) {
394
+ const child = node.child(i);
395
+ if (!child) continue;
396
+ if (child.type === 'html_start_tag') hasStartTag = true;
397
+ else if (child.type === 'html_end_tag') hasEndTag = true;
398
+ else if (child.type === 'html_forced_end_tag') return true;
399
+ else if (!child.type.startsWith('_')) hasContentChildren = true;
400
+ }
401
+ // Void elements (start tag only, no content, no end tag) aren't boundary-crossing
402
+ if (hasStartTag && !hasEndTag && hasContentChildren) return true;
403
+ }
404
+
405
+ // Check children recursively
406
+ for (let i = 0; i < node.childCount; i++) {
407
+ const child = node.child(i);
408
+ if (child && hasImplicitEndTagsRecursive(child)) {
409
+ return true;
410
+ }
411
+ }
412
+
413
+ return false;
414
+ }
415
+
416
+ /**
417
+ * Check if a node is inline content that participates in text flow.
418
+ * Mustache interpolation, triple, and partial nodes behave like text.
419
+ */
420
+ function isInlineContentNode(node: SyntaxNode): boolean {
421
+ if (node.type === 'text') return node.text.trim().length > 0;
422
+ return (
423
+ node.type === 'mustache_interpolation' ||
424
+ node.type === 'mustache_triple' ||
425
+ node.type === 'mustache_partial'
426
+ );
427
+ }
428
+
429
+ /**
430
+ * Check if a node is part of a text flow (adjacent to non-whitespace text).
431
+ * Nodes that are part of text flow should stay inline.
432
+ */
433
+ export function isInTextFlow(
434
+ node: SyntaxNode,
435
+ index: number,
436
+ nodes: SyntaxNode[]
437
+ ): boolean {
438
+ // Check previous sibling
439
+ if (index > 0) {
440
+ const prev = nodes[index - 1];
441
+ if (prev.type === 'text' && prev.text.trim().length > 0) {
442
+ return true;
443
+ }
444
+ }
445
+
446
+ // Check next sibling
447
+ if (index < nodes.length - 1) {
448
+ const next = nodes[index + 1];
449
+ if (next.type === 'text' && next.text.trim().length > 0) {
450
+ return true;
451
+ }
452
+ }
453
+
454
+ return false;
455
+ }
456
+
457
+ /**
458
+ * Check if there's inline content (mustache interpolation, non-empty text, etc.)
459
+ * adjacent to the node at `index`, looking past whitespace-only text nodes.
460
+ */
461
+ function hasAdjacentInlineContent(
462
+ index: number,
463
+ nodes: SyntaxNode[]
464
+ ): boolean {
465
+ // Look backward past whitespace-only text
466
+ for (let i = index - 1; i >= 0; i--) {
467
+ const n = nodes[i];
468
+ if (n.type === 'text' && n.text.trim().length === 0) continue;
469
+ if (isInlineContentNode(n)) return true;
470
+ break;
471
+ }
472
+ // Look forward past whitespace-only text
473
+ for (let i = index + 1; i < nodes.length; i++) {
474
+ const n = nodes[i];
475
+ if (n.type === 'text' && n.text.trim().length === 0) continue;
476
+ if (isInlineContentNode(n)) return true;
477
+ break;
478
+ }
479
+ return false;
480
+ }
481
+
482
+ /**
483
+ * Check if an HTML element should stay inline.
484
+ * Elements stay inline if they're part of a text flow or adjacent to other inline elements.
485
+ */
486
+ export function shouldHtmlElementStayInline(
487
+ node: SyntaxNode,
488
+ index: number,
489
+ nodes: SyntaxNode[],
490
+ customTags: Map<string, CustomCodeTagConfig> = EMPTY_MAP
491
+ ): boolean {
492
+ if (node.type !== 'html_element') {
493
+ return false;
494
+ }
495
+
496
+ // Block-display elements (table, div, etc.) should never stay inline
497
+ if (isWhitespaceInsensitive(getCSSDisplay(node, customTags))) {
498
+ return false;
499
+ }
500
+
501
+ // If the element is part of a text flow, keep it inline
502
+ if (isInTextFlow(node, index, nodes)) {
503
+ return true;
504
+ }
505
+
506
+ // Check for adjacent inline content (mustache interpolation, text, etc.)
507
+ // past whitespace-only text nodes — e.g., <i>icon</i>\n {{partial}}%
508
+ if (hasAdjacentInlineContent(index, nodes)) {
509
+ return true;
510
+ }
511
+
512
+ // If adjacent to another inline HTML element, stay inline (e.g., <code>5</code><code>-17</code>)
513
+ // Check if there's a chain of HTML elements with text - they should all stay inline together
514
+ if (index > 0) {
515
+ const prev = nodes[index - 1];
516
+ if (prev.type === 'html_element' && isInTextFlow(prev, index - 1, nodes)) {
517
+ return true;
518
+ }
519
+ }
520
+ if (index < nodes.length - 1) {
521
+ const next = nodes[index + 1];
522
+ if (next.type === 'html_element' && isInTextFlow(next, index + 1, nodes)) {
523
+ return true;
524
+ }
525
+ }
526
+
527
+ return false;
528
+ }
529
+
530
+ /**
531
+ * Determine if a node should be treated as block-level for formatting purposes.
532
+ */
533
+ export function shouldTreatAsBlock(
534
+ node: SyntaxNode,
535
+ index: number,
536
+ nodes: SyntaxNode[],
537
+ customTags: Map<string, CustomCodeTagConfig> = EMPTY_MAP
538
+ ): boolean {
539
+ const isHtmlEl = isHtmlElementType(node);
540
+ const isMustacheSec = isMustacheSection(node);
541
+
542
+ if (node.type === 'html_erroneous_end_tag') return true;
543
+
544
+ return (
545
+ (isHtmlEl && !shouldHtmlElementStayInline(node, index, nodes, customTags)) ||
546
+ (isMustacheSec && !isInTextFlow(node, index, nodes)) ||
547
+ (isBlockLevel(node, customTags) && !isInTextFlow(node, index, nodes))
548
+ );
549
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Shared embedded-region formatter used by both the CLI and the browser entry.
3
+ * Takes the already-parsed rootNode, extracts `<script>` / `<style>` regions,
4
+ * and returns a map of startIndex → prettier-formatted content. If no prettier
5
+ * is provided, returns an empty map (caller falls back to leaving regions as-is).
6
+ */
7
+
8
+ import type { Node as SyntaxNode } from 'web-tree-sitter';
9
+ import { collectEmbeddedRegions } from '../embeddedRegions.js';
10
+ import type { FormattingOptions } from './index.js';
11
+
12
+ export const LANGUAGE_TO_PRETTIER_PARSER: Record<string, string> = {
13
+ javascript: 'babel',
14
+ typescript: 'typescript',
15
+ css: 'css',
16
+ };
17
+
18
+ export interface PrettierLike {
19
+ format(source: string, options: {
20
+ parser: string;
21
+ tabWidth?: number;
22
+ useTabs?: boolean;
23
+ plugins?: unknown[];
24
+ }): string | Promise<string>;
25
+ }
26
+
27
+ export async function formatEmbeddedRegions(
28
+ rootNode: SyntaxNode,
29
+ options: FormattingOptions,
30
+ prettier: PrettierLike | null | undefined,
31
+ ): Promise<Map<number, string>> {
32
+ const result = new Map<number, string>();
33
+ if (!prettier) return result;
34
+
35
+ const regions = collectEmbeddedRegions(rootNode);
36
+ if (regions.length === 0) return result;
37
+
38
+ await Promise.all(
39
+ regions.map(async (region) => {
40
+ const parser = LANGUAGE_TO_PRETTIER_PARSER[region.languageId];
41
+ if (!parser) return;
42
+ try {
43
+ const formatted = await prettier.format(region.content, {
44
+ parser,
45
+ tabWidth: options.tabSize,
46
+ useTabs: !options.insertSpaces,
47
+ });
48
+ result.set(region.startIndex, formatted);
49
+ } catch {
50
+ // Snippet had a syntax error — skip, leave the region as-is.
51
+ }
52
+ })
53
+ );
54
+
55
+ return result;
56
+ }