sasso 0.8.0

A pure-Rust SCSS to CSS compiler (a dart-sass alternative). Zero dependencies, wasm-friendly, embeddable as a library and usable as a CLI.
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
//! The parsed syntax tree.
//!
//! Selectors and property names are templates ([`TplPiece`]) because they
//! may contain `#{...}` interpolation that is only resolved at eval time.

use std::rc::Rc;

use crate::scanner::Pos;
use crate::value::{Color, ListSep};

/// A parsed stylesheet: an ordered list of top-level statements.
pub(crate) struct Stylesheet {
    pub stmts: Vec<Stmt>,
}

/// 1-based source-line metadata for the serializer's trailing-comment rule
/// (dart `_isTrailingComment`). `file` is an evaluator-interned source id
/// (0 = unknown, never trailing).
#[derive(Clone, Copy, Default, PartialEq)]
pub(crate) struct SrcLines {
    pub file: u32,
    /// A comment's `/*` line; a block's opening-brace line.
    pub start: u32,
    /// The construct's last line (closing delimiter / end of value).
    pub end: u32,
    /// A comment's 1-based `/*` column (0 elsewhere): two comments with the
    /// same file/lines/column are span-identical CLONES (the same file
    /// imported twice), which dart's span-containment branch rejects as
    /// trailing — distinct same-line comments differ in column and still join.
    pub col: u32,
    /// The 0-based source COLUMN of the mapped token's start (a selector's
    /// first character, a declaration/at-rule keyword, a comment's `/*`), used
    /// ONLY by source-map generation — never by the serializer. Default 0 when
    /// unknown. Distinct from [`Self::col`], which is overloaded for the
    /// trailing-comment / custom-property-reindent heuristics; this field is a
    /// pure additive carrier and changes no CSS output.
    pub start_col: u32,
    /// Source-map-only OVERRIDE for the mapped source file id (0 = fall back to
    /// [`Self::file`]). A bubbled `@media`/`@at-root` wrapper re-uses the
    /// enclosing rule's selector position for its mapping while keeping
    /// `file`/`start`/`end` at 0 so the trailing-comment heuristic stays
    /// disabled (dart maps the duplicated parent selector to the ORIGINAL rule's
    /// span). Like [`Self::start_col`] this changes no CSS output.
    pub map_file: u32,
    /// Source-map-only OVERRIDE for the mapped 1-based source line (0 = fall
    /// back to [`Self::start`]). Set together with [`Self::map_file`] on a
    /// bubbled-selector wrapper; see that field.
    pub map_line: u32,
}

/// A statement, valid at the top level or inside a rule body.
pub(crate) enum Stmt {
    /// `$name: value [!default] [!global];`
    VarDecl(VarDecl),
    /// `selector { ... }`
    Rule(Rule),
    /// `property: value [!important];`
    Decl(Declaration),
    /// A nested property set: `property: [value] { children }`. The optional
    /// value is emitted as `property: value;` first, then each child
    /// declaration is namespaced as `property-<child>` and emitted in source
    /// order (dart-sass property-set / namespaced-declaration form).
    PropertySet(PropertySet),
    /// A custom-property declaration (`--name: value`) whose name *literally*
    /// begins with `--`. Its value is captured verbatim (a template) — only
    /// `#{…}` interpolation is resolved, never SassScript — matching dart-sass
    /// `_interpolatedDeclarationValue`.
    CustomDecl(CustomDecl),
    /// `@import "a", "b";` — each entry is either a Sass path to inline or a
    /// plain CSS import emitted verbatim.
    Import(Vec<ImportArg>),
    /// `@use "<url>" [as <namespace>|as *] [with (...)];`. Built-in `sass:*`
    /// modules and user stylesheets are both supported: the namespace defaults
    /// to the final URL segment (or the part after `sass:`), `as ns` overrides
    /// it, and `as *` exposes the members unprefixed. `with (...)` overrides the
    /// loaded module's `!default` variables before it is evaluated.
    Use {
        url: String,
        namespace: Option<String>,
        star: bool,
        config: Vec<ConfigEntry>,
        pos: Pos,
    },
    /// `@forward "<url>" [as <prefix>-*] [show ...|hide ...] [with (...)];` —
    /// re-export another module's members from the current module.
    Forward {
        url: String,
        prefix: Option<String>,
        show: Option<Vec<ForwardMember>>,
        hide: Option<Vec<ForwardMember>>,
        config: Vec<ConfigEntry>,
        pos: Pos,
    },
    /// `/* ... */` loud comment (inner text, without the delimiters). The body
    /// is a template so `#{…}` interpolation is resolved at eval time. The
    /// [`SrcLines`] carry the comment's source lines (parser fills start/end,
    /// `file` stays 0 until the evaluator stamps it).
    Comment(Vec<TplPiece>, SrcLines),
    /// `@if`/`@else if`/`@else` — evaluated top to bottom, first match wins.
    If(Vec<IfBranch>),
    /// `@for $i from A through|to B { … }`.
    For {
        var: String,
        from: Expr,
        to: Expr,
        inclusive: bool,
        body: Vec<Stmt>,
    },
    /// `@each $v[, $k…] in <list> { … }`.
    Each {
        vars: Vec<String>,
        list: Expr,
        body: Vec<Stmt>,
    },
    /// `@while <cond> { … }`.
    While { cond: Expr, body: Vec<Stmt> },
    /// `@function name(params) { … @return … }`. Shared in an `Rc` so the
    /// definition survives in the environment after its source drops.
    FunctionDef(Rc<Callable>),
    /// `@return <expr>;`
    Return(Expr),
    /// `@mixin name(params) { … }`.
    MixinDef(Rc<Callable>),
    /// `@include name(args) [{ content }];`. `module` is the namespace of a
    /// `@include ns.mixin(...)` reference; `None` for an unqualified include.
    Include {
        name: String,
        args: Vec<CallArg>,
        content: Option<Rc<Vec<Stmt>>>,
        /// The `using (params)` clause's parameters, if any — the content block
        /// declares these and they're bound from the `@content(args)` call.
        content_params: Option<Rc<ParamList>>,
        module: Option<String>,
        /// 1-based position of the `@include` keyword (the `@`), used as the
        /// call-site span for diagnostic stack frames.
        pos: Pos,
        /// Byte length of the include span (`@include name(args)`, excluding the
        /// trailing `;` or content block), used to size the diagnostic caret.
        length: usize,
    },
    /// `@content;` or `@content(args)` — runs the `@include`'s content block,
    /// passing any arguments to its `using (params)`.
    Content(Vec<CallArg>),
    /// A generic at-rule: `@name <prelude> { body }` or `@name <prelude>;`.
    /// `body == None` is the statement (`;`) form. Used for `@font-face`,
    /// `@page`, `@charset`, `@supports`, vendor `@foo`, and unknown
    /// directives alike.
    AtRule {
        name: String,
        prelude: Vec<TplPiece>,
        body: Option<Vec<Stmt>>,
        /// Source lines: `start` = the `{` line (or the rule's own line for the
        /// `;` form), `end` = the `}` line (or the same line).
        lines: SrcLines,
    },
    /// A generic at-rule whose NAME contains interpolation
    /// (`@#{"media"} … {}`): always treated as unknown — no Sass parse-time
    /// behavior — except `@keyframes`, whose frame handling happens at eval
    /// time once the name resolves.
    InterpAtRule {
        name: Vec<TplPiece>,
        prelude: Vec<TplPiece>,
        body: Option<Vec<Stmt>>,
    },
    /// A plain-CSS custom `@function`/`@mixin` whose name begins with `--`.
    /// dart-sass does not treat these as Sass definitions: the whole construct
    /// is emitted verbatim as a generic at-rule. `name` is the keyword exactly
    /// as written (`function`/`FUNCTION`/`mixin`). The prelude (`--a(...)
    /// [returns ...]`) and each body declaration value are preserved literally;
    /// only `#{...}` interpolation is resolved.
    CssCustomAtRule {
        name: String,
        prelude: Vec<TplPiece>,
        body: Vec<CssCustomItem>,
    },
    /// `@media <media-query-list> { body }`. The query is parsed into a
    /// structured form so SassScript inside feature values is resolved and the
    /// query is re-serialized (and bubbled/merged) exactly like dart-sass.
    Media {
        query: MediaQueryList,
        body: Vec<Stmt>,
        /// `{`/`}` source lines for the serializer's trailing-comment rule.
        lines: SrcLines,
    },
    /// `@supports <condition> { body }`. The condition is parsed into a
    /// structured form (`SupportsCondition`) so it serializes canonically and
    /// malformed conditions are rejected; the body bubbles like any at-rule.
    Supports {
        condition: SupportsCondition,
        body: Vec<Stmt>,
        /// `{`/`}` source lines (+ the `@` keyword column in `start_col`) for
        /// the serializer's trailing-comment rule and source-map output.
        lines: SrcLines,
    },
    /// `@at-root [query] { body }` — runs the body with the parent selector
    /// reset to the document root.
    AtRoot {
        query: Option<Vec<TplPiece>>,
        body: Vec<Stmt>,
    },
    /// `@keyframes <name> { from {…} 50% {…} … }`. The inner block selectors
    /// are keyframe selectors, not CSS selectors (no `&`/parent resolution),
    /// so the body is run with the parent context reset to root.
    Keyframes {
        name: String,
        prelude: Vec<TplPiece>,
        body: Vec<Stmt>,
        /// Source lines: `start` = the `{` line, `end` = the `}` line.
        lines: SrcLines,
    },
    /// `@extend <selector> [!optional];` — registers an extension of the
    /// enclosing rule onto the target selector. The selector is a template
    /// so `#{...}` interpolation resolves at eval time.
    Extend {
        selector: Vec<TplPiece>,
        optional: bool,
        pos: Pos,
    },
    /// `@warn <expr>;` — writes to stderr, emits no CSS. `pos` is the 1-based
    /// position of the `@warn` keyword (the innermost stack frame).
    Warn { value: Expr, pos: Pos },
    /// `@debug <expr>;` — writes to stderr, emits no CSS. `pos` is the 1-based
    /// position of the `@debug` keyword (only its line is reported).
    Debug { value: Expr, pos: Pos },
    /// `@error <expr>;` — aborts compilation with the message. `pos` is the
    /// 1-based position of the `@error` keyword and `length` the byte length of
    /// the `@error <expr>` span (used as the snippet span only at the document
    /// root; inside a call the innermost call site is used instead).
    Error { value: Expr, pos: Pos, length: usize },
}

/// One `$name: value [!default]` entry in a `@use`/`@forward` `with (...)`
/// configuration clause.
pub(crate) struct ConfigEntry {
    pub name: String,
    pub value: Expr,
    /// `!default` on a `@forward ... with` entry: the override only applies if
    /// the downstream module does not configure the variable itself.
    pub is_default: bool,
}

/// A member name in a `@forward ... show/hide` clause. Variables keep their
/// leading `$`; functions/mixins are bare identifiers.
pub(crate) enum ForwardMember {
    /// A `$variable` name (stored without the `$`).
    Var(String),
    /// A function or mixin name.
    Name(String),
}

/// One entry in an `@import` statement.
pub(crate) enum ImportArg {
    /// A Sass import: the (unquoted) path of a partial to resolve and inline,
    /// plus the 1-based position and byte length of the quoted URL token (for
    /// the `[import]` deprecation snippet).
    Sass { path: String, pos: Pos, length: usize },
    /// A plain CSS `@import`: emitted as `@import <url> <modifiers>;`. The URL
    /// is a template (only `#{…}` interpolation resolves); the modifiers are
    /// parsed structurally so `supports(...)` and media queries re-serialize
    /// canonically (dart-sass `tryImportModifiers`).
    Css {
        url: Vec<TplPiece>,
        modifiers: Vec<ImportModifier>,
    },
}

/// One parsed `@import` modifier (dart-sass `tryImportModifiers`).
pub(crate) enum ImportModifier {
    /// A run of bare identifiers and unknown functions (`b`, `c(d)`), joined
    /// by single spaces; captured as a template (only `#{…}` resolves).
    Raw(Vec<TplPiece>),
    /// `supports(<query>)`. `declaration` is true for a bare `supports(a: b)`
    /// query, whose `Declaration` serialization already carries its own parens
    /// (`supports((a: b))` also unwraps to this); every other condition gets
    /// wrapped in one explicit pair.
    Supports {
        condition: SupportsCondition,
        declaration: bool,
    },
    /// The trailing media query list (always the final modifier).
    /// `comma_before` distinguishes `b, (c: d)` (a list continued after a
    /// bare-identifier query) from `b (c: d)` (a space-joined list start).
    Media {
        list: MediaQueryList,
        comma_before: bool,
    },
}

/// One arm of an `@if` chain. `cond == None` is the trailing `@else`.
pub(crate) struct IfBranch {
    pub cond: Option<Expr>,
    pub body: Vec<Stmt>,
}

/// A `@function` or `@mixin` definition (same shape).
pub(crate) struct Callable {
    pub name: String,
    pub params: ParamList,
    pub body: Vec<Stmt>,
}

/// A declared parameter list: positional/defaulted params plus an optional
/// trailing rest parameter (`$args...`).
pub(crate) struct ParamList {
    pub params: Vec<Param>,
    pub rest: Option<String>,
}

/// One declared parameter, with an optional default expression.
pub(crate) struct Param {
    pub name: String,
    pub default: Option<Expr>,
}

pub(crate) struct VarDecl {
    pub name: String,
    pub value: Expr,
    pub is_default: bool,
    pub is_global: bool,
    /// `Some(ns)` for a namespaced assignment `ns.$name: value`, which updates
    /// the variable in the `@use`d module bound to `ns`.
    pub namespace: Option<String>,
}

pub(crate) struct Rule {
    pub selector: Vec<TplPiece>,
    pub body: Vec<Stmt>,
    /// 1-based position where the selector text starts, for mapping a
    /// resolved-selector error column back to the source.
    pub selector_pos: crate::scanner::Pos,
    /// Expression spans (line, start col, col of `}`) of the selector's
    /// top-level `#{…}` interpolations, parallel to its `Interp` pieces —
    /// for the dual-span "error in interpolated output" diagnostic.
    pub selector_interp_spans: Vec<(u32, u32, u32)>,
    /// 1-based line of the rule's opening `{` (0 when unavailable), for the
    /// serializer's trailing-comment rule.
    pub brace_line: u32,
    /// 1-based line of the rule's closing `}` (0 when unavailable).
    pub end_line: u32,
}

/// One top-level item in a plain-CSS custom `@function`/`@mixin` body. The
/// value is captured as a template (preserving arbitrary CSS characters
/// verbatim, resolving only `#{...}`) when the property is a plain literal,
/// or as a SassScript expression when the property contains interpolation —
/// matching dart-sass's declaration parsing inside these constructs.
pub(crate) struct CssCustomItem {
    pub property: Vec<TplPiece>,
    /// `Err` holds the verbatim value template; `Ok` holds a SassScript value.
    pub value: CssCustomValue,
}

pub(crate) enum CssCustomValue {
    /// Verbatim value template (literal property): whitespace runs collapse to
    /// single spaces and `#{...}` resolves, otherwise emitted exactly.
    Raw(Vec<TplPiece>),
    /// SassScript value (interpolated property): evaluated normally.
    Script(Expr),
    /// A nested property set on an interpolated property (`#{re}sult: {b: c}`):
    /// each `(suffix, value)` child emits as `property-suffix: value`.
    Set(Vec<(Vec<TplPiece>, Expr)>),
}

pub(crate) struct Declaration {
    pub property: Vec<TplPiece>,
    pub value: Expr,
    pub important: bool,
    pub pos: Pos,
    /// 1-based line where the declaration's value ends (0 when unavailable),
    /// for the serializer's trailing-comment rule.
    pub end_line: u32,
}

/// A custom-property declaration (`--name: value`). The name and the value
/// are both templates so `#{…}` interpolation resolves at eval time; the value
/// is otherwise emitted verbatim (no SassScript evaluation).
pub(crate) struct CustomDecl {
    pub property: Vec<TplPiece>,
    pub value: Vec<TplPiece>,
    pub pos: Pos,
    /// 1-based line where the declaration's value ends (0 when unavailable),
    /// for the serializer's trailing-comment rule.
    pub end_line: u32,
}

/// A nested property set: a declaration whose value (which may be empty) is
/// followed by a `{ … }` block whose children are namespaced with the parent
/// property name joined by `-`.
pub(crate) struct PropertySet {
    pub property: Vec<TplPiece>,
    /// The optional leading value (`b: c { … }` keeps `c`; `b: { … }` is
    /// `None`). When present it is emitted as `<property>: <value>;` first.
    pub value: Option<Expr>,
    pub important: bool,
    pub body: Vec<Stmt>,
    pub pos: Pos,
}

/// One piece of an interpolated template: literal text or an embedded
/// expression (`#{...}`).
pub(crate) enum TplPiece {
    Lit(String),
    Interp(Expr),
}

/// A value expression.
pub(crate) enum Expr {
    /// Numeric literal: value + unit (`""` for unitless).
    Number(f64, String),
    /// Color literal (hex or named), parsed eagerly.
    Color(Color),
    /// Quoted string, possibly with interpolation.
    QuotedString(Vec<TplPiece>),
    /// Unquoted identifier/string, possibly with interpolation
    /// (e.g. `solid`, `sans-serif`, `col-#{$n}`).
    Ident(Vec<TplPiece>),
    /// `true` / `false`.
    Bool(bool),
    /// `null`.
    Null,
    /// `$name` variable reference.
    Var { name: String, pos: Pos },
    /// `ns.$name` — a module variable reference (e.g. `math.$pi`). Resolved by
    /// the evaluator against the used module bound to `module`.
    NsVar { module: String, name: String },
    /// The parent selector `&` used in value position. Resolves to the current
    /// resolved selector as a comma-separated list of space-separated
    /// compound-selector strings, or `null` at the document root.
    Parent,
    /// Binary arithmetic / string concatenation.
    Binary {
        op: BinOp,
        lhs: Box<Expr>,
        rhs: Box<Expr>,
        pos: Pos,
    },
    /// The `a / b` slash operator. When `slash` is true (both operands are
    /// number literals or themselves slash divisions), it produces a
    /// slash-separated value that serializes as `a/b`; otherwise it performs
    /// real division.
    Div {
        lhs: Box<Expr>,
        rhs: Box<Expr>,
        slash: bool,
        pos: Pos,
    },
    /// A `calc()` calculation whose interior is the wrapped expression.
    /// Evaluated with calc simplification rules (numeric subtrees fold,
    /// everything else is preserved verbatim).
    Calc { inner: Box<Expr> },
    /// Unary negation.
    Unary { op: UnOp, operand: Box<Expr> },
    /// Function call. `module` is the namespace of a `ns.fn(...)` call (the
    /// part before the dot); `None` for an ordinary unqualified call.
    Func {
        name: String,
        args: Vec<CallArg>,
        /// 1-based position of the function-name start (the call's primary span
        /// origin for diagnostics — `rgb(…)` points at `rgb`).
        pos: Pos,
        /// Byte length of the whole call `name(args)`, for the diagnostic caret.
        length: usize,
        module: Option<String>,
    },
    /// A function call whose name contains interpolation (`qu#{o}te(arg)`).
    /// dart-sass treats these as *plain CSS* calls: the name resolves at
    /// eval time, the arguments are evaluated, and the call serializes
    /// verbatim — never dispatched to a built-in or user function.
    InterpFunc {
        name: Vec<TplPiece>,
        args: Vec<CallArg>,
        pos: Pos,
    },
    /// A space- or comma-separated list. `bracketed` marks `[a b]`/`[a, b]`
    /// literals, which serialize wrapped in square brackets.
    List {
        items: Vec<Expr>,
        sep: ListSep,
        bracketed: bool,
    },
    /// `( expr )`.
    Paren(Box<Expr>),
    /// A map literal `(k1: v1, k2: v2)`. Disambiguated from a parenthesised
    /// expression / list by the `:` after the first key. An empty map is
    /// written `()` but parses as the empty list; only a non-empty
    /// `(k: v, …)` produces this node.
    Map(Vec<(Expr, Expr)>),
    /// `#{ expr }` used in value position — always yields an unquoted
    /// string.
    Interp(Box<Expr>),
    /// The modern CSS `if()` conditional: a `;`-separated list of clauses,
    /// each `<condition>: <value>` (or `else: <value>`). Conditions mix
    /// `sass(<expr>)` (evaluated) and `css(...)`/other "arbitrary
    /// substitution" pieces (kept verbatim). Distinct from the legacy
    /// `if($cond, $t, $f)` builtin, which routes through [`Expr::Func`].
    ModernIf(Vec<IfClause>),
}

/// One clause of a modern `if()`: an optional condition (absent for the
/// trailing `else` clause) and its value expression.
pub(crate) struct IfClause {
    /// `None` for the `else` clause; otherwise the parsed condition tree.
    pub condition: Option<IfCond>,
    pub value: Expr,
}

/// A modern `if()` condition tree. Atoms are either an evaluated
/// `sass(<expr>)` or a verbatim "raw" CSS sequence (`css(...)`, `var(...)`,
/// nested `if(...)`, interpolation, ...); combined with `not`/`and`/`or`
/// and parentheses.
pub(crate) enum IfCond {
    /// `sass(<expr>)` — evaluated for truthiness.
    Sass(Box<Expr>),
    /// One or more space-separated raw substitution tokens forming a single
    /// non-evaluable CSS condition (each token is a template so embedded
    /// `#{...}` resolves at eval time). `multi` is true when the sequence has
    /// more than one token (an "arbitrary substitution", which may not
    /// coexist with `sass()` in the same condition).
    Raw { pieces: Vec<TplPiece>, multi: bool },
    /// `not <cond>`.
    Not(Box<IfCond>),
    /// `<cond> and <cond>` (one or more, left-associative chain).
    And(Vec<IfCond>),
    /// `<cond> or <cond>` (one or more, left-associative chain).
    Or(Vec<IfCond>),
    /// `( <cond> )`.
    Paren(Box<IfCond>),
}

/// A call argument, optionally named (`$name: value`).
///
/// A `splat` argument (`$list...`) is expanded at call time: a list spreads
/// into positional arguments and a map spreads into keyword arguments. A
/// splat argument never carries a `name`.
pub(crate) struct CallArg {
    pub name: Option<String>,
    pub value: Expr,
    pub splat: bool,
}

#[derive(Clone, Copy)]
pub(crate) enum BinOp {
    Add,
    Sub,
    Mul,
    Mod,
    Eq,
    Neq,
    Lt,
    Gt,
    Le,
    Ge,
    And,
    Or,
    /// The single-`=` Microsoft-filter operator, valid only inside a function
    /// argument list (`alpha(opacity=80)`). It is the lowest-precedence value
    /// operator: both sides are evaluated and joined with `=` (no spaces) as an
    /// unquoted string.
    SingleEq,
}

#[derive(Clone, Copy)]
pub(crate) enum UnOp {
    Neg,
    /// Unary `+`: numeric identity (`+5` -> `5`); on any other operand an
    /// unquoted string prefixed with `+` (`+foo` -> `+foo`), matching
    /// dart-sass's `unaryPlus`.
    Plus,
    Not,
}

// ---- media queries -----------------------------------------------------

/// A `@media` prelude: a comma-separated list of media queries.
pub(crate) struct MediaQueryList {
    pub queries: Vec<MediaQuery>,
}

/// One media query: either a media-type form (`[not|only]? <type> [and
/// <cond>]*`) or a condition-only form (`<cond> [<and|or> <cond>]*`).
pub(crate) enum MediaQuery {
    /// `[modifier]? <type> [and <cond>]*`. The modifier (`not`/`only`,
    /// already lowercased) and type may contain interpolation.
    Type {
        modifier: Option<Vec<TplPiece>>,
        mtype: Vec<TplPiece>,
        conditions: Vec<MediaInParens>,
    },
    /// `<cond> [<and|or> <cond>]*`.
    Condition {
        conditions: Vec<MediaInParens>,
        conjunction: Conjunction,
    },
}

/// `and` / `or`, the conjunction joining media conditions.
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum Conjunction {
    And,
    Or,
}

/// A single "media in parens": one operand of a media condition.
pub(crate) enum MediaInParens {
    /// `(<feature>)` — serialized wrapped in parentheses. Boxed because a
    /// [`MediaFeature`] is large relative to the other variants.
    Feature(Box<MediaFeature>),
    /// `not <media-in-parens>` — serialized without wrapping parentheses.
    Not(Box<MediaInParens>),
    /// `(<cond> <and|or> <cond>…)` — a parenthesised sub-condition group,
    /// serialized keeping its parentheses.
    Group {
        conditions: Vec<MediaInParens>,
        conjunction: Conjunction,
    },
    /// Raw interpolation spliced into the query verbatim, e.g.
    /// `(a) and #{"(b) and (c)"}`.
    Interp(Expr),
}

/// The interior of a single `(...)` media feature.
pub(crate) enum MediaFeature {
    /// `(<name>)` or `(<name>: <value>)`.
    Decl { name: Expr, value: Option<Expr> },
    /// A range feature: `<first> <op1> <second> [<op2> <third>]`.
    Range {
        first: Expr,
        op1: String,
        second: Expr,
        rest: Option<(String, Expr)>,
    },
}

// ---- @supports conditions ----------------------------------------------

/// A parsed `@supports` condition (dart-sass `SupportsCondition`). The
/// condition is re-serialized canonically at eval time: `(a: b)` declaration
/// spacing, stripped trivia comments, normalized whitespace.
pub(crate) enum SupportsCondition {
    /// `(<name>: <value>)`. `custom` is true when the name is a literal
    /// unquoted identifier starting with `--`; its value is then captured
    /// verbatim (a template) and serialized with no space after the colon.
    Declaration {
        name: Expr,
        value: Box<SupportsValue>,
        custom: bool,
    },
    /// `not <condition-in-parens>`.
    Negation(Box<SupportsCondition>),
    /// `<left> <and|or> <right>`.
    Operation {
        left: Box<SupportsCondition>,
        right: Box<SupportsCondition>,
        op: Conjunction,
    },
    /// A lone `#{…}` interpolation spliced in verbatim (unquoted).
    Interpolation(Expr),
    /// `<name>(<arguments>)` — a `supports()`-style function call. Both the
    /// name and the arguments are templates captured verbatim.
    Function {
        name: Vec<TplPiece>,
        arguments: Vec<TplPiece>,
    },
    /// `(<anything>)` — an arbitrary parenthesised value captured verbatim
    /// (the declaration grammar didn't match, e.g. `(a b)` or `(a !&$)`).
    Anything(Vec<TplPiece>),
}

/// The right-hand side of a `@supports` declaration condition.
pub(crate) enum SupportsValue {
    /// A normal SassScript expression value (`(a: 1 + 1)` -> `(a: 2)`).
    Expr(Expr),
    /// A custom-property value captured verbatim as a template (only `#{…}`
    /// interpolation resolves); serialized with no space after the colon.
    Raw(Vec<TplPiece>),
}