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
//! CSS value printing
//!
//! Handles printing of all CSS value types:
//! - Simple values (identifiers, strings, dimensions, colors)
//! - Compound values (lists, functions)
//! - Semantic formatting with source fidelity
//!
//! ## Architecture
//!
//! This module uses a doc-first approach where all formatting logic lives in
//! `build_*_doc()` methods. The `print_*` methods are thin wrappers that call
//! the corresponding doc builder and write the result.
//!
//! The main entry point is `build_css_value_doc()`, which dispatches to
//! specialized doc builders for each value type.

use super::{Printer, value_normalization};
use crate::ast::{Color, CssValue, StringCooked};
use crate::keyword_set::ascii_keyword_set;
use crate::lexer::hyphen_starts_own_token;
use crate::number::sign_starts_number;
use std::borrow::Cow;
use tsv_lang::Span;
use tsv_lang::doc::{DocBuf, arena::DocId};
use tsv_lang::printing::format_string_literal;

/// A value operator's kind, read back off the single byte
/// `parser::value::operators::split_value_run` emitted as a
/// [`CssValue::Operator`].
///
/// The kind is what the separator rule turns on, and it is not stored on the node: the
/// byte IS the kind, so reading it at print time keeps the AST at one field (the
/// span-for-verbatim idiom every other leaf takes).
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum ValueOperator {
    /// `/` — a division. Glued or spaced by its neighbours' kinds.
    Division,
    /// `*` — a multiplication. Always spaced.
    Multiplication,
    /// `+` — an addition. Like `Division`, but reached by a different disjunct.
    Addition,
    /// `-` — a subtraction. Never spaced by tsv where the author glued it.
    Subtraction,
    /// `:` — a key/value colon inside a group. Never spaced *before*; the space after it
    /// is the colon's own, not a separator.
    Colon,
}

impl ValueOperator {
    /// The operator this member is, or `None` when the member is an operand.
    fn of(value: &CssValue<'_>, source: &str) -> Option<Self> {
        let CssValue::Operator { span } = value else {
            return None;
        };
        match span.extract(source).as_bytes() {
            [b'/'] => Some(Self::Division),
            [b'*'] => Some(Self::Multiplication),
            [b'+'] => Some(Self::Addition),
            [b'-'] => Some(Self::Subtraction),
            [b':'] => Some(Self::Colon),
            // Unreachable: the value splitter is the only producer of the node and emits
            // one of the five above. Reading an operand rather than defaulting to a kind
            // keeps the conservative answer (an ordinary member gap) if it ever isn't.
            _ => {
                debug_assert!(
                    false,
                    "a CssValue::Operator over {:?}",
                    span.extract(source)
                );
                None
            }
        }
    }

    /// Is this one of the `<calc-sum>` operators css-values-4 requires whitespace around
    /// — the pair prettier prints exactly as authored inside `calc()`?
    const fn is_sum(self) -> bool {
        matches!(self, Self::Addition | Self::Subtraction)
    }
}

/// Where in the document a value sits, as its separator rule reads it — the part of
/// [`ValueCtx`] that is fixed before the value is reached and does not change as the
/// recursion descends.
///
/// prettier asks both of these of the *path* (`getPropOfDeclNode`, `insideAtRuleNode`),
/// walking up from the node. tsv has no path, so the printer parks the answers for the
/// declaration's duration (`Printer::value_scope`) instead of threading them through
/// every doc builder.
#[derive(Clone, Copy, Default)]
pub(crate) struct ValueScope {
    /// The declaration is `font` or a custom property, whose glued `/` beside a font-size
    /// operand stays glued (prettier's §"Formatting `font` property", `isPossibleFontSize`).
    pub(crate) font_shorthand: bool,
    /// The declaration sits in an `@utility` block, where a word followed by `*` is
    /// glued — Tailwind's `--value(--tab-size-*)`.
    pub(crate) in_utility_at_rule: bool,
    /// The declaration's whole value is one `{…}` block (`--x: { color: red; }`), and
    /// this is its span. css-syntax-3 singles that block out ("a top-level {}-block is
    /// only allowed as the entire value of a non-custom property"), and prettier never
    /// grades it at the value level — it reads the declaration as a nested rule, or
    /// throws — so its own braces keep their authored gaps where a brace *inside* a
    /// value is glued to its interior. `css/values/variables/block_value_*`.
    pub(crate) whole_value_block: Option<Span>,
}

/// What a value's surroundings contribute to its separator rule: its [`ValueScope`] plus
/// the one bit the value recursion itself accumulates.
#[derive(Clone, Copy)]
pub(crate) struct ValueCtx {
    /// Fixed before the value is reached.
    pub(crate) scope: ValueScope,
    /// Some enclosing function is `calc()` — prettier's
    /// `insideValueFunctionNode(path, "calc")`. Its `+` / `-` gaps print as authored,
    /// because css-values-4 makes that gap the difference between a valid expression and
    /// an invalid one, so a formatter must not invent or remove one. ⚠️ `calc()` only:
    /// `min()` / `max()` / `clamp()` take the ordinary rule, as they do in prettier.
    pub(crate) in_calc: bool,
    /// The **immediately** enclosing function is one of prettier's colour adjusters
    /// (`isColorAdjusterFuncNode`), which takes a head sign's rule out inside a group of
    /// its own. ⚠️ Unlike [`Self::in_calc`] this is a PARENT test, not an ancestor
    /// walk: prettier reads `path.grandparent` — the function whose paren group holds this
    /// very comma group — so a nameless group nested inside an adjuster clears it.
    pub(crate) in_color_adjuster: bool,
}

impl ValueCtx {
    /// The context a function's arguments inherit: `calc()` is sticky once entered, where
    /// the adjuster bit belongs to this function alone and is rewritten at every level.
    fn within_function(self, is_calc: bool, is_color_adjuster: bool) -> Self {
        Self {
            in_calc: self.in_calc || is_calc,
            in_color_adjuster: is_color_adjuster,
            ..self
        }
    }
}

ascii_keyword_set! {
    /// prettier's `colorAdjusterFunctions` (`src/language-css/utilities/index.js`),
    /// transcribed — the colour-component functions a head `+` / `-` is an ADJUSTMENT
    /// inside of rather than an operator.
    static COLOR_ADJUSTER_FUNCTIONS;

    /// Is `name` one of prettier's colour-adjuster functions
    /// (`isColorAdjusterFuncNode`, which lowercases the node's value before probing)?
    ///
    /// Asked of the function name **verbatim**, which is what prettier asks too: its
    /// value parser hands back the author's own bytes, so an escape-spelled `\\72 gb(` is
    /// no `rgb` on either side (measured). That is the one place this differs from
    /// `Printer::function_name_is`, whose `url` / `calc` / `var` recognition resolves an
    /// escape — there the name is a grammar keyword, here it is prettier's own table.
    fn is_color_adjuster_function;

    "red",
    "green",
    "blue",
    "alpha",
    "a",
    "rgb",
    "hue",
    "h",
    "saturation",
    "s",
    "lightness",
    "l",
    "whiteness",
    "w",
    "blackness",
    "b",
    "tint",
    "shade",
    "blend",
    "blenda",
    "contrast",
    "hsl",
    "hsla",
    "hwb",
    "hwba",
}

/// Is this member one of prettier's `value-word`s — the kind that makes an adjacent
/// operator take a space?
///
/// A named or hex colour is one: postcss gives `red` and `#fff` a `value-word` node and
/// only flags them, where tsv folds the flag into the node kind. An `rgb()` / `hsl()`
/// colour is a **function** there, so it answers [`is_func_member`] instead.
fn is_word_member(value: &CssValue<'_>) -> bool {
    matches!(
        value,
        CssValue::Identifier { .. }
            | CssValue::Color {
                color: Color::Named | Color::Hex,
                ..
            }
    )
}

/// Is this member one of prettier's `value-number`s, or a hex colour — the two the
/// colour-adjuster gate accepts after its head sign?
///
/// prettier asks `iNextNode.type === "value-number" || iNextNode.isHex`. A unit or a `%`
/// rides the number node there (`20%`, `5px` are both `value-number`, measured), which is
/// exactly what tsv's [`CssValue::Dimension`] is, and the `isHex` flag postcss puts on a
/// word is the [`Color::Hex`] tsv folds into the node kind.
fn is_number_or_hex_member(value: &CssValue<'_>) -> bool {
    matches!(
        value,
        CssValue::Dimension { .. }
            | CssValue::Color {
                color: Color::Hex,
                ..
            }
    )
}

/// Is this member one of prettier's `value-func`s?
///
/// ⚠️ A **nameless** group is not: postcss gives `(1.5)` a `value-paren_group`, a node
/// neither this nor [`is_word_member`] claims, and the whole of the operator-before-group
/// rule turns on that. A colour *function* is one, tsv having folded it into `Color`.
fn is_func_member(value: &CssValue<'_>, source: &str) -> bool {
    match value {
        CssValue::Function { name_span, .. } => !name_span.extract(source).is_empty(),
        CssValue::Color {
            color: Color::Rgb { .. } | Color::Hsl { .. },
            ..
        } => true,
        CssValue::SupportsCondition { .. } => true,
        _ => false,
    }
}

/// Could this member stand where a `font` shorthand's font-size does — prettier's
/// `isPossibleFontSize`, the operand its `/` carve-out keys on?
fn is_possible_font_size(value: &CssValue<'_>, source: &str) -> bool {
    match value {
        CssValue::Dimension { .. } => true,
        CssValue::Function { name_span, .. } => {
            let name = name_span.extract(source);
            ["var", "calc", "min", "max", "clamp"]
                .iter()
                .any(|kw| name.eq_ignore_ascii_case(kw))
                || name.starts_with("--")
        }
        _ => false,
    }
}

/// Is this member one no arm of [`Printer::value_gap_is_glued`] reads as its own
/// trigger — not an operator, and not a one-byte word, which is what the lone `@` and
/// the `{` / `}` members are ([`is_word_spelled`])? Any other one-byte identifier
/// (`a`) answers `false` too and simply takes the full rule.
fn is_plain_operand(value: &CssValue<'_>) -> bool {
    match value {
        CssValue::Operator { .. } => false,
        CssValue::Identifier { span } => span.end - span.start != 1,
        _ => true,
    }
}

/// Is this member a word spelled exactly `text` — the `{`, `}` and lone `@` members
/// the run splitter emits, which postcss also carries as `value-word` nodes keyed on
/// their text (`isLeftCurlyBraceNode`, `isRightCurlyBraceNode`, the empty `atword`)?
fn is_word_spelled(value: &CssValue<'_>, source: &str, text: &str) -> bool {
    matches!(value, CssValue::Identifier { span } if span.extract(source) == text)
}

/// Would gluing a head operator to the member after it change what the pair LEXES AS —
/// the refusal tsv puts on prettier's head rule?
///
/// The head rule is one sentence: an operator with nothing before it binds to the member
/// after it. Prettier's version reads the *node* beside the operator and never asks what
/// the two spell together, so it welds `- a` into `-a` and `+ 2.5` into `+2.5` — a
/// different token stream, not a different spacing, and in a custom property's value the
/// token sequence IS the value (css-variables-1 §"Custom Property Value Syntax"). tsv
/// keeps the gap wherever this answers `true`, which is a deliberate, cataloged
/// divergence (`css/values/operators/operator_head_weld_prettier_divergence`); where the
/// glue is lossless tsv glues exactly as prettier does
/// (`css/values/operators/operator_head_glue`).
///
/// A `*` is out of the head rule entirely, and a `/` is a `<delim-token>` beside
/// everything **but a `*`** — the one lexical merge a `/` can make, and the sharpest of
/// them all: css-syntax-3 §4.3.1 consumes comments (§4.3.2) ahead of every token, so the
/// glued pair is not two tokens at all but the opener of a comment that never closes.
/// Neither parser re-reads the result — Svelte's `parseCss` raises `Expected token */`
/// and tsv's own `Unterminated comment` — which is prettier's answer for the same input
/// and a divergence tsv takes rather than emit output it cannot itself parse
/// (`css/values/operators/slash_before_star_prettier_divergence`). The question is
/// asked of the member's own source text, the same document reading [`authored_glued`]
/// takes.
///
/// The `-` is answered by the lexer's [`hyphen_starts_own_token`] — css-syntax-3 §4.3.9's
/// ident and §4.3.10's number, the one reading the value-run splitter takes for the same
/// `-` at an operand position, so the parser and the printer cannot part on it. A `+`
/// opens no ident of its own, so only the number half can take it
/// ([`sign_starts_number`]).
fn operator_glue_merges(op: ValueOperator, next: &str) -> bool {
    match op {
        ValueOperator::Division => next.starts_with('*'),
        ValueOperator::Addition => sign_starts_number(b'+', next),
        ValueOperator::Subtraction => hyphen_starts_own_token(next),
        _ => false,
    }
}

/// Does the source hold nothing at all between these two members?
///
/// prettier's `hasEmptyRawBefore`, read off the document rather than off a `raws` field —
/// which is the whole of tsv's divergence at a parenthesized group, whose node prettier
/// synthesizes without one and therefore can only ever read as "not glued". See
/// [conformance_prettier_css.md §CSS: Values](../../../../docs/conformance_prettier_css.md#css-values).
fn authored_glued(prev: &CssValue<'_>, next: &CssValue<'_>) -> bool {
    prev.span().end == next.span().start
}

impl<'a> Printer<'a> {
    /// Format a CSS value
    ///
    /// Uses the doc builder which handles source fidelity and proper formatting.
    ///
    /// A value whose doc is one source-span text — a verbatim identifier, an operator, an
    /// already-canonical dimension or colour, which is most declaration values — is its
    /// own source slice at any column, so it is appended directly instead of rendered:
    /// the render entry would measure the column, borrow the scratch and the work
    /// buffers, and copy the same bytes out. The audit seams the renderer hosts lose
    /// nothing by it: this printer builds no line-comment text and tags no comment doc
    /// (its comments are recorded where they are written), so a rendered value text only
    /// ever reached the swallow check as content after a `//` this printer cannot emit.
    pub(super) fn print_css_value(&mut self, value: &CssValue<'_>) {
        let doc = self.build_css_value_doc(value);
        if let Some(span) = self.arena.as_source_span_text(doc) {
            let source = self.source;
            #[cfg(debug_assertions)]
            {
                let mut rendered = String::new();
                tsv_lang::doc::arena_print_css_doc_into(
                    self.arena,
                    doc,
                    &self.embed,
                    self.current_column(),
                    self.indent_level,
                    source,
                    &mut rendered,
                );
                assert_eq!(
                    rendered,
                    span.extract(source),
                    "a lone source-span value rendered as something other than its slice"
                );
            }
            self.write(span.extract(source));
            return;
        }
        self.write_arena_doc(doc);
    }

    /// Format a nested value (function arg, list item)
    ///
    /// Alias for `print_css_value` - kept for semantic clarity in call sites.
    #[inline]
    pub(super) fn print_nested_value(&mut self, value: &CssValue<'_>) {
        self.print_css_value(value);
    }

    /// Emit a value leaf that prints as its own source slice, for a caller that has
    /// already PROVED the slice holds no byte the width depends on — no `\t`, no `\n`,
    /// nothing at or above `0x80` — so its width is its byte length and
    /// `DocArena::source_span_plain` need not measure it. Every caller knows it from a
    /// fact it already had: a test it ran for its own reasons, or the leaf's grammar.
    ///
    /// ⚠️ **A wrong claim is a SILENT width error** — nothing downstream re-derives the
    /// width, so it moves a fits verdict and changes no other observable. Every claim is
    /// therefore graded against the slice's own bytes in every debug build.
    fn plain_span_doc(&self, span: Span) -> DocId {
        debug_assert!(
            !span
                .extract(self.source)
                .bytes()
                .any(tsv_lang::printing::is_width_relevant),
            "a CSS leaf claimed plain holds a width-relevant byte: {:?}",
            span.extract(self.source)
        );
        self.d().source_span_plain(span)
    }

    //
    // Doc Builders - all formatting logic expressed as doc IR
    //

    /// Build a doc for a CSS value
    ///
    /// Main entry point for value formatting. Dispatches to specialized doc
    /// builders for each value type. Handles source fidelity by extracting
    /// from source where appropriate.
    pub(super) fn build_css_value_doc(&self, value: &CssValue<'_>) -> DocId {
        self.build_css_value_doc_in(value, self.value_ctx())
    }

    /// The value context the printer's own state supplies: the two declaration-scoped
    /// bits, with `in_calc` left for the recursion to set where it enters one.
    pub(super) fn value_ctx(&self) -> ValueCtx {
        ValueCtx {
            scope: self.value_scope,
            in_calc: false,
            in_color_adjuster: false,
        }
    }

    /// [`Self::build_css_value_doc`] carrying the surroundings its separator rule reads
    /// ([`ValueCtx`]) — an enclosing `calc()` and the `font`/custom-property carve-out.
    /// Neither is a property of the value, so both ride down the recursion rather than
    /// being re-derived at each level.
    pub(super) fn build_css_value_doc_in(&self, value: &CssValue<'_>, ctx: ValueCtx) -> DocId {
        match value {
            CssValue::Identifier { span } => self.build_identifier_doc(*span),
            // The operator's single byte, verbatim. Whether a space stands beside it is
            // the enclosing list's question, never this node's (`value_gap_is_glued`).
            // The byte is one of `ValueOperator`'s ASCII spellings, so its width is 1.
            CssValue::Operator { span } => self.plain_span_doc(*span),
            CssValue::String { content, span } => self.build_string_doc(content, *span),
            CssValue::Dimension { span } => self.build_dimension_doc(*span),
            CssValue::Color { color, span } => self.build_color_doc(color, *span),
            CssValue::Function {
                name_span,
                args,
                span,
            } => self.build_value_function_doc(*name_span, args, *span, ctx),
            CssValue::SupportsCondition {
                name, condition, ..
            } => self.build_supports_condition_doc(name, condition),
            CssValue::List { values, .. } => self.build_value_list_doc(values, ctx),
            CssValue::CommaSeparated { values, span } => {
                let joined = self.build_separated_values_doc(values, ", ");
                // Joining N elements writes N-1 commas, so an authored closing comma —
                // one that terminated no element — is spelled back here.
                if super::declarations::list_has_closing_comma(
                    self.source,
                    values,
                    span.end_usize(),
                ) {
                    let d = self.d();
                    d.concat(&[joined, d.text(",")])
                } else {
                    joined
                }
            }
        }
    }

    /// Build a doc for an identifier value
    ///
    /// The ordinary leaf printer — everything the classifiers did not claim as a string,
    /// dimension, colour or function (`red`, `flex`, `100%`, 98.6% of identifier values on
    /// a real corpus). The text is source-extracted so escapes survive verbatim, with
    /// whitespace normalization for the interior runs of a token the value parser left
    /// opaque: a comment run, or a paren shell whose first `(` does not close at the value's
    /// end (`(a b c)(d)`). A *balanced* parenthesized group is not one of those — it parses
    /// as a nameless `CssValue::Function` (css-syntax-3's `()`-block) and prints through
    /// `build_value_function_doc`, so its members reach the ordinary value printers and its
    /// layout is the function group's.
    fn build_identifier_doc(&self, span: Span) -> DocId {
        let d = self.d();
        // The identifier text is recovered from source (escapes preserved verbatim).
        if span.end_usize() <= self.source.len() {
            let raw = span.extract(self.source);
            if !raw.is_empty() {
                // The verbatim case first, asked of the document's word: a value holding no
                // byte the normalizer acts on — the overwhelmingly-common single-token
                // identifier (`red`, `flex`, `100%`), 98.6% of identifier values on a real
                // corpus — is emitted as a zero-allocation `DocText::SourceSpan` (the same
                // source borrow the number / dimension normalizers take) without the
                // normalizer's own byte-at-a-time scan-skip test over the bare slice.
                //
                // The same test is the width proof: a no-op byte is ASCII and none of `\t` /
                // `\n`, so the slice is one column a byte and is not measured again.
                if value_normalization::normalize_is_noop_in(self.source, span) {
                    return self.plain_span_doc(span);
                }
                // Normalize whitespace for parenthesized expressions
                // (e.g., "(  100%  -  40px  )" → "(100% - 40px)").
                let normalized = value_normalization::normalize_css_whitespace(raw);

                return match normalized {
                    // Verbatim value the host-word test refused (it holds a control byte the
                    // normalizer keeps, `<VT>` and its kin): normalized == source[span], so
                    // the same zero-allocation `DocText::SourceSpan`.
                    Cow::Borrowed(_) => d.source_span(span, self.source),
                    Cow::Owned(s) => d.text_pooled(&s),
                };
            }
        }
        // Empty / whitespace-only span (the empty-identifier sentinel) or an
        // out-of-range span (never in practice — spans index the printer's source):
        // nothing to emit.
        d.text("")
    }

    /// Build a doc for a string value
    ///
    /// Source-extracts the raw string so escape sequences are preserved verbatim
    /// (`\a`, `\41`, `\\`, line continuations), normalizing only the quote char
    /// (`"` → `'`) to match prettier. Re-serializing the *decoded* `content` would
    /// corrupt escapes — e.g. emit `\a` as a literal newline (content loss). Mirrors
    /// `build_identifier_doc` and the plain-declaration-value path
    /// (`extract_string_value`); the decoded `content` is only the fallback when the
    /// span is unavailable.
    fn build_string_doc(&self, content: &StringCooked<'_>, span: Span) -> DocId {
        if span.end_usize() <= self.source.len() {
            let raw = span.extract(self.source);
            // The span covers the full literal including quotes (see
            // `parse_string_literal`); strip them and re-emit with quote normalization.
            // The original quote is the first byte of the span (recovered from source,
            // not stored).
            if raw.len() >= 2 && (raw.starts_with('\'') || raw.starts_with('"')) {
                let quote = raw.as_bytes()[0] as char;
                let inner = &raw[1..raw.len() - 1];
                return self.d().text_pooled(&format_string_literal(inner, quote));
            }
        }
        // Fallback: span unavailable (never in practice — spans index the printer's
        // source). Re-emit the decoded content; a `Verbatim` value has no recoverable
        // text without its span, so emit nothing (mirrors `build_identifier_doc`).
        match content {
            StringCooked::Decoded(s) => self.d().text_pooled(&format_string_literal(s, '\'')),
            StringCooked::Verbatim => self.d().text(""),
        }
    }

    /// Build a doc for a dimension value (number + unit)
    ///
    /// Normalizes trailing zeros and adds leading zeros, preserving source
    /// characteristics like leading zeros and signs. An already-canonical
    /// dimension (`10px`, `0.5rem`) borrows its source slice, so it emits a
    /// zero-allocation `source_span`; only a rewritten dimension allocates.
    /// Mirrors the TS literal path (`Printer::build_number_literal_doc`).
    ///
    /// The number half is ASCII by grammar, but the unit is an identifier, and an
    /// identifier can carry a non-ASCII code point or a hex escape's `\t` / `\n`
    /// terminator — so the slice's width is asked of its bytes here, a few of them
    /// (`px`, `rem`, `%`), in line rather than through `DocArena::source_span`'s
    /// outlined measure.
    fn build_dimension_doc(&self, span: Span) -> DocId {
        let raw = span.extract(self.source);
        match value_normalization::normalize_dimension_from_source(raw) {
            Cow::Borrowed(_) => {
                if raw.bytes().any(tsv_lang::printing::is_width_relevant) {
                    self.d().source_span(span, self.source)
                } else {
                    self.plain_span_doc(span)
                }
            }
            Cow::Owned(s) => self.d().text_pooled(&s),
        }
    }

    /// Build a doc for a color value
    ///
    /// Preserves color syntax (hex, rgb, hsl, etc.) from source.
    ///
    /// Kept out of line on purpose: inlined, the colour-function reconstruction lands in
    /// [`Self::build_css_value_doc_in`]'s frame, which the value recursion re-enters at
    /// every nested function (`calc(` inside `calc(`), and that frame is measurably
    /// larger for it — for a leaf most values never reach.
    #[inline(never)]
    fn build_color_doc(&self, color: &Color, span: Span) -> DocId {
        // A verbatim named color, and a hex color already in lowercase, come back
        // `Cow::Borrowed` (== source[span]) and are emitted as a zero-allocation
        // `DocText::SourceSpan`, like the identifier / dimension paths; the function
        // syntaxes own their reconstructed text. Both borrowed forms are ASCII by grammar —
        // a named color is a run of letters (`is_named_color`), a hex color `#` and hex
        // digits — so the slice is one column a byte and is not measured.
        match value_normalization::format_color_from_source(color, self.source, span) {
            Cow::Borrowed(_) => self.plain_span_doc(span),
            Cow::Owned(s) => self.d().text_pooled(&s),
        }
    }

    /// Build a flat (non-wrapping) `name(args_doc)` function doc.
    ///
    /// The shared `name(` … `)` envelope for the `url()` and `var(--a,)`
    /// empty-fallback paths, which are kept flat by design (opaque / no break
    /// points). Every other function goes through the wrapping path
    /// (`build_value_function_doc`'s `group(…softline…)` structure) so it can
    /// break when it exceeds width.
    fn flat_function_doc(&self, name_span: Span, args_doc: DocId) -> DocId {
        let d = self.d();
        d.concat(&[
            d.source_span(name_span, self.source),
            d.text("("),
            args_doc,
            d.text(")"),
        ])
    }

    /// Build a doc for a function value with automatic wrapping
    ///
    /// Uses proper doc structure with group/softline/indent so the renderer
    /// decides wrapping based on actual line position (like Prettier). Every
    /// function gets break points (a softline after `(` and before `)`), so a
    /// single over-width arg wraps onto its own line just like a multi-arg list
    /// — matching prettier's `parenthesized-value-group`.
    ///
    /// - Multi-arg functions: wrap each arg on its own line when exceeds width
    /// - Single-arg List (e.g., drop-shadow): wrap on space separators
    /// - Single-arg non-List (e.g., `fn(token)`): wrap the arg onto its own line
    ///   when it exceeds width
    /// - `url()` and the `var(--a,)` empty fallback: kept flat — handled before
    ///   this point
    /// - a blank argument region: closes flush (`f(  )` → `f()`)
    ///
    /// A **nameless** `name_span` is the parenthesized group (css-syntax-3's `()`-block,
    /// `value/mod.rs::extract_function_parts`), and it takes every rule above unchanged —
    /// the name simply emits nothing. `url` / `var` recognition refuses it on length, so no
    /// arm needs a clause for it.
    pub(super) fn build_value_function_doc(
        &self,
        name_span: Span,
        args: &[CssValue<'_>],
        span: Span,
        ctx: ValueCtx,
    ) -> DocId {
        let d = self.d();
        // `calc()` is sticky down the whole subtree, which is prettier's
        // `insideValueFunctionNode(path, "calc")` — an ancestor walk, not a parent test.
        let ctx = ctx.within_function(
            self.function_name_is(name_span, "calc"),
            is_color_adjuster_function(name_span.extract(self.source)),
        );
        // `url` is opaque whether or not its content was parsed, so it answers first and
        // in one place — the prelude path leaves `@import url(a.css)` unparsed (empty
        // args) while a declaration value parses them, and both want the same verbatim
        // form. Matched ASCII-case-insensitively (css-syntax): `URL(…)` is a url too, so
        // it takes the same path (casing preserved via `span`) rather than generic-function
        // normalization — which would space an interior `/*` in the now-lexed `URL(x/*y)`
        // url-token.
        if self.function_name_is(name_span, "url") {
            // Quoted url() — a single string arg. Print it through the normal string
            // path so the quote is normalized (`"x"` → `'x'`), matching prettier.
            if let [arg @ CssValue::String { .. }] = args {
                return self.flat_function_doc(name_span, self.build_css_value_doc(arg));
            }
            // The same string plus a trailing comment region (`url('a.css' /* c */)`) —
            // still the string path, so the quote still normalizes; the region joins
            // single-spaced, the crate's uniform comment-spacing rule (prettier freezes the
            // authored spacing — conformance_prettier_css.md §CSS: Comments). Kept separate
            // from the arm above rather than folded into it: `join` mints a separator
            // `text` node the single-arg case would never spend, and that case is every
            // `url()` in a stylesheet. A *leading* comment cannot reach here at all — it
            // makes `url(` an opaque `<url-token>`, which lands on the verbatim arm below.
            if let [CssValue::String { .. }, ..] = args {
                return self
                    .flat_function_doc(name_span, self.build_separated_values_doc(args, " "));
            }
            // Unquoted url() — the content is opaque. Emit the raw source verbatim,
            // stripping only the whitespace right after `url(` and right before `)`
            // (prettier's `printer-postcss.js` url handling). Rejoining parsed args
            // would drop empty/trailing comma segments (`url(a,b,)` → `url(a,b)`),
            // silently changing the URL — the comma is part of the resource ref — so
            // raw text wins over the args even when it isn't parenthesized at all (which
            // a parsed function's span cannot be; the arm is defensive).
            //
            // Deliberately pooled, not `source_span`: this arm is hot on CSS corpora
            // (every `url(...)`) and the span form's render-time resolution hop measured
            // +0.07% instructions there for no allocation win (the pool is amortized).
            // TODO: re-measure that verdict — it predates the render's inlined `resolve_text`,
            // and TWO span moves have since read no hop at all: the selector leaf
            // (`span_leaf_doc`, ~12-byte slices, −0.34%) and this function's own name, whose
            // `name_span` retired both a copy and the guard that had made the site a wash.
            // A url is longer than either, so the copy it spends is larger. Only the
            // `None` arm here needs the pool; `trim_url_raw`'s `Borrowed` arm is the
            // document's own bytes and would take the span form directly.
            if span.end_usize() <= self.source.len() {
                let raw = span.extract(self.source);
                return match crate::url::trim_url_raw(raw) {
                    Some(trimmed) => d.text_pooled(&trimmed),
                    None => d.text_pooled(raw),
                };
            }
            // Fallback (span unavailable): rejoin args with no space after commas.
            let args_doc = d.join(args.iter().map(|arg| self.build_css_value_doc(arg)), ",");
            return self.flat_function_doc(name_span, args_doc);
        }

        // No argument came out of the region between the parens, which happens two ways.
        //
        // The region held nothing but whitespace — `parse_function_arguments` trims it away
        // with the same `trim_start_css` / `trim_end_preserving_escape` pair `escapes::trim_css`
        // composes, so the parser's verdict and the test below cannot disagree — and there is
        // then nothing to print, so the call closes flush: `f(  )` → `f()`,
        // `layer(  )` → `layer()`, and the nameless group `(  )` → `()`. That is the same
        // gap collapse every other value interior takes, and it is one rule for all three
        // (before the group parsed, its collapse came from the identifier path's whitespace
        // normalizer instead).
        //
        // Or the region was never parsed at all: an `@import` prelude consumes an unknown
        // function's argument opaquely (`scope((.a) to (.b))` — `preludes::consume_function_args`),
        // leaving real content behind no argument. That text is the only record of it, so it
        // stays verbatim.
        if args.is_empty() && span.end_usize() <= self.source.len() {
            let raw = span.extract(self.source);
            if let Some(interior) = function_paren_interior(raw, name_span, span)
                && crate::escapes::trim_css(interior).is_empty()
            {
                return self.flat_function_doc(name_span, d.text(""));
            }
            return d.text_pooled(raw);
        }

        // A comma **closing** the argument list (`rgb(1, 2, 3,)`, `var(--a,)`,
        // `linear-gradient(red, ,)`) terminated no argument, so joining the args would
        // drop it. `extract_function_parts` requires the closing paren to be the value's
        // last byte, which is what bounds the list at `span.end - 1`.
        let closing_comma = super::declarations::list_has_closing_comma(
            self.source,
            args,
            span.end_usize().saturating_sub(1),
        );

        // var()'s empty fallback (`var(--a,)`, `var(--a, ,)`) is kept flat: the generic
        // path below spells the same closing comma, but wraps the argument list in a
        // breakable group, and prettier never breaks a `var()`. `var(--a, red)` has no
        // closing comma and takes the generic path with the normal `, ` separator.
        if closing_comma && self.function_name_is(name_span, "var") {
            let args_doc = d.join(args.iter().map(|arg| self.build_css_value_doc(arg)), ", ");
            let comma = d.text(",");
            return self.flat_function_doc(name_span, d.concat(&[args_doc, comma]));
        }

        // Build with group/softline structure for automatic wrapping
        // Structure: name(
        //   arg1,
        //   arg2,
        //   arg3
        // )
        // When flat: name(arg1, arg2, arg3)
        let mut inner_parts = DocBuf::new();
        for (i, arg) in args.iter().enumerate() {
            // For List args (space-separated values like calc math expressions),
            // use fill with line() separators so content can break at operators.
            // Matches prettier's group(indent(fill(parts))) pattern.
            if let CssValue::List { values, .. } = arg {
                inner_parts.push(self.build_space_fill_value_doc(values, ctx));
            } else {
                inner_parts.push(self.build_css_value_doc_in(arg, ctx));
            }
            if i < args.len() - 1 {
                inner_parts.push(d.text(","));
                inner_parts.push(d.line()); // space when flat, newline when broken
            }
        }
        // The closing comma the args' own separators can't spell: `red, ` is a ONE-argument
        // list, so `linear-gradient(red,,)` would lose its empty argument — and with it the
        // reason the UA drops the declaration — while `rgb(1, 2, 3,)` would lose the comma
        // that makes it invalid in the first place. `var()` takes its own flat form above.
        // The comma joins the last argument rather than standing as its own part, so the
        // group's break can never strand it on a line of its own.
        if closing_comma && let Some(last) = inner_parts.pop() {
            inner_parts.push(d.concat(&[last, d.text(",")]));
        }

        let name_doc = d.source_span(name_span, self.source);
        let inner = d.concat(&inner_parts);
        d.group(d.concat(&[
            name_doc,
            d.text("("),
            d.indent(d.concat(&[d.softline(), inner])),
            d.softline(),
            d.text(")"),
        ]))
    }

    /// Build a doc for space-separated values inside a function argument
    ///
    /// Uses fill with line() separators so the renderer can break at space boundaries
    /// when content exceeds print width. Wrapped in group(indent(fill(...))) to match
    /// prettier's CSS value group pattern — continuation lines get extra indent.
    ///
    /// Example: `calc(0.5 * (100vw - var(--a)))` breaks as:
    /// ```text
    /// calc(
    ///   0.5 *
    ///     (100vw - var(--a))
    /// )
    /// ```
    pub(super) fn build_space_fill_value_doc(
        &self,
        values: &[CssValue<'_>],
        ctx: ValueCtx,
    ) -> DocId {
        let d = self.d();
        let parts = self.build_space_fill_parts(values, ctx);
        d.group(d.indent(d.fill(&parts)))
    }

    /// Is the function name at `name_span` — read **verbatim** from the source — the
    /// keyword `kw`?
    ///
    /// Function names are ASCII case-insensitive ("like keywords, function names are
    /// ASCII case-insensitive" — css-values-4 §"Functional Notations"), and a name may be
    /// escape-spelled: `\75 rl(` is a `url()`. The value subtree stores the author's own
    /// bytes (`CssValue::Function::name_span`), so recognition — not emission — is where
    /// an escape is resolved.
    ///
    /// Two byte tests answer every name a stylesheet really holds, which is what keeps
    /// the escape walk off this path (this question is asked of every function value):
    ///
    /// - **An escape spends at least two bytes on the one character it spells**, so a
    ///   decoded spelling of `kw` is always LONGER than `kw` — an exact-length name is
    ///   answered by the bytes alone, and 82% of names are three bytes.
    /// - **A longer name's first decoded character is either its first byte verbatim or
    ///   an escape's `\`**, so a first byte that is neither `\` nor `kw`'s own opening
    ///   character cannot spell `kw` however it decodes — `calc(` refuses `url` on one
    ///   compare.
    ///
    /// What survives both is a LONGER name whose first byte could still begin `kw`, which
    /// since the value classifier reads postcss's word rather than the spec's ident sequence
    /// (`parser::value::is_function_name`) is two things rather than one: an escape-spelled
    /// name, from an `@import` prelude or from that classifier, and a name carrying the
    /// punctuation a word admits (`u/rl`, `url/`). Only the first can match, and the walk
    /// below is where that is settled — it requires a `\` in the name before it decodes
    /// anything, so a punctuation-bearing name refuses on one `memchr` and `url/(a.png)`
    /// stays the generic function prettier also prints. No corpus holds either.
    #[inline]
    fn function_name_is(&self, name_span: Span, kw: &str) -> bool {
        let name = &self.source.as_bytes()[name_span.range()];
        let kw = kw.as_bytes();
        if name.len() == kw.len() {
            return name.eq_ignore_ascii_case(kw);
        }
        name.len() > kw.len()
            && (name[0].eq_ignore_ascii_case(&kw[0]) || name[0] == b'\\')
            && self.escaped_function_name_is(name_span, kw)
    }

    /// The escaped tail of `function_name_is` — outlined and cold, so the escape walk it
    /// runs stays off every function value's path. The walk compares the name as it
    /// decodes (`escapes::decodes_to_ascii_ignore_case`) rather than decoding it into a
    /// `String` first: the same verdict, with nothing allocated on a path no corpus reaches.
    #[cold]
    #[inline(never)]
    fn escaped_function_name_is(&self, name_span: Span, kw: &[u8]) -> bool {
        let name = name_span.extract(self.source);
        name.as_bytes().contains(&b'\\') && crate::escapes::decodes_to_ascii_ignore_case(name, kw)
    }

    /// The members of a space-separated value list, each gap decided on its own, with
    /// `separator` standing in every gap that takes one.
    ///
    /// **One walk for both shapes**: a flat list spends a `" "` and a wrapping fill spends
    /// a `line`, and that is the whole of the difference. Spelled twice, the two would
    /// drift on which gaps exist — the hazard the call-argument printers already paid for
    /// — and a gap that is a separator in one and glue in the other is a formatter that
    /// contradicts itself across the print-width boundary.
    ///
    /// The parts ALTERNATE — an item, a separator, an item — which is what a fill reads by
    /// position, so a glued gap may not simply contribute nothing: its members would shift
    /// every part behind them across that parity, a member measured as a separator and a
    /// `line` as content, and where the value wraps would then depend on how many members
    /// the run before it holds (`100%- var(…) * 2` kept an over-wide `var(…)` flat that
    /// `100% - var(…) * 2` breaks). The members a glued gap joins fold into ONE item
    /// instead ([`Self::glued_run_doc`]), so a glued run travels whole whatever it holds —
    /// plain operands (`1px+2px`, `12px/1.5`), a sign on its group, alone or behind
    /// another operator (`-(…)`, `1px+-(…)`), or a function (`100%-var(…)`) — and a
    /// function or group in it that no line holds breaks inside its own parens, glued to
    /// the members before it.
    ///
    /// No glued gap is a wrap point, because a break in one changes what the run IS
    /// rather than how it is spaced: `1px+2px` is a dimension and a signed dimension;
    /// `1px-2px` is a single dimension whose unit is `px-2px`, split by whitespace on
    /// either side of the `-`; `fn(a)-webkit-x` holds a `-` an ident takes as its own
    /// first code point once whitespace stands ahead of it; `-var(` is a single
    /// `<function-token>` (css-syntax-3 §"Consume an ident-like token"), so `100%-var(…)`
    /// holds no `var()` function until a break makes one — a declaration a browser drops
    /// at parse time becoming one it accepts; and a sign parted from its group is a bare
    /// operator ending a line, which reads back as a different run on the next pass.
    fn build_value_member_parts(
        &self,
        values: &[CssValue<'_>],
        ctx: ValueCtx,
        separator: DocId,
    ) -> DocBuf {
        let mut parts = DocBuf::with_capacity(values.len() * 2);
        // Where the item being built opens in `parts`.
        let mut item_start = 0;
        for (i, value) in values.iter().enumerate() {
            parts.push(self.build_css_value_doc_in(value, ctx));
            let last = i + 1 == values.len();
            let glued = !last && self.value_gap_is_glued(values, i, ctx);
            if glued {
                continue;
            }
            if parts.len() - item_start > 1 {
                let item = self.glued_run_doc(&parts[item_start..]);
                parts.truncate(item_start);
                parts.push(item);
            }
            if !last {
                parts.push(separator);
            }
            item_start = parts.len();
        }
        parts
    }

    /// The one item a glued run's member docs make.
    ///
    /// A group, so the run measures as a whole wherever it is rendered: when it does not
    /// fit, its members are laid out in break mode, and each function or parenthesized
    /// group in it then stays flat when the run fits up to the next one's opening paren —
    /// the LAST one that has to break is the one that does (`var(--a)-fn(⏎…⏎)`). Laid out
    /// flat-and-remeasured instead, as a fill renders its final item, the first group
    /// would measure the whole rest of the run and break for it.
    pub(super) fn glued_run_doc(&self, members: &[DocId]) -> DocId {
        let d = self.d();
        d.group(d.concat(members))
    }

    /// Build a doc for a space-separated value list — the **flat** spelling of
    /// [`Self::build_value_member_parts`].
    pub(super) fn build_value_list_doc(&self, values: &[CssValue<'_>], ctx: ValueCtx) -> DocId {
        let d = self.d();
        let parts = self.build_value_member_parts(values, ctx, d.text(" "));
        d.concat(&parts)
    }

    /// The same members as `line`-separated fill parts — the **wrapping** spelling.
    ///
    /// `[val1, line, val2, line, val3]`, suitable for `DocArena::fill`, used by both
    /// declaration wrapping and function-argument wrapping.
    pub(super) fn build_space_fill_parts(&self, values: &[CssValue<'_>], ctx: ValueCtx) -> DocBuf {
        let d = self.d();
        self.build_value_member_parts(values, ctx, d.line())
    }

    /// Does the gap between members `i` and `i + 1` take **no** separator?
    ///
    /// prettier's `printCommaSeparatedValueGroup` loop, transcribed for the `css` parser:
    /// every arm below is one of its `continue`s, in its order, and anything that reaches
    /// the end takes the default `line`. Its SCSS/Less arms (interpolation, at-words,
    /// control directives, `~`, property lookups) are dropped — none of those nodes can
    /// exist here — as is the `url()` arm, whose interior tsv keeps opaque, and the
    /// `grid` arm, which tsv answers with its own row plan (`grid_multirow_plan`).
    ///
    /// ⚠️ Two deliberate divergences. One is in [`authored_glued`]: prettier asks
    /// `hasEmptyRawBefore`, a field its parser never sets on a synthesized
    /// `value-paren_group`, so an operator before a group can only ever read as
    /// un-glued there. tsv asks the document instead, so `1.5/(2.5)` keeps the glue
    /// `(1.5)/2.5` already keeps. The other is [`operator_glue_merges`], the refusal on
    /// the sign arm's head glue: prettier's arm reads the node beside the operator and
    /// never asks what the two spell together, so it welds `- a` into the single
    /// `<ident -a>`. Both are cataloged in `docs/conformance_prettier_css.md`
    /// §CSS: Values.
    ///
    /// Every arm that glues reads an operator on one side of the gap, or a word spelled
    /// `@`, `{` or `}` there — so a gap between two members that are neither
    /// ([`is_plain_operand`]) takes the separator, and is answered before any of the
    /// arms' source reads. That is most gaps: a value list is mostly operands.
    pub(super) fn value_gap_is_glued(
        &self,
        values: &[CssValue<'_>],
        i: usize,
        ctx: ValueCtx,
    ) -> bool {
        if let Some(next) = values.get(i + 1)
            && is_plain_operand(&values[i])
            && is_plain_operand(next)
        {
            debug_assert!(
                !self.value_gap_is_glued_by_rule(values, i, ctx),
                "a gap between two plain operands was glued by an arm"
            );
            return false;
        }
        self.value_gap_is_glued_by_rule(values, i, ctx)
    }

    /// [`Self::value_gap_is_glued`]'s arms, in prettier's order — the whole rule, which
    /// the gate in front of it answers for a gap no arm can glue.
    fn value_gap_is_glued_by_rule(&self, values: &[CssValue<'_>], i: usize, ctx: ValueCtx) -> bool {
        let source = self.source;
        let current = &values[i];
        let Some(next) = values.get(i + 1) else {
            return true;
        };
        let previous = i.checked_sub(1).map(|p| &values[p]);
        let next_next = values.get(i + 2);

        let current_op = ValueOperator::of(current, source);
        let next_op = ValueOperator::of(next, source);

        // "Ignore colon": the loop contributes no separator on either side of a `:`, and
        // prettier's `value-colon` node then prints `[":", line]` — so the space belongs
        // to the colon, not to the gap. ⚠️ The two arms are ORDERED: read the other way
        // round, a `:` followed by a `:` (`f(a::b)` → `f(a: : b)`) would lose the first
        // one's space to the second one's "nothing before a colon".
        if current_op == Some(ValueOperator::Colon) {
            return false;
        }
        if next_op == Some(ValueOperator::Colon) {
            return true;
        }

        // "Ignore `@` in Less (i.e. `@@var;`)": an EMPTY `@`-word — a lone `@` — glues to
        // the member after it. ⚠️ tsv glues it only where the author did: prettier's arm
        // welds `@ a` into `@a`, turning a `<delim-token>` and an `<ident-token>` into
        // one `<at-keyword-token>`, which in a custom property's value is a different
        // token sequence, not a different spacing
        // (`css/values/operators/atword_empty_gap_prettier_divergence`). A non-empty
        // `@`-word has no glued arm at all — the one that looks like it,
        // `isAtWordPlaceholderNode`, requires prettier's own template placeholder — so
        // `@a(2.5)` takes the ordinary member gap below like `(1.5)(2.5)` does.
        if is_word_spelled(current, source, "@") && authored_glued(current, next) {
            return true;
        }

        // "Ignore spaces after `{` and before `}`" (prettier's SCSS-interpolation arms,
        // which run for a `{` / `}` word in plain CSS too — reachable in a custom
        // property's value): a brace is always glued to the block's interior, and to the
        // member outside it only as authored. ⚠️ The **whole-value** block's own pair is
        // the exception, spaced as authored on both faces (`ValueScope::whole_value_block`).
        let inward = is_word_spelled(current, source, "{") || is_word_spelled(next, source, "}");
        let outward = is_word_spelled(next, source, "{") || is_word_spelled(current, source, "}");
        let whole = ctx.scope.whole_value_block;
        let on_whole_block_face = whole.is_some_and(|w| {
            (current.span().start == w.start && is_word_spelled(current, source, "{"))
                || (next.span().end == w.end && is_word_spelled(next, source, "}"))
        });
        if (inward && !on_whole_block_face)
            || ((inward || outward) && authored_glued(current, next))
        {
            return true;
        }

        // ⚠️ The would-merge refusal, read ahead of every arm that can GLUE: gluing the
        // operator to the member after it must not rewrite the author's two tokens into
        // one ([`operator_glue_merges`]) — a `<number-token>` or an `<ident-token>` at the
        // sign arm below, and at a `/` the sharpest of them, the `/*` that opens a comment
        // that never closes. It withholds only glue a rule would INTRODUCE: where the
        // author glued the pair already, the merge is the source's and spelling the bytes
        // back is what preserves it (`1.5+2.5`, `1.5-2.5`). Three arms ask it — the head
        // `/` below, the sign arm, and the `font` carve-out at the end, which is a glue
        // like any other and reached the same `/*` through its own door
        // (`css/values/operators/slash_before_star_prettier_divergence`).
        let introduces_merge = !authored_glued(current, next)
            && current_op.is_some_and(|op| operator_glue_merges(op, next.span().extract(source)));

        // A `/` with nothing before it binds to what follows (prettier's
        // `!iPrevNode && isDivisionNode(iNode)`, written for `-fb-url(/abs/path/)`).
        if previous.is_none() && current_op == Some(ValueOperator::Division) && !introduces_merge {
            return true;
        }

        let is_word = |v: Option<&CssValue<'_>>| v.is_some_and(is_word_member);
        let is_func = |v: Option<&CssValue<'_>>| v.is_some_and(|v| is_func_member(v, source));
        let require_space_before = is_func(next_next)
            || is_word(next_next)
            || is_func(Some(current))
            || is_word(Some(current));
        let require_space_after =
            is_func(Some(next)) || is_word(Some(next)) || is_func(previous) || is_word(previous);

        // "Ignore Tailwind `@utility` directive": a word followed by `*` is glued there,
        // which is the one place a `*` takes no space (`--value(--tab-size-*)`).
        if ctx.scope.in_utility_at_rule
            && next_op == Some(ValueOperator::Multiplication)
            && is_word_member(current)
        {
            return true;
        }

        // "Formatting `/`, `+`, `-` sign". A `*` on either side takes the rule out, and so
        // does `calc()`, whose own arm follows.
        //
        // So does prettier's `isColorAdjusterNode`, the conjunct the arm opens on: a head
        // `+` / `-` the author SPACED from a number or a hex, inside one of the colour
        // ADJUSTER functions ([`is_color_adjuster_function`]), is an adjustment rather
        // than an operator and keeps the author's gap
        // (`prettier/tests/format/css/color/color-adjuster.css`,
        // `css/values/functions/color_adjuster_sign`). ⚠️ Its three positional
        // conjuncts are each a bound the fixture pins: the adjuster is the group's OWN
        // function (`path.grandparent`, so a nameless group inside one clears the bit),
        // `i === 0` is asked per COMMA GROUP — which is what a tsv function argument's
        // member list already is — and `!hasEmptyRawBefore` leaves an authored glue
        // glued. At a NUMBER the arm below reaches the same answer by a different road
        // (the merge refusal), so only a hex ever showed the absence.
        let is_color_adjuster_node = ctx.in_color_adjuster
            && previous.is_none()
            && matches!(
                current_op,
                Some(ValueOperator::Addition | ValueOperator::Subtraction)
            )
            && is_number_or_hex_member(next)
            && !authored_glued(current, next);

        let beside_multiplication = current_op == Some(ValueOperator::Multiplication)
            || next_op == Some(ValueOperator::Multiplication);
        let sign_keeps_its_gap = (next_op == Some(ValueOperator::Division)
            && !require_space_before)
            || (current_op == Some(ValueOperator::Division) && !require_space_after)
            || (next_op == Some(ValueOperator::Addition) && !require_space_before)
            || (current_op == Some(ValueOperator::Addition) && !require_space_after)
            || next_op == Some(ValueOperator::Subtraction)
            || current_op == Some(ValueOperator::Subtraction);
        // A run of operators is glued from its first onward even where the author spaced
        // it — prettier's second disjunct, which reads the operator's own neighbours.
        let glued = authored_glued(current, next)
            || (current_op.is_some()
                && previous.is_none_or(|p| ValueOperator::of(p, source).is_some()));
        // …unless the glue would MERGE the pair into one token (`introduces_merge` above)
        // — the one place tsv does not follow the arm.
        if !beside_multiplication
            && !ctx.in_calc
            && !is_color_adjuster_node
            && sign_keeps_its_gap
            && glued
            && !introduces_merge
        {
            return true;
        }

        // Inside `calc()` a `+` / `-` gap prints exactly as authored: css-values-4
        // requires whitespace on both sides of those two, so the gap is the difference
        // between a valid expression and an invalid one and is not a formatter's to move.
        if ctx.in_calc
            && (current_op.is_some_and(ValueOperator::is_sum)
                || next_op.is_some_and(ValueOperator::is_sum))
            && authored_glued(current, next)
        {
            return true;
        }

        // The `font` shorthand and a custom property keep a glued `/` beside a font-size
        // operand (prettier's §"Formatting `font` property"). ⚠️ The second arm reads the
        // gap on the operator's OWN left, not this gap — so it is the one arm that carries
        // a glue ACROSS a gap the author left open, and it takes `introduces_merge` for
        // that reason: `font: 12px/ * 2` reached `12px/* 2`, output neither parser reads
        // back, through a door the head rule's own refusal does not stand in.
        if ctx.scope.font_shorthand && !introduces_merge {
            if next_op == Some(ValueOperator::Division)
                && authored_glued(current, next)
                && is_possible_font_size(current, source)
            {
                return true;
            }
            if current_op == Some(ValueOperator::Division)
                && previous.is_some_and(|p| authored_glued(p, current))
                && previous.is_some_and(|p| is_possible_font_size(p, source))
            {
                return true;
            }
        }

        false
    }

    /// Build a doc for a value list joined by `sep` — `", "` for a comma-separated list
    /// (`CssValue::CommaSeparated`), `" "` for the `url()` string-plus-comment region.
    pub(crate) fn build_separated_values_doc(
        &self,
        values: &[CssValue<'_>],
        sep: &'static str,
    ) -> DocId {
        self.d()
            .join(values.iter().map(|v| self.build_css_value_doc(v)), sep)
    }
}

/// The text between a function's parentheses, read off `raw` — the function's own source
/// slice (`span`), whose last byte the value parser guarantees is the closing `)`.
///
/// The opening paren is found past the **name**, never from the start of `raw`: a name may
/// carry an escaped one (`a\(b(…)` is refused as a name, but `a\28 b(…)` is the name `a(b`),
/// and a `(` inside the name opens no argument list. `None` when the shape isn't
/// `<name>(<interior>)` at all, which is the defensive case the sole caller keeps verbatim.
fn function_paren_interior(raw: &str, name_span: Span, span: Span) -> Option<&str> {
    let after_name = raw.get(name_span.end_usize().checked_sub(span.start_usize())?..)?;
    after_name
        .strip_suffix(')')?
        .split_once('(')
        .map(|(_, i)| i)
}

#[cfg(test)]
mod tests {
    use super::{ValueOperator, operator_glue_merges};

    /// The plain-operand gate against the rule it stands in front of.
    ///
    /// `value_gap_is_glued` asserts, in debug builds, that a gap its gate answers is one
    /// the full rule would not have glued. This drives every pair of members, followed
    /// by a word, an operator and a one-byte word — each operand kind, each operator, the
    /// three one-byte words the arms key on, and a one-byte identifier that is none of
    /// them — glued and spaced, through every scope the rule reads (a plain value, `font`,
    /// a custom property, `calc()`, a colour adjuster, a nameless group, Tailwind's
    /// `@utility`), so the assertion sees each arm with a plain operand on either side.
    ///
    /// The grading is the debug assertion, so this test proves nothing in a build without
    /// debug assertions; the fixtures hold the gate's own answer.
    #[test]
    fn the_plain_operand_gate_never_overrides_an_arm() {
        let members = [
            "ab", "a", "1.5", "12px", "#fff", "'s'", "f(a)", "(a)", "/", "*", "+", "-", ":", "@",
            "{", "}",
        ];
        let scopes: [fn(&str) -> String; 7] = [
            |v| format!("a{{x:{v}}}"),
            |v| format!("a{{font:{v}}}"),
            |v| format!("a{{--x:{v}}}"),
            |v| format!("a{{x:calc({v})}}"),
            |v| format!("a{{x:color-mod({v})}}"),
            |v| format!("a{{x:({v})}}"),
            |v| format!("@utility u{{x:--value({v})}}"),
        ];
        let mut formatted = 0u32;
        for first in members {
            for second in members {
                for third in ["ab", "/", "@"] {
                    for (gap1, gap2) in [("", ""), (" ", ""), ("", " "), (" ", " ")] {
                        let value = format!("{first}{gap1}{second}{gap2}{third}");
                        for scope in scopes {
                            // A value the parser refuses reaches no gap; the rest are
                            // graded by the assertion inside the rule.
                            formatted += u32::from(crate::format(&scope(&value)).is_ok());
                        }
                    }
                }
            }
        }
        assert!(formatted > 10_000, "{formatted} values formatted");
    }

    /// The would-merge refusal, cell by cell: gluing a head `-` or `+` to the member
    /// after it must be refused exactly where css-syntax-3 reads the pair as one token.
    #[test]
    fn a_head_sign_merges_only_where_the_pair_is_one_token() {
        let minus = |next| operator_glue_merges(ValueOperator::Subtraction, next);
        let plus = |next| operator_glue_merges(ValueOperator::Addition, next);

        // §4.3.9's first bullet: an ident-start code point — an ASCII letter, `_`, and
        // every code point at U+00A0 and above (tsv's lexer's set, §4.2's two holes at
        // U+00D7 / U+00F7 included)
        assert!(minus("a"));
        assert!(minus("f(2.5)"));
        assert!(minus("_a"));
        assert!(minus("é"));
        assert!(minus("×a"));
        assert!(minus("÷a"));
        // …and its second and third: another `-`, and a valid escape whatever it spells
        assert!(minus("-a"));
        assert!(minus(r"\61"));
        assert!(minus(r"\?"));
        assert!(minus(r"\31"));
        // §4.3.10: a number, which can open at a `.`
        assert!(minus("2.5"));
        assert!(minus(".5"));
        assert!(plus("2.5"));
        assert!(plus(".5"));

        // the lossless glues, which tsv takes exactly as prettier does: none of these
        // bytes can continue the operator's token
        for next in ["@a", "(2.5)", "[a]", "'x'", "+a", "*", "/"] {
            assert!(!minus(next), "{next:?} does not merge with a `-`");
        }
        // a `+` starts no ident, so only its number half ever refuses
        for next in ["a", "_a", "é", "-a", r"\61", "@a", "(2.5)", "[a]", "'x'"] {
            assert!(!plus(next), "{next:?} does not merge with a `+`");
        }
        // a `\` that starts no escape at all (§4.3.8) leaves the pair two tokens
        assert!(!minus("\\\n"));
        // a `/` merges with one thing only: a `*`, where the pair opens a comment
        let slash = |next| operator_glue_merges(ValueOperator::Division, next);
        assert!(slash("*"));
        assert!(slash("*2.5"));
        for next in [
            "2.5", "a", "/2.5", "@a", "(2.5)", "[a]", "'x'", "-a", "+a", "",
        ] {
            assert!(!slash(next), "{next:?} does not merge with a `/`");
        }
        // and the operators the rule never asks about
        assert!(!operator_glue_merges(ValueOperator::Multiplication, "2.5"));
        assert!(!operator_glue_merges(ValueOperator::Colon, "2.5"));
        // a member that is nothing at all merges with nothing
        assert!(!minus(""));
    }
}