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
// CSS AST modules

#[cfg(feature = "json")]
pub(crate) mod convert;

// Internal AST - CSS-specific types optimized for traversal and manipulation
//
// ARCHITECTURE: This is our internal AST representation, optimized for traversal,
// manipulation, and formatting. The wire-JSON writer (`convert/write.rs`) emits
// the public JSON directly from it.

pub use tsv_lang::Comment;
use tsv_lang::Span;

/// CSS Stylesheet - top-level container for CSS nodes and comments
///
/// Comments are stored in a separate Vec, sorted by span.start.
/// This matches the TS/Svelte pattern and enables efficient range queries
/// using `comments_to_emit_in_range()` from tsv_lang.
#[derive(Debug, Clone)]
pub struct CssStyleSheet<'arena> {
    /// CSS nodes (rules, at-rules) - no longer includes Comment variant
    pub nodes: &'arena [CssNode<'arena>],

    /// Top-level detached comments (between rules), sorted by span.start.
    ///
    /// Does **not** include value comments (inside property values like
    /// `font-size: /* comment */ 12px;`): those are never lexed as `Comment`s — they're
    /// re-derived from source at print time — so they're out of scope by construction.
    /// Nor the in-block ones, which ride `CssBlockChild::Comment`. **Three disjoint
    /// places, one per printer need** — the wire's flat `CSSComment[]` is a fourth
    /// reading that spans all of them, rebuilt by `ast/convert/write.rs`'s
    /// `write_children` as it emits.
    ///
    /// Use `tsv_lang::comments_to_emit_in_range()` for efficient range lookups.
    pub comments: Vec<Comment>,
}

impl<'arena> CssStyleSheet<'arena> {
    /// Create a new empty stylesheet
    pub fn new() -> Self {
        Self {
            nodes: &[],
            comments: Vec::new(),
        }
    }
}

impl Default for CssStyleSheet<'_> {
    fn default() -> Self {
        Self::new()
    }
}

/// CSS AST node types
///
/// Comments are stored separately in `CssStyleSheet.comments` and looked up by position.
#[derive(Debug, Clone)]
pub enum CssNode<'arena> {
    Rule(CssRule<'arena>),
    Atrule(CssAtrule<'arena>), // @media, @keyframes, @supports, etc.
}

impl CssNode<'_> {
    pub fn span(&self) -> Span {
        match self {
            CssNode::Rule(rule) => rule.span,
            CssNode::Atrule(atrule) => atrule.span,
        }
    }
}

/// CSS Rule - selector with declaration block
#[derive(Debug, Clone)]
pub struct CssRule<'arena> {
    pub selector: SelectorList<'arena>,
    pub block_span: Span, // Span of the block including braces
    pub declarations: &'arena [CssBlockChild<'arena>], // Declarations and comments
    pub span: Span,       // Full rule span
}

//
// Selector AST
//
//
// Implements Selectors Level 4 specification:
// https://drafts.csswg.org/selectors-4/
//
// Structure:
//   SelectorList → ComplexSelector → RelativeSelector → SimpleSelector
//
// This enables:
//   - Specificity calculation
//   - Selector matching (for Svelte's unused CSS detection)
//   - CSS scoping (adding .svelte-{hash} classes)
//   - Proper formatting with correct precedence

/// Selector list - comma-separated selectors
#[derive(Debug, Clone, Copy)]
pub struct SelectorList<'arena> {
    pub selectors: &'arena [ComplexSelector<'arena>],
    pub span: Span,
}

/// Complex selector - one or more relative selectors connected by combinators
#[derive(Debug, Clone)]
pub struct ComplexSelector<'arena> {
    pub children: &'arena [RelativeSelector<'arena>],
    pub span: Span,
}

/// Relative selector - combinator + simple selectors
#[derive(Debug, Clone)]
pub struct RelativeSelector<'arena> {
    pub combinator: Option<Combinator>,
    pub combinator_span: Option<Span>, // Position of the combinator symbol
    pub selectors: &'arena [SimpleSelector<'arena>],
    pub span: Span,
}

/// Combinator between selectors
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Combinator {
    Descendant = 0,        // space (ancestor-descendant)
    Child = 1,             // > (parent-child)
    NextSibling = 2,       // + (adjacent sibling)
    SubsequentSibling = 3, // ~ (general sibling)
    Column = 4,            // || (column combinator)
}

impl Combinator {
    /// Returns the combinator symbol
    #[inline]
    pub const fn as_str(self) -> &'static str {
        match self {
            Combinator::Descendant => " ",
            Combinator::Child => ">",
            Combinator::NextSibling => "+",
            Combinator::SubsequentSibling => "~",
            Combinator::Column => "||",
        }
    }
}

/// Simple selector - the atomic units that make up a complex selector
#[derive(Debug, Clone)]
pub enum SimpleSelector<'arena> {
    Type {
        /// Span of the `<ns-prefix>`'s leading token — the `svg` of `svg|rect`, the `*`
        /// of `*|div`, or an EMPTY span at the `|` of `|div` — and `None` for a bare type
        /// selector. Verbatim, never a decoded copy: an escape can put a `|` *inside* the
        /// prefix (`a\|b|rect`), so a reader that scans for the first `|` reads the
        /// escape as the separator and the rest of the prefix as the element name. Every
        /// reader instead steps forward from this span's end, where the next non-trivia
        /// byte is the separator by construction. The element name is likewise recovered
        /// from `span` at print time, so no `name` copy is stored (span-for-verbatim).
        namespace_span: Option<Span>,
        span: Span,
    },
    Universal {
        /// The `<ns-prefix>` span, as [`Self::Type`]'s — the `svg` of `svg|*`, the `*` of
        /// `*|*`, an EMPTY span at the `|` of `|*`. `None` for a bare `*`.
        namespace_span: Option<Span>,
        span: Span,
    },
    /// Class selector. Name (including the `.`) recovered verbatim from `span`.
    Class { span: Span },
    /// Id selector. Name (including the `#`) recovered verbatim from `span`.
    Id { span: Span },
    Attribute {
        /// Span of the `<ns-prefix>`'s leading token, as [`Self::Type`]'s — the `svg` of
        /// `[svg|attr]`, the `*` of `[*|attr]`, an EMPTY span at the `|` of `[|attr]`.
        /// The printer emits it verbatim and the wire half-decodes it like every other
        /// selector name; neither re-derives it from a scan for `|`.
        namespace_span: Option<Span>,
        /// Span of the attribute name only (the `attr` in `[ns|attr op 'value' flags]`),
        /// verbatim. The name carries no decoded copy: the printer emits it raw from source
        /// (escapes preserved — `[f\oo]` stays `[f\oo]`, never `[foo]`) and convert
        /// half-decodes it via `raw_selector_name`, matching Svelte's `read_identifier`
        /// (see convert/mod.rs). The value is likewise a raw span (`value_span`); only
        /// `flags` stays an owned string.
        name_span: Span,
        matcher: Option<AttributeMatcher>,
        /// Raw span of the value token (`None` for bare `[attr]`): a string keeps its
        /// delimiting quotes, an identifier its escapes (`[a=x\27]` stays `x\27`). The
        /// wire and the scoping compiler derive their text via [`attribute_value_text`];
        /// the printer re-quotes from the raw form.
        value_span: Option<Span>,
        flags: Option<&'arena str>, // i (case-insensitive), s (case-sensitive)
        span: Span,
    },
    // Pseudo selectors carry no `name` field: the name is recovered verbatim from `span`
    // (the printer reads `source` directly; convert half-decodes via `raw_selector_name`,
    // matching Svelte's `read_identifier` — see convert/mod.rs). Storing a decoded name would
    // be a redundant copy that, for identity escapes (`:f\oo`), disagrees with the public
    // (Svelte) form anyway.
    PseudoClass {
        args: Option<PseudoClassArgs<'arena>>,
        /// End of the name token, from the parser rather than from a scan of `span`.
        /// A functional pseudo's name runs to the `<function-token>`'s `(` — which is
        /// **not** the first `(` in the span, since an identity escape can put one
        /// inside the name (`:foo\(bar(.x)`, name `foo\(bar`). Equals `span.end` when
        /// there are no args.
        name_end: u32,
        span: Span,
    },
    PseudoElement {
        args: Option<PseudoClassArgs<'arena>>,
        /// End of the name token, as [`Self::PseudoClass`]'s.
        name_end: u32,
        span: Span,
    },
    Nesting {
        span: Span, // & selector (CSS Nesting spec)
    },
    Percentage {
        value: f64,
        span: Span, // @keyframes percentage selectors (0%, 50%, 100%)
    },
    /// `An+B` term (`2n`, `2n + 1`, `odd`, `123`) appearing as a simple selector
    /// inside functional pseudo-class arguments (`:is(.a, 123)`, `:foo(2n + 1)`).
    /// Matches Svelte's `read_selector` Nth production, which is active only inside
    /// pseudo-class args (top-level `123 {}` still rejects). `span` covers the An+B
    /// value text verbatim: the public `Nth` node's `value` is that source slice
    /// (parseCss stores it raw) and the printer normalizes the spacing
    /// (`2n+1` → `2n + 1`), like the dedicated `:nth-child` path. For an `An+B of S`
    /// term the span folds in the ` of ` (`2n of `), matching Svelte's `\s+of\s+`
    /// terminator; `S` follows as ordinary sibling selectors (NOT nested), unlike the
    /// dedicated `:nth-*()` path which nests `S` under `Nth.selector`.
    Nth { span: Span },
    /// Invalid selector - unparseable syntax preserved for forgiving parsing
    ///
    /// Used in :is() and :where() pseudo-classes which use forgiving selector lists.
    /// Per CSS Selectors Level 4, invalid selectors are ignored for matching purposes
    /// but preserved in the source for formatter output.
    ///
    /// Examples: `.` (incomplete class), `[` (incomplete attribute), etc.
    Invalid {
        // Raw selector text as written — recovered verbatim from `span` (trimmed) at
        // print time (span-for-verbatim); convert filters Invalid selectors out.
        span: Span,
    },
}

impl SimpleSelector<'_> {
    /// Get the span of this simple selector.
    pub fn span(&self) -> Span {
        match self {
            SimpleSelector::Type { span, .. }
            | SimpleSelector::Universal { span, .. }
            | SimpleSelector::Class { span }
            | SimpleSelector::Id { span }
            | SimpleSelector::Attribute { span, .. }
            | SimpleSelector::PseudoClass { span, .. }
            | SimpleSelector::PseudoElement { span, .. }
            | SimpleSelector::Nesting { span }
            | SimpleSelector::Percentage { span, .. }
            | SimpleSelector::Nth { span }
            | SimpleSelector::Invalid { span } => *span,
        }
    }
}

/// The quote-aware split of an attribute selector's raw value token: the delimiting
/// quote (`None` for a bare identifier value) and the text inside it (the whole raw
/// token when bare). The single statement of "a raw value whose first byte is a quote
/// is a string; its interior is `raw[1..len-1]`" — the wire, the scoping compiler, and
/// the printer's re-quoting all read this split rather than re-deriving it. The slice
/// is safe: a `TokenKind::String` span always carries both delimiters (the lexer
/// errors on an unterminated string).
pub(crate) fn attribute_value_quote_and_text(
    source: &str,
    value_span: Span,
) -> (Option<char>, &str) {
    let raw = value_span.extract(source);
    match raw.as_bytes().first() {
        Some(&q @ (b'\'' | b'"')) => (Some(q as char), &raw[1..raw.len() - 1]),
        _ => (None, raw),
    }
}

/// The public-AST text of an attribute selector's value (`AttributeSelector.value` on
/// the wire, and what the scoping compiler matches against): the raw token with a
/// string's delimiting quotes stripped, then trimmed. Escapes stay encoded in both forms —
/// Svelte's `parseCss` emits `[a=x\27]` as `x\27` and `[a='it\'s']` as `it\'s`, never
/// decoding.
///
/// The trim is `read_attribute_value`'s own `value.trim()` — JS `\s`, over a quoted
/// interior as much as a bare value, and blind to escapes — so `[a=' b ']` is the value `b`
/// and `[a=b\ ]` the value `b\` on the wire, as they are in Svelte. The printer reads the
/// untrimmed [`attribute_value_quote_and_text`] instead: the padding is the author's content
/// and is kept.
pub fn attribute_value_text(source: &str, value_span: Span) -> &str {
    tsv_lang::trim_js_whitespace(attribute_value_quote_and_text(source, value_span).1)
}

/// Pseudo-class/pseudo-element argument types (semantic representation)
///
/// Stores semantic data (what the args mean), not output structure.
/// Conversion layer generates Svelte's wrapper format.
#[derive(Debug, Clone)]
pub enum PseudoClassArgs<'arena> {
    /// Nth expression for :nth-child(), :nth-of-type(), :nth-last-child(), :nth-last-of-type()
    ///
    /// Values: "2n + 1", "odd", "even", "3", "-n+6", etc.
    /// Optional selector list for `:nth-child(An+B of S)` syntax (CSS Selectors Level 4)
    /// Span covers the argument content (inside the parentheses); `value_span`
    /// covers just the trimmed An+B text, so the printer can find the comments
    /// in the gaps around it (before the An+B, around `of`, after the list)
    Nth {
        value: &'arena str,
        of_selector: Option<SelectorList<'arena>>,
        span: Span,
        value_span: Span,
    },

    /// Selector list for `:is()`, `:not()`, `:where()`, `:has()`, and `::slotted()`
    ///
    /// Contains a full SelectorList that can include multiple complex selectors.
    /// Used for logical combinations, relational selectors, and `::slotted()` — whose
    /// spec grammar is `<compound-selector>` but which parseCss parses (and tsv
    /// reproduces) as a lenient `<complex-selector-list>`.
    SelectorList {
        selectors: SelectorList<'arena>,
        span: Span,
    },

    /// Part names for ::part() pseudo-element
    ///
    /// Per CSS Shadow Parts Specification: `::part( <ident>+ )`
    /// One or more space-separated identifiers (NOT selectors).
    ///
    /// Multiple idents = intersection semantics (element must have ALL part names).
    /// Order-independent: `::part(tab active)` = `::part(active tab)`
    ///
    /// Examples: `label`, `tab`, `tab active`, `button primary`
    ///
    /// `span` covers the argument content (inside the parentheses). `ident_spans`
    /// holds one span per part name, and the names are read from them: the printer
    /// emits each verbatim (an escaped name included), so no decoded copy is stored.
    /// The spans also let the printer find the comments in every gap — around the
    /// run (before the first name, after the last) and *between* names — and derive
    /// the whole-run span (first name's start .. last name's end). Always non-empty
    /// (the parser rejects an empty `::part()`).
    ///
    /// ⚠️ The **wire** reads `ident_spans` too. `parseCss` has no `::part` grammar
    /// — it reads every pseudo-element argument as a selector list — so the writer
    /// projects this run onto that shape (`ast/convert/write.rs`'s
    /// `write_part_args`: one `ComplexSelector` of `TypeSelector`s joined by
    /// descendant combinators, the gaps taken from these spans). A parser change
    /// that admits a shape the projection can't express — a comma list, anything
    /// but a plain ident run — must move the projection in the same breath, or the
    /// wire goes wrong rather than merely different.
    Part {
        ident_spans: &'arena [Span], // One span per space-separated part name
        span: Span,
    },
}

impl PseudoClassArgs<'_> {
    /// Get the span of the pseudo-class/pseudo-element arguments
    pub fn span(&self) -> Span {
        match self {
            PseudoClassArgs::Nth { span, .. } => *span,
            PseudoClassArgs::SelectorList { span, .. } => *span,
            PseudoClassArgs::Part { span, .. } => *span,
        }
    }
}

/// Attribute selector matcher type
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum AttributeMatcher {
    Exact = 0,     // [attr="value"] - exact match
    Contains = 1,  // [attr~="value"] - whitespace-separated list contains value
    DashMatch = 2, // [attr|="value"] - exact or starts with value followed by -
    Prefix = 3,    // [attr^="value"] - starts with
    Suffix = 4,    // [attr$="value"] - ends with
    Substring = 5, // [attr*="value"] - contains substring
}

impl AttributeMatcher {
    /// Returns the matcher operator symbol
    #[inline]
    pub const fn as_str(self) -> &'static str {
        match self {
            AttributeMatcher::Exact => "=",
            AttributeMatcher::Contains => "~=",
            AttributeMatcher::DashMatch => "|=",
            AttributeMatcher::Prefix => "^=",
            AttributeMatcher::Suffix => "$=",
            AttributeMatcher::Substring => "*=",
        }
    }
}

/// CSS Declaration - property: value pair
///
/// Maintains semantic representation:
/// - `value`: Rich semantic AST for manipulation, formatting, linting
/// - Source text extracted via span when needed (e.g., for JSON output)
#[derive(Debug, Clone)]
pub struct CssDeclaration<'arena> {
    /// Property name, **escape-decoded** (spec-canonical `<ident-token>` value, e.g. `\63 olor`
    /// → `color`; CSS Syntax §4.3.11). This is NOT a verbatim slice of `span`, so it is kept as
    /// an arena string rather than recovered from a span (a span would silently un-decode it).
    /// Used internally for the `--`-prefix check, keyword matches (`grid*`), and inline-width
    /// math. The printer's *output* and the public AST both emit the **raw** property text from
    /// source instead (`extract_property_name` / convert), preserving escapes — a documented
    /// Svelte quirk. So decoded(internal) / raw(output+public) is intentional (the
    /// spec-vs-Svelte escape-encoding split).
    pub property: &'arena str,
    pub value: CssValue<'arena>, // Semantic representation (normalized)
    /// End position including !important (span.end excludes it for the formatter).
    /// `None` means no !important. Use `is_important()` for the bool check.
    pub important_end: Option<u32>,
    pub span: Span,
    /// Absolute (host-coordinate, like `span`) byte offset of the real
    /// `property : value` colon — the one the parser `expect`s, outside any
    /// comment/string. Recorded at parse time so the wire-JSON writer splits
    /// property/value without re-scanning for it. The colon is one ASCII byte, so
    /// the value starts at `colon_offset + 1`.
    pub colon_offset: u32,
    /// Whether a `/* … */` block comment appears anywhere in the declaration
    /// extent (the property→colon gap, or the value / `!important` / trailing
    /// region up to the terminator). Precomputed from the lexer's comment tokens so
    /// the writer takes a zero-scan fast path — split at `colon_offset`, value just
    /// trimmed — in the common no-comment case, and the comment-aware split/strip
    /// path only when true.
    pub has_block_comment: bool,
}

impl CssDeclaration<'_> {
    pub fn is_important(&self) -> bool {
        self.important_end.is_some()
    }

    /// Byte offset of the `property : value` colon **within `span.extract(source)`**
    /// (equivalently `ctx.source[span.start..]`) — the absolute [`colon_offset`] rebased
    /// to the declaration slice. Every source-extracting path indexes its slice by this
    /// and finds the value at `colon_pos() + 1`; recorded at parse time, so no path
    /// re-scans for the colon.
    ///
    /// [`colon_offset`]: Self::colon_offset
    pub fn colon_pos(&self) -> usize {
        (self.colon_offset - self.span.start) as usize
    }
}

//
// CSS Value AST
//
//
// Implements CSS Values and Units Level 4 specification:
// https://drafts.csswg.org/css-values-4/
//
// Structure enables:
//   - Value validation (type checking)
//   - Value transformation (calc evaluation, var substitution)
//   - Minification (removing unnecessary spaces)
//   - Pretty-printing with correct precedence

/// The decoded value of a CSS string literal, mirroring `tsv_ts`'s `StringCooked`.
///
/// `Verbatim` (the common no-escape case) carries **no allocation** — the decoded
/// value equals the inner source slice (the literal's `span` minus the two quote
/// bytes). Only strings with escape sequences own arena bytes.
#[derive(Debug, Clone)]
pub enum StringCooked<'arena> {
    /// Decoded value == the inner source slice (no escapes to decode).
    Verbatim,
    /// Escapes were decoded into a value distinct from the raw inner text.
    Decoded(&'arena str),
}

impl<'arena> StringCooked<'arena> {
    /// The decoded string value. `span` is the owning [`CssValue::String`]'s span
    /// (the quoted literal); `source` is the host document. `Verbatim` slices the
    /// inner text (zero-copy); `Decoded` returns the arena bytes.
    #[inline]
    pub fn resolve<'s>(&'s self, span: Span, source: &'s str) -> &'s str {
        match self {
            StringCooked::Verbatim => {
                let raw = span.extract(source);
                &raw[1..raw.len() - 1]
            }
            StringCooked::Decoded(decoded) => decoded,
        }
    }
}

/// CSS value - right-hand side of a declaration
///
/// Internal representation optimized for traversal and manipulation.
/// Converted to public JSON AST via the convert layer (see ast/convert/mod.rs).
/// Never serialized directly - serde not needed!
#[derive(Debug, Clone)]
pub enum CssValue<'arena> {
    /// Identifier: auto, bold, inherit, currentColor, etc.
    ///
    /// The text is recovered verbatim from `span` at print time
    /// (`build_identifier_doc`, escapes preserved) — span-for-verbatim, so no copied
    /// string is stored. An empty/whitespace-only `span` marks the empty-identifier
    /// sentinel (`var(--a,)`'s trailing fallback, an empty custom-property value).
    Identifier { span: Span },

    /// String literal: `"Arial"`, `'font.woff'`.
    ///
    /// The printed text is recovered verbatim from `span` (escapes preserved, quote
    /// normalized) — span-for-verbatim, like `Identifier`. The quote char is the
    /// first byte of `span` (`source[span.start]`), not stored. `content` carries the
    /// *decoded* value for the defensive span-unavailable fallback only: `Verbatim`
    /// for no-escape strings (zero alloc), `Decoded` when escapes were applied.
    String {
        content: StringCooked<'arena>,
        span: Span,
    },

    /// Number with optional unit: 10, 10px, 1.5em, 50%, etc.
    ///
    /// The variant tag classifies the token as a dimension (vs an identifier) so the
    /// printer applies number normalization; the numeric value and unit text are
    /// recovered verbatim from `span` at print time (`build_dimension_doc`), so they
    /// are not stored — span-for-verbatim (see the arena string-representation idiom).
    Dimension { span: Span },

    /// Color - various formats (rgb, hsl, hex, named)
    Color { color: Color, span: Span },

    /// Function call: calc(), var(), rgb(), url(), etc.
    ///
    /// `name_span` is the **verbatim** span of the text before `(` — span-for-verbatim,
    /// like `Identifier` and `Dimension`, so the author's bytes reach the output
    /// unchanged and no name is copied into the arena. The value subtree is
    /// source-faithful, never re-serialized (see `convert/mod.rs`), which is what lets a
    /// span stand in for the text: an escape-spelled name (`\6c ayer(`) prints as
    /// written rather than decoded, matching prettier. Unlike `CssAtrule.name` /
    /// `CssDeclaration.property`, which are decoded identifiers.
    ///
    /// Recognition (`url`, `var`) reads the span and decodes only when it holds a `\`
    /// — see `printer::values::function_name_is`.
    Function {
        name_span: Span,
        args: &'arena [CssValue<'arena>],
        span: Span,
    },

    /// An `@import` prelude's `supports()` call, whose argument is parsed as the
    /// condition it is rather than kept as opaque text.
    ///
    /// `supports( <supports-condition> | <declaration> )` (css-cascade-5
    /// §"Conditional import rules") is the grammar `@supports` takes, so the argument
    /// carries the same `ConditionQuery` and prints through the same printer — one
    /// condition, one form, in both positions. The function's own parentheses **are**
    /// the condition part's, so the query holds exactly one part and `span` (the whole
    /// call, `supports` through its `)`) needs no separate paren bookkeeping.
    ///
    /// `name` is the **verbatim** token slice — the author's own bytes, not the decoded
    /// identifier the recognition test above it runs on, so an escape-spelled
    /// `\73 upports(` prints as written (prettier preserves it too). `Function` answers
    /// the same question with a `name_span` and no copy at all; this variant keeps the
    /// text because its printer spells the condition rather than the name, and the copy
    /// is an `@import` prelude's alone.
    SupportsCondition {
        name: &'arena str,
        condition: ConditionQuery<'arena>,
        span: Span,
    },

    /// A value **operator** token — `/`, `*`, `+`, `-` or `:` — split out of a run by
    /// `parser::value::operators::split_value_run`.
    ///
    /// The byte is recovered from `span` at print time (span-for-verbatim, like
    /// `Identifier`), so the variant stores only where it is. Its *kind* is read back off
    /// that byte by the printer's separator rule
    /// (`printer::values::ValueOperator::at`), which is the only consumer that needs it.
    ///
    /// An operator is never a value in its own right: it exists only as a member of the
    /// `List` its run was flattened into, beside the operands it joins.
    Operator { span: Span },

    /// Space-separated list of values
    List {
        values: &'arena [CssValue<'arena>],
        span: Span,
    },

    /// Comma-separated list of values
    CommaSeparated {
        values: &'arena [CssValue<'arena>],
        span: Span,
    },
}

impl CssValue<'_> {
    /// Get the span of this value
    pub fn span(&self) -> Span {
        match self {
            CssValue::Identifier { span, .. } => *span,
            CssValue::Operator { span, .. } => *span,
            CssValue::String { span, .. } => *span,
            CssValue::Dimension { span } => *span,
            CssValue::Color { span, .. } => *span,
            CssValue::Function { span, .. } => *span,
            CssValue::SupportsCondition { span, .. } => *span,
            CssValue::List { span, .. } => *span,
            CssValue::CommaSeparated { span, .. } => *span,
        }
    }
}

/// CSS color value
///
/// Color channel value - supports numbers, percentages, and CSS Color 4 `none`
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ColorChannel {
    /// Numeric value: 255, 0.5, etc.
    Number(f64),
    /// Percentage value: 50%, 100%, etc.
    Percentage(f64),
    /// CSS Color 4 `none` keyword
    None,
}

/// Angle unit for hue values in HSL
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum AngleUnit {
    /// Degrees (default, can be omitted)
    Deg = 0,
    /// Radians
    Rad = 1,
    /// Turns (1turn = 360deg)
    Turn = 2,
    /// Gradians (400grad = 360deg)
    Grad = 3,
}

impl AngleUnit {
    /// Returns the unit suffix string
    #[inline]
    pub const fn as_str(self) -> &'static str {
        match self {
            AngleUnit::Deg => "deg",
            AngleUnit::Rad => "rad",
            AngleUnit::Turn => "turn",
            AngleUnit::Grad => "grad",
        }
    }
}

/// Internal representation - converted to JSON via convert layer.
///
/// `Named`/`Hex` carry no text: it is recovered verbatim from the enclosing
/// `CssValue::Color.span` at print time (span-for-verbatim), so `Color` holds no
/// arena reference and needs no lifetime.
#[derive(Debug, Clone)]
pub enum Color {
    /// Named color: red, blue, currentColor, etc.
    Named,

    /// Hex color: #ff0000, #f00, etc.
    Hex,

    /// RGB color: rgb(255, 0, 0) or rgb(255 0 0 / 1) or rgb(100% 0% 0%)
    /// Supports CSS Color 4: numbers, percentages, none, alpha as percentage
    Rgb {
        r: ColorChannel,
        g: ColorChannel,
        b: ColorChannel,
        alpha: Option<ColorChannel>,
    },

    /// HSL color: hsl(0, 100%, 50%) or hsl(120deg 75% 25%)
    /// Supports CSS Color 4: angle units, none keyword, alpha as percentage
    Hsl {
        hue: ColorChannel,
        hue_unit: Option<AngleUnit>, // None = unitless number (treated as degrees)
        saturation: ColorChannel,
        lightness: ColorChannel,
        alpha: Option<ColorChannel>,
    },
}

//
// At-Rule AST
//
//
// Implements CSS Syntax Module Level 3 at-rules:
// https://drafts.csswg.org/css-syntax-3/#at-rules
//
// Examples:
//   @media screen and (min-width: 768px) { ... }
//   @keyframes slide { ... }
//   @supports (display: grid) { ... }
//   @font-face { ... }
//   @import url('styles.css');
//
// Prelude is kept as raw string (not parsed) - matches Svelte's approach.
// Block content varies by at-rule type:
//   - Conditional at-rules (@media, @supports, @layer): Contains rules
//   - Descriptor at-rules (@font-face, @page): Contains declarations
//   - Statement at-rules (@import, @charset): No block

/// At-rule prelude value
///
/// At-rules have different prelude structures:
/// - @import: structured values (url, layer, supports, media)
/// - @supports: structured conditions for line-width wrapping
/// - @media, @container: raw condition strings
/// - @keyframes: raw animation name
#[derive(Debug, Clone)]
pub enum PreludeValue<'arena> {
    /// Structured values (for @import): a `url()`/string head and `layer`/`supports()`
    /// calls, then the rest of the prelude to its end as one verbatim `Identifier` tail —
    /// the media-query list when the author wrote one, any other value otherwise
    /// (`parse_import_prelude`).
    /// Example: `url('styles.css') layer(base) screen` → [Function(url), Function(layer),
    /// Identifier(screen)]
    Values {
        values: &'arena [CssValue<'arena>],
        span: Span,
    },

    /// Raw prelude (for `@keyframes`, `@layer`, `@namespace`, `@page`, … — at-rules with
    /// no `property: value` / media-query grammar). `content` is the **printer-facing**
    /// string: verbatim source with internal whitespace + comments preserved and only
    /// `url()` inner whitespace trimmed (`@namespace` is the exception — its prelude is
    /// whitespace-normalized to match postcss). The public AST is reproduced separately
    /// from `span` (source-verbatim), so `content` never feeds the AST.
    /// Example: `@layer` → `a , b`; `@keyframes` → `my-anim`.
    Raw { content: &'arena str, span: Span },

    /// Selector lists (for @scope). Both clauses are independently optional per
    /// css-cascade-6 (`@scope [(<scope-start>)]? [to (<scope-end>)]?`), so a bare
    /// `@scope { … }` has `root: None, limit: None`, and `@scope to (.footer)` has
    /// `root: None, limit: Some(…)`. Each clause's selector list is a
    /// `<forgiving-selector-list>` (the same production `:is()`/`:where()` use), so it
    /// may be empty or hold `Invalid` items — the printer re-emits them verbatim.
    /// Example: `@scope (.card) to (.footer)` → root: Some([.card]), limit: Some([.footer])
    Selectors {
        root: Option<ScopeClause<'arena>>,
        limit: Option<ScopeLimit<'arena>>,
        span: Span,
    },

    /// `@custom-selector` (css-extensions-1: `@custom-selector <custom-selector>
    /// <selector-list> ;`): the `:--name` and the strict complex-selector list it
    /// aliases. The name is a span, printed from source — an `<extension-name>` is
    /// author-defined and keeps its case, like `@custom-media`'s. A prelude that is not
    /// this shape (no name, nothing after the name, a list the selector parser rejects)
    /// takes the `Raw` arm instead (`parse_custom_selector_prelude`).
    /// Example: `:--heading h1, h2` → name `:--heading`, list [h1, h2]
    CustomSelector {
        /// The `:--name` span, from the `:` through the ident.
        name: Span,
        list: SelectorList<'arena>,
        /// The prelude's raw-reader span: first token after the at-rule name to the
        /// `;` / `{` terminator (trailing whitespace included), the same bounds the `Raw`
        /// arm captures, so routing between the two moves no wire byte.
        span: Span,
    },

    /// @supports condition (structured for line-width wrapping)
    /// Example: `(display: grid) and (flex: 1)` → parts connected by `and`/`or`
    Supports {
        condition: ConditionQuery<'arena>,
        span: Span,
    },

    /// @container query (structured for line-width wrapping)
    /// Example: `sidebar (min-width: 100px) and (max-width: 200px)`
    Container {
        /// Optional container name (e.g., "sidebar"), **escape-decoded** (spec-canonical
        /// `<ident-token>`). Printer-only — convert emits the whole prelude from `span`
        /// (raw), not this field. Kept as an arena string (not a span) because the span
        /// holds the raw escaped form, which would diverge from the decoded value; same
        /// category as `CssAtrule.name`.
        name: Option<&'arena str>,
        /// The condition parts connected by `and`/`or`
        condition: ConditionQuery<'arena>,
        span: Span,
    },

    /// @media query - uses raw string with printer-side wrapping
    ///
    /// Unlike @supports/@container which use structured parsing, @media uses
    /// raw string parsing to preserve comments. Wrapping is handled in the
    /// printer by finding `and`/`or` boundaries in the raw string.
    ///
    /// Fully structuring preludes (vs. this raw form) is a deferred design option
    /// — see docs/architecture.md § "Red-Green Trees (Deferred)".
    Media { content: &'arena str, span: Span },
}

/// One `@scope` clause: a `<forgiving-selector-list>` and its enclosing `(…)` span.
/// Used for the root (`@scope (<scope-start>)`) and, via `ScopeLimit`, the limit.
/// Bundling the list with its paren span makes the printer-only span **structurally**
/// present whenever the list is (no separate `Option<Span>` to keep in lockstep): the
/// paren bounds the gaps where a comment inside the parens but outside the list sits
/// (`@scope (/* c */ .a)`), which the printer re-emits via `comments_to_emit_in_range` — the
/// same wrapping `:is()` args use. Wire-ignored (the prelude is extracted from the
/// `Selectors` `span`, comments stripped).
#[derive(Debug, Clone)]
pub struct ScopeClause<'arena> {
    pub list: SelectorList<'arena>,
    /// The clause's full `(…)` span (end one past `)`, the pseudo-arg convention).
    pub paren: Span,
}

/// The `@scope` limit clause: `to (<scope-end>)`. Carries the `to` keyword span (so the
/// printer can tell a between-clause comment before `to` from an after-`to` one) plus
/// the limit `ScopeClause`. Present iff the source has a `to` clause.
#[derive(Debug, Clone)]
pub struct ScopeLimit<'arena> {
    /// The `to` keyword span (a case-insensitive grammar keyword, lowercased at print).
    pub to_span: Span,
    pub clause: ScopeClause<'arena>,
}

/// A boolean condition query — the structured prelude shared by `@supports`
/// (`<supports-condition>`) and `@container` (`<container-query>`), whose
/// grammars are identical. Structured (rather than raw) so the printer can wrap
/// at `and`/`or` boundaries, keeping the keyword on the current line and the
/// condition on the next.
#[derive(Debug, Clone)]
pub struct ConditionQuery<'arena> {
    /// The condition parts connected by `and`/`or`
    pub parts: &'arena [ConditionPart<'arena>],
    /// The run of boolean operators the reader consumed but could not bind, with the
    /// comments between them, in source order and source case (`(a: b) /* c */ and`).
    ///
    /// Every operator in both grammars binds its operand to the **right** — `<media-and>
    /// = and <media-in-parens>`, `<media-not> = not <media-in-parens>` (mediaqueries-4
    /// §"Syntax"), and `<supports-condition> = not <supports-in-parens> |
    /// <supports-in-parens> [ and <supports-in-parens> ]*` (css-conditional-3
    /// §"@supports") — so an operator with nothing after it is the query's own last word
    /// rather than a term of it, and has no part to ride on. It is malformed input, but
    /// the parser is deliberately permissive: dropping the token loses content the
    /// canonical parser keeps in its `prelude`, so the run is carried here instead.
    /// `None` whenever the query bound everything it consumed, which is every well-formed
    /// condition.
    ///
    /// Held as one normalized text rather than a keyword list because the comments among
    /// the operators keep their authored positions (`/* c */ and` and `and /* c */` are
    /// distinct), and it is the query's **span** that makes it whole: the span reaches
    /// past the run, so the wire prelude (`span.extract`) carries it too.
    pub trailing_operators: Option<&'arena str>,
}

/// One piece of a `ConditionPart`'s content.
///
/// A condition is normalized text — the parser re-emits its tokens with the
/// spacing the grammar calls for — with one exception: the argument of
/// `selector( <complex-selector> )` (css-conditional-4) is a **selector**, so it
/// is parsed as one and printed by the selector printer, exactly as a rule's own
/// selector is. An argument that doesn't parse as a selector never becomes a
/// `Selectors` segment: it is a `<general-enclosed>`, whose contents the grammar
/// leaves undefined, and it stays `Text`.
///
/// The grammar's argument is a single complex selector, but a comma-separated
/// list is read (and printed) as the list it lexically is — the singular
/// production decides whether the *condition* is true, which is not a formatting
/// question.
#[derive(Debug, Clone, Copy)]
pub enum ConditionSegment<'arena> {
    /// Normalized condition text (`(display: `, `selector(`, `)`, …).
    Text(&'arena str),
    /// A `selector()` argument, parsed as the selectors it is, with the function's `(…)` span
    /// (end one past the `)`, the pseudo-argument convention) — the bounds of the gaps between
    /// the parens and the list, where the printer claims a boundary run the parser stepped
    /// (`selector(a <NBSP>)`) as it does in a pseudo-class's argument list.
    Selectors {
        list: SelectorList<'arena>,
        paren: Span,
    },
}

/// A single part of a `ConditionQuery` (one `(prop: val)` term, optionally
/// `not`-prefixed or function-style like `selector(...)`).
#[derive(Debug, Clone)]
pub struct ConditionPart<'arena> {
    /// The run of connectors this part binds to (None for a first part with no leading
    /// connector).
    pub connector: Option<ConnectorRun<'arena>>,
    /// The condition content (e.g., `(display: grid)` or `not (color: red)`) as a
    /// run of segments — one `Text` segment unless the part holds a `selector()`
    /// argument. A leading `not` keeps its source case (preserved like the
    /// connectors).
    pub segments: &'arena [ConditionSegment<'arena>],
    /// From the part's first token to its last — never over the gap ahead of it, which the
    /// query reader steps (whitespace, a boundary run, comments) before the part begins.
    pub span: Span,
}

/// The run of connectors (`and`/`or`) a `ConditionPart` binds to: its last connector's kind,
/// its text, and where it sits.
///
/// One value because the three are meaningless apart — a kind with no text, or a span for a
/// run that never landed, are states a part must not be able to hold.
#[derive(Debug, Clone, Copy)]
pub struct ConnectorRun<'arena> {
    /// The **last** connector of the run, normalized to the `And`/`Or` enum for logic (which
    /// side of it the gap's comments split to).
    pub kind: ConditionConnector,
    /// The run verbatim from source — the words from the first through the last,
    /// single-space joined, with whatever stood *between* them (comments, a boundary run)
    /// carried along in source order.
    ///
    /// Usually one word (`and`/`AND`/`Or`/…), emitted by the printer so the author's case
    /// is preserved (matching prettier). A grammar-legal query has exactly one, but the
    /// reader is permissive and `parseCss` keeps every word of a run
    /// (`(a: b) and or (c: d)`), so dropping all but the last would be content loss. A
    /// comment inside the run is **un-registered** when the run claims it (the `rewind_to`
    /// pairing) — the run's own text prints it, and a registration would have the printer's
    /// gap sweep print it a second time. Comments *outside* the run (before its first word,
    /// after its last) stay registered and are claimed by that sweep, which is what keeps
    /// them on their authored side of the run.
    pub text: &'arena str,
    /// From the run's first word to its last: the bound that splits the gap between two
    /// parts into the side before the run and the side after it, each of which the printer
    /// claims on its own.
    pub span: Span,
}

/// Connector (`and`/`or`) between `ConditionQuery` parts.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConditionConnector {
    And,
    Or,
}

impl PreludeValue<'_> {
    pub fn span(&self) -> Span {
        match self {
            PreludeValue::Values { span, .. } => *span,
            PreludeValue::Raw { span, .. } => *span,
            PreludeValue::Selectors { span, .. } => *span,
            PreludeValue::CustomSelector { span, .. } => *span,
            PreludeValue::Supports { span, .. } => *span,
            PreludeValue::Container { span, .. } => *span,
            PreludeValue::Media { span, .. } => *span,
        }
    }
}

/// At-rule (@media, @keyframes, @supports, @import, @layer, @font-face, etc.)
#[derive(Debug, Clone)]
pub struct CssAtrule<'arena> {
    /// At-rule name without `@` (e.g., "media", "keyframes"), **escape-decoded**
    /// (spec-canonical `<at-keyword-token>`, CSS Syntax §4.3.3 — `@\6d edia` → `"media"`),
    /// the form every at-rule dispatch compares against. The printer emits `name_span`
    /// instead, and the wire writer half-decodes `name_span` the way parseCss does. Kept
    /// as an arena string, NOT recovered from `span`: the span holds the raw escaped
    /// bytes, so a span read would silently un-decode the name for every dispatch. Same
    /// category as `CssDeclaration.property` / `Container.name`.
    pub name: &'arena str,

    /// Source span of the name token (the `<ident>` after `@`, delimiter-excluded).
    /// The **printer** emits the name from this span, not from decoded `name`: an
    /// escaped name (`@m\A x`) decodes to a raw control char, and emitting that
    /// verbatim injects it into the output (content loss on reparse). Emitting the
    /// source slice preserves the escape, matching every other decoded-name site in
    /// this crate (`CssDeclaration.property`, preludes). The wire writer reads it too,
    /// half-decoding it like a selector name, since parseCss reads an at-rule name with
    /// the same `read_identifier` (an identity escape keeps its backslash there).
    pub name_span: Span,

    /// Prelude value (structured for @import, raw string for others)
    pub prelude: PreludeValue<'arena>,

    /// Block contents (Some for conditional/descriptor, None for statement at-rules)
    pub block: Option<CssAtruleBlock<'arena>>,

    pub span: Span,
}

impl CssAtrule<'_> {
    /// The at-rule's **public-AST prelude** — the exact string Svelte's parser exposes as
    /// `node.prelude` (its `read_value(...).trim()`, block comments elided). Routes through
    /// the wire-JSON prelude reconstruction ([`convert_prelude_to_string`]), the single
    /// fixture-gated definition of that shape, so a consumer that must agree with the
    /// oracle's `node.prelude` byte-for-byte — the Svelte compiler's `@keyframes` name
    /// collection and its `-global-` prefix test — reads the identical bytes rather than the
    /// printer-facing [`PreludeValue::Raw`] `content`, which is CSS-whitespace-trimmed and
    /// comment-preserving and so diverges on a prelude carrying a comment or JS-only
    /// whitespace (`@keyframes spin /* c */`, a trailing vertical tab / NBSP).
    ///
    /// A pure visibility bridge over the writer's own reconstruction — no parse, printer, or
    /// wire behavior of its own.
    ///
    /// [`convert_prelude_to_string`]: crate::ast::convert::convert_prelude_to_string
    #[cfg(feature = "json")]
    pub fn public_prelude<'src>(&self, source: &'src str) -> std::borrow::Cow<'src, str> {
        convert::convert_prelude_to_string(&self.prelude, source)
    }
}

/// At-rule block - can contain rules, declarations, or nested at-rules
#[derive(Debug, Clone)]
pub struct CssAtruleBlock<'arena> {
    /// Block children - mixture depends on at-rule type
    ///
    /// @media, @supports, @layer: `Vec<CssNode>` (rules + nested at-rules)
    /// @font-face, @page: `Vec<CssDeclaration>` (declarations only)
    ///
    /// We store as `Vec<CssBlockChild>` to support both:
    pub children: &'arena [CssBlockChild<'arena>],
    pub span: Span,
}

/// At-rule block child - can be rule, declaration, or nested at-rule
#[derive(Debug, Clone)]
pub enum CssBlockChild<'arena> {
    Rule(CssRule<'arena>),
    Declaration(CssDeclaration<'arena>),
    Atrule(CssAtrule<'arena>),
    Comment(Comment),
}

impl CssBlockChild<'_> {
    pub fn span(&self) -> Span {
        match self {
            CssBlockChild::Rule(rule) => rule.span,
            CssBlockChild::Declaration(decl) => decl.span,
            CssBlockChild::Atrule(atrule) => atrule.span,
            CssBlockChild::Comment(comment) => comment.span,
        }
    }
}