tsv_ts 0.8.0

TypeScript/JS formatter, type stripper and acorn-compatible parser — 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
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
// Module statement printing for TypeScript (import and export)
//
// ## Module Organization
//
// - **mod.rs** (this file): Shared constants, entry-point methods (import/export
//   declarations), and re-exports for submodules
// - **header_comments.rs**: Header-gap comment continuation helpers, `from`/source
//   rendering, and namespace `as` bindings
// - **import_attributes.rs**: `with { … }` import-attribute clause printing
// - **specifier_list.rs**: Braced comma-separated specifier/attribute list machinery

mod header_comments;
mod import_attributes;
mod specifier_list;
use crate::printer::statements::TerminatorGap;
use specifier_list::SpecifierListSpans;

// Re-export for submodules to use `super::X` instead of `super::super::X`
pub(super) use super::{Printer, build_entity_name_doc};

use crate::ast;
use crate::printer::calls::PartitionedComments;
use crate::printer::comments::left_spine_shell_is_comment_free;
use crate::printer::needs_parens::{LeftmostText, export_default_needs_parens};
use crate::printer::statements::StatementContext;
use crate::printer::{ParenContext, needs_parens};
use smallvec::SmallVec;
use smallvec::smallvec;
use tsv_lang::Span;
use tsv_lang::doc::arena::DocId;
use tsv_lang::doc::{DocBuf, arena::LeadingBreak};

/// Byte length of the leading `import`/`export` keyword (both 6 chars). Added to
/// a declaration's span start to reach the position just past the keyword.
pub(super) const MODULE_KW_LEN: u32 = 6;
/// Byte length of `import type` / `export type` (both 11 chars) — fallback when
/// the `type` keyword can't be located by scanning (e.g. malformed source).
pub(super) const MODULE_TYPE_KW_LEN: u32 = 11;

impl<'a> Printer<'a> {
    /// Build a Doc for a TypeScript export assignment
    pub(super) fn build_export_assignment_doc(
        &self,
        decl: &ast::TSExportAssignment<'_>,
        span: Span,
    ) -> DocId {
        let d = self.d();
        let value_span = decl.expression.span();
        // `export =` word by word: the `export`→`=` gap is a position an author can
        // comment in. Emitting the two as one text never scans it — the comment would
        // be dropped. The `=`→value gap is a freeze head, `export default`'s twin, so the
        // value is built from the keyword's end and an own-line directive there claims it
        // whole. (The `export`→`=` gap one delimiter earlier is not a head — a `=` begins
        // no node — and cannot reach this window either.)
        let argument_end = value_span.end;
        // A `//` inside the value's own grouping parens keeps them, for the reason
        // `export default`'s twin arm gives: deferring it past the `)` and the `;` merges
        // it with whatever already trails that line. Decided ahead of the header, because
        // the shell wraps the VALUE and the value is built inside the closure below.
        let shell_close = self.value_paren_line_comment_close(argument_end, span.end);
        let (keyword_doc, keyword_end) =
            self.build_keyword_words_doc(&["export", "="], span.start, value_span.start);
        let value = self.build_value_head_doc(keyword_end, &decl.expression, || {
            self.build_expression_doc_claiming_outermost(&decl.expression)
        });
        // An assignment as the exported value takes clarity parens, the same answer every
        // other value position gives (`ParenContext::ExportAssignment`) — outside the freeze
        // head, so a frozen value keeps the position's pair, exactly as `export default` does.
        let value = if needs_parens(&decl.expression, ParenContext::ExportAssignment, false) {
            d.parens(value)
        } else {
            value
        };
        let value = match shell_close {
            Some(close) => self
                .build_paren_operand_comment_doc(argument_end, close, value, ")")
                .unwrap_or(value),
            None => value,
        };
        // The keyword→value gate `export default` asks: a comment that forces its own line, or a
        // run the author broke after whose break is FORCED, hangs the value, since the inline
        // continuation would weld the run onto the keyword's line — and, for a `//` run ending in
        // a block glued to a paren, weld the value onto the block's `*/`.
        let head = if self
            .keyword_value_hang_doc(keyword_end, value_span.start, || value)
            .is_some()
        {
            let mut parts: DocBuf = smallvec![keyword_doc];
            self.append_keyword_value_line_comments(
                &mut parts,
                keyword_end,
                value_span.start,
                value,
            );
            d.concat(&parts)
        } else {
            d.concat(&[
                keyword_doc,
                self.build_keyword_to_name_continuation(keyword_end, value_span.start, value),
            ])
        };
        let gap_start = shell_close.map_or(argument_end, |close| {
            Self::past_grouping_close(close, span.end)
        });
        let has_trailing_comments = self.has_comments_to_emit_between(gap_start, span.end);
        if has_trailing_comments {
            // `export =` keeps a same-line trailing block comment *before* the `;`
            // (operand-attached — prettier 3.9 does not move it, unlike `export default`
            // / named exports). A line comment still floats after the `;` via `line_suffix`.
            let mut parts = smallvec![head];
            self.append_trailing_paren_comments(&mut parts, gap_start, span.end);
            parts.push(d.text(";"));
            d.concat(&parts)
        } else {
            d.concat(&[head, d.text(";")])
        }
    }

    /// Build a Doc for an export named declaration
    ///
    /// Uses d.group() for width-based wrapping of specifiers.
    /// When the line exceeds print_width (100 chars), wraps to multiline format.
    pub(super) fn build_export_named_declaration_doc(
        &self,
        decl: &ast::ExportNamedDeclaration<'_>,
        span: Span,
    ) -> DocId {
        let d = self.d();
        if let Some(declaration) = &decl.declaration {
            // When exporting a declaration, always use plain "export " because
            // the type/interface/declare keyword is part of the declaration itself
            let export_keyword = "export";
            let export_keyword_end = span.start + export_keyword.len() as u32;
            let decl_span = declaration.span();
            let decl_start = decl_span.start;

            // A comment between `export` and the declaration is preserved in
            // place; a line comment indents the declaration one level (uniform
            // header rule). The keyword→declaration gap routes through the shared
            // continuation helper, so block/no-comment cases stay inline.
            //
            // An own-line directive in that gap freezes the declaration whole (Rule A),
            // and the shared helper keeps its own line for free (`keyword_gap_breaks`).
            // Resolved BEFORE the decorator fork below: an export-first decorated class's
            // slice already carries its own decorators, so a frozen one wants the plain
            // continuation rather than the decorator layout.
            let frozen = self.value_head_frozen_span(export_keyword_end, decl_span);
            // Decorated classes are the exception: the decorators and `export` can
            // appear in either order (`@dec export class` / `export @dec class`), so
            // they print in the source order rather than through the plain
            // keyword→declaration continuation.
            if frozen.is_none()
                && let ast::StatementKind::ClassDeclaration(class) = &declaration.kind
                && let Some(doc) =
                    self.build_export_named_decorated_class_doc(span, class, export_keyword_end)
            {
                return doc;
            }
            // `declaration` is always a declaration form (never an
            // ExpressionStatement), so `in_program_or_block` is never consulted here.
            let continuation = match frozen {
                // The frozen span is the declaration's own; the statement-aware emitter
                // restores the `;` an ASI-reliant `export const` owes, exactly as at the
                // statement-list seams ([`Printer::build_frozen_statement_doc`]).
                Some(_) => self.build_frozen_statement_doc(declaration),
                None => self.build_statement_doc(declaration, StatementContext::PROGRAM_OR_BLOCK),
            };
            let tail = self.build_keyword_to_name_continuation(
                export_keyword_end,
                decl_start,
                continuation,
            );
            d.concat(&[d.text(export_keyword), tail])
        } else {
            // export { x, y as z } or export { x } from "y"
            // Check if the overall export is type-only
            let is_type_export = decl.export_kind == ast::ExportKind::Type;

            let mut parts = if is_type_export {
                // Split the keyword so a type-only re-export (empty `{}` or named
                // specifiers) can keep a comment between `export` and `type` in place
                // — prettier relocates it (after `from` for empty, into the braces for
                // named specifiers). A line comment indents the `type …` continuation
                // one level; the leading space comes from the `export ` token. The
                // `type`→`{` gap is handled beside each brace path below. Mirrors the
                // import side.
                let kw_end = span.start + MODULE_KW_LEN;
                let search_end = decl.source.as_ref().map_or(span.end, |s| s.span.start);
                let type_start = self
                    .find_keyword_in_range(kw_end, search_end, "type")
                    .unwrap_or(kw_end);
                smallvec![
                    d.text("export "),
                    self.gap_comment_continuation_tail(kw_end, type_start, d.text("type ")),
                ]
            } else {
                smallvec![d.text("export ")]
            };

            // Position just past the specifier list's closing `}` (no-source case);
            // used to scan for comments between `}` and the terminating `;`.
            // Set unconditionally in both the empty and non-empty branches below.
            let close_brace_end: u32;

            if decl.specifiers.is_empty() {
                // Empty braces case: `export {}` or `export /* c */ {}`
                // Extract comments between keyword and braces + inside braces.
                // Prettier relocates comments from inside empty braces:
                //   `export { /* c */ }` → `export /* c */ {}`
                //   `export { /* c */ } from 'a'` → `export {} from /* c */ 'a'`
                let semi_or_source = decl.source.as_ref().map_or(span.end, |s| s.span.start);
                let keyword_end = self.export_header_end(decl, span, semi_or_source);
                // Find closing brace outside of comments — naive find('}') matches
                // inside comments like `export // {}\n{}`, breaking comment extraction.
                let brace_close = self
                    .find_char_outside_comments(keyword_end, semi_or_source, b'}')
                    .unwrap_or(semi_or_source);
                close_brace_end = brace_close + 1;
                if decl.source.is_none() {
                    // No re-export (`export {}`): keyword→`{}` gap, preserved in place.
                    // A line comment indents the `{}` continuation one level (indent-only
                    // divergence — prettier keeps the comment in place and flat). The gap
                    // spans to `}` so inside-braces comments are captured too (prettier
                    // relocates `export {/* c */}` → `export /* c */ {}`). The leading
                    // space comes from the `export `/`type ` token.
                    parts.push(self.gap_comment_continuation_tail(
                        keyword_end,
                        brace_close,
                        d.text("{}"),
                    ));
                } else {
                    // Re-export (`export {} from 'x'`): divergence — prettier relocates
                    // the keyword→`{` comment after `from`, so tsv preserves it in place
                    // and a line comment indents the `{}` continuation one level (the
                    // leading space comes from the `export `/`type ` token). Without
                    // this, `export /* c */ {} from 'x'` silently drops the comment.
                    // Inside-braces comments are relocated after `from` below.
                    let braces_doc = if let Some(brace_pos) =
                        self.find_char_outside_comments(keyword_end, semi_or_source, b'{')
                    {
                        self.gap_comment_continuation_tail(keyword_end, brace_pos, d.text("{}"))
                    } else {
                        d.text("{}")
                    };
                    parts.push(braces_doc);
                }
            } else {
                // Named specifiers: comment-aware braced list. `close_brace_end`
                // is the offset past `}`, for the trailing pre-`;` comment scan.
                let kw_end = self.export_header_end(decl, span, decl.specifiers[0].span.start);
                let bound = decl.source.as_ref().map_or(span.end, |s| s.span.start);
                // Export named specifiers always have the `{` directly after the
                // header (no default/namespace binding), so the keyword→`{` comment
                // (`export /* c */ {a}`, `export type /* c */ {a}`) is always
                // captured and the lone-specifier no-break rule always applies (a
                // comment on the specifier still restores breaking — `can_break`).
                close_brace_end = self.push_braced_specifier_list(
                    &mut parts,
                    decl.specifiers,
                    SpecifierListSpans {
                        header_start: span.start,
                        kw_end,
                        gap_start: kw_end,
                        bound,
                    },
                    true,
                    |s| s.span,
                    |s| self.build_export_specifier_doc(s, is_type_export),
                );
            }

            // Comments between the last content token (attribute `}`, source
            // literal, or closing `}`) and the terminating `;` — preserved where
            // the user placed them (prettier relocates no-`from` ones inside the
            // braces). Emitted outside the content group so a line-comment break
            // doesn't expand the braces.
            let content_end = if let Some(source) = &decl.source {
                let empty_brace_start = if decl.specifiers.is_empty() {
                    Some(self.export_header_end(decl, span, source.span.start))
                } else {
                    None
                };
                // Named specifiers: preserve a comment in the `}`→`from` gap in place
                // (prettier relocates it into the braces). Empty braces relocate after
                // `from` instead, so skip the in-place scan there.
                let from_content_end = if decl.specifiers.is_empty() {
                    None
                } else {
                    Some(close_brace_end)
                };
                parts.push(self.build_from_source_doc(
                    span.start,
                    source,
                    empty_brace_start,
                    from_content_end,
                ));
                // Import attributes: `export { x } from "y" with { type: "json" }`
                self.push_import_attributes_clause(
                    &mut parts,
                    decl.attributes,
                    source.span.end,
                    span.end,
                )
            } else {
                close_brace_end
            };
            self.finish_with_pre_semi(parts, content_end, span.end, true)
        }
    }

    /// The DECORATED `export <class>` forms, which emit the decorators and `export` in the
    /// author's own source order rather than through the plain keyword→declaration
    /// continuation — `None` when the class carries no decorators and the ordinary path
    /// applies.
    ///
    /// Decorators can sit before OR after `export` (`@dec export class` / `export @dec
    /// class`), and prettier preserves the author's choice as two distinct stable forms. The
    /// parser records which by the export declaration's start: the first-decorator start for
    /// the decorator-first form, the `export` keyword for the export-first form — so a first
    /// decorator positioned *after* the export start is the export-first form.
    ///
    /// Asked only where the keyword→declaration gap did NOT freeze: an export-first
    /// decorated class's frozen slice already carries its own decorators, so a frozen one
    /// wants the plain continuation instead.
    fn build_export_named_decorated_class_doc(
        &self,
        span: Span,
        class: &ast::ClassDeclaration<'_>,
        export_keyword_end: u32,
    ) -> Option<DocId> {
        let d = self.d();
        let decorators = class.decorators.filter(|dec| !dec.is_empty())?;
        let continuation = self.build_class_declaration_without_decorators_doc(class);
        let export_first = decorators[0].span.start > span.start;

        // The token right after the decorators is the trailing-comment boundary for
        // `build_decorators_doc`: the class's own first keyword for the export-first
        // form, `export` for the decorator-first form. That first keyword must come
        // from `class_declaration_keyword_start` — the same helper the class printer
        // starts its own modifier walk at — so the two agree on where the decorator
        // run ends. Re-deriving it here as `abstract`/`class` skipped `declare`,
        // handing the boundary to the `class` keyword instead: the decorator run then
        // claimed the `declare`→`class` gap that the class printer also emits, and a
        // comment there (`export @dec declare /* c */ class D {}`) printed TWICE.
        let next_after_decorators = if export_first {
            self.class_declaration_keyword_start(class)
        } else {
            self.find_keyword_after_decorators(class.decorators, "export", span.start)
        };

        // `decorators` is non-empty, so this is always `Some`; the `?` keeps the code off
        // `expect()` (clippy::expect_used) and falls back to the plain declaration path in
        // the (unreachable) `None` case.
        let dec_doc = self.build_decorators_doc(class.decorators, next_after_decorators)?;
        if export_first {
            // `export` on its own line, then the (always own-line) decorators, then the
            // class. A comment between `export` and the first decorator is rare but must be
            // preserved (never dropped).
            let mut parts: DocBuf = smallvec![d.text("export")];
            if let Some(c) = self
                .build_inline_comments_between_doc_opt(export_keyword_end, decorators[0].span.start)
            {
                parts.push(c);
            }
            parts.push(d.hardline());
            parts.push(dec_doc);
            parts.push(continuation);
            return Some(d.concat(&parts));
        }

        // Decorator-first (`@dec export class`): decorators, then `export`, then the class.
        // The gap runs from the REAL `export` keyword — `next_after_decorators` here — to
        // the class's own first keyword. `span.start` is the DECORATOR in this form, so
        // anchoring the scan on it inverted the window and dropped every comment in the gap.
        // The gap sits inside the declaration's span rather than before it, so nothing
        // freezes here: there is no following node for Rule A to bind to (the
        // decorator→declaration gap's mirror image).
        let tail = self.build_keyword_to_name_continuation(
            next_after_decorators + MODULE_KW_LEN,
            self.class_declaration_keyword_start(class),
            continuation,
        );
        Some(d.concat(&[dec_doc, d.text("export"), tail]))
    }

    /// Build a Doc for an export default declaration
    pub(super) fn build_export_default_declaration_doc(
        &self,
        decl: &ast::ExportDefaultDeclaration<'_>,
        span: Span,
    ) -> DocId {
        let d = self.d();
        // The two decorated forms print declaration-style layouts of their own, neither of
        // which is the keyword→value shape below.
        if let Some(doc) = self.build_export_default_decorated_doc(decl, span) {
            return doc;
        }

        let value_span = decl.declaration.span();
        let decl_start = value_span.start;
        // The `export default`→value gap (a line comment indents the value). The
        // keyword's own words are located, not measured — the gap *between* them is a
        // position an author can comment in, and measuring never scans it. Located
        // before the value is built, so the value can ask whether this gap freezes it.
        let (keyword_doc, keyword_end) =
            self.build_keyword_words_doc(&["export", "default"], span.start, decl_start);
        let value_doc = self.build_export_default_value_doc(decl, span, keyword_end, value_span);

        // A comment that can't stay inline forces the value onto its own indented
        // line, keeping the comment where the author wrote it. This gap uses the
        // SHARED keyword→value gate — the same one `as`/`satisfies`, `keyof`/`typeof`,
        // `infer`, and the type-alias `=` use — so only a **line** comment (runs to
        // end-of-line) or a **multiline** block the author broke after hangs the value.
        // A single-line block does not: nothing forces it off the line, so the author's
        // break is ordinary layout and is reflowed (§Authored breaks in value position).
        // Carving this gap out via `comment_hangs_binary_operand`,
        // which also hangs a single-line block, would make `export default` the lone
        // value gap preserving an unforced break, disagreeing with its own twin
        // `export =`. Prettier keeps the break at both; tsv reflows at both.
        //
        // The same comment one shell deeper — a run hoisted to the head of the value's doc
        // from the grouping parens of its LEFTMOST node (`export default (⏎// c⏎a as any).b`)
        // — is read off the built doc ([`Self::export_default_leading_edge`]), since which
        // emitter prints it, and inside which group, is the value printer's business. A
        // run that ends its line there in every layout, or behind a line in no group of
        // the value, hangs under the keyword; left on the keyword line it rendered flush,
        // and the reparse — reading it as the keyword→value gap's — indented it, a second
        // fixed point one pass away. A run behind a line the value's own group may break
        // takes this gap's own separator instead, glued.
        let (value_doc, left_spine_run) = match &decl.declaration {
            ast::ExportDefaultValue::Expression(expr)
                if !left_spine_shell_is_comment_free(self.source, expr.span().start) =>
            {
                self.export_default_leading_edge(expr, value_doc)
            }
            _ => (value_doc, false),
        };
        // A run the author broke after whose break is FORCED hangs too — by a multi-line comment
        // glued to the value (`export default /* x */⏎/* y⏎*/ v`), which cannot share the line of
        // one that broke before it, or by the value's own hard break. The inline continuation
        // below would weld the run onto one line, and the paren spelling's first pass already
        // lands here, so the two spellings had two fixed points one pass apart.
        //
        // And the own-line JSDoc cast every value gap owes a hang
        // ([`Printer::is_own_line_jsdoc_cast`]): the cast prints a hardline between its comment
        // and its `(`, which left flush below the keyword the next pass collapses
        // (`export default⏎/** @type {T} */⏎(x).b + q` joined on pass 2). Not under a
        // sequence, which prints its own `(` ahead of the cast.
        if left_spine_run
            || matches!(&decl.declaration, ast::ExportDefaultValue::Expression(expr)
                if !matches!(expr.kind, ast::ExpressionKind::SequenceExpression(_))
                    && self.is_own_line_jsdoc_cast(expr))
            || self
                .keyword_value_hang_doc(keyword_end, decl_start, || value_doc)
                .is_some()
        {
            let mut parts: DocBuf = smallvec![keyword_doc];
            self.append_keyword_value_line_comments(&mut parts, keyword_end, decl_start, value_doc);
            return d.concat(&parts);
        }
        // No forcing comment (inline block / none): the value stays on the keyword
        // line via the shared continuation helper.
        d.concat(&[
            keyword_doc,
            self.build_keyword_to_name_continuation(keyword_end, decl_start, value_doc),
        ])
    }

    /// What the `export default` value's doc prints ahead of its first token, read for the
    /// keyword→value seam: the value doc to print, and whether it hangs under the keyword.
    ///
    /// A comment there that ends its line in every layout hangs the value, as one in the
    /// gap itself does, and so does one behind a breakable line in no group of the value,
    /// which takes the statement's broken mode. One behind a line the value's own group
    /// may break is the gap's glued single-line block one shell deeper: the gap prints that
    /// block glued to what follows (`export default /* d */⏎a.b` prints `export default
    /// /* d */ a.b`), so its line becomes the same space
    /// ([`tsv_lang::doc::arena::DocArena::glue_leading_comment_lines`]); where the doc cannot be
    /// glued in every layout it is left to its group. A JSDoc cast the parser built
    /// ([`Printer::left_spine_cast_owns`]) is the value's own syntax, printed with its own
    /// glue, and is no run; cast-shaped text it read as an ordinary comment — glued to an
    /// arrow's parameter `(`, or to anything else — is an ordinary block.
    fn export_default_leading_edge(
        &self,
        value: &ast::Expression<'_>,
        value_doc: DocId,
    ) -> (DocId, bool) {
        let d = self.d();
        let edge = d.leading_comment_break(value_doc, self.source);
        if edge
            .first_comment
            .is_some_and(|span| self.left_spine_cast_owns(value, span))
        {
            return (value_doc, false);
        }
        match edge.brk {
            LeadingBreak::None => (value_doc, false),
            LeadingBreak::Soft if !edge.ungrouped => (
                d.glue_leading_comment_lines(value_doc, self.source)
                    .unwrap_or(value_doc),
                false,
            ),
            LeadingBreak::Soft | LeadingBreak::Hard => (value_doc, true),
        }
    }

    /// The `export default` value's own doc, built from `keyword_end` — the start of the
    /// keyword→value gap, which the caller emits around this.
    ///
    /// An own-line directive in that gap freezes the value whole (Rule A). A DECLARATION
    /// value's whole doc is the slice, so each form asks the shared statement head
    /// ([`Printer::build_declaration_value_head_doc`]); an EXPRESSION value keeps its `;`, its
    /// trailing comments and the `export default (…)` clarity parens outside the freeze, so
    /// only the expression's own doc is swapped.
    fn build_export_default_value_doc(
        &self,
        decl: &ast::ExportDefaultDeclaration<'_>,
        span: Span,
        keyword_end: u32,
        value_span: Span,
    ) -> DocId {
        let d = self.d();
        match &decl.declaration {
            ast::ExportDefaultValue::Expression(expr) => {
                // A multi-line comment the value owns prints outside the value's own group (the
                // keyword gap's run never sees it), beneath the clarity parens added below.
                let mut expr_doc = self.build_value_head_doc(keyword_end, expr, || {
                    self.build_expression_doc_claiming_outermost(expr)
                });
                // Prettier wraps the exported expression when its leftmost
                // (first-printed) token is a function/class keyword — else
                // `export default function () {}.m()` reparses the function as a
                // *declaration* and the trailing `.m()` / `= …` / `as T` dangles.
                // Mirrors prettier's `startsWithNoLookaheadToken(expr, isFunctionOrClass)`
                // (parentheses/needs-parentheses.js). Decorated class expressions are
                // handled by the decorated forms; the FunctionDeclaration/ClassDeclaration
                // arms cover bare `export default function/class …`. A frozen value is read
                // as its verbatim slice, which carries no pair the printer would add inside.
                let frozen = self.value_head_frozen_span(keyword_end, expr.span());
                if export_default_needs_parens(expr, LeftmostText::of_value(frozen.is_some())) {
                    expr_doc = d.concat(&[d.text("("), expr_doc, d.text(")")]);
                }
                let argument_end = value_span.end;
                if !self.has_comments_to_emit_between(argument_end, span.end) {
                    return d.concat(&[expr_doc, d.text(";")]);
                }
                // A `//` the author wrote inside the value's own grouping parens keeps
                // those parens: the terminator split below would carry it past the `)`
                // and the `;`, onto a line that may already hold a `//`, where the two
                // MERGE into one comment. The retained shell is the same answer the
                // `return`/`throw` operand and the `await`/`yield` operand shell give.
                if let Some((shell, after_close)) =
                    self.build_value_paren_line_comment_shell(expr_doc, argument_end, span.end)
                {
                    let mut parts = smallvec![shell];
                    let after = self.split_terminator_gap_comments(
                        &mut parts,
                        after_close,
                        span.end,
                        false,
                        TerminatorGap::ListClaims,
                    );
                    parts.push(d.text(";"));
                    parts.extend(after);
                    return d.concat(&parts);
                }
                let mut parts = smallvec![expr_doc];
                // `operand_parens_printed: false` — this position's pair closes at the
                // EXPRESSION's end, so it never encloses the terminator gap: a comment
                // written inside the author's shell (`export default (x /* c */);`) is
                // outside the `)` this printer emits. Passing the wrap flag read the
                // SOURCE shell's `)` instead, which is the fixed-point failure that
                // helper's doc names — pass 1 held the comment before the `;`
                // (`(function () {}) /* c */;`), pass 2 saw no shell and moved it past.
                // A `return`/`throw` argument passes `true` because ITS retained parens
                // are built around the comment.
                let after = self.split_terminator_gap_comments(
                    &mut parts,
                    argument_end,
                    span.end,
                    false,
                    TerminatorGap::ListClaims,
                );
                parts.push(d.text(";"));
                parts.extend(after);
                d.concat(&parts)
            }
            ast::ExportDefaultValue::FunctionDeclaration(func) => self
                .build_declaration_value_head_doc(keyword_end, value_span, || {
                    self.build_function_declaration_doc(func)
                }),
            ast::ExportDefaultValue::TSDeclareFunction(func) => self
                .build_declaration_value_head_doc(keyword_end, value_span, || {
                    self.build_declare_function_doc(func, None)
                }),
            ast::ExportDefaultValue::ClassDeclaration(class) => self
                .build_declaration_value_head_doc(keyword_end, value_span, || {
                    self.build_class_declaration_doc(class)
                }),
            ast::ExportDefaultValue::TSInterfaceDeclaration(iface) => self
                .build_declaration_value_head_doc(keyword_end, value_span, || {
                    self.build_interface_declaration_doc(iface)
                }),
        }
    }

    /// The DECORATED `export default` forms, which print declaration-style layouts of their
    /// own rather than the plain keyword→value shape — `None` when the value carries no
    /// decorators and the ordinary path applies.
    ///
    /// Two source orders, two AST shapes. Decorator-*first* `@dec export default class`
    /// parses as a ClassDeclaration whose span opens at the decorator, so the decorators are
    /// emitted, then `export default`, then the class without them.
    /// Decorator-*after*-`default` `export default @dec class {}` is a class *expression*
    /// (acorn), which prettier still formats declaration-style: `export default` on its own
    /// line, the (always own-line) decorators, and NO trailing `;`.
    fn build_export_default_decorated_doc(
        &self,
        decl: &ast::ExportDefaultDeclaration<'_>,
        span: Span,
    ) -> Option<DocId> {
        let d = self.d();
        if let ast::ExportDefaultValue::ClassDeclaration(class) = &decl.declaration {
            let export_start =
                self.find_keyword_after_decorators(class.decorators, "export", span.start);
            let dec_doc = self.build_decorators_doc(class.decorators, export_start)?;
            // `export default` word by word, bounded by the class's own first keyword:
            // both the gap *inside* the keyword and the one after it are positions an
            // author can comment in. Emitting the two words as one fixed text scans
            // neither, so both comments are dropped.
            //
            // Nothing freezes in that gap: the declaration begins at its first decorator,
            // so the gap sits INSIDE the value rather than before it and there is no
            // following node for Rule A to bind to (the decorator→declaration gap's
            // mirror image, and the same reason the decorator-first named export doesn't
            // freeze).
            return Some(d.concat(&[
                dec_doc,
                self.build_keyword_header_doc(
                    &["export", "default"],
                    export_start,
                    self.class_declaration_keyword_start(class),
                    self.build_class_declaration_without_decorators_doc(class),
                ),
            ]));
        }

        let ast::ExportDefaultValue::Expression(ast::Expression {
            span: class_span,
            kind: ast::ExpressionKind::ClassExpression(class_expr),
        }) = &decl.declaration
        else {
            return None;
        };
        let decorators = class_expr.decorators.filter(|dec| !dec.is_empty())?;
        // `export default` word by word: the gap *inside* the keyword is a position an
        // author can comment in, so the words are located rather than measured (measuring
        // never scans that gap — the comment would be dropped).
        let (keyword_doc, keyword_end) = self.build_keyword_words_doc(
            &["export", "default"],
            span.start,
            decorators[0].span.start,
        );
        let mut parts: DocBuf = smallvec![keyword_doc];
        // An own-line directive in the gap freezes the class expression whole — its
        // slice carries the decorators, so this path's hand-rolled decorator layout
        // yields to the verbatim form. The shared own-line-preserving run keeps the
        // directive's line, which the inline emitter below would take away: a
        // keyword-trailing placement is inert under the floor, so the relocated form
        // would lose the freeze on the second pass.
        if let Some(frozen) = self.value_head_frozen_span(keyword_end, *class_span) {
            self.append_keyword_value_line_comments(
                &mut parts,
                keyword_end,
                frozen.start,
                self.build_frozen_node_doc(frozen),
            );
            return Some(d.concat(&parts));
        }
        // A comment between `export default` and the first decorator is rare but
        // must be preserved (never dropped). Two authorings, two owners: a comment
        // *glued* to `@dec` is owned by the class expression (every glued block
        // comment is), so it is skipped here by design and claimed below; one the
        // author left on its own line is unowned and belongs to this gap.
        if let Some(c) =
            self.build_inline_comments_between_doc_opt(keyword_end, decorators[0].span.start)
        {
            parts.push(c);
        }
        parts.push(d.hardline());
        // This path **reassembles** the class expression rather than routing it
        // through `build_expression_doc`, so the owned-comment seam there never
        // runs for it — the comment must be claimed here or nothing prints it.
        parts.push(self.prepend_owned_leading_comment_at(
            class_span.start,
            self.build_class_expression_doc(class_expr, *class_span),
        ));
        Some(d.concat(&parts))
    }

    /// Build a Doc for an export all declaration
    pub(super) fn build_export_all_declaration_doc(
        &self,
        decl: &ast::ExportAllDeclaration<'_>,
        span: Span,
    ) -> DocId {
        let d = self.d();
        let is_type = decl.export_kind == ast::ExportKind::Type;
        let export_end = span.start + MODULE_KW_LEN;
        // The `*` token, after the keyword(s) and before any `as`/`from`.
        let star_limit = decl
            .exported
            .as_ref()
            .map_or(decl.source.span.start, |e| e.span().start);
        let star_pos = self
            .find_char_outside_comments(export_end, star_limit, b'*')
            .unwrap_or(export_end);

        // Header comments (around `export`, `type`, `*`) are preserved where the user
        // placed them; prettier relocates every one to after `from`. A line comment
        // in any header gap indents the continuation one level (statement
        // continuation); the `*` is emitted via the tail helper so an `export`→`*`
        // or `type`→`*` line comment carries it onto the indented line.
        let mut parts = smallvec![d.text("export ")];
        // Which gap precedes the `*` — `export`→`*`, or `type`→`*` past an emitted
        // `type ` — is all the two branches decide; the star and its binding are the
        // shared emitter's.
        let star_gap_start = if is_type {
            let type_start = self
                .find_keyword_in_range(export_end, star_pos, "type")
                .unwrap_or(export_end);
            parts.push(self.gap_comment_continuation_tail(export_end, type_start, d.text("type ")));
            self.find_keyword_end("type", export_end, star_pos)
                .unwrap_or(export_end)
        } else {
            export_end
        };
        self.push_namespace_star_binding(
            &mut parts,
            star_gap_start,
            star_pos,
            decl.exported.as_ref(),
        );
        let star_end = star_pos + 1; // position just past `*`

        // Comment between `*` (or `as ns`) and `from`, preserved in place — a same-line
        // block comment trails inline (`* /* c */ from`), a line comment indents the
        // `from …` continuation; prettier relocates it after `from`. Handled by
        // `build_from_source_doc`'s binding→`from` gap (the end of `*`/`as ns`).
        let prev_end = decl.exported.as_ref().map_or(star_end, |e| e.span().end);
        parts.push(self.build_from_source_doc(span.start, &decl.source, None, Some(prev_end)));
        // Import attributes: `export * from "y" with { type: "json" }`.
        // Returns the offset past the attribute `}` (or source) for the trailing
        // pre-`;` comment scan, preserved in place.
        let content_end = self.push_import_attributes_clause(
            &mut parts,
            decl.attributes,
            decl.source.span.end,
            span.end,
        );
        self.finish_with_pre_semi(parts, content_end, span.end, false)
    }

    /// Emit an import declaration's leading header keyword — an import-phase keyword
    /// (`source` / `defer`) or `type` — preserving any comment the author wrote in the
    /// `import`→keyword gap.
    ///
    /// The two are mutually exclusive and ask one question, so they share one emitter.
    /// Scanning that gap is what makes the comment *reachable*: emitting the keyword as a
    /// bare fixed text scans none of it, so a comment there reaches no emitter at all and
    /// is DROPPED. Prettier relocates such a comment past the keyword (for `type`, to a
    /// target that varies by form — after `from` for empty, into the braces for named
    /// specifiers, to the binding side for default/namespace); tsv preserves it —
    /// `phase_keyword_comment_svelte_prettier_divergence`.
    ///
    /// A line comment in the gap indents the keyword's continuation one level (statement
    /// continuation); the leading space comes from the `import ` token the caller has
    /// already pushed, and the trailing one from here.
    fn push_import_header_keyword(
        &self,
        parts: &mut DocBuf,
        decl: &ast::ImportDeclaration<'_>,
        span: Span,
        keyword: &'static str,
    ) {
        let d = self.d();
        let kw_end = span.start + MODULE_KW_LEN;
        let keyword_start = self
            .find_keyword_in_range(kw_end, decl.source.span.start, keyword)
            .unwrap_or(kw_end);
        parts.push(self.gap_comment_continuation_tail(kw_end, keyword_start, d.text(keyword)));
        parts.push(d.text(" "));
    }

    /// Build a Doc for an import declaration
    ///
    /// Uses d.group() for width-based wrapping of named specifiers.
    /// When the line exceeds print_width (100 chars), wraps to multiline format.
    pub(super) fn build_import_declaration_doc(
        &self,
        decl: &ast::ImportDeclaration<'_>,
        span: Span,
    ) -> DocId {
        let d = self.d();
        // Check if source has empty braces (for `import {} from 'x'`)
        let has_empty_braces = self.has_empty_named_braces(decl, span);

        // Check if this is a type-only import
        let is_type_import = decl.import_kind == ast::ImportKind::Type;

        // Collect specifiers
        let mut has_default = false;
        // Inline up to 8 named specifiers (covers the common import) before spilling.
        let mut named_specs: SmallVec<[&ast::ImportNamedSpecifier<'_>; 8]> = SmallVec::new();
        let mut default_name = ast::IdentName {
            escaped: None,
            raw_len: 0,
            plain_ascii: true,
        };
        let mut default_name_start = 0u32;
        let mut default_spec_start = 0u32;
        let mut default_spec_end = 0u32;
        let mut namespace_spec: Option<&ast::ImportNamespaceSpecifier<'_>> = None;

        for spec in decl.specifiers {
            match spec {
                ast::ImportSpecifier::Default(default_spec) => {
                    has_default = true;
                    default_name = default_spec.local.ident_name();
                    default_name_start = default_spec.local.span.start;
                    default_spec_start = default_spec.span.start;
                    default_spec_end = default_spec.span.end;
                }
                ast::ImportSpecifier::Named(named_spec) => {
                    named_specs.push(named_spec);
                }
                ast::ImportSpecifier::Namespace(ns_spec) => {
                    namespace_spec = Some(ns_spec);
                }
            }
        }

        // Build the full import statement
        let mut parts = smallvec![d.text("import ")];

        // The leading header keyword, if any — the import-phase proposals'
        // `import source …` (binding) / `import defer …` (namespace), whose spelling is
        // `ImportPhase::as_str`'s, or `type`. Mutually exclusive, and one emitter: see
        // `push_import_header_keyword` for why the gap before it must be scanned. The
        // `type`/phase→binding/`{` gap is handled beside each form below.
        if let Some(keyword) = decl.phase.as_str() {
            self.push_import_header_keyword(&mut parts, decl, span, keyword);
        }
        if is_type_import {
            self.push_import_header_keyword(&mut parts, decl, span, "type");
        }

        // Position just past the leading keyword(s), used to bound the scan for
        // comments preserved before a default binding or namespace `*`.
        let header_end = self.import_header_end(decl, span, decl.source.span.start);

        // End of the last binding/specifier, used to scan for a comment in the
        // gap before `from` (preserved in place). Set in each binding branch below;
        // left None for bare imports and empty braces (which relocate after `from`).
        let mut from_content_end: Option<u32> = None;

        // Add default import
        if has_default {
            // keyword(s)→default-binding gap, preserved in place. A line comment
            // indents the binding one level (statement continuation); prettier keeps
            // the comment in place and flattens the binding, so this is an indent-only
            // divergence (like `as`→binding). The leading space comes from the
            // `import `/`type ` token.
            parts.push(self.gap_comment_continuation_tail(
                header_end,
                default_spec_start,
                self.ident_name_doc(default_name, default_name_start),
            ));
            from_content_end = Some(default_spec_end);
        }

        // Add namespace import
        if let Some(ns_spec) = namespace_spec {
            // Rule A over the LEADING specifier: an own-line directive in the
            // keyword(s)→`*` gap freezes the whole `* as ns` binding, the node it precedes —
            // so the slice covers the `as` binding too and the `*`-plus-binding pair below is
            // skipped. Only reachable without a default binding, whose own `,` gap is the
            // anchor there instead.
            let frozen_binding = if has_default {
                None
            } else {
                self.gap_frozen_span(header_end, ns_spec.span)
            };
            if let Some(frozen) = frozen_binding {
                parts.push(self.gap_comment_continuation_tail(
                    header_end,
                    ns_spec.span.start,
                    self.build_frozen_node_doc(frozen),
                ));
            } else {
                // The gap before the `*`, preserved in place. Which gap that is — the
                // keyword(s)→`*` one, or the separator gap past a default binding's `,`
                // — is the only difference between the two arms; both indent their
                // continuation one level (an indent-only divergence: prettier keeps the
                // comment in place and flat). The separator gap splits by the shared
                // pure-separator rule — a `//` trails past the comma, a block keeps its
                // authored side, hence the scan bounded at the comma (the named-list arm
                // below scans past it, since prettier hoists a block there instead).
                let gap_start = if has_default {
                    let comma_pos = self.comma_between(default_spec_end, ns_spec.span.start);
                    let split = self.binding_separator_split(default_spec_end, comma_pos);
                    parts.push(self.build_inline_comments_between_doc(default_spec_end, split));
                    parts.push(d.text(", "));
                    split
                } else {
                    header_end
                };
                // An import namespace binding is always an identifier; wrap it to share
                // the `ModuleExportName`-based renderer — and the whole star emitter —
                // with the export-all side.
                let binding = ast::ModuleExportName::Identifier(ns_spec.local.clone());
                self.push_namespace_star_binding(
                    &mut parts,
                    gap_start,
                    ns_spec.span.start,
                    Some(&binding),
                );
            }
            from_content_end = Some(ns_spec.span.end);
        }

        // Build named specifiers with group wrapping (or empty braces if source had them).
        //
        // An empty named group *after a default/namespace binding* (`import a, {}
        // from 'x'`) carries no specifiers, so it's dropped to match prettier
        // (`import a from 'x'`): the binding→`from` gap — and any comment in it — is
        // then handled exactly like a plain default import (a plain match, and
        // idempotent; emitting `, {}` here instead duplicated the gap comment on each
        // reformat). A *bare* empty group (`import {} from 'x'`, no binding) is kept.
        // A leading `default` / `* as ns` binding (as opposed to the named group).
        let has_binding = has_default || namespace_spec.is_some();
        let drop_empty_after_binding = has_empty_braces && named_specs.is_empty() && has_binding;

        if !named_specs.is_empty() || (has_empty_braces && !drop_empty_after_binding) {
            if named_specs.is_empty() {
                // Bare empty braces: `import {} from 'x'` (no binding — the binding
                // case was dropped above). Preserve the keyword→`{` (or `type`→`{`)
                // comment in place — prettier relocates it after `from`; a line
                // comment indents the `{}` continuation one level (the leading space
                // comes from the `import `/`type ` token). Without this,
                // `import /* c */ {} from 'x'` silently drops the comment.
                let braces_doc = if let Some(brace_pos) =
                    self.find_char_outside_comments(header_end, decl.source.span.start, b'{')
                {
                    self.gap_comment_continuation_tail(header_end, brace_pos, d.text("{}"))
                } else {
                    d.text("{}")
                };
                parts.push(braces_doc);
            } else {
                // Named specifiers: comment-aware braced list. `from_content_end`
                // is the offset past `}`, for the `}`→`from` gap comment scan.
                let kw_end = self.import_header_end(decl, span, named_specs[0].span.start);
                // A default/namespace binding sits between the header and the `{`
                // (prettier's `standaloneSpecifiers`), which lets a lone specifier
                // break. (Empty braces after a binding were dropped above, so
                // `named_specs` is non-empty here.)
                let brace_follows_header = !has_binding;
                // Where the list's own gap claim opens. Without a binding that is the
                // header (`import /* c */ {a}`); with one, the separator gap past the
                // binding's `,`, split by the shared pure-separator rule — a block
                // keeps its authored side (prettier agrees, `import x /* c */, {a}`),
                // a `//` trails past the comma and is emitted by the list, which
                // indents the whole `{ … }` under it.
                let mut brace_gap_start = kw_end;
                if has_binding {
                    let prev_end = namespace_spec.map_or(default_spec_end, |ns| ns.span.end);
                    // The gap ends at the `{`, not at the first specifier: a comment
                    // *inside* the braces is the list's, and the `{` is found outside
                    // comments so a `{` glyph in one isn't mistaken for it.
                    let brace_pos = self
                        .find_char_outside_comments(prev_end, named_specs[0].span.start, b'{')
                        .unwrap_or(named_specs[0].span.start);
                    let split = self.binding_separator_split(prev_end, brace_pos);
                    parts.push(self.build_inline_comments_between_doc(prev_end, split));
                    parts.push(d.text(", "));
                    brace_gap_start = split;
                }
                from_content_end = Some(self.push_braced_specifier_list(
                    &mut parts,
                    &named_specs,
                    SpecifierListSpans {
                        header_start: span.start,
                        kw_end,
                        gap_start: brace_gap_start,
                        bound: decl.source.span.start,
                    },
                    brace_follows_header,
                    |s| s.span,
                    |s| self.build_import_specifier_doc(s, is_type_import),
                ));
            }
        }

        // Add "from" and source, extracting comments between keywords and source literal
        if !decl.specifiers.is_empty() || has_empty_braces {
            // Only a *bare* empty group relocates its inside-braces comment after
            // `from`; a dropped empty group (after a binding) carries no braces, so
            // its gap comment stays in place via `from_content_end` (plain-default path).
            let empty_brace_start =
                if has_empty_braces && named_specs.is_empty() && !drop_empty_after_binding {
                    Some(span.start)
                } else {
                    None
                };
            parts.push(self.build_from_source_doc(
                span.start,
                &decl.source,
                empty_brace_start,
                from_content_end,
            ));
        } else {
            // Bare import (`import 'x'`): keyword→source gap, preserved in place. A line
            // comment indents the source one level (indent-only divergence — prettier
            // keeps it flat in place); the leading space comes from the `import ` token.
            let keyword = if is_type_import {
                "import type"
            } else {
                "import"
            };
            let keyword_end = span.start + keyword.len() as u32;
            parts.push(self.gap_comment_continuation_tail(
                keyword_end,
                decl.source.span.start,
                self.build_literal_doc(&decl.source),
            ));
        }

        // Add import attributes: `with { type: "json" }`. Returns the offset
        // past the attribute `}` (or source literal) — the anchor for the
        // trailing pre-`;` comment scan, preserved where the user placed it.
        // Emitted outside the content group (see export above).
        let content_end = self.push_import_attributes_clause(
            &mut parts,
            decl.attributes,
            decl.source.span.end,
            span.end,
        );
        self.finish_with_pre_semi(parts, content_end, span.end, true)
    }

    /// Build doc for `import x = require("y")` or `import x = A.B`
    pub(super) fn build_import_equals_declaration_doc(
        &self,
        decl: &ast::TSImportEqualsDeclaration<'_>,
        span: Span,
    ) -> DocId {
        let d = self.d();
        let mut parts = DocBuf::new();

        // The header keyword, word by word — an optional `export` prefix, `import`, and
        // an optional `type` modifier. Every gap between them is a position an author
        // can comment in; emitting the words as separate fixed texts scans none of
        // them, so a comment in any gap is dropped. Prettier preserves all of these
        // except `import`→`type`, which it moves to the binding side.
        let mut words: SmallVec<[&'static str; 3]> = SmallVec::new();
        if decl.is_export {
            words.push("export");
        }
        words.push("import");
        if matches!(decl.import_kind, ast::ImportKind::Type) {
            words.push("type");
        }
        parts.push(self.build_keyword_header_doc(
            &words,
            span.start,
            decl.id.span.start,
            self.identifier_name_doc(&decl.id),
        ));

        // One walk of the reference: its span bounds and its doc. Built whole (rather
        // than pushed piecemeal) so the `=`→reference gap below can pass it as its
        // continuation — a line comment there indents the reference, and every break
        // *inside* it has to land at that level too.
        let (module_ref_start, ref_end, module_ref_doc) = match &decl.module_reference {
            ast::TSModuleReference::ExternalModuleReference(ext) => (
                ext.span.start,
                ext.span.end,
                self.build_external_module_reference_doc(ext),
            ),
            ast::TSModuleReference::EntityName(entity) => (
                entity.span().start,
                entity.span().end,
                build_entity_name_doc(self, entity),
            ),
        };

        // identifier→`=` and `=`→module-reference gaps. Both are preserved in place
        // (prettier keeps them there too). Nothing else scans them — a module
        // reference is not an expression, so a comment glued to `require` is not
        // owned by any node and would be dropped if this gap didn't emit it. The `=` is
        // located as a byte outside comments, not as a whole word: an identifier glued to
        // it (`import a=require(…)`, `import a /* c */=A.B`) would fail a word-boundary
        // test, and the window holds nothing else but trivia.
        match self.find_char_outside_comments(decl.id.span.end, module_ref_start, b'=') {
            Some(eq_start) => {
                // Both gaps FLAT, one `indent` for the whole tail — the multi-word
                // keyword rule (`build_keyword_words_doc`), which this header is the
                // other instance of. Indenting per gap compounds; and the tail must be
                // *inside* the indent, not a sibling after it, or the reference's own
                // line breaks resolve at the outer level — leaving `require(`'s
                // contents level with it and its `)` a level above.
                let (before_eq, line_before) =
                    self.build_keyword_gap_doc(decl.id.span.end, eq_start);
                let (after_eq, line_after) =
                    self.build_keyword_gap_doc(eq_start + 1, module_ref_start);
                let tail = d.concat(&[before_eq, d.text("="), after_eq, module_ref_doc]);
                parts.push(if line_before || line_after {
                    d.indent(tail)
                } else {
                    tail
                });
            }
            None => {
                // Same landmine as `build_keyword_words_doc`'s fallback: this arm scans
                // no gap, so a comment either side of the `=` is dropped. An
                // import-equals always spells its `=` between the name and the module
                // reference, so this is unreachable — assert it in debug rather than
                // let a future caller degrade silently.
                debug_assert!(
                    false,
                    "import-equals has no `=` in source[{}..{module_ref_start}]",
                    decl.id.span.end
                );
                parts.push(d.text(" = "));
                parts.push(module_ref_doc);
            }
        }

        // Comments between the module reference and `;`: like `export =` (and unlike
        // named imports / re-exports), a same-line trailing **block** comment stays
        // *before* the `;` (operand-attached — prettier 3.9 keeps it), while a same-line
        // **line** comment floats after the `;` via `line_suffix`. So this uses the
        // comma-style `block_after_separator: false`, not `finish_with_pre_semi`.
        self.push_semicolon_with_gap_comments(&mut parts, ref_end, span.end, false, None);
        d.concat(&parts)
    }

    /// The `require('m')` form of an import-equals module reference, as one doc.
    ///
    /// **Both** in-paren gaps are emitted — `require(`→literal and literal→`)`. Nothing
    /// else scans either: a module reference is not an expression, so no node owns a
    /// comment written here and a gap this printer skips is a gap whose comment is
    /// dropped outright (silent content loss, which is what the literal→`)` gap used to
    /// do).
    ///
    /// The close gap takes the shape a call's **last argument** already has
    /// ([`PartitionedComments::emit_last_arg_comments`]): a same-line comment trails the
    /// literal, an own-line one dangles above the `)`. There is no trailing comma to
    /// split around (`trailingComma: 'none'`), which is exactly that helper's premise.
    fn build_external_module_reference_doc(
        &self,
        ext_ref: &ast::TSExternalModuleReference<'_>,
    ) -> DocId {
        let d = self.d();
        // From the end of the `require` KEYWORD, not from a computed `(`: `require(` is a
        // fixed string in the OUTPUT, never in the source, so measuring the paren off the
        // span start (`+ "require(".len()`) lands inside a comment written before it and
        // the three scans below then start past that comment — dropping it outright
        // (`require/* c */('m')`, every payload, and the first comment of a run). A
        // keyword's own bytes can hold no comment, so its end bounds the region exactly and
        // claims both sides of the `(` for the one emitter that prints this slot.
        let require_open_end = ext_ref.span.start + "require".len() as u32;
        let literal_start = ext_ref.expression.span.start;
        let close_paren = ext_ref.span.end.saturating_sub(1);

        // The literal plus whatever trails it inside the parens. A last-item→`)` gap, so
        // the claim is the source reading (`for_closer_gap`) even though no comma can sit
        // in it: a preceding comment's `*/` is text no argument span covers either, and the
        // delimiter reading dangles a comment glued behind it below the literal
        // (`docs/comments.md` §Own-line-ness is a SOURCE question).
        let close =
            PartitionedComments::for_closer_gap(self, ext_ref.expression.span.end, close_paren);
        let mut value: DocBuf = smallvec![self.build_literal_doc(&ext_ref.expression)];
        close.emit_last_arg_comments(&mut value, self);
        let value_doc = d.concat(&value);

        // A line comment runs to EOL and an own-line comment must keep its own line, so
        // either side having one forces the parens open; a lone same-line block stays
        // inline. Same rule as the dynamic-import parens (`calls/import_expr.rs`).
        let open_has_line = self.has_line_comments_between(require_open_end, literal_start);
        let open_has_any = self.has_comments_to_emit_between(require_open_end, literal_start);
        let close_forces_break = close.has_trailing_line() || !close.leading.is_empty();

        if open_has_line || close_forces_break {
            let mut inner = DocBuf::new();
            if open_has_line {
                // Each open-gap comment on its own line — a line comment there can't
                // share one with the literal.
                for comment in self.comments_to_emit_between(require_open_end, literal_start) {
                    inner.push(self.build_comment_doc(comment));
                    inner.push(d.hardline());
                }
            } else if open_has_any {
                // Only the close gap forced the break, so an open-gap block keeps
                // hugging the literal rather than being relocated to its own line.
                inner.push(self.build_inline_comments_between_doc_trailing_space(
                    require_open_end,
                    literal_start,
                ));
            }
            inner.push(value_doc);
            return d.concat(&[
                d.text("require("),
                d.indent_hardline(d.concat(&inner)),
                d.hardline(),
                d.text(")"),
            ]);
        }
        if open_has_any {
            // The comment hugs the `(` and keeps its space on the literal's side
            // (`require(/* c */ 'm')`) — the dotted pair's after-`.` rule, and prettier's.
            // An author who wrote it *before* the `(` lands here too: both formatters move
            // it inside, so the two authorings converge (`unformatted_before_paren`).
            return d.concat(&[
                d.text("require("),
                self.build_inline_comments_between_doc_trailing_space(
                    require_open_end,
                    literal_start,
                ),
                value_doc,
                d.text(")"),
            ]);
        }
        d.concat(&[d.text("require("), value_doc, d.text(")")])
    }

    /// `export as namespace Foo;` — TypeScript UMD global export declaration.
    pub(super) fn build_namespace_export_declaration_doc(
        &self,
        decl: &ast::TSNamespaceExportDeclaration<'_>,
        span: Span,
    ) -> DocId {
        let d = self.d();
        let mut parts = DocBuf::new();
        // `export as namespace` word by word: each gap between the three is a position
        // an author can comment in, and the `namespace`→name gap is one prettier keeps
        // too. Emitting the keyword as one text never scans any of them.
        parts.push(self.build_keyword_header_doc(
            &["export", "as", "namespace"],
            span.start,
            decl.id.span.start,
            self.identifier_name_doc(&decl.id),
        ));
        // Trailing comment between the name and `;` (mirrors `export =` / import-equals):
        // a same-line block comment stays before `;`, a line comment floats after it.
        self.push_semicolon_with_gap_comments(&mut parts, decl.id.span.end, span.end, false, None);
        d.concat(&parts)
    }
}