tsv_svelte 0.8.0

Svelte formatter and parser compatible with Svelte's parse() — part of tsv
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
// Helper utilities for node formatting
//
// Fragment-node classification predicates plus the pattern/expression doc
// builders shared by the block and tag builders, and source position tracking
// used in inline run grouping and multiline formatting decisions.

use super::blocks_doc::AwaitShorthand;
use crate::ast::{EachBlock, EachKey, Fragment, FragmentNode};
use crate::printer::{CommentRun, HeadExpr, Printer};
use smallvec::{SmallVec, smallvec};
use std::borrow::Borrow;
use tsv_lang::doc::DocBuf;
use tsv_lang::doc::arena::DocId;
use tsv_lang::source_scan::find_char_skipping_comments;
use tsv_lang::{Comment, Span};
use tsv_ts::ast::{Expression, ExpressionKind};

/// What a block head's own syntax prints right after its expression — the question the
/// instantiation pair asks of the host ([`Printer::build_expression_doc_for_block`]).
///
/// A word that can start an expression there — the `{#each}` block's `as`, a shorthand
/// `{#await}` block's `then` — makes a head that prints ending on an instantiation's closing
/// `>` take a pair (`tsv_ts::__private::prints_ending_on_instantiation_close`): acorn-typescript,
/// the parser Svelte reads the head with, gives up a type argument list ahead of a same-line word
/// that starts an expression, so a bare `{#each f<T> as item}` is the comparison `f < T > as` and
/// does not parse. A shorthand `{#await}` block's `catch` starts none, so it joins nothing to the
/// close (`{#await f<T> catch e}` keeps its list for every parser). The constructors are the one
/// statement of which heads are followed by such a word.
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum HeadTail {
    /// The head's own syntax continues with a word that can start an expression.
    Keyword,
    /// The head's own `}` follows, or a word that starts no expression (`catch`); either
    /// joins nothing to the close.
    Delimiter,
    /// A delimiter Svelte reads right behind the value, with no comment allowed in between
    /// — the `}` or `,` of an `{#each}` with no `as` binding (`{#each a /* c */}` does not
    /// parse). A value built to end on a comment is built again under
    /// `EmbedContext::value_end_takes_no_comment`, which keeps the author's grouping pair
    /// holding it.
    CommentlessDelimiter,
}

impl HeadTail {
    /// An `{#each}` head is followed by its `as` exactly when it binds a context.
    pub(super) fn of_each(block: &EachBlock<'_>) -> Self {
        if block.context.is_some() {
            Self::Keyword
        } else {
            Self::CommentlessDelimiter
        }
    }

    /// An `{#await}` head is followed by `then` in the `then` shorthand, by `catch` in the
    /// `catch` shorthand, and by its own `}` in the full form.
    pub(super) fn of_await(shorthand: AwaitShorthand) -> Self {
        match shorthand {
            AwaitShorthand::Then => Self::Keyword,
            AwaitShorthand::Catch | AwaitShorthand::None => Self::Delimiter,
        }
    }
}

/// Trailing-comment range end for an `{#each}` head expression: the start of whatever
/// the head puts after the iterable — the `as`-pattern when there is one, else the
/// iterable's own end, which closes the range.
///
/// Narrowing to the pattern start keeps a comment authored inside the pattern
/// (`{#each items /* c */ as item}`) in place rather than relocating it to trail
/// the collection expression. The no-`as` head owes the same narrowing for a stronger
/// reason: its `, index (key)` tail is printed by builders that scan their own gaps, so
/// a range reaching the head's `}` would **overlap** them and print the key's comments a
/// second time after the iterable (`{#each items /* c */, i (i /* c */)}` — a form
/// neither parser accepts). The tail has no trailing-comment region of its own to claim:
/// every position in it outside the key parens is a parse error in canonical Svelte, so
/// closing the range here drops nothing. Shared by the standard and whitespace-sensitive
/// each builders so the two can't drift.
///
/// An `{#each}` context is always written in its own head, so the narrowing needs no ceiling.
/// Its `{#await}` twin `await_expr_comment_end` does — the head there can fold in a binding the
/// author wrote past the body — and both answer the one question this seam asks: how far into
/// the head does the expression's comment range reach, without spanning text someone else
/// prints.
pub(super) fn each_expr_comment_end(block: &EachBlock<'_>) -> u32 {
    match block.context {
        Some(context) => context.span().start,
        None => block.expression.span().end,
    }
}

/// Check if a fragment node is a control flow block (if/each/await/key/snippet).
///
/// Control flow blocks can hug adjacent inline content when directly adjacent,
/// unlike HTML block elements (`<div>`, `<p>`) which get their own lines.
pub(crate) fn is_control_flow_block(node: &FragmentNode<'_>) -> bool {
    matches!(
        node,
        FragmentNode::IfBlock(_)
            | FragmentNode::EachBlock(_)
            | FragmentNode::AwaitBlock(_)
            | FragmentNode::KeyBlock(_)
            | FragmentNode::SnippetBlock(_)
    )
}

/// Check if a fragment node is a control flow block that forces block elements to expand.
///
/// Only if/each/key blocks force expansion. Await blocks do NOT - they stay inline
/// in block elements (e.g., `<div>{#await promise}loading{/await}</div>` stays inline).
pub(super) fn is_expanding_control_flow_block(node: &FragmentNode<'_>) -> bool {
    matches!(
        node,
        FragmentNode::IfBlock(_) | FragmentNode::EachBlock(_) | FragmentNode::KeyBlock(_)
    )
}

/// Check if nodes contain any expanding blocks, either directly or nested in await blocks.
///
/// This is a convenience function combining `is_expanding_control_flow_block` and
/// `has_expanding_block_in_await` checks that are commonly used together.
pub(super) fn has_any_expanding_blocks(nodes: &[FragmentNode<'_>]) -> bool {
    nodes.iter().any(is_expanding_control_flow_block) || has_expanding_block_in_await(nodes)
}

/// Whether a fragment node is inline content (a non-text node that participates in fill).
///
/// This is NOT the same as `!crate::html::is_block_element` (HTML classification): a block
/// element like `<div>` is still inline *content* here. The set is elements / components /
/// expression-or-html-or-render tags — the nodes that can break before a control-flow
/// block. Plain text, comments, and the control-flow blocks themselves are not.
///
/// Single source of truth for the `has_preceding_breakable` test in `fragment_doc`'s node
/// loops and for [`has_control_flow_after_sibling`]'s breakable-sibling gate.
pub(super) fn is_inline_content(node: &FragmentNode<'_>) -> bool {
    matches!(
        node,
        FragmentNode::Element(_)
            | FragmentNode::SpecialElement(_)
            | FragmentNode::ExpressionTag(_)
            | FragmentNode::RenderTag(_)
            | FragmentNode::HtmlTag(_)
    )
}

/// Whether a control-flow block is preceded by a **breakable** sibling — one that is
/// [`is_inline_content`], the same set that sets `has_preceding_breakable`.
///
/// `{#await}` doesn't force its parent multiline on its own (a lone
/// `<div>{#await p}x{/await}</div>` stays inline, matching prettier), unlike if/each/key
/// (`has_any_expanding_blocks`). After a preceding **breakable** sibling it does: forcing a
/// block parent multiline there puts the sibling and the block on the parent's own lines, so
/// the inline-element closing-`>` dangle (`block_sibling_takes_gt`) and a block-element
/// sibling's own-line separation resolve in one pass (the parent break itself is a cataloged
/// divergence — prettier keeps the short construct inline). A non-breakable preceding sibling
/// (plain text, a comment) gets no force — it would only diverge from prettier — so such
/// siblings are skipped. (Block elements are `Element`, hence breakable, so their separation
/// still fires.) The force is also gated on `kind.is_block()` at the call site, so it only
/// applies to block-element parents: an inline parent (a component, an inline element) keeps
/// its content in the inline arm of `build_nodes_doc_trimmed`, which resolves the same dangle
/// and head wrap once the block's own layout breaks the parent.
///
/// A `{#snippet}` mostly precedes this check now: one that owns its line already forced
/// the parent multiline (`Printer::has_own_line_declaration`, asked first at the call
/// site), so the snippet half matters only for a snippet glued to content on both sides —
/// which keeps the author's line and still wants the one-pass resolution after a
/// breakable sibling.
pub(super) fn has_control_flow_after_sibling(nodes: &[FragmentNode<'_>]) -> bool {
    let mut seen_breakable = false;
    for node in nodes {
        if node.is_whitespace_only_text() {
            continue;
        }
        if seen_breakable && is_control_flow_block(node) {
            return true;
        }
        if is_inline_content(node) {
            seen_breakable = true;
        }
    }
    false
}

/// The section fragments of a control-flow block, in source order — `{#if}`'s consequent and
/// alternate, `{#each}`'s body and fallback, `{#await}`'s pending / `{:then}` / `{:catch}`
/// sections, `{#key}`'s body and `{#snippet}`'s body. All `None` for any other node.
///
/// The one enumeration of "the fragments a block holds", for every reader that looks through a
/// block at what it lays out: [`has_expanding_block_in_await`], and the element analysis's
/// block-flow cause (`Printer::block_flow_cause`), which reads the sections the way the block
/// builder does.
pub(super) fn control_flow_sections<'n, 'a>(
    node: &'n FragmentNode<'a>,
) -> [Option<&'n Fragment<'a>>; 3] {
    match node {
        FragmentNode::IfBlock(b) => [Some(&b.consequent), b.alternate.as_ref(), None],
        FragmentNode::EachBlock(b) => [Some(&b.body), b.fallback.as_ref(), None],
        FragmentNode::AwaitBlock(b) => [b.pending.as_ref(), b.then.as_ref(), b.catch.as_ref()],
        FragmentNode::KeyBlock(b) => [Some(&b.fragment), None, None],
        FragmentNode::SnippetBlock(b) => [Some(&b.body), None, None],
        _ => [None, None, None],
    }
}

/// Check if any await block contains expanding blocks (if/each/key) in its content.
///
/// Prettier treats expanding blocks inside await blocks as if they were directly
/// in the parent element, forcing multiline. For example:
/// `<a>{#await p}{#if c}text{/if}{/await}</a>` breaks because the if block
/// is effectively inside the inline element.
///
/// This function recursively checks nested await blocks, so deeply nested
/// structures like `{#await p1}{#await p2}{#if c}...{/if}{/await}{/await}`
/// are also detected.
fn has_expanding_block_in_await(nodes: &[FragmentNode<'_>]) -> bool {
    nodes.iter().any(|n| {
        matches!(n, FragmentNode::AwaitBlock(_))
            && control_flow_sections(n).into_iter().flatten().any(|f| {
                f.nodes.iter().any(is_expanding_control_flow_block)
                    || has_expanding_block_in_await(f.nodes)
            })
    })
}

impl<'a> Printer<'a> {
    /// Collect the leading-comment run in `[from, to)` — one doc per comment via
    /// [`build_leading_js_comment_doc_before`](Self::build_leading_js_comment_doc_before), so every
    /// leading-comment site (directive/tag values, block heads, `{@const}` init,
    /// braced attribute expressions, destructure patterns) flows through the one
    /// emitter that reindents+breaks a multi-line block. Empty when the range holds
    /// none. Callers concat it, or `extend` an in-progress buffer with it. Each comment is
    /// built with what follows it in hand — the next comment, or the value at `to` — so an
    /// author blank after a `//` survives
    /// ([`build_leading_js_comment_doc_before`](Self::build_leading_js_comment_doc_before)).
    ///
    /// Nearly every head and value gap holds no comment, so the gate is inline at the caller
    /// ([`Self::gap_known_comment_free`]): the empty run is written in place, with no call
    /// and neither buffer built; only a gap a search must answer reaches the outlined collect.
    #[inline]
    pub(in crate::printer) fn leading_comment_docs(&self, from: u32, to: u32) -> DocBuf {
        if self.gap_known_comment_free(from, to) {
            return DocBuf::new();
        }
        self.leading_comment_docs_wide(from, to)
    }

    /// The search half of [`Self::leading_comment_docs`] — one outlined copy.
    #[inline(never)]
    fn leading_comment_docs_wide(&self, from: u32, to: u32) -> DocBuf {
        let run: CommentRun<'_> = self.comments_to_emit_between(from, to).collect();
        self.leading_comment_run_docs(&run, to)
    }

    /// [`leading_comment_docs`](Self::leading_comment_docs) on the **in-source** axis, for
    /// the one builder that is the sole positional emitter of every comment in its content
    /// and so must print an owned glued block too — `{@debug}`
    /// ([`build_debug_tag_doc`](Self::build_debug_tag_doc)). Same emitter, same blank rule;
    /// only which comments the range yields differs.
    pub(in crate::printer) fn leading_comment_docs_in_source(&self, from: u32, to: u32) -> DocBuf {
        let run: CommentRun<'_> = self.comments_in_source_between(from, to).collect();
        self.leading_comment_run_docs(&run, to)
    }

    /// The two above over a run the caller already holds — `to` is what follows the run's
    /// last comment, so the blank below it is read the same way.
    pub(in crate::printer) fn leading_comment_run_docs(&self, run: &[&Comment], to: u32) -> DocBuf {
        run.iter()
            .enumerate()
            .map(|(i, comment)| {
                let next_start = run.get(i + 1).map_or(to, |next| next.span.start);
                self.build_leading_js_comment_doc_before(comment, next_start)
            })
            .collect()
    }

    /// Collect the trailing-comment run in `[from, to)` — one doc per comment via
    /// [`build_trailing_js_comment_doc`](Self::build_trailing_js_comment_doc) — paired with
    /// whether the run **ends** in a line comment.
    ///
    /// That flag is [`HeadExpr::ends_with_line_comment`], read off the run being emitted
    /// rather than rescanned from source, so the two answers cannot disagree — which is why
    /// the run and the flag come from one call.
    ///
    /// `closer_owns_break` — **whether the caller's closing token sits outside an
    /// `indent(…)` wrapping this content.** It decides which emitter supplies the newline
    /// after a run-final `//`, and therefore what column the closer lands in: the renderer
    /// indents a line from the line command that produced it, so a break emitted from inside
    /// the indent puts the closer at CONTENT indent while the closer's own break puts it at
    /// the closer's. Default `false` — the closer usually sits on the content's own level and
    /// the comment's `hardline` is exactly the break it needs. Interior comments always keep
    /// their `hardline` — the break *between* two comments is the run's own business.
    ///
    /// ⚠️ **The question is what indents the CONTENT, not which builder is calling.** Four
    /// things do, and each is a `true`; a caller that answers off its freeze verdict alone
    /// has only found the first of them:
    ///
    /// 1. **content that opens on its own line** — a freeze, or a leading `//` the author
    ///    put on its own line: [`Printer::indent_own_line_head`], at every prefixed head.
    /// 2. an **always-block** value — `wrap_in_block_structure`, the `bind:` path.
    /// 3. a **leading line comment** hanging a braced head's continuation —
    ///    [`Printer::head_layout`], at every braced head: the block
    ///    heads, the prefixed tags, and the unprefixed `{…}` values alike.
    /// 4. the **break-after-operator** layout of a `{@const}` init, which indents the value
    ///    under the `=` while the tag's `}` stays outside — `build_assignment_tag_doc`.
    ///
    /// 3 and 4 are the ones a caller can get wrong for free, because both decide the indent
    /// *after* building this run: the answer has to be hoisted above the call, not read off
    /// the shape that is assembled below it. 3 is one shared predicate rather than a scan per
    /// builder for a second reason — asked in each builder's own words, the family answered it
    /// three different ways and only the block heads got it right.
    ///
    /// Getting this wrong is not subtle in one direction and invisible in the other: too
    /// eager and a `//` swallows the closer (`deno task swallow:audit` gates it), too lazy
    /// and the closer is indented one level too deep — cosmetic, idempotent, and therefore
    /// invisible to every standing gate, so it is fixture-pinned instead
    /// ([`expr_trailing_indented_content`](../../../../../tests/fixtures/svelte/syntax/comments/expr_trailing_indented_content_prettier_divergence/)).
    #[inline]
    pub(in crate::printer) fn trailing_comment_docs(
        &self,
        from: u32,
        to: u32,
        closer_owns_break: bool,
    ) -> (DocBuf, bool) {
        // The common gap holds no comment — an empty run, which ends in no line comment —
        // answered inline at the caller; only a gap a search must answer pays the call.
        if self.gap_known_comment_free(from, to) {
            return (DocBuf::new(), false);
        }
        self.trailing_comment_docs_wide(from, to, closer_owns_break)
    }

    /// The search half of [`Self::trailing_comment_docs`] — one outlined copy.
    #[inline(never)]
    fn trailing_comment_docs_wide(
        &self,
        from: u32,
        to: u32,
        closer_owns_break: bool,
    ) -> (DocBuf, bool) {
        self.trailing_comment_run_docs(self.comments_to_emit_between(from, to), closer_owns_break)
    }

    /// [`Self::trailing_comment_docs`] over an arbitrary run — the **pairing** itself,
    /// without the lookup: one doc per comment, whether the run ENDS in a line comment, and
    /// `closer_owns_break`'s dedent on the run's LAST comment only, since only the last can
    /// supply the break the closer reuses.
    ///
    /// Split out so a builder standing on a different comment **axis** shares the pairing
    /// rather than restating it. `build_debug_tag_doc` is that caller and the reason the seam
    /// exists: it is the sole positional emitter of every comment in its content, so it works
    /// the **in-source** axis, and the to-emit lookup above would skip an owned one and drop
    /// it (see that builder's doc comment, and
    /// [the three axes](../../../../../docs/comments.md)).
    pub(in crate::printer) fn trailing_comment_run_docs<'c>(
        &self,
        comments: impl Iterator<Item = &'c Comment>,
        closer_owns_break: bool,
    ) -> (DocBuf, bool) {
        let mut comments = comments.peekable();
        let mut ends_with_line_comment = false;
        let mut docs = DocBuf::new();
        let mut prev: Option<&Comment> = None;
        while let Some(c) = comments.next() {
            ends_with_line_comment = !c.is_block;
            let last = comments.peek().is_none();
            // A `//` this run emitted ended its line, so the next comment starts one — the
            // run's own anchor, never a source peek, which a stripped `)` between the two
            // (`...(a // c1⏎) // c2`) reads as content and spaces the comment off the margin.
            let starts_line = match prev {
                Some(p) => !p.is_block,
                None => self.trailing_comment_starts_line(c),
            };
            docs.push(self.build_trailing_js_comment_doc_at(
                c,
                last && closer_owns_break,
                starts_line,
            ));
            prev = Some(c);
        }
        (docs, ends_with_line_comment)
    }

    /// Emit every comment in `[start, end)` in **leading** style, inline: each block comment
    /// as `/* … */ ` (trailing space), on the line of what follows it wherever the author
    /// wrote it — the twin's answer at every gap this inline builder prints, since an own-line
    /// comment in a list gap routes the whole pattern to the twin's layout instead
    /// ([`Self::build_block_pattern_doc`]), and at a key's `:` or a default's `=` the twin
    /// collapses it (`{ a =⏎/* c */⏎1 }` → `{ a = /* c */ 1 }`). Empty doc when the range holds
    /// no comments.
    ///
    /// A `//` or a multi-line block breaks the pattern and so never reaches here; should one,
    /// the run takes the shared leading emitter, whose break keeps the `//` from swallowing
    /// what follows it.
    fn build_pattern_leading_comments(&self, start: u32, end: u32) -> DocId {
        let d = self.d();
        if self.gap_known_comment_free(start, end) {
            return d.empty();
        }
        let run: SmallVec<[&Comment; 2]> = self.comments_to_emit_between(start, end).collect();
        if run.iter().any(|c| !c.is_block || c.multiline) {
            return d.concat(&self.leading_comment_docs(start, end));
        }
        let inputs = self.ts_inputs();
        let docs: DocBuf = run
            .iter()
            .flat_map(|c| {
                [
                    tsv_ts::__private::build_comment_doc(d, c, &inputs),
                    d.text(" "),
                ]
            })
            .collect();
        d.concat(&docs)
    }

    /// Emit every comment in `[start, end)` in **trailing** style: a block comment as
    /// ` /* … */` (inline, leading space); a line comment as ` // …` + `hardline`. Empty
    /// doc when the range holds no comments.
    fn build_pattern_trailing_comments(&self, start: u32, end: u32) -> DocId {
        let (docs, _) = self.trailing_comment_docs(start, end, false);
        self.d().concat(&docs)
    }

    /// Build a two-sided delimiter gap (`,` / `:` / `=`) between two pattern pieces,
    /// keeping each comment on the side of the delimiter the author wrote it: comments
    /// before `delim` trail the left piece, comments after lead the right piece — so an
    /// association like `a /* c */ = 1` (comment on the binding) is never flipped onto the
    /// value. `delim_text` is the rendered separator (`", "`, `": "`, `" = "`). Falls back
    /// to the bare separator when the delimiter isn't found (defensive — the parser
    /// guarantees one in every gap this is called on).
    fn build_pattern_delim_gap(
        &self,
        left_end: u32,
        right_start: u32,
        delim: u8,
        delim_text: &'static str,
    ) -> DocId {
        let d = self.d();
        match find_char_skipping_comments(
            self.source.as_bytes(),
            left_end as usize,
            right_start as usize,
            delim,
        ) {
            Some(pos) => {
                let pos = pos as u32;
                let before = self.build_pattern_trailing_comments(left_end, pos);
                let after = self.build_pattern_leading_comments(pos + 1, right_start);
                d.concat(&[before, d.text(delim_text), after])
            }
            None => d.text(delim_text),
        }
    }

    /// Build a `...rest` binding, threading any comment in the `...`→binding gap
    /// (`.../* c */ rest`). Shared by the array-pattern and object-pattern rest arms and the
    /// default value's spread.
    ///
    /// The node's stripped-paren interior (`...(rest /* c */)`) is NOT this doc's: the
    /// enclosing brackets anchor their next gap at the binding's printed end
    /// ([`element_printed_end`]), so the interior and the gap past the `)` are one trailing
    /// run — this builder only prints a pattern no comment breaks (an own-line one routes
    /// the whole pattern to the twin's layout, [`Self::build_block_pattern_doc`]), so there
    /// is no own-line share to split off, and one run is what lets each comment take its
    /// separator from the one before it.
    fn build_rest_pattern_doc(&self, rest_span_start: u32, argument: &Expression<'_>) -> DocId {
        let d = self.d();
        let dots_end = rest_span_start + 3; // past "..."
        let lead = self.build_pattern_leading_comments(dots_end, argument.span().start);
        d.concat(&[d.text("..."), lead, self.build_pattern_doc(argument)])
    }

    /// Build a property key for a non-shorthand object-pattern property, returning
    /// `(doc, key_region_end)` where `key_region_end` is the source position just past the
    /// key (after `]` for a computed key) — the lower bound for the following colon gap. A
    /// computed key threads comments inside its `[ … ]` brackets.
    fn build_pattern_key_doc(
        &self,
        computed: bool,
        key: &Expression<'_>,
        prop_start: u32,
        value_start: u32,
    ) -> (DocId, u32) {
        let d = self.d();
        if computed {
            let key_start = key.span().start;
            let key_end = key.span().end;
            let close = find_char_skipping_comments(
                self.source.as_bytes(),
                key_end as usize,
                value_start as usize,
                b']',
            )
            .map_or(key_end, |p| p as u32);
            let lead = self.build_pattern_leading_comments(prop_start + 1, key_start);
            // Comment-aware so an owned leading comment glued to the key (`[/* c */ k]`)
            // is claimed by the key's own doc — the `[`→key gap emitter above skips it
            // (owned comments are off the positional axis), so nothing else would print it.
            //
            // The non-computed arm below is comment-aware too, and there it is inert rather
            // than load-bearing: `bind_leading_comment` has a single call site, the expression
            // HEAD path, and a non-computed property key is read by the property parser without
            // passing through it — so a comment glued to such a key is never `owned_by_node`,
            // and the enclosing `{`→property gap emitter prints it positionally. Both builders
            // therefore emit the same doc for that key (byte-identical over tests/fixtures and
            // a pattern-key shape matrix). Using the comment-aware one anyway keeps the two
            // arms the same shape, so neither can quietly become a comment sink.
            //
            // Cannot-hang, like the TS twin (`build_computed_key_expr_doc`): the `[`→key
            // gap has no operator line to end, so a leading cast's break reflows and the
            // pattern formats the same in both contexts.
            let key_doc = self.build_ts_expression_doc_cannot_hang(key);
            let trail = self.build_pattern_trailing_comments(key_end, close);
            let doc = d.concat(&[d.text("["), lead, key_doc, trail, d.text("]")]);
            (doc, close + 1)
        } else {
            (self.build_ts_expression_doc(key), key.span().end)
        }
    }

    /// Build a single object property (`key: value` / shorthand `key`), threading comments
    /// through the computed-key brackets and the `key`→`value` colon gap. A shorthand
    /// property routes its value (an `AssignmentPattern` for a default) back through
    /// `build_pattern_doc`, whose arms carry any interior comments. Shared by the binding
    /// (`ObjectPattern`) and the default-value (`ObjectExpression`) property dispatchers,
    /// which differ only in their enum types — both wrap the same `Property` fields.
    fn build_pattern_property_kv(
        &self,
        shorthand: bool,
        computed: bool,
        key: &Expression<'_>,
        value: &Expression<'_>,
        prop_start: u32,
    ) -> DocId {
        let d = self.d();
        if shorthand {
            self.build_pattern_doc(value)
        } else {
            let value_start = value.span().start;
            let (key_doc, key_region_end) =
                self.build_pattern_key_doc(computed, key, prop_start, value_start);
            let colon_gap = self.build_pattern_delim_gap(key_region_end, value_start, b':', ": ");
            d.concat(&[key_doc, colon_gap, self.build_pattern_doc(value)])
        }
    }

    /// Property dispatcher for a binding `ObjectPattern`.
    fn build_object_pattern_property_doc(
        &self,
        prop: &tsv_ts::ast::ObjectPatternProperty<'_>,
    ) -> DocId {
        match prop {
            tsv_ts::ast::ObjectPatternProperty::Property(p) => self.build_pattern_property_kv(
                p.shorthand,
                p.computed,
                p.key,
                p.value,
                p.span.start,
            ),
            tsv_ts::ast::ObjectPatternProperty::RestElement(r) => {
                self.build_rest_pattern_doc(r.span.start, r.argument)
            }
        }
    }

    /// Property dispatcher for an `ObjectExpression` reached as an assignment-pattern
    /// **default value** (`{ a = { b: /* c */ 1 } }`). Same shape as the binding
    /// dispatcher; only the enum (`ObjectProperty` / `SpreadElement`) differs.
    fn build_object_expr_property_doc(&self, prop: &tsv_ts::ast::ObjectProperty<'_>) -> DocId {
        match prop {
            tsv_ts::ast::ObjectProperty::Property(p) => self.build_pattern_property_kv(
                p.shorthand,
                p.computed,
                p.key,
                p.value,
                p.span.start,
            ),
            tsv_ts::ast::ObjectProperty::SpreadElement(s) => {
                self.build_rest_pattern_doc(s.span.start, s.argument)
            }
        }
    }

    /// Build comment-aware object braces (`{ … }`) from pre-built property entries
    /// `(span_start, span_end, doc)`. `bracketSpacing: true` pads non-empty braces;
    /// comments thread through every gap (after `{`, around each `,`, before `}`) and a
    /// dangling comment in an empty pattern is preserved. Shared by the `ObjectPattern`
    /// (binding) and `ObjectExpression` (default-value) arms.
    ///
    /// Printing the opening `{` is what makes this the node's own emitter, so the
    /// owned-comment claim ([`Printer::claim_owned_leading_comment`]) is made here rather
    /// than by each arm — a comment glued before the brace is otherwise printed by
    /// nobody.
    fn build_object_braces(
        &self,
        span_start: u32,
        span_end: u32,
        entries: &[(u32, u32, DocId)],
    ) -> DocId {
        let d = self.d();
        let mut parts: DocBuf = smallvec![d.text("{")];
        if entries.is_empty() {
            // Empty pattern stays tight (`{}`), but preserve a dangling comment.
            parts.push(self.build_pattern_leading_comments(span_start + 1, span_end - 1));
        } else {
            parts.push(d.text(" "));
            let mut prev_end = span_start + 1; // past `{`
            for (i, &(estart, eend, edoc)) in entries.iter().enumerate() {
                if i == 0 {
                    parts.push(self.build_pattern_leading_comments(prev_end, estart));
                } else {
                    parts.push(self.build_pattern_delim_gap(prev_end, estart, b',', ", "));
                }
                parts.push(edoc);
                prev_end = eend;
            }
            parts.push(self.build_pattern_trailing_comments(prev_end, span_end - 1));
            parts.push(d.text(" "));
        }
        parts.push(d.text("}"));
        self.claim_owned_leading_comment(d.concat(&parts), span_start)
    }

    /// Build comment-aware array brackets (`[ … ]`) — no bracket spacing in either
    /// formatter. Comments thread through every gap (after `[`, around each `,`, before
    /// `]`); a hole (`[a, , b]`) has no element span to anchor against, so its separator
    /// stays a bare comma. Shared by the `ArrayPattern` (binding) and `ArrayExpression`
    /// (default-value) arms — generic over the slot's element, since the pattern holds
    /// `Option<Expression>` and the literal `Option<&Expression>`.
    /// Claims the owned leading comment for the same reason its object twin does.
    fn build_array_brackets<'e, E: Borrow<Expression<'e>>>(
        &self,
        elements: &[Option<E>],
        span_start: u32,
        span_end: u32,
    ) -> DocId {
        let d = self.d();
        let mut parts: DocBuf = smallvec![d.text("[")];
        let mut prev_end = span_start + 1; // past `[`
        for (i, elem) in elements.iter().enumerate() {
            let elem = elem.as_ref().map(Borrow::borrow);
            if i == 0 {
                if let Some(e) = elem {
                    parts.push(self.build_pattern_leading_comments(prev_end, e.span().start));
                }
            } else if let Some(e) = elem {
                parts.push(self.build_pattern_delim_gap(prev_end, e.span().start, b',', ", "));
            } else {
                parts.push(d.text(", "));
            }
            if let Some(e) = elem {
                parts.push(self.build_pattern_doc(e));
                // The element's PRINTED end: a rest's or a default value's stripped parens
                // (`[b, ...(a /* c */)]`, `[a = (1 /* c */), b]`) are this gap's.
                prev_end = element_printed_end(e.paren_interior(), e.printed_end());
            }
        }
        parts.push(self.build_pattern_trailing_comments(prev_end, span_end - 1));
        parts.push(d.text("]"));
        self.claim_owned_leading_comment(d.concat(&parts), span_start)
    }

    /// Append a destructuring pattern's `: T` tail (`{#each xs as { a }: T}`).
    ///
    /// A binding **identifier** carries its annotation through the TypeScript
    /// pattern printer, which `build_pattern_doc`'s default arm routes to. A
    /// destructuring pattern does not: its braces/brackets are built here, on the
    /// Svelte side's own comment-preserving path, so the tail has to be appended
    /// explicitly or it prints nowhere — and an annotation that prints nowhere is
    /// content loss on a format round-trip, not a formatting choice.
    fn append_pattern_type_annotation(
        &self,
        pattern: DocId,
        type_annotation: Option<&tsv_ts::ast::TSTypeAnnotation<'_>>,
    ) -> DocId {
        let Some(annotation) = type_annotation else {
            return pattern;
        };
        let d = self.d();
        let tail = tsv_ts::__private::build_type_annotation_doc(
            d,
            annotation,
            &self.ts_inputs(),
            self.embed,
        );
        d.concat(&[pattern, tail])
    }

    /// Build a **block binding pattern** — the `{#each … as P}`, `{#await … then P}`,
    /// `{:then P}` and `{:catch P}` binding — as a whole: the one entry the block builders
    /// call, where [`Self::build_pattern_doc`] is the inline builder it recurses through.
    ///
    /// **A block pattern lays out like its TypeScript twin wherever a comment breaks it.** A
    /// `//`, a multi-line block, or a block the author put on its own line opens the twin
    /// (`const P = x`) one entry per line; the same comment opens the block pattern the same
    /// way, through the twin's own builder
    /// (`tsv_ts::__private::build_block_pattern_broken_doc`), so the delimiter-line rule, the
    /// element-comma seam, the author blanks and the `: T` tail are the twin's by
    /// construction. Every other pattern stays on one line — at any width, and whatever it
    /// nests — which is how prettier-plugin-svelte keeps these heads, and is the one twin rule
    /// the block pattern does not take (the declaration's nested-pattern break). See
    /// conformance_prettier_svelte.md §Svelte: destructuring binding-pattern comments.
    ///
    /// A caller whose layout depends on whether the pattern broke asks `will_break` of this
    /// doc before wrapping it — the `{#each}` clause rides inside an `if_break`, which
    /// `will_break` cannot see through.
    pub(super) fn build_block_pattern_doc(&self, expr: &Expression<'_>) -> DocId {
        if matches!(
            expr.kind,
            ExpressionKind::ObjectPattern(_) | ExpressionKind::ArrayPattern(_)
        ) && !self.gap_known_comment_free(
            expr.span.start,
            tsv_ts::__private::pattern_binding_end(expr),
        ) && let Some(doc) = tsv_ts::__private::build_block_pattern_broken_doc(
            self.d(),
            expr,
            &self.ts_inputs(),
            self.embed,
        ) {
            return doc;
        }
        self.build_pattern_doc(expr)
    }

    /// Build the **inline** layout of a pattern (destructuring context) — every block
    /// pattern no comment breaks, reached through [`Self::build_block_pattern_doc`], and the
    /// patterns and default values nested in one.
    ///
    /// Patterns use specific whitespace rules:
    /// - Object patterns: `{ a, b }` (inner-padded braces, `bracketSpacing: true`)
    /// - Array patterns: `[a, b]` (no spaces inside brackets — `bracketSpacing`
    ///   governs object braces, not array brackets)
    ///
    /// Non-empty object braces carry an inner space (`{ a, b }`) under the project-wide
    /// `bracketSpacing: true`, consistent with every other object tsv emits and matching
    /// prettier-plugin-svelte; an empty `{}` stays tight.
    ///
    /// **Comments are preserved in place.** A single-line block comment in any position —
    /// after a brace/bracket, around a `,` / `:` / `=`, inside a property, before the close —
    /// is threaded through via `comments_to_emit_in_range` and kept where the author wrote
    /// it, on the pattern's one line. A comment that breaks the twin (`const P = x`) never
    /// reaches here — a `//`, a multi-line block, an own-line block in a list gap or one that
    /// opens a default value: the caller hands the whole pattern to the twin's layout. An
    /// own-line block anywhere else (at a key's `:`, a default's `=`) collapses onto the
    /// line, as the twin collapses it. prettier-plugin-svelte prints these patterns from a
    /// comment-blind path and drops them, so this is a `_prettier_divergence` (see
    /// conformance_prettier_svelte.md §Svelte: destructuring binding-pattern comments).
    ///
    /// Literal **default values** normalize through the TS printer (string quotes +
    /// numeric form), where prettier-plugin-svelte preserves the author's source
    /// token — a separate deliberate divergence (see conformance_prettier_svelte.md §Svelte:
    /// destructuring literal normalization).
    pub(super) fn build_pattern_doc(&self, expr: &Expression<'_>) -> DocId {
        match &expr.kind {
            // Comments thread through every gap so a comment in any pattern position is
            // preserved in place — a `_svelte_prettier_divergence` from prettier-plugin-svelte,
            // which drops it. The `*Expression` variants are reached as assignment-pattern
            // **default values** (`{ a = { … } }` / `{ a = [ … ] }`), which prettier likewise
            // keeps inline (so they share the always-inline binding shape, not the breakable
            // expression printer); they just need the same comment-awareness.
            //
            // These four arms assemble their docs here rather than through the TS
            // expression builder, so the owned-comment claim that builder would have made
            // for a block glued right before them (`{ a = /* c */ { … } }`,
            // `{ a: /* c */ { … } }`, `[/* c */ { … }]`) is the brace/bracket builders' —
            // made where the opening delimiter is printed, so it lands inside any type
            // annotation rather than between the pattern and its `: T`.
            ExpressionKind::ObjectPattern(obj) => {
                let entries: SmallVec<[(u32, u32, DocId); 8]> = obj
                    .properties
                    .iter()
                    .map(|p| {
                        let s = p.span();
                        // The property's PRINTED end: a value's or a rest's stripped parens
                        // (`{ b: (a /* c */) }`, `{ ...(a /* c */) }`) are the next gap's.
                        let end = element_printed_end(p.paren_interior(), p.value_end());
                        (s.start, end, self.build_object_pattern_property_doc(p))
                    })
                    .collect();
                let braces = self.build_object_braces(expr.span.start, expr.span.end, &entries);
                self.append_pattern_type_annotation(braces, obj.type_annotation.as_ref())
            }
            ExpressionKind::ObjectExpression(obj) => {
                let entries: SmallVec<[(u32, u32, DocId); 8]> = obj
                    .properties
                    .iter()
                    .map(|p| {
                        let s = p.span();
                        let end = element_printed_end(p.paren_interior(), p.value_end());
                        (s.start, end, self.build_object_expr_property_doc(p))
                    })
                    .collect();
                self.build_object_braces(expr.span.start, expr.span.end, &entries)
            }
            ExpressionKind::ArrayPattern(arr) => {
                let brackets =
                    self.build_array_brackets(arr.elements, expr.span.start, expr.span.end);
                self.append_pattern_type_annotation(brackets, arr.type_annotation.as_ref())
            }
            ExpressionKind::ArrayExpression(arr) => {
                self.build_array_brackets(arr.elements, expr.span.start, expr.span.end)
            }
            ExpressionKind::RestElement(rest) => {
                self.build_rest_pattern_doc(rest.span.start, rest.argument)
            }
            // Comments around the `=` stay on the side the author wrote them
            // (`a /* c */ = 1` vs `a = /* c */ 1`). The `Expression` variant is the
            // default-value form of the same `=`.
            ExpressionKind::AssignmentPattern(assign) => {
                self.build_pattern_assignment(assign.left, assign.right)
            }
            ExpressionKind::AssignmentExpression(assign) => {
                self.build_pattern_assignment(assign.left, assign.right)
            }
            // Default: build doc through the comment-aware TS builder. Literals route
            // here too, so string and numeric defaults normalize through the TS printer
            // (single quotes + escaping, lowercase hex/exponent, leading/trailing zeros) —
            // identical to `{@const}` and every other literal tsv emits.
            // prettier-plugin-svelte instead prints these binding patterns from raw
            // source, preserving the author's quote style and numeric form; tsv
            // normalizes uniformly (a deliberate divergence — see
            // conformance_prettier_svelte.md §Svelte: destructuring literal normalization).
            //
            // Comment-aware (not `_no_comments`) so an owned leading comment glued to the
            // leaf — a default value (`{ a = /* c */ 1 }`), a rename value (`{ g: /* c */ h }`),
            // an array element (`[/* c */ m]`), a rest argument (`.../* c */ rest`) — is
            // claimed by the leaf's own doc via `prepend_owned_leading_comment`. Every gap
            // emitter around it skips owned comments (the to-emit axis), so without this the
            // comment reaches no printer and is dropped. The surrounding positional gap
            // comments stay with the pattern printer: `build_expression_doc` prepends only
            // the owned leading comment, never a positional one, so there is no double-emit.
            _ => self.build_ts_expression_doc(expr),
        }
    }

    /// Build a doc for a binding assignment (`left = right`) — the shared body of
    /// the `AssignmentPattern` (default-value binding) and `AssignmentExpression`
    /// pattern arms. Comments around the `=` stay on the side the author wrote
    /// them (`a /* c */ = 1` vs `a = /* c */ 1`) via `build_pattern_delim_gap`.
    ///
    /// A default value can DEFER a comment to its own end — a block the author put on its own
    /// line inside the value's stripped grouping pair (`{ v = a || (b⏎/* c */⏎) }`) — and the
    /// inline pattern has no line end before the head's `}`: left pending, the comment flushes
    /// past it into the block's body. An embed end behind the value
    /// ([`Printer::value_trailing_docs`] states the node) flushes it there, glued onto the
    /// value's line (`{ v = a || b /* c */ }`) — the same-line block the next pass reads, which
    /// the inline pattern keeps. A `//` never reaches this: it breaks the pattern into its
    /// twin's layout first ([`Self::build_block_pattern_doc`]).
    fn build_pattern_assignment(&self, left: &Expression<'_>, right: &Expression<'_>) -> DocId {
        let d = self.d();
        let left_doc = self.build_pattern_doc(left);
        let eq = self.build_pattern_delim_gap(left.span().end, right.span().start, b'=', " = ");
        let right_doc = self.build_pattern_doc(right);
        if !self.gap_known_comment_free(right.span().start, right.span().end)
            && d.holds_line_suffix(right_doc)
        {
            let pending = d.pending_run_at_end(right_doc, self.source);
            return d.concat(&[left_doc, eq, right_doc, d.embed_end(pending, false).doc()]);
        }
        d.concat(&[left_doc, eq, right_doc])
    }

    /// Build a doc for an expression with leading and trailing comments — the **prefixed
    /// tag** head (`{@html …}`, `{@render …}`).
    ///
    /// Only the value is this builder's own; the comment runs, their hang verdict and the
    /// indent are [`Printer::assemble_head_expr`]'s. The value is built under the shared
    /// braced-head [`Printer::head_embed`], so a ROOT binary chain uses ContinuationIndent
    /// style (nested binaries format as in `<script>`).
    ///
    /// This is [`Self::build_expression_doc_for_block`] minus its `remove_lines` step (a
    /// prefixed tag is never built for an inline context) and minus the `}` dangle its
    /// caller supplies — the two differ in nothing else, which is what the shared tail makes
    /// true rather than merely claims.
    ///
    /// Returns the [`Printer::honored_directive_in_gap`] verdict alongside the doc, the way
    /// the attribute-value builder does: an own-line directive in the prefix→value gap
    /// freezes the value whole, and the caller owes it the broken prefixed form
    /// ([`Printer::build_prefixed_head_doc`]), which is what keeps the directive on its own
    /// line. See [`HeadExpr`] for why the verdict travels back out rather than in.
    pub(super) fn build_expression_with_comments_doc(
        &self,
        expr: &Expression<'_>,
        span_start: u32,
        span_end: u32,
    ) -> HeadExpr {
        let frozen = self.honored_directive_in_gap(span_start, expr.span().start);

        // The shared braced-head embed ([`Printer::head_embed`], which carries the reasoning).
        //
        // Built directly in the shared arena — the surrounding doc tree (closing `}`, etc.)
        // provides natural lookahead via arena_fits_with_lookahead's rest_commands.
        let value_doc = self.build_head_value_doc(expr, frozen, &self.head_embed());
        let value_doc = self.wrap_value_clarity_parens(expr, value_doc);

        self.assemble_head_expr(value_doc, span_start, expr, span_end, frozen)
    }

    /// Build expression doc for block expressions (if, each, await, key).
    ///
    /// # Context-dependent behavior
    ///
    /// - **Inline context** (`in_multiline_context=false`): Applies `remove_lines()` to prevent
    ///   the block condition from breaking. When the line exceeds print_width, EARLIER content
    ///   should break instead. Example: `{expr}{#if cond}` - expr breaks, cond stays flat.
    ///
    /// - **Multiline context** (`in_multiline_context=true`): The condition is on its own line.
    ///   No `remove_lines()` is applied, allowing long chains to wrap naturally.
    ///   Uses `LayoutMode::Embedded` for continuation indent on a wrapped ROOT binary, plus
    ///   `EmbedContext::root_sequence_indents` for the same indent on a wrapped ROOT
    ///   sequence — the block head's own field, which the shared recipe deliberately omits
    ///   (see [`Printer::head_embed`]). Both reach the `{#each}` key built through here too;
    ///   it is inside the head prettier declines to wrap, so the same argument covers it.
    ///
    /// # Parameters
    /// - `in_multiline_context` - Whether the block is on its own line (multiline) or inline
    /// - `tail` - What the head's own syntax prints right after the expression ([`HeadTail`]).
    ///
    /// The embed and `remove_lines` above are all this builder owns; the comment runs, their
    /// hang verdict and the indent are [`Printer::assemble_head_expr`]'s.
    ///
    /// Returns the [`Printer::honored_directive_in_gap`] verdict alongside the doc — see
    /// [`Self::build_expression_with_comments_doc`] for why the verdict travels back out.
    /// An own-line head is already broken (`Printer::indent_own_line_head`), so its caller owes
    /// it the space-less prefix and must not hug its tail onto the slice's last line.
    pub(super) fn build_expression_doc_for_block(
        &self,
        expr: &Expression<'_>,
        span_start: u32,
        span_end: u32,
        in_multiline_context: bool,
        tail: HeadTail,
    ) -> HeadExpr {
        let d = self.d();
        let frozen = self.honored_directive_in_gap(span_start, expr.span().start);

        // In multiline contexts, set up embedded expression context so a ROOT binary
        // chain uses ContinuationIndent style ([`Printer::head_embed`]). An INLINE head
        // keeps the host's embed: `remove_lines` below holds it flat, so there is no
        // continuation for the mode to style. The leading-cast reflow applies on both
        // arms — an inline head is still a braced head with no hang to offer.
        let embed = if in_multiline_context {
            // `root_sequence_indents` is the block head's own field, not the shared
            // recipe's: it is the one braced head prettier never width-wraps, so a root
            // sequence's wrapped operands take the continuation indent a root binary
            // already takes here. A prefixed tag builds under the same recipe WITHOUT it
            // — prettier wraps `{@html (a,⏎b)}` flush and is the oracle there.
            tsv_lang::EmbedContext {
                root_sequence_indents: true,
                ..self.head_embed()
            }
        } else {
            self.cannot_hang_embed()
        };

        // Build expression doc tree
        let mut inner_doc = self.build_head_value_doc(expr, frozen, &embed);
        // A head that takes no comment at its end, built so it ends on one — a run the
        // value defers to its end, or a comment it prints there inline — is built again with
        // the author's grouping pairs kept around those comments
        // (`EmbedContext::value_end_takes_no_comment`). Asked of the built doc rather than
        // of the source, so a run the value flushes inside itself (at a list's comma, a
        // broken call's closer) keeps the ordinary strip.
        if tail == HeadTail::CommentlessDelimiter
            && !frozen
            && !self.gap_known_comment_free(expr.span().start, expr.span().end)
            && ((d.holds_line_suffix(inner_doc)
                && d.pending_run_at_end(inner_doc, self.source).is_some())
                || d.may_end_on_comment_text(inner_doc, self.source))
        {
            let embed = tsv_lang::EmbedContext {
                value_end_takes_no_comment: true,
                ..embed
            };
            inner_doc = self.build_head_value_doc(expr, frozen, &embed);
        }
        // The instantiation pair and the assignment clarity pair are exclusive: the walk
        // never descends into an assignment, so at most one of the two wraps the head.
        let expr_doc = if tail == HeadTail::Keyword
            && tsv_ts::__private::prints_ending_on_instantiation_close(expr)
        {
            d.parens(inner_doc)
        } else {
            self.wrap_value_clarity_parens(expr, inner_doc)
        };

        // Apply remove_lines() only in INLINE contexts to prevent the condition
        // from being the first thing to break when there's other content on the line.
        // In multiline contexts, the condition is on its own line and can wrap naturally.
        let expr_doc = if in_multiline_context {
            expr_doc
        } else {
            d.remove_lines(expr_doc)
        };

        self.assemble_head_expr(expr_doc, span_start, expr, span_end, frozen)
    }

    /// Build a block head's expression doc, deriving the comment-scan start offset from the
    /// opening literal `open` — so it can't drift from the emitted text (the same invariant
    /// the `*_BLOCK_OPEN` const comment guards). `comment_end` bounds the leading/trailing-comment scan (the head
    /// end for `{#if}`/`{:else if}`/`{#key}`, or the pattern-start-narrowed end for
    /// `{#each}`/`{#await}` via `each_expr_comment_end` / the await shorthand). `wrapping`
    /// is `in_multiline_context` (always false inside a whitespace-sensitive element).
    ///
    /// Wraps [`Printer::build_expression_doc_for_block`] for the standard block heads; the
    /// parenthesized `{#each (key)}` scan is derived from the key's own paren span, not
    /// `open` — [`Printer::build_each_key_expr`] is that twin.
    pub(super) fn build_block_head_expr(
        &self,
        open: &'static str,
        opening_tag_span: Span,
        expr: &Expression<'_>,
        comment_end: u32,
        wrapping: bool,
        tail: HeadTail,
    ) -> HeadExpr {
        self.build_expression_doc_for_block(
            expr,
            opening_tag_span.start + open.len() as u32,
            comment_end,
            wrapping,
            tail,
        )
    }

    /// The `{#each (key)}` twin of [`Printer::build_block_head_expr`]: the key expression
    /// sits inside parens the head assembles itself, so the comment scan comes from the
    /// key's own paren span rather than an opening literal — it covers the paren interior.
    /// One statement of that arithmetic for the two each-key layouts (the block builder and
    /// the whitespace-sensitive element's inline form).
    pub(super) fn build_each_key_expr(&self, key: &EachKey<'_>, wrapping: bool) -> HeadExpr {
        self.build_expression_doc_for_block(
            key.expression,
            key.span.start + 1, // after "("
            key.span.end - 1,   // before ")"
            wrapping,
            HeadTail::Delimiter,
        )
    }

    /// Assemble `[leading…, expr, trailing…]` into one doc, returning `expr` unchanged
    /// when there are no surrounding comments. Shared tail of the expression+comment
    /// builders (`build_expression_with_comments_doc`, `build_expression_doc_for_block`,
    /// `build_const_init_doc`, `build_expression_content_with_comments`).
    pub(in crate::printer) fn concat_with_surrounding_comments(
        &self,
        leading_docs: DocBuf,
        expr_doc: DocId,
        trailing_docs: DocBuf,
    ) -> DocId {
        if leading_docs.is_empty() && trailing_docs.is_empty() {
            expr_doc
        } else {
            let mut parts: DocBuf =
                DocBuf::with_capacity(leading_docs.len() + 1 + trailing_docs.len());
            parts.extend(leading_docs);
            parts.push(expr_doc);
            parts.extend(trailing_docs);
            self.d().concat(&parts)
        }
    }
}

/// Where a binding-pattern element's doc stops printing — the anchor its trailing gap takes.
///
/// A rest or spread stops at its argument: the stripped-paren interior (`...(a /* c */)`)
/// is the gap's, one run with the comments past the `)` ([`Printer::build_rest_pattern_doc`]).
/// Anything else stops at `end` — its printed end, so a default's `(1 /* c */)` shell is the
/// gap's too. `interior` is the element's `paren_interior()`, `None` for all but a rest or
/// spread; one rule for a pattern element and for either kind of object property.
fn element_printed_end(interior: Option<Span>, end: u32) -> u32 {
    interior.map_or(end, |interior| interior.start)
}

#[cfg(test)]
mod tests {
    use super::{has_any_expanding_blocks, is_control_flow_block, is_expanding_control_flow_block};
    use crate::ast::FragmentNode;

    /// Parse a Svelte template and return its top-level fragment nodes. The
    /// nodes borrow the caller-owned `arena`, so each test holds the `Bump`.
    fn parse_nodes<'arena>(
        arena: &'arena bumpalo::Bump,
        src: &str,
    ) -> &'arena [FragmentNode<'arena>] {
        // The nodes borrow only the arena, so the returned slice is self-contained —
        // these tests inspect node structure, not name text.
        crate::__private::parse(src, arena)
            .expect("template should parse")
            .fragment
            .nodes
    }

    #[test]
    fn control_flow_classification_await_is_not_expanding() {
        let arena = bumpalo::Bump::new();
        let if_nodes = parse_nodes(&arena, "{#if c}x{/if}");
        assert!(is_control_flow_block(&if_nodes[0]));
        assert!(is_expanding_control_flow_block(&if_nodes[0]));

        let await_nodes = parse_nodes(&arena, "{#await p}x{/await}");
        assert!(is_control_flow_block(&await_nodes[0]));
        // Await blocks are control-flow but do NOT force expansion.
        assert!(!is_expanding_control_flow_block(&await_nodes[0]));
    }

    #[test]
    fn expanding_block_detected_through_nested_awaits() {
        let arena = bumpalo::Bump::new();
        // An if directly inside an await is detected.
        assert!(has_any_expanding_blocks(parse_nodes(
            &arena,
            "{#await p}{#if c}x{/if}{/await}"
        )));
        // ...and through a second level of await nesting (recursion).
        assert!(has_any_expanding_blocks(parse_nodes(
            &arena,
            "{#await p}{#await q}{#if c}x{/if}{/await}{/await}"
        )));
        // An await with only inline/element content does NOT expand.
        assert!(!has_any_expanding_blocks(parse_nodes(
            &arena,
            "{#await p}<span>x</span>{/await}"
        )));
    }
}