tsv_css 0.8.0

CSS formatter and parser compatible with Svelte's parseCss — 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
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
// CSS printer - converts internal AST back to formatted source code
//
// ## Architecture
//
// This module is organized by concern to support future expansion:
//
// - **mod.rs** (this file): Orchestration - core Printer, top-level node printing, and
//   the shared block-body routine (`print_css_block_children`, used by rules + at-rules)
// - **boundary_ws.rs**: Boundary-whitespace preservation — the printer half of the run the
//   parser skips at every `allow_whitespace()` juncture (used by every module below)
// - **selectors.rs**: Selector printing (reusable across rules and at-rules)
// - **rules.rs**: CSS rule printing (selector + block structure)
// - **declarations.rs**: Declaration printing + wrapping logic
// - **values.rs**: CSS value printing (all value types)
// - **atrules.rs**: At-rule printing (@media, @keyframes, etc., uses selectors and rules)
//
// ## Design Principles
//
// 1. **Match Prettier**: Output matches prettier for compatibility
// 2. **Preserve Semantics**: Never change CSS rendering semantics
// 3. **Modularity**: Each module has single responsibility for future maintainability
// 4. **Reusability**: Shared printing logic (selectors, block-body iteration) used by multiple modules
// 5. **Hierarchy-Following**: Module structure mirrors CSS spec (rules → declarations → values)

mod atrules;
mod boundary_ws;
pub(crate) use boundary_ws::source_holds_boundary_ws;
mod declarations;
mod rules;
mod selectors;
pub(crate) mod value_normalization;
mod values;

use crate::ast::{Comment, CssBlockChild, CssDeclaration, CssNode, CssStyleSheet};
use crate::lexer::{Lexer, TokenKind};
use tsv_lang::{
    CommentFreeWindow, EmbedContext, INDENT, OutputBuffer, Span, TAB_WIDTH, comments_to_emit_from,
    doc::{
        self, DocContext,
        arena::{DocArena, DocId},
    },
    has_comments_to_emit_from, is_format_ignore_directive,
    printing::{self, FoldedSource, LineBreaks, LineTable},
};

/// Printer state for building output
pub(crate) struct Printer<'a> {
    /// Output buffer
    buffer: OutputBuffer,
    /// Current indentation level
    pub(crate) indent_level: usize,
    /// Embedding context (base indent offset, first-line offset, layout mode, etc.).
    pub(crate) embed: EmbedContext,
    /// Arena allocator for doc nodes (borrowed so a multi-file driver can reuse
    /// one arena across files; see [`DocArena::reset`]).
    pub(crate) arena: &'a DocArena,
    /// Original source (for blank line detection and raw value extraction)
    pub(crate) source: &'a str,
    /// All comments sorted by span.start
    pub(crate) comments: &'a [Comment],
    /// The document's line-break table with its builder's verdict — what the two line
    /// questions below read, by a bounded scan of `source` with the table as the fallback
    /// (`printing::is_same_line_scan` and siblings).
    pub(crate) line_table: LineTable<'a>,
    /// True while printing the body of an `@keyframes` block, so the `from`/`to`
    /// keyframe-selector keywords can be lowercased (they're case-insensitive
    /// keywords there; outside keyframes a `from`/`to` type selector is preserved).
    pub(crate) in_keyframes: bool,
    /// Where the value being printed sits, as its separator rule reads it: inside an
    /// `@utility` block (Tailwind's directive, whose `--value(--name-*)` glues its
    /// trailing `*`), and whether the declaration is a `font` shorthand or a custom
    /// property. Both are fixed before the value is reached, so they are parked here for
    /// the declaration's duration rather than threaded through every value doc builder;
    /// the one genuinely value-scoped bit, an enclosing `calc()`, rides the recursion
    /// instead (`values::ValueCtx`).
    pub(crate) value_scope: values::ValueScope,
    /// Whether `source` holds a boundary-whitespace member ANYWHERE — the document-level
    /// precondition every claim in [`boundary_ws`] is gated on. See
    /// [`boundary_ws::source_holds_boundary_ws`] for why one scan can answer for all of them.
    pub(crate) holds_boundary_ws: bool,
    /// The comment-free window this printer's searches drew (`tsv_lang::CommentFreeWindow`):
    /// every comment lookup here reads it ahead of the search, through the two wrappers
    /// below rather than the free function over `self.comments`. A stylesheet's array is
    /// its own — a `<style>` block's printer shares nothing with the Svelte printer around
    /// it — so the window starts empty here and is never handed across.
    comment_free_gap: CommentFreeWindow<'a>,
    /// The anchor whose LEAD gap a container has already printed whole, if any — a selector
    /// list's comma, a pseudo-argument list's `(`, an `of` keyword claims the gap ahead of
    /// the selector it opens when that gap holds a boundary member, and the selector's own
    /// first anchor must then claim nothing. See `boundary_ws::Printer::claim_lead_gap`.
    pub(crate) claimed_lead: std::cell::Cell<Option<u32>>,
}

impl<'a> Printer<'a> {
    /// Create a new printer for a standalone stylesheet with source, comments and the
    /// document's line table.
    pub(crate) fn new(
        arena: &'a DocArena,
        source: &'a str,
        comments: &'a [Comment],
        line_table: LineTable<'a>,
    ) -> Self {
        let holds_boundary_ws = source_holds_boundary_ws(source);
        Self::with_embed(
            arena,
            source,
            comments,
            line_table,
            EmbedContext::default(),
            holds_boundary_ws,
        )
    }

    /// Create a new printer with the given embedding context. `source` is always a whole
    /// document — a standalone stylesheet, or the Svelte host every `<style>` island (top
    /// level or nested in markup) is spanned against — so a U+FEFF at its offset 0 is a
    /// byte-order mark, and an island's content never sits there. `holds_boundary_ws` is
    /// [`source_holds_boundary_ws`] over that whole `source` — taken by the caller so a host
    /// with several islands scans once (`crate::__private::HostBoundaryScan`).
    pub(crate) fn with_embed(
        arena: &'a DocArena,
        source: &'a str,
        comments: &'a [Comment],
        line_table: LineTable<'a>,
        embed: EmbedContext,
        holds_boundary_ws: bool,
    ) -> Self {
        Self {
            buffer: OutputBuffer::with_capacity(source.len()),
            indent_level: 0,
            embed,
            arena,
            source,
            comments,
            line_table,
            in_keyframes: false,
            value_scope: values::ValueScope::default(),
            holds_boundary_ws,
            claimed_lead: std::cell::Cell::new(None),
            comment_free_gap: CommentFreeWindow::new(comments),
        }
    }

    /// The index a comment walk over `[start, end)` starts at: `len` (an empty walk) for a
    /// gap too narrow to hold a comment or lying inside the comment-free window
    /// ([`Self::comment_free_gap`]), else the outlined search — the same inline gate over
    /// one `#[inline(never)]` body the `tsv_ts` printer spells (see `CommentFreeWindow`).
    #[inline]
    fn first_index_between(&self, start: u32, end: u32) -> usize {
        if self.comment_free_gap.known_comment_free(start, end) {
            self.comment_free_gap.comments().len()
        } else {
            self.first_index_between_wide(start)
        }
    }

    /// The search half of [`Self::first_index_between`] — one outlined copy, taking the
    /// printer.
    #[inline(never)]
    fn first_index_between_wide(&self, start: u32) -> usize {
        self.comment_free_gap.search_from(start)
    }

    /// **to emit**: the comments in `[start, end)` that *this* caller must print — the
    /// printer's form of `tsv_lang::comments_to_emit_in_range`, reading the comment-free
    /// window ([`Self::comment_free_gap`]) ahead of the search.
    #[inline]
    pub(crate) fn comments_to_emit_between(
        &self,
        start: u32,
        end: u32,
    ) -> impl Iterator<Item = &'a Comment> + use<'a> {
        comments_to_emit_from(
            self.comment_free_gap.comments(),
            self.first_index_between(start, end),
            end,
        )
    }

    /// **to emit**: whether *this* caller has a comment to print in `[start, end)` — the
    /// existence form of [`Self::comments_to_emit_between`].
    #[inline]
    pub(crate) fn has_comments_to_emit_between(&self, start: u32, end: u32) -> bool {
        has_comments_to_emit_from(
            self.comment_free_gap.comments(),
            self.first_index_between(start, end),
            end,
        )
    }

    /// Get a reference to the doc arena (convenience for `self.arena`).
    #[inline]
    pub(crate) fn d(&self) -> &DocArena {
        self.arena
    }

    /// Check if two positions are on the same line (a bounded scan of the source, the
    /// line table as the fallback — `printing::is_same_line_scan`)
    ///
    /// Deliberately the shared (ECMAScript) class, unlike
    /// [`Self::has_blank_line_between`]: this one decides whether a comment TRAILS a node,
    /// where the wider reading is the conservative one (the comment gets its own line) and
    /// nothing is regenerated beside it. It is also a RANGE question where prettier's
    /// `hasNewline` is positional (`skipSpaces(" \t")`, then one terminator), and the two can
    /// only disagree when something outside `" \t"` sits between the node and the newline —
    /// an input prettier's own CSS parser **rejects** (`a { color: red;<NBSP>⏎ /* c */ }` →
    /// `CssSyntaxError: Unknown word`). There is no oracle to match there, which is why this
    /// stays a range question and the blank-line rule does not.
    #[inline]
    pub(crate) fn is_same_line(&self, prev_end: u32, curr_start: u32) -> bool {
        printing::is_same_line_scan(
            self.source.as_bytes(),
            self.line_table,
            prev_end,
            curr_start,
        )
    }

    /// Is there a blank line between two positions? — the **formatter oracle's** rule, read
    /// straight off the source bytes.
    ///
    /// ⚠️ The oracle here is prettier, not css-syntax-3. A blank line is cosmetic, so the
    /// question is "what does the printer that owns this decision do", and prettier's answer
    /// is `isNextLineEmpty` (`src/utilities/is-next-line-empty.js`), which is **positional and
    /// not a count**: skip `,; \t`, take **one** terminator, skip `" \t"`, require a second.
    /// Its terminator class is `\n` / `\r` / `\r\n` / `<LS>` / `<PS>`, and `<FF>` is in
    /// neither that class nor the `" \t"` it skips between the two. So a form feed **ends the
    /// search** rather than counting: `}\n<FF>\n{` is one line to prettier, where css-syntax-3
    /// §3.3 (which folds `<FF>` to `<LF>` in input preprocessing) would call it two.
    /// [`css_blank_line_between`] is that rule, and reading §3.3 into it instead was the whole
    /// of tsv's disagreement with prettier over ASCII gaps — three shapes, all of them a
    /// `<FF>` standing between two newlines.
    ///
    /// ⚠️ `<LS>` / `<PS>` are the one place tsv cannot follow, and the reason the class is
    /// re-derived rather than read off the shared table. The printer **preserves** a boundary
    /// run's non-ASCII members into its output (see `preserved_boundary_ws`) while
    /// regenerating the line break beside them, so an authored `a {…}<LS>b {…}` — one line
    /// break either way — comes back as `}\n<LS>b`, which a class holding `<LS>` then counts
    /// as TWO and turns into a blank line on the *next* pass. The document formats two ways:
    /// an F1 violation, and one no fixture can carry (no fixture holds a raw `<LS>`).
    /// Counting them is prettier's answer and tsv declines it — a cataloged divergence, see
    /// [conformance_prettier_css.md](../../../../docs/conformance_prettier_css.md) §CSS:
    /// Comments and whitespace.
    ///
    /// ⚠️ **The shared `line_breaks` table cannot answer this and is deliberately not asked.**
    /// It counts terminators of the ECMAScript class anywhere in the gap, where this rule needs
    /// the first two things the walk meets, so it accepts strictly more — and `{<LF>, <CR>,
    /// <CR><LF>}` is a subset of its class, so it can refuse nothing the walk accepts. Consulting
    /// it as a pre-filter is therefore a `partition_point` — a branchless, and so serially
    /// dependent, probe chain over a whole-file table — bought ahead of a walk that reads a
    /// handful of bytes and rejects ~87% of the time on its own. The `debug_assert` is what keeps
    /// the subset relation under test rather than merely asserted here. `is_same_line`
    /// deliberately keeps the shared class: it decides whether a comment TRAILS a node, where the
    /// wider reading is the conservative one (its own line) and nothing is regenerated beside it.
    pub(crate) fn has_blank_line_between(&self, prev_end: u32, curr_start: u32) -> bool {
        let blank = css_blank_line_between(self.source, prev_end, curr_start);
        debug_assert!(
            !blank
                || printing::has_blank_line_between_scan(
                    self.source.as_bytes(),
                    self.line_table,
                    prev_end,
                    curr_start
                ),
            "the positional rule accepted a gap the shared line-break table holds no pair for"
        );
        blank
    }

    /// Check if a declaration has value comments (comments inside the value, not property name)
    ///
    /// Value comments are comments that appear after the colon, e.g., `color: /* comment */ red;`
    /// Detected by scanning the source text directly (value comments are not stored in the Vec).
    ///
    /// The gate is inline and the scan is one outlined copy
    /// ([`Self::has_value_comments_in_decl_scan`]) — the inline-gate-over-outlined-body split
    /// every comment lookup in the printers spells. `has_block_comment` is false for nearly
    /// every declaration of a real stylesheet, and as one function the whole body was outlined
    /// at its three sites, so each ask paid a call, four callee-saved pushes and the epilogue
    /// for a one-byte answer.
    #[inline]
    pub(crate) fn has_value_comments_in_decl(&self, decl: &CssDeclaration<'_>) -> bool {
        // Free O(1) negative gate: `has_block_comment` (recorded at parse time from the
        // lexer's comment tokens) is false iff no `/* … */` appears anywhere in the
        // declaration — property→colon gap or value/`!important`/trailing region. No block
        // comment anywhere ⟹ no value comment, so skip the colon scan + `/*` substring check
        // + value re-lex entirely on the common comment-free path (this fn runs up to 3× per
        // declaration). When a comment is present the scan runs unchanged, so the result is
        // byte-identical.
        if !decl.has_block_comment {
            return false;
        }
        self.has_value_comments_in_decl_scan(decl)
    }

    /// The scan half of [`Self::has_value_comments_in_decl`], reached only by a declaration
    /// that holds a block comment somewhere: the colon scan, the `/*` substring check and the
    /// value re-lex.
    #[inline(never)]
    fn has_value_comments_in_decl_scan(&self, decl: &CssDeclaration<'_>) -> bool {
        let decl_source = decl.span.extract(self.source);
        let value_start = decl.colon_pos() + 1;
        let value_part = &decl_source[value_start..];
        // Fast path: no `/*` at all → no block comment possible.
        if !value_part.contains("/*") {
            return false;
        }
        // A `/*` is present, but it's only a real value comment when it isn't opaque
        // token content — inside a string (`content: "a/*b"`) or an unquoted `url()`
        // (`url(a/*b)`) the `/*` is literal, not a comment start. Lex the value and look
        // for an actual `Comment` token (the lexer consumes String/Url tokens whole), so
        // the naive substring scan can't false-positive there.
        let mut lexer = Lexer::at_offset(value_part, decl.span.start as usize + value_start);
        loop {
            match lexer.next_token() {
                Ok(t) if t.kind == TokenKind::Comment => return true,
                Ok(t) if t.kind == TokenKind::Eof => return false,
                Ok(_) => {}
                // A lex error (e.g. a genuinely unterminated `/* …`) — fall back to the
                // conservative substring answer rather than dropping a real comment.
                Err(_) => return true,
            }
        }
    }

    /// How many **nodes** the value spanning `span` holds at the comma list's top level
    /// (paren-depth 0), counting from `from` on — or from the span's own start when `from`
    /// is `None`.
    ///
    /// This is prettier's unit of value structure, and the one thing its comma-list
    /// layout turns on: `shouldBreakList` breaks a list when some element is still a
    /// `value-comma_group` after `flattenGroups` collapses the single-node ones — i.e.
    /// when some element holds more than one node. A node is a comment, or a run of
    /// non-comment tokens with no whitespace inside it — where a **function's closing `)`
    /// ends the run**: prettier splits `f(x)g` into two nodes and prints `f(x) g`, so the
    /// count must too. That falls out of `prev_end` tracking only depth-0 tokens: the
    /// argument list is skipped wholesale, leaving `prev_end` at the `(`, so the token
    /// after the `)` reads as gapped and opens a node. Do not "fix" that by updating
    /// `prev_end` on every token — it would fuse `f(x)g` into one node and stop matching
    /// prettier.
    ///
    /// Counting comments as nodes is what makes the glued and spaced authorings agree:
    /// `x,/* c */y` and `x, /* c */ y` both give element 1 two nodes, so both break in
    /// one pass. Keying the break on the space-separated `List` the value parser builds
    /// *only* when a space follows the comment would flip the break between passes —
    /// the value normalizer inserts a space after a comment's `*/`, which is the F1
    /// 2-cycle the `css-normalize-value-text-context-blind` class produces.
    ///
    /// Lexing (not a raw `/*` substring scan) is what keeps it honest: the lexer
    /// consumes strings and unquoted `url(...)` whole, so a `/*` inside them is not a
    /// comment token, and a `(` inside a comment is part of the one Comment token —
    /// neither can miscount the depth.
    pub(crate) fn value_node_count(&self, span: Span, from: Option<u32>) -> usize {
        let start = from.map_or(span.start, |from| span.start.max(from));
        if start >= span.end {
            return 0;
        }
        let text = Span::new(start, span.end).extract(self.source);
        let mut lexer = Lexer::at_offset(text, start as usize);
        let mut paren_depth: u32 = 0;
        let mut count = 0usize;
        // End of the previous depth-0 token, and whether it was a comment — a token
        // opens a new node when whitespace precedes it, or when either side of the
        // boundary is a comment (a comment is always its own node, glued or not).
        let mut prev_end: Option<u32> = None;
        let mut prev_was_comment = false;
        loop {
            // A lex error (e.g. an unterminated `/* …`): report what was counted so far
            // rather than guess. This governs only layout, never content.
            let Ok(token) = lexer.next_token() else {
                return count;
            };
            match token.kind {
                TokenKind::Eof => return count,
                // Whitespace is a node *boundary*, not a node; the gap it leaves is
                // what `prev_end` detects.
                TokenKind::Whitespace => continue,
                _ => {}
            }
            if paren_depth == 0 {
                let is_comment = token.kind == TokenKind::Comment;
                if prev_end.is_none_or(|end| end < token.start) || is_comment || prev_was_comment {
                    count += 1;
                }
                prev_was_comment = is_comment;
                prev_end = Some(token.end);
            }
            match token.kind {
                TokenKind::LeftParen => paren_depth += 1,
                TokenKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
                _ => {}
            }
        }
    }

    /// The block-comment run a value opens with: the text to emit after the
    /// declaration's `:`, and the absolute offset where the value's own content starts.
    /// `None` when the value does not open with a comment, or holds nothing but
    /// comments.
    ///
    /// postcss keeps everything between a declaration's property and its value in
    /// `raws.between`, so a comment right after the colon is not part of the value at
    /// all — which is why prettier neither counts it toward the list's node counts nor
    /// indents it with the first element, but leaves it on the colon's line
    /// (`font-family: /* c */⏎\tcolor 0.3s,⏎\tb;`). tsv keeps such a comment inside the
    /// value span (the wire `value` is source-sliced, so the span is a printer-internal
    /// boundary), and recovers the same split here.
    ///
    /// Returns the run's own span and the absolute offset where content begins. The
    /// caller emits the run through `normalize_css_whitespace`, so multiple comments are
    /// joined single-spaced — the same normalizer every other comment-bearing value path
    /// uses, and the same rule the property→colon gap takes
    /// (`multi_comment_before_colon`). Spans rather than built text keeps this allocation-
    /// free, so the predicate path can ask the question as cheaply as the emit path.
    pub(crate) fn leading_value_comment_run(&self, value_span: Span) -> Option<(Span, u32)> {
        let text = value_span.extract(self.source);
        let mut lexer = Lexer::at_offset(text, value_span.start as usize);
        let mut run_end = None;
        loop {
            let token = lexer.next_token().ok()?;
            match token.kind {
                TokenKind::Comment => run_end = Some(token.end),
                TokenKind::Whitespace => {}
                // A value of nothing but comments has no content to separate them from.
                TokenKind::Eof => return None,
                _ => {
                    let run_end = run_end?;
                    let run = Span::new(value_span.start, value_span.start + run_end);
                    return Some((run, value_span.start + token.start));
                }
            }
        }
    }

    /// Write a string to the buffer
    pub(crate) fn write(&mut self, s: &str) {
        self.buffer.write(s);
    }

    /// Write a block opener: `" {"` — or a bare `"{"` when the prelude already ends in
    /// a space.
    ///
    /// Only one thing can leave a space there: a selector (or at-rule prelude) ending
    /// in a CSS **escape's payload** — `.a\ ` is a class named `a `, and that space is
    /// content, so it survives the render (see `write_arena_doc`). It also
    /// already separates the prelude from the brace, so adding a second space would
    /// emit `.a\  {` where every other formatter emits `.a\ {`. Absorbing the separator
    /// is the fix; trimming the payload instead is not — that strands the backslash
    /// onto the `{`, which it would then escape.
    ///
    /// This is the literal-escape twin of the hex-escape rule in
    /// `selectors.rs::span_leaf_doc`: a *terminator* is dropped because the following
    /// separator re-terminates the escape, while a *payload* is kept and the separator
    /// is dropped instead. Either way exactly one space reaches the output.
    pub(crate) fn write_block_open(&mut self) {
        if self.buffer.ends_with(' ') {
            self.write("{\n");
        } else {
            self.write(" {\n");
        }
    }

    /// Write indentation based on current indent level
    ///
    /// Used for printing nested structures like CSS rules.
    pub(crate) fn write_indent(&mut self) {
        tsv_lang::write_indent(&mut self.buffer, self.indent_level, INDENT);
    }

    /// Write indentation at the current level plus `extra` additional levels.
    ///
    /// For continuation lines in the imperative `@import` media-query list wrap, which
    /// indents one or two levels past the statement. The caller writes the preceding
    /// newline; this only emits the (deeper) indentation.
    pub(crate) fn write_indent_extra(&mut self, extra: usize) {
        self.indent_level += extra;
        self.write_indent();
        self.indent_level -= extra;
    }

    /// Remove trailing newline from buffer (for inline comment handling)
    pub(crate) fn buffer_remove_trailing_newline(&mut self) {
        self.buffer.pop_if_ends_with('\n');
    }

    /// The current column position (for doc-builder width calculations).
    pub(crate) fn current_column(&self) -> usize {
        self.buffer.current_column(TAB_WIDTH)
    }

    /// Get the visual width of current indentation in characters
    ///
    /// Converts indent level to actual character width based on tab_width.
    pub(crate) fn indent_width(&self) -> usize {
        self.indent_level * TAB_WIDTH
    }

    /// Write a value DocId to the buffer, accounting for current column and indent level.
    ///
    /// This handles the common pattern of:
    /// 1. Get current column position
    /// 2. Print doc with indent-aware width calculations
    /// 3. Write the result to the buffer
    ///
    /// # A doc that fits its line is written without a render
    ///
    /// Nearly every piece this printer writes — a selector, a value list, a function —
    /// fits the rest of its line, and the renderer would print it flat: its texts in
    /// order, a space for each `line`. `doc::arena_try_print_flat_into` writes exactly
    /// that straight onto the buffer in one walk, charging widths as it goes, and
    /// refuses any doc it cannot prove the renderer prints the same way (a doc too wide
    /// for the line above all); only a refused doc pays for the render below — the
    /// scratch, the command stack, the fits walks. The proof and the debug twin that
    /// holds it are in `tsv_lang`'s `doc/arena_render_flat.rs`. The renderer's audit
    /// seams lose nothing: that walk refuses a doc holding a line-comment text or a
    /// tagged comment node while the matching check is armed, and this printer builds
    /// neither (a comment in its docs is plain text, and the ledger is fed where the
    /// comment is written) — so the refusal never fires here, and in an audit build
    /// the twin is what grades the flat path.
    ///
    /// # Trailing whitespace is preserved
    ///
    /// A rendered piece's last line can end in whitespace that is **content** rather than
    /// layout: a CSS escape's payload may be a space (`width: 50px\ ;` — css-syntax-3
    /// §4.3.4/§4.3.7 make `\` + whitespace a valid escape whose escaped code point IS that
    /// whitespace). The value renders as its own piece and the terminator (`;`, the
    /// `!important` tail) is appended to the buffer *afterwards* by `write_declaration_end`,
    /// so that escaped space sits at the very end of the piece. The default entry point's
    /// final-line trim would strip it and strand the backslash onto the `;`, whose output no
    /// longer parses — so this uses the CSS entry point (`arena_print_css_doc_into`),
    /// which skips the final-line trim as the HTML `<pre>`/`<textarea>` entry does.
    ///
    /// The same payload can end an **interior** line too, where a break lands right after
    /// it (the last argument of a wrapped `f(…, b\ )` or `:not(…, .c\ )`, a value fill
    /// breaking after `b\ `): that entry's line-break trim spares the one whitespace byte
    /// an odd run of `\` escapes, and trims as everywhere else otherwise. An ordinary
    /// value's rendered piece never ends in whitespace, so the final-line rule is a no-op
    /// everywhere else — except a selector, which can end in an escaped
    /// space too (`.a\ `, a class named `a `), with the `,` / ` {` its caller appends landing
    /// right after it: trim the payload and the stranded backslash escapes that punctuation
    /// instead (`.a\ , .b` → `.a\,`, one class named `a,` — the selector list is gone).
    /// [`Self::write_arena_doc_reserving`] — selectors, at-rule preludes and the `@scope`
    /// clause's selector list — does the same, for the same reason.
    pub(crate) fn write_arena_doc(&mut self, d: DocId) {
        if !self.try_write_arena_doc_flat(d, 0) {
            self.render_arena_doc(d);
        }
    }

    /// The flat write-out every writer here tries first: `true` when the doc fit the rest
    /// of its line, `reserve` columns short of the print width, and was appended; `false`
    /// (buffer untouched) when it is left to [`Self::render_arena_doc`].
    fn try_write_arena_doc_flat(&mut self, d: DocId, reserve: usize) -> bool {
        let current_col = self.current_column();
        doc::arena_try_print_flat_into(
            self.arena,
            d,
            reserve,
            current_col,
            self.indent_level,
            self.source,
            self.buffer.as_append_target(),
        )
    }

    /// Render a doc through the CSS entry point (escape-aware line trims, no final-line
    /// trim) and write it.
    fn render_arena_doc(&mut self, d: DocId) {
        let current_col = self.current_column();
        // Render into the arena-parked scratch: one warm buffer across the
        // file's rules instead of an alloc/free per rule.
        let mut output = self.arena.take_render_scratch();
        doc::arena_print_css_doc_into(
            self.arena,
            d,
            &self.embed,
            current_col,
            self.indent_level,
            self.source,
            &mut output,
        );
        self.write(&output);
        self.arena.park_render_scratch(output);
    }

    /// Render a doc, charging `reserve` columns of trailing punctuation the caller
    /// writes after it (a declaration's `;`, a selector's `,` or ` {`, the ` {`/`;` after
    /// an at-rule prelude, the `)` after an `@scope` clause's selector list) to the doc's
    /// **last line** only.
    ///
    /// The render appends a measurement-only trailing node (it renders nothing) after the
    /// doc. A group whose flat line runs to the doc's end reaches that node and counts it,
    /// but a group a later break separates from it (a nested `calc`, a pseudo's argument
    /// list followed by another one, a `:global(…)` argument's inner line) stops its
    /// look-ahead at that break and never reserves the columns — prettier's rule, where
    /// the punctuation is printed after the doc and only the line it lands on carries it.
    /// A fill-based prelude carries the reserve as its own context too
    /// (`DocContext::reserving`), for the final-segment measures that take no look-ahead.
    ///
    /// The flat write-out is tried first with the reserve charged against its budget, so
    /// the doc that fits — nearly every one — builds no marker: a doc written flat is one
    /// line, its last, and the flat walk charges the reserve as the trailing node would
    /// (`print_width − reserve − column` against the doc's width), so the two accept the
    /// same docs.
    ///
    /// Preserves the rendered piece's trailing whitespace for the same reason
    /// [`Self::write_arena_doc`] does — and the reserve is *precisely* the punctuation
    /// that makes it matter: a selector can end in an escaped space (`.a\ `), and the `,`,
    /// ` {` or `)` the caller appends lands right after it.
    pub(crate) fn write_arena_doc_reserving(&mut self, doc: DocId, reserve: usize) {
        if self.try_write_arena_doc_flat(doc, reserve) {
            return;
        }
        let reserved = if reserve == 0 {
            doc
        } else {
            let d = self.d();
            let marker = d.with_context(d.empty(), DocContext::reserving(reserve));
            d.concat(&[doc, marker])
        };
        self.render_arena_doc(reserved);
    }

    /// Get the formatted output
    pub(crate) fn into_string(self) -> String {
        self.buffer.into_string()
    }

    /// Print a list of CSS nodes (rules) with comments interspersed by position
    pub(crate) fn print_css_nodes(&mut self, nodes: &[CssNode<'_>]) {
        // Use comment index for efficient traversal (comments are sorted)
        let mut comment_idx = 0;
        let mut prev_end: u32 = 0;
        let mut printed_any = false;

        for (i, node) in nodes.iter().enumerate() {
            let node_start = node.span().start;
            let node_end = node.span().end;
            // The next sibling's start bounds this node's trailing claim (see
            // `print_inline_comments_after_node`); the last node's runs to the end.
            let next_start = nodes.get(i + 1).map(|next| next.span().start);

            // Print comments between prev_end and this node
            let idx_before = comment_idx;
            let comments_before =
                self.print_leading_comments(prev_end, node_start, &mut comment_idx);

            // Check printed comments for a format-ignore directive (O(k) where k = comments_before)
            let has_ignore = self.comments[idx_before..comment_idx]
                .iter()
                .any(|c| is_format_ignore_directive(c.content(self.source)));

            // Add separator before node
            if printed_any || comments_before > 0 {
                // Determine where to measure blank line from
                let gap_start = if comments_before > 0 {
                    self.comments
                        .get(comment_idx.saturating_sub(1))
                        .map_or(prev_end, |c| c.span.end)
                } else {
                    prev_end
                };

                if self.has_blank_line_between(gap_start, node_start) {
                    self.write("\n\n");
                } else {
                    self.write("\n");
                }
            }

            // Leading indent for this top-level node (matches the nested
            // `print_css_block_children` pattern). No-op at `indent_level` 0
            // (standalone); the embedded stylesheet renders at the wrapper level.
            self.write_indent();

            // format-ignore: emit raw source instead of formatting
            if has_ignore {
                self.write_verbatim_span(self.frozen_slice_span(node.span()));
            } else {
                self.print_css_node(node);
            }

            // Check for inline comments on same line as node's closing brace
            let inline_count =
                self.print_inline_comments_after_node(node_end, next_start, &mut comment_idx);

            prev_end = if inline_count > 0 {
                self.comments
                    .get(comment_idx - 1)
                    .map_or(node_end, |c| c.span.end)
            } else {
                node_end
            };

            printed_any = true;
        }

        // Print trailing comments after all nodes
        let had_trailing = self.print_trailing_comments(prev_end, &mut comment_idx);
        if had_trailing {
            printed_any = true;
        }

        // Add trailing newline (only if there's content — empty files stay empty)
        if printed_any {
            self.write("\n");
        }
    }

    /// Print leading comments between prev_end and curr_start
    /// Returns the number of comments printed
    fn print_leading_comments(
        &mut self,
        prev_end: u32,
        curr_start: u32,
        comment_idx: &mut usize,
    ) -> usize {
        let mut printed = 0;
        let mut last_end = prev_end;

        while *comment_idx < self.comments.len() {
            let comment = &self.comments[*comment_idx];
            if comment.span.start >= curr_start {
                break;
            }

            // Skip comments that are inside previous node's span (e.g., prelude comments in at-rules)
            // These are handled by the node's own printing logic via comments_to_emit_in_range()
            if comment.span.end <= prev_end {
                *comment_idx += 1;
                continue;
            }

            // A comment on the previous node's line is never here: `print_inline_comments_after_node`
            // claimed the whole same-line run before this scan started, with the same
            // `is_same_line` test and the same cursor, and `prev_end` is that run's end. (This
            // scan once re-classified each comment and SKIPPED a `Trailing` one — a silent drop
            // had the two ever disagreed; 5.3M injections never reached it.)

            // Print with proper spacing
            let mut starts_line = true;
            if printed > 0 {
                // Check if this comment is on the same line as the previous comment
                if self.is_same_line(last_end, comment.span.start) {
                    self.write(" ");
                    starts_line = false;
                } else if self.has_blank_line_between(last_end, comment.span.start) {
                    self.write("\n\n");
                } else {
                    self.write("\n");
                }
            } else if prev_end > 0 {
                // First comment after a node
                if self.has_blank_line_between(last_end, comment.span.start) {
                    self.write("\n\n");
                } else {
                    self.write("\n");
                }
            }

            // Leading indent for a top-level comment that starts its own line
            // (skip a mid-line comment joined to the previous by a space; no-op
            // at indent_level 0 / standalone).
            if starts_line {
                self.write_indent();
            }
            self.write_head_boundary_ws(comment.span.start);
            self.print_css_comment(comment);
            last_end = comment.span.end;
            *comment_idx += 1;
            printed += 1;
        }

        printed
    }

    /// Print inline comments on the same line after a node
    /// Returns the number of comments printed
    ///
    /// The claim runs from the node's end to the NEXT sibling's start, never past it: a
    /// comment on this line but inside the next node — a prelude comment of an `@import` /
    /// `@supports` / `@container` / `@scope` glued on after `;` or `}` — belongs to that
    /// node's own emitter, which prints it again. Unbounded, this scan claimed every such
    /// comment to the end of the line and each printed twice (the statement-gap seam's
    /// rule: the trailing claim stops where the next statement begins). The nested block
    /// loops need no such bound — they walk the child list, where a comment is a child
    /// and the next rule is the stop by construction.
    fn print_inline_comments_after_node(
        &mut self,
        node_end: u32,
        next_start: Option<u32>,
        comment_idx: &mut usize,
    ) -> usize {
        let mut printed = 0;
        let mut last_end = node_end;

        while *comment_idx < self.comments.len() {
            let comment = &self.comments[*comment_idx];

            // Skip comments inside the node's span (e.g. at-rule prelude comments,
            // block-interior comments). These are emitted by the node's own printing
            // logic via comments_to_emit_in_range(); without this guard the loop would break on
            // an interior comment (its start precedes node_end, so is_same_line reverses
            // to false) and the genuine trailing comment after `;`/`}` would be dropped.
            if comment.span.end <= node_end {
                *comment_idx += 1;
                continue;
            }

            if next_start.is_some_and(|next| comment.span.start >= next)
                || !self.is_same_line(last_end, comment.span.start)
            {
                break;
            }

            self.write(" ");
            self.write_head_boundary_ws(comment.span.start);
            self.print_css_comment(comment);
            last_end = comment.span.end;
            *comment_idx += 1;
            printed += 1;
        }

        printed
    }

    /// Print trailing comments after all nodes
    /// Returns true if any comments were printed
    fn print_trailing_comments(&mut self, prev_end: u32, comment_idx: &mut usize) -> bool {
        let mut last_end = prev_end;
        let mut printed = false;

        while *comment_idx < self.comments.len() {
            let comment = &self.comments[*comment_idx];

            // Skip comments that are inside previous node's span (e.g., prelude comments in at-rules)
            // These are handled by the node's own printing logic via comments_to_emit_in_range()
            if comment.span.end <= prev_end {
                *comment_idx += 1;
                continue;
            }

            // Skip inline comments (same line as last item) - already handled
            // But only if there was a last item (prev_end > 0) - otherwise this is the first content
            if prev_end > 0 && self.is_same_line(prev_end, comment.span.start) {
                *comment_idx += 1;
                last_end = comment.span.end;
                continue;
            }

            // Print with proper spacing (but no leading newline for first content)
            if last_end > 0 {
                if self.has_blank_line_between(last_end, comment.span.start) {
                    self.write("\n\n");
                } else {
                    self.write("\n");
                }
            }

            // Leading indent for this top-level trailing comment (no-op standalone).
            self.write_indent();
            self.write_head_boundary_ws(comment.span.start);
            self.print_css_comment(comment);
            last_end = comment.span.end;
            *comment_idx += 1;
            printed = true;
        }

        printed
    }

    /// Print a single CSS node
    fn print_css_node(&mut self, node: &CssNode<'_>) {
        match node {
            CssNode::Rule(rule) => self.print_css_rule(rule),
            CssNode::Atrule(atrule) => self.print_css_atrule(atrule),
        }
    }

    /// Print a CSS comment
    /// Write `span` of the source **verbatim**.
    ///
    /// The format-ignore seam: the ignored node's own comments ride out inside the raw
    /// slice, never reaching a comment emitter, so the ledger is told the range is
    /// covered (see `tsv_lang::comment_ledger`). Doc-side raw slices —
    /// `d.source_span(…)` for a glued compound selector or an unparseable one — record
    /// through `record_verbatim_range` at their own sites.
    pub(crate) fn write_verbatim_span(&mut self, span: Span) {
        #[cfg(feature = "_internal_comment_check")]
        tsv_lang::comment_ledger::record_verbatim_range(self.source, span.start, span.end);

        self.write(span.extract(self.source));
    }

    pub(crate) fn print_css_comment(&mut self, comment: &Comment) {
        // One of the crate's two comment-emission seams (the other is
        // `comment_blocks_in_range`). Also prints the *AST-node* comments — a
        // `CssBlockChild::Comment` — whose span is registered too (by
        // `register_stylesheet_comments` at the format entry), so this emit matches a
        // registered entry and this seam records it, same as for a detached comment.
        #[cfg(feature = "_internal_comment_check")]
        tsv_lang::comment_ledger::record_emitted(self.source, comment.span);

        // Write comment with delimiters - content is preserved exactly as written
        self.write("/*");
        self.write(comment.content(self.source));
        self.write("*/");
    }

    /// Join the comments fully within `[start, end)` as space-separated `/*…*/`
    /// blocks (empty string when there are none). The single source-to-string form of
    /// a comment run, shared by the at-rule prelude and selector comment interleaving.
    /// Multi-line comment interiors stay verbatim under Svelte `<style>` embedding:
    /// the CSS renders at its final indent, so an interior line keeps its content at
    /// column 0 with no post-hoc re-indent.
    pub(crate) fn comment_blocks_in_range(&self, start: u32, end: u32) -> String {
        let mut out = String::new();
        self.push_comment_blocks_in_range(&mut out, start, end, " ");
        out
    }

    /// [`Self::comment_blocks_in_range`] appended in place, with the run's own separator
    /// named.
    ///
    /// Every ordinary gap joins single-spaced; the empty separator is for the
    /// **whitespace-forbidden** selector gaps — the components of a `<wq-name>` or an
    /// `<attr-matcher>` — where the space would be a `<whitespace-token>` the grammar
    /// rejects, so a glued run must stay glued (the same rule that keeps
    /// `.a/* c *//* d */.b` a compound).
    ///
    /// Appends rather than returning a `String` because the attribute selector's rebuild
    /// asks this once per interior gap, into one buffer it is already building.
    pub(crate) fn push_comment_blocks_in_range(
        &self,
        out: &mut String,
        start: u32,
        end: u32,
        sep: &str,
    ) {
        let mut first = true;
        for comment in self.comments_to_emit_between(start, end) {
            // The crate's other comment-emission seam (see `print_css_comment`). Sound to
            // record at build time: the CSS printer builds each doc once and renders it
            // once — no `conditional_group` candidate rebuilds.
            #[cfg(feature = "_internal_comment_check")]
            tsv_lang::comment_ledger::record_emitted(self.source, comment.span);

            if first {
                first = false;
            } else {
                out.push_str(sep);
            }
            out.push_str("/*");
            out.push_str(comment.content(self.source));
            out.push_str("*/");
        }
    }

    /// Try to print inline comments after the current item
    ///
    /// Checks if the next item is a comment on the same line as `prev_end`.
    /// If so, prints it inline and returns the number of comments consumed.
    ///
    /// This consolidates the repeated pattern across rules.rs and atrules.rs.
    pub(crate) fn try_print_inline_comments(
        &mut self,
        children: &[CssBlockChild<'_>],
        current_idx: usize,
        prev_end: u32,
    ) -> usize {
        let mut consumed = 0;
        let mut last_end = prev_end;

        while let Some(CssBlockChild::Comment(next_comment)) =
            children.get(current_idx + 1 + consumed)
            && self.is_same_line(last_end, next_comment.span.start)
        {
            self.write(" ");
            self.write_head_boundary_ws(next_comment.span.start);
            self.print_css_comment(next_comment);
            last_end = next_comment.span.end;
            consumed += 1;
        }

        consumed
    }

    /// Try to print inline comments after a declaration
    ///
    /// Like `try_print_inline_comments`, but a declaration ends with its own `\n`:
    /// pull that newline back before trailing the first same-line comment, then
    /// re-add it after. Delegates the per-comment loop so the two stay in lockstep.
    pub(crate) fn try_print_inline_comments_after_decl(
        &mut self,
        children: &[CssBlockChild<'_>],
        current_idx: usize,
        prev_end: u32,
    ) -> usize {
        // Only a same-line comment trails the declaration; if the next child isn't
        // one, leave the declaration's trailing newline in place.
        let has_inline = matches!(
            children.get(current_idx + 1),
            Some(CssBlockChild::Comment(next)) if self.is_same_line(prev_end, next.span.start)
        );
        if !has_inline {
            return 0;
        }

        self.buffer_remove_trailing_newline();
        let consumed = self.try_print_inline_comments(children, current_idx, prev_end);
        self.write("\n");
        consumed
    }

    /// Print a declaration — honoring a pending format-ignore directive — then any
    /// same-line trailing comments, returning the number of comments consumed (advance
    /// the loop index by it). Shared by the three CSS block-body loops (top-level rule
    /// body, nested-rule body, at-rule direct-declaration body) so their declaration
    /// handling stays in lockstep; drift between two of them was the at-rule
    /// trailing-comment bug.
    ///
    /// The pending-ignore flag is passed by `&mut` and **consumed here** (read, then
    /// reset to false) so the directive applies to exactly this one declaration. Folding
    /// the reset into the helper means a caller cannot honor the flag without also
    /// clearing it — the failure mode that left the at-rule body loop ignoring the
    /// directive (it hardcoded `false`, skipping both honor and reset).
    pub(crate) fn print_decl_with_inline_comments(
        &mut self,
        children: &[CssBlockChild<'_>],
        index: usize,
        decl: &CssDeclaration<'_>,
        format_ignore_next: &mut bool,
    ) -> usize {
        if std::mem::take(format_ignore_next) {
            self.write_format_ignore_declaration(decl);
        } else {
            self.print_css_declaration(decl);
        }
        self.try_print_inline_comments_after_decl(children, index, decl.span.end)
    }

    /// Print the children of a CSS block body — a rule's declaration list or an
    /// at-rule block's child list — applying the per-child policy that the three
    /// former block loops (top-level rule body, nested-rule body, at-rule direct
    /// body) each re-implemented and drifted on: source-blank-line preservation, a
    /// trailing-newline separator, inline trailing-comment capture after a nested
    /// rule/at-rule, and a pending format-ignore directive.
    ///
    /// `start_index` skips any pre-`{` comments the caller already consumed inline
    /// before the opening brace (0 for an at-rule block, whose prelude owns that
    /// region). Centralizing the loop is what makes that drift impossible: every
    /// child type is handled in exactly one place. The two earlier drift bugs —
    /// a trailing comment after a nested at-rule's `}` and a format-ignore inside
    /// an at-rule body — were each "one of the three loops handled case X, another
    /// didn't"; with a single routine there is no "another".
    pub(crate) fn print_css_block_children(
        &mut self,
        children: &[CssBlockChild<'_>],
        start_index: usize,
    ) {
        let mut i = start_index;
        let mut format_ignore_next = false;
        while i < children.len() {
            // Preserve a source blank line before any child (uniform across types).
            if i > start_index && self.has_blank_line_before_child(children, i) {
                self.write("\n");
            }
            match &children[i] {
                CssBlockChild::Declaration(decl) => {
                    i += self.print_decl_with_inline_comments(
                        children,
                        i,
                        decl,
                        &mut format_ignore_next,
                    );
                }
                CssBlockChild::Comment(comment) => {
                    // A format-ignore directive applies to the next child.
                    if is_format_ignore_directive(comment.content(self.source)) {
                        format_ignore_next = true;
                    }
                    self.write_indent();
                    self.write_head_boundary_ws(comment.span.start);
                    self.print_css_comment(comment);
                    self.write("\n");
                }
                CssBlockChild::Rule(rule) => {
                    self.write_indent();
                    if std::mem::take(&mut format_ignore_next) {
                        self.write_verbatim_span(self.frozen_slice_span(rule.span));
                    } else {
                        self.print_css_rule(rule);
                    }
                    // Keep a same-line comment after the closing `}` on its line.
                    i += self.try_print_inline_comments(children, i, rule.span.end);
                    self.write("\n");
                }
                CssBlockChild::Atrule(atrule) => {
                    self.write_indent();
                    if std::mem::take(&mut format_ignore_next) {
                        self.write_verbatim_span(self.frozen_slice_span(atrule.span));
                    } else {
                        self.print_css_atrule(atrule);
                    }
                    i += self.try_print_inline_comments(children, i, atrule.span.end);
                    self.write("\n");
                }
            }
            i += 1;
        }
    }

    /// Where a declaration's text really ends: one past its `;`, or its span end when there
    /// is none (a block's last child, terminated by the `}`).
    ///
    /// A declaration's internal span stops at its parsed VALUE, so the `;` is outside it —
    /// and so is everything Svelte's `read_declaration` counts as the declaration's tail:
    /// whitespace, block comments, and `!important`. The gap this end opens is the one
    /// [`Self::has_blank_line_between`] reads, and that reader is positional, so a tail left
    /// inside the gap is not skipped over but *answered on* — `color: red !important;` handed
    /// it a gap beginning ` !important;` and lost the author's blank line, on real code.
    ///
    /// ⚠️ Hence [`crate::comments::scan_to_terminator`] rather than a whitespace loop: it is
    /// the one definition of that tail, shared with the wire writer's declaration extent. Its
    /// contract is exactly this region — see the ⚠️ there before pointing it anywhere else.
    /// Only a `;` is stepped past; a `}` terminator belongs to the enclosing block, not to
    /// this child.
    fn end_after_semicolon(&self, span_end: u32) -> u32 {
        let at = crate::comments::scan_to_terminator(self.source, span_end as usize);
        if self.source.as_bytes().get(at) == Some(&b';') {
            (at + 1) as u32
        } else {
            span_end
        }
    }

    /// Check if there's a blank line before a block child, accounting for the
    /// previous sibling's trailing `;` (not included in declaration spans).
    pub(crate) fn has_blank_line_before_child(
        &self,
        children: &[CssBlockChild<'_>],
        index: usize,
    ) -> bool {
        let Some(prev) = children.get(index.wrapping_sub(1)) else {
            return false;
        };
        let curr_start = children[index].span().start;
        let prev_end = prev.span().end;
        let effective_end = if matches!(prev, CssBlockChild::Declaration(_)) {
            self.end_after_semicolon(prev_end)
        } else {
            prev_end
        };
        self.has_blank_line_between(effective_end, curr_start)
    }
}

/// Does `[from, to)` open a blank line? — prettier's `isNextLineEmpty`, in bytes.
///
/// The confirming half of [`Printer::has_blank_line_between`], and a transcription rather than
/// a re-derivation: prettier's `src/utilities/is-next-line-empty.js` skips `,; \t`
/// (`skipToLineEnd`, which subsumes its `skipSpaces` loop once comments are out of the
/// picture), takes **one** terminator (`skipNewline`), then asks `hasNewline` — `skipSpaces`
/// over `" \t"` and one more terminator. Two things fall out of the shape that a count gets
/// wrong, and both were live:
///
/// - a character in **neither** set ENDS the search rather than being stepped over, so a
///   `<FF>` between two newlines is not a blank line (`}\n<FF>\n{`), and neither is one
///   *before* them (`}<FF>\n\n{`);
/// - the two terminators must be the FIRST two things the walk meets, so a count over the
///   whole gap can answer `true` where the walk stops early.
///
/// ⚠️ The terminator class here is prettier's **minus `<LS>` / `<PS>`** — see
/// [`Printer::has_blank_line_between`] for why counting those would fabricate a blank line on
/// the next pass. `<CR>` cannot actually reach this scan (every parse-then-format entry folds
/// it first, `tsv_lang::printing::normalize_carriage_returns`); it is spelled anyway so the
/// class reads as the oracle's, with one documented exclusion rather than two undocumented
/// ones. Byte-wise on purpose: every member is a single ASCII byte, so no UTF-8 decode is
/// needed and a multi-byte `<LS>` cannot be mistaken for one.
///
/// Comments never reach here either — prettier's walk skips them because a node's own span can
/// be followed by a trailing `//`-style comment, where CSS makes every comment a child of its
/// own, so the gap between two children is whitespace and separators alone.
fn css_blank_line_between(source: &str, from: u32, to: u32) -> bool {
    let Some(gap) = source
        .as_bytes()
        .get(from as usize..(to as usize).min(source.len()))
    else {
        return false;
    };
    // `skipToLineEnd` — the separators a child's own span can leave behind (a declaration's
    // `;`, a selector list's `,`) plus the spaces prettier's loop folds into the same step.
    let mut i = 0;
    while matches!(gap.get(i), Some(b',' | b';' | b' ' | b'\t')) {
        i += 1;
    }
    let Some(first) = css_terminator_len(gap, i) else {
        return false;
    };
    i += first;
    // `hasNewline`'s own `skipSpaces` — `" \t"` ONLY, which is what makes a `<FF>` here an
    // answer rather than a step.
    while matches!(gap.get(i), Some(b' ' | b'\t')) {
        i += 1;
    }
    css_terminator_len(gap, i).is_some()
}

/// The length of the line terminator at `gap[i]`, or `None` — prettier's `skipNewline` over
/// the class [`css_blank_line_between`] documents, with `<CR><LF>` taken as one.
fn css_terminator_len(gap: &[u8], i: usize) -> Option<usize> {
    match gap.get(i)? {
        b'\n' => Some(1),
        b'\r' => Some(if gap.get(i + 1) == Some(&b'\n') { 2 } else { 1 }),
        _ => None,
    }
}

/// Register a stylesheet's comments with the print-once ledger: the **detached** comments on
/// `CssStyleSheet.comments`, plus every in-block `CssBlockChild::Comment` AST node (the CSS
/// analog of a Svelte `<!-- -->`), collected by recursing into nested rules and at-rule
/// blocks. Both `print_css_comment` seams already record the emits, so registering the
/// in-block comments here is what turns a dropped/double-printed one into a ledger finding
/// instead of a silent, unregistered emit. A declaration's *value* comments are never lexed
/// as `Comment`s, so they stay out of scope by construction.
#[cfg(feature = "_internal_comment_check")]
fn register_stylesheet_comments(stylesheet: &CssStyleSheet<'_>, source: &str) {
    tsv_lang::comment_ledger::register_parsed(source, &stylesheet.comments);
    let mut block_comment_spans = Vec::new();
    collect_block_comment_spans(stylesheet.nodes, &mut block_comment_spans);
    tsv_lang::comment_ledger::register_parsed_spans(source, block_comment_spans);
}

/// Collect the spans of every in-block `CssBlockChild::Comment` under the top-level nodes.
#[cfg(feature = "_internal_comment_check")]
fn collect_block_comment_spans(nodes: &[CssNode<'_>], out: &mut Vec<Span>) {
    for node in nodes {
        match node {
            CssNode::Rule(rule) => collect_block_child_comment_spans(rule.declarations, out),
            CssNode::Atrule(atrule) => {
                if let Some(block) = &atrule.block {
                    collect_block_child_comment_spans(block.children, out);
                }
            }
        }
    }
}

/// Recurse a block's children, collecting `Comment` spans and descending into nested rules
/// and at-rule blocks.
#[cfg(feature = "_internal_comment_check")]
fn collect_block_child_comment_spans(children: &[CssBlockChild<'_>], out: &mut Vec<Span>) {
    for child in children {
        match child {
            CssBlockChild::Comment(comment) => out.push(comment.span),
            CssBlockChild::Rule(rule) => collect_block_child_comment_spans(rule.declarations, out),
            CssBlockChild::Atrule(atrule) => {
                if let Some(block) = &atrule.block {
                    collect_block_child_comment_spans(block.children, out);
                }
            }
            CssBlockChild::Declaration(_) => {}
        }
    }
}

/// Format CSS stylesheet to a string
/// Requires source for blank line preservation and raw value extraction
pub(crate) fn format_css(stylesheet: &CssStyleSheet<'_>, source: &str) -> String {
    let arena = DocArena::for_source(source);
    format_css_in(stylesheet, source, &arena)
}

/// Format a CSS stylesheet into a caller-provided doc arena (the reuse path).
pub(crate) fn format_css_in(
    stylesheet: &CssStyleSheet<'_>,
    source: &str,
    arena: &DocArena,
) -> String {
    // The document's line verdict, over the arena-parked table (one warm table across a
    // multi-file driver's files instead of a fresh Vec per file — filled only if a line
    // question falls back to it).
    let line_breaks = LineBreaks::new(source, arena.take_line_breaks_scratch());
    format_stylesheet(stylesheet, source, line_breaks, arena)
}

/// [`format_css_in`] over a document the caller folded ahead of the parse: the line
/// verdict comes from the fold's own pass (`LineBreaks::of_folded`), not a second walk.
pub(crate) fn format_css_folded_in(
    stylesheet: &CssStyleSheet<'_>,
    folded: &FoldedSource<'_>,
    arena: &DocArena,
) -> String {
    let line_breaks = LineBreaks::of_folded(folded, arena.take_line_breaks_scratch());
    format_stylesheet(stylesheet, folded.text(), line_breaks, arena)
}

/// The shared body of the standalone entry points: register the stylesheet's comments,
/// build the printer on its line table, print, park the table, and write a BOM ahead of a
/// leading content U+FEFF (`tsv_lang::printing::encode_leading_zwnbsp`).
fn format_stylesheet(
    stylesheet: &CssStyleSheet<'_>,
    source: &str,
    line_breaks: LineBreaks<'_>,
    arena: &DocArena,
) -> String {
    debug_assert!(
        !source.contains('\r'),
        "a format entry point takes CR-folded text (`tsv_lang::printing::normalize_carriage_returns`)"
    );
    // The print-once comment ledger's expectation for this stylesheet — detached comments
    // plus in-block `CssBlockChild::Comment` AST nodes (diagnostic; see
    // `tsv_lang::comment_ledger`).
    #[cfg(feature = "_internal_comment_check")]
    register_stylesheet_comments(stylesheet, source);

    let mut printer = Printer::new(arena, source, &stylesheet.comments, line_breaks.table());
    printer.print_css_nodes(stylesheet.nodes);
    let output = printer.into_string();
    arena.park_line_breaks_scratch(line_breaks.into_scratch());
    // A content U+FEFF at output byte 0 needs a BOM ahead of it, or the next read strips it.
    printing::encode_leading_zwnbsp(output)
}

/// Embedded CSS formatting into a caller-provided doc arena — the arena-sharing
/// path the Svelte host uses so a `<style>` block reuses the host document's
/// `DocArena` instead of allocating a second whole-host-sized one per block.
///
/// The embedded CSS still renders through its own column-0 render pass to an
/// owned `String`; only the doc-node *storage* is shared. The arena is not
/// reset, so the host's in-flight nodes remain valid, and the CSS's transient
/// nodes are reclaimed at the host driver's next `DocArena::reset()` (between
/// files) like any other island's.
pub(crate) fn format_css_embedded_in(
    stylesheet: &CssStyleSheet<'_>,
    source: &str,
    line_table: LineTable<'_>,
    embed: EmbedContext,
    host_scan: crate::__private::HostBoundaryScan,
    arena: &DocArena,
) -> String {
    // Render at the host's final indentation directly (fold `base_indent_offset`
    // into the starting `indent_level`, zeroing the offset), mirroring how the
    // `<script>` embed renders the TS doc at `start_indent_level=1`. Structural
    // lines get the wrapper indent from `write_indent` / hardlines; verbatim
    // content (raw at-rule preludes, comment interiors, `Invalid` selector text)
    // is written with no indent, so its embedded newlines stay at column 0 — the
    // same as standalone, matching prettier. This retires the Svelte host's
    // post-hoc line re-indenter, which compounded a preserved newline one indent
    // level per format pass (an F1 non-idempotency; see script_style.rs).
    // The print-once comment ledger's expectation for this stylesheet — detached comments
    // plus in-block `CssBlockChild::Comment` AST nodes (diagnostic; see
    // `tsv_lang::comment_ledger`). The Svelte host's `Root.comments` deliberately excludes
    // `<style>` comments, so this is their only registration.
    #[cfg(feature = "_internal_comment_check")]
    register_stylesheet_comments(stylesheet, source);

    let base = embed.base_indent_offset;
    let embed = EmbedContext {
        base_indent_offset: 0,
        ..embed
    };
    let mut printer = Printer::with_embed(
        arena,
        source,
        &stylesheet.comments,
        line_table,
        embed,
        host_scan.holds_boundary_ws,
    );
    printer.indent_level = base;
    printer.print_css_nodes(stylesheet.nodes);
    printer.into_string()
}