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
//! CSS escape sequence decoding.

use crate::whitespace::is_css_whitespace;
use bumpalo::Bump;
use bumpalo::collections::String as BumpString;
use std::borrow::Cow;

/// Decode CSS escape sequences in a string.
///
/// Converts CSS escape sequences to their actual character values:
/// - `\\` → `\` (escaped backslash)
/// - `\"` → `"` (escaped quote)
/// - `\'` → `'` (escaped quote)
/// - `\n` → newline (escaped newline)
/// - `\XXXXXX` → Unicode character (1-6 hex digits)
///
/// Returns a borrowed `Cow` for the common escape-free string (no allocation);
/// only an input that actually contains `\` is decoded into an owned `String`.
///
/// This is the decoder's **reference** form. The parser decodes a string literal
/// through [`decode_escape_sequences_in`], which builds the same text in the document's
/// arena and is held to this one (a debug assertion at its call site, and a generated
/// test here); [`decodes_to_ascii_ignore_case`] is held to it the same way. Nothing in a
/// release build calls it.
pub(crate) fn decode_escape_sequences(source: &str) -> Cow<'_, str> {
    if !source.contains('\\') {
        return Cow::Borrowed(source);
    }

    let mut result = String::with_capacity(source.len());
    let mut chars = source.chars().peekable();

    while let Some(ch) = chars.next() {
        if ch == '\\' {
            if let Some(&next_ch) = chars.peek() {
                if next_ch.is_ascii_hexdigit() {
                    // Unicode escape sequence — accumulate the 1-6 hex digits directly
                    // into a `u32` (no throwaway `String` + `from_str_radix`). Six
                    // digits max (0xFFFFFF), so `code_point` never overflows. The outer
                    // `is_ascii_hexdigit` guard guarantees at least one digit is read.
                    let mut code_point: u32 = 0;
                    for _ in 0..6 {
                        match chars.peek().and_then(|d| d.to_digit(16)) {
                            Some(d) => {
                                code_point = code_point * 16 + d;
                                chars.next();
                            }
                            None => break,
                        }
                    }
                    // Optional whitespace terminator — the same CSS whitespace set
                    // `escape_len` uses (a hand-rolled subset here once omitted `\r`
                    // and form feed, disagreeing with every other escape scanner).
                    if let Some(&ws) = chars.peek()
                        && is_css_whitespace(ws)
                    {
                        chars.next();
                    }
                    if let Some(c) = char::from_u32(code_point) {
                        result.push(c);
                    }
                } else {
                    // Simple escape: \X → X
                    result.push(next_ch);
                    chars.next();
                }
            } else {
                // Trailing backslash
                result.push('\\');
            }
        } else {
            result.push(ch);
        }
    }

    Cow::Owned(result)
}

/// [`decode_escape_sequences`] for a string literal's payload, decoded straight into the
/// document's arena: `None` for the escape-free string (one byte search and no
/// allocation), else the decoded text, built where it will live rather than in a
/// heap `String` copied over afterwards.
#[inline]
pub(crate) fn decode_escape_sequences_in<'arena>(
    source: &str,
    arena: &'arena Bump,
) -> Option<&'arena str> {
    if !source.as_bytes().contains(&b'\\') {
        return None;
    }
    Some(decode_escapes_into_arena(source, arena))
}

/// The decode behind [`decode_escape_sequences_in`], for a source that holds a `\` —
/// [`decode_escape_sequences`]'s rules, arm for arm. Out of line, so the walk's locals
/// stay out of the value parser's frame.
#[inline(never)]
fn decode_escapes_into_arena<'arena>(source: &str, arena: &'arena Bump) -> &'arena str {
    // An escape never decodes to more bytes than it spells: `\` plus n hex digits names a
    // code point below 16^n, whose UTF-8 is at most n bytes for n <= 4 and four bytes for
    // five or six digits; a simple escape drops its `\`; a trailing `\` is itself.
    let mut result = BumpString::with_capacity_in(source.len(), arena);
    let mut chars = source.chars().peekable();

    while let Some(ch) = chars.next() {
        if ch != '\\' {
            result.push(ch);
            continue;
        }
        let Some(&next_ch) = chars.peek() else {
            // Trailing backslash
            result.push('\\');
            break;
        };
        if !next_ch.is_ascii_hexdigit() {
            // Simple escape: \X → X
            result.push(next_ch);
            chars.next();
            continue;
        }
        // Hex escape: one to six digits, one optional CSS-whitespace terminator, and
        // nothing at all for a value that names no scalar (a surrogate, past U+10FFFF).
        let mut code_point: u32 = 0;
        for _ in 0..6 {
            match chars.peek().and_then(|d| d.to_digit(16)) {
                Some(d) => {
                    code_point = code_point * 16 + d;
                    chars.next();
                }
                None => break,
            }
        }
        if let Some(&ws) = chars.peek()
            && is_css_whitespace(ws)
        {
            chars.next();
        }
        if let Some(c) = char::from_u32(code_point) {
            result.push(c);
        }
    }

    result.into_bump_str()
}

/// Whether `c` is a character [`trim_end_preserving_escape`] must hand back to an escape
/// as its **payload**.
///
/// §4.3.7's final branch escapes *any* code point, so an escape's payload is not restricted
/// to whitespace at all — `\<NBSP>` is a perfectly good escape. But this predicate only ever
/// sees a character the trim was about to *eat*, and that trim removes **CSS** whitespace
/// only ([`is_css_whitespace`]'s five — §4.2's three plus the `<CR>` and form feed §3.3 would
/// have folded away). Of those five, a newline is the one shape §4.3.4 excludes from starting
/// an escape — leaving exactly the space and the tab.
///
/// So an NBSP payload needs no rescue here: it is content the trim never touches.
fn is_escapable_whitespace(c: char) -> bool {
    matches!(c, ' ' | '\t')
}

/// The byte length of the CSS escape starting at `s[i]` (which must be a `\`), or
/// `None` when that `\` does not start a valid escape.
///
/// Per CSS Syntax 3 §4.3.7 "Consume an escaped code point":
///
/// - a **hex** escape takes up to **six** hex digits and then, optionally, **one**
///   whitespace character — the *terminator*, which belongs to the escape rather than
///   separating it from what follows. So `\41 2px` is the single ident `A2px`, not
///   `\41` followed by `2px`, and a splitter that treats that space as a separator
///   tears one token in half;
/// - any other code point is escaped literally (`\ ` is a space, `\,` is a comma),
///   consuming exactly that one code point;
/// - a `\` before a newline is **not** an escape at all (§4.3.4) — `None`.
///
/// The single definition of "how far does this escape reach", so every scanner that walks
/// a CSS value — the whitespace normalizer, the top-level splitter, the value cursor —
/// steps over escapes identically and none of them can mistake an escape's interior for
/// structure.
///
/// A CRLF terminator is taken as the `\r` only, leaving the `\n` outside the escape. That
/// is one code point short of §3.3's input preprocessing (which folds CRLF to a single
/// newline; tsv does not run it — see [`is_css_whitespace`]),
/// but harmless rather than merely tolerable: the leftover `\n` is then ordinary
/// whitespace, and every consumer of this function normalizes a whitespace run to one
/// space, so `\41<CR><LF>2px` still emits `\41 2px` — the same ident `A2px`, which
/// re-parses identically. The safety comes from that downstream join, not from this arm.
pub(crate) fn escape_len(s: &str, i: usize) -> Option<usize> {
    debug_assert_eq!(s.as_bytes().get(i), Some(&b'\\'));
    let rest = s.get(i + 1..)?;
    let first = rest.chars().next()?;
    if matches!(first, '\n' | '\r' | '\x0C') {
        return None;
    }
    if !first.is_ascii_hexdigit() {
        return Some(1 + first.len_utf8());
    }
    let hex = rest
        .bytes()
        .take(6)
        .take_while(u8::is_ascii_hexdigit)
        .count();
    let terminator = rest[hex..]
        .chars()
        .next()
        .filter(|&c| is_css_whitespace(c))
        .map_or(0, char::len_utf8);
    Some(1 + hex + terminator)
}

/// If a CSS escape starts at byte `i`, the index just past it (so `s[i..end]` is the whole
/// escape, its optional whitespace terminator included); otherwise `None`.
///
/// The guarded, end-returning face of [`escape_len`], and the one spelling of the
/// `\`-then-`escape_len` pair every scanner that walks a value, prelude or name uses.
/// `None` covers both "not an escape here" and "a `\` that starts none" (trailing, or
/// before a newline — §4.3.4); a caller that must tell those apart tests the byte itself
/// first. `escape_len` itself asserts its `\`, so this guard is what makes the
/// position-agnostic call legal.
pub(crate) fn escape_span_at(s: &str, i: usize) -> Option<usize> {
    if s.as_bytes().get(i) != Some(&b'\\') {
        return None;
    }
    Some(i + escape_len(s, i)?)
}

/// Does `s`, decoded as [`decode_escape_sequences`] would decode it, equal `kw` ASCII
/// case-insensitively?
///
/// The allocation-free spelling of
/// `decode_escape_sequences(s).as_bytes().eq_ignore_ascii_case(kw)`: a byte walk that steps
/// over each escape by [`escape_len`], comparing a verbatim byte as itself and an escape as
/// the UTF-8 bytes of the code point it spells, so no `String` is built and no `Cow` dropped.
/// That keeps a decoder out of the one printer question that reads an escape-spelled
/// identifier (function-name recognition, where `\75 rl(` must read as `url`).
///
/// Byte-for-byte the decoder's verdict, including where the two scanners disagree on what
/// an escape *is*: `escape_len` refuses a `\` before a newline (§4.3.4) where the decoder
/// takes it as a simple escape of the newline, and a trailing `\` is the decoder's literal
/// backslash — both spelled here the decoder's way, since the decoder is the oracle (and a
/// debug build asks it on every call). A hex escape naming no scalar value (a surrogate, past
/// U+10FFFF) decodes to nothing on both sides. A `kw` that is not ASCII compares through the
/// decoded code point's UTF-8 bytes.
pub(crate) fn decodes_to_ascii_ignore_case(s: &str, kw: &[u8]) -> bool {
    let verdict = decoded_bytes_eq_ignore_ascii_case(s, kw);
    debug_assert_eq!(
        verdict,
        decode_escape_sequences(s)
            .as_bytes()
            .eq_ignore_ascii_case(kw)
    );
    verdict
}

/// The walk behind [`decodes_to_ascii_ignore_case`].
fn decoded_bytes_eq_ignore_ascii_case(s: &str, kw: &[u8]) -> bool {
    /// Take `text`'s bytes off the front of `kw`, ASCII case-insensitively.
    fn take(kw: &mut std::slice::Iter<'_, u8>, text: &[u8]) -> bool {
        text.iter()
            .all(|b| kw.next().is_some_and(|k| k.eq_ignore_ascii_case(b)))
    }

    let bytes = s.as_bytes();
    let mut kw = kw.iter();
    let mut i = 0;
    while let Some(&b) = bytes.get(i) {
        if b != b'\\' {
            // a verbatim byte compares as itself — a non-ASCII code point's UTF-8 bytes
            // included, which is exactly how the decoded text's would
            if !take(&mut kw, &[b]) {
                return false;
            }
            i += 1;
            continue;
        }
        let Some(len) = escape_len(s, i) else {
            // `\` before a newline (a simple escape of it to the decoder — one ASCII byte,
            // since only `\n` / `\r` / form feed refuse here) or a trailing `\` (its
            // literal backslash)
            let escaped = match bytes.get(i + 1) {
                Some(next) => {
                    i += 2;
                    std::slice::from_ref(next)
                }
                None => {
                    i += 1;
                    b"\\"
                }
            };
            if !take(&mut kw, escaped) {
                return false;
            }
            continue;
        };
        let body = &bytes[i + 1..i + len];
        i += len;
        if body.first().is_some_and(u8::is_ascii_hexdigit) {
            // the body is up to six hex digits (`escape_len` counted them) plus an optional
            // whitespace terminator, which is not a digit and so contributes nothing
            let code_point = body
                .iter()
                .filter_map(|&d| char::from(d).to_digit(16))
                .fold(0, |cp, d| cp * 16 + d);
            let mut utf8 = [0u8; 4];
            if let Some(c) = char::from_u32(code_point)
                && !take(&mut kw, c.encode_utf8(&mut utf8).as_bytes())
            {
                return false;
            }
        } else if !take(&mut kw, body) {
            // a literally escaped code point is its own bytes
            return false;
        }
    }
    kw.next().is_none()
}

/// Trim trailing **CSS** whitespace, but never the whitespace a CSS **escape** owns.
///
/// Two rules, and the first is the one that makes the second small.
///
/// **Trim only CSS whitespace** (tab, newline, form feed, carriage return, space — ASCII, and
/// exactly what [`is_css_whitespace`] answers; §4.2 plus the two §3.3 folds, see there). `str::trim_end` follows Unicode
/// `White_Space`, which would also strip NBSP, U+3000 and friends — and those are ordinary
/// **content** to CSS: `parseCss` and prettier both keep them inside the token, so eating
/// one silently renames a class (`.a<NBSP>, .b` → `.a,`) or rewrites a value
/// (`translate(a<NBSP>)` → `translate(a)`).
///
/// **Never take an escape's payload.** `\` + whitespace is a valid escape whose escaped
/// code point *is* that whitespace (§4.3.4, and §4.3.7 "Consume an escaped code point",
/// whose final branch returns the code point itself), so the trailing space in
/// `width: 50px\ ;` is content too. Trimming it strands the backslash, which then escapes
/// whatever follows — the declaration's `;`, a function's `)`, a selector's `,` — and the
/// output no longer parses. Exactly one character is therefore kept when an odd-length
/// backslash run precedes it (an even run is a completed `\\`); anything past the escaped
/// one is ordinary padding and still goes.
///
/// Because the trim is CSS-only, the payload can only ever be a space or a tab: a newline
/// is the one shape §4.3.4 excludes from being an escape at all, and every *other*
/// whitespace character is never eaten in the first place. There is no "restore what the
/// Unicode trim took" case, because nothing takes it.
///
/// The single definition of this rule for a CSS slice. The parser's value/argument spans,
/// the printer's whitespace normalizer and top-level splitter, the selector leaf/pseudo
/// printers, the at-rule prelude, and `url()` trimming all route through it, so they cannot
/// drift. Its "open escape" question is `tsv_lang`'s
/// [`ends_with_open_escape`](tsv_lang::printing::ends_with_open_escape), which the doc
/// renderer's line-break trim also asks under the CSS render entry
/// (`tsv_lang::doc::arena_print_css_doc_into`) — the same payload kept where a break lands
/// right after it, so the two trims cannot drift either.
///
/// This is **not** the lexer's escape-terminator rule (`lexer::identifiers`), which is
/// Unicode-wide because its oracle is `parseCss`'s `\s`-based regex. Different question,
/// different oracle — see [`escape_len`] and the comment there.
#[inline]
pub(crate) fn trim_end_preserving_escape(s: &str) -> &str {
    // `str::trim_ascii_end` IS the CSS class: [`is_css_whitespace`] is exactly
    // `char::is_ascii_whitespace`, five single-byte members, so the byte loop answers every
    // input the char searcher did — and the common ask (nothing to trim, or one space) is a
    // few instructions inline instead of a call into a decoding searcher. Only an ask that
    // trimmed something can owe an escape its payload back, and that is the cold body.
    let trimmed = s.trim_ascii_end();
    debug_assert_eq!(trimmed, s.trim_end_matches(is_css_whitespace));
    if trimmed.len() == s.len() {
        return trimmed;
    }
    restore_escape_payload(s, trimmed)
}

/// The arm [`trim_end_preserving_escape`] hands a trimmed input to: give an open escape
/// back its one payload character.
#[cold]
#[inline(never)]
fn restore_escape_payload<'a>(s: &'a str, trimmed: &'a str) -> &'a str {
    if !tsv_lang::printing::ends_with_open_escape(trimmed.as_bytes()) {
        return trimmed;
    }
    match s[trimmed.len()..].chars().next() {
        Some(c) if is_escapable_whitespace(c) => &s[..trimmed.len() + c.len_utf8()],
        _ => trimmed,
    }
}

/// Trim leading **CSS** whitespace — the leading-side counterpart of
/// [`trim_end_preserving_escape`], and needed for the same reason: `str::trim_start` is
/// Unicode-wide and would eat an NBSP that is content. No escape can own a *leading*
/// whitespace (an escape's payload always follows its `\`), so there is nothing to restore.
#[inline]
pub(crate) fn trim_start_css(s: &str) -> &str {
    let trimmed = s.trim_ascii_start();
    debug_assert_eq!(trimmed, s.trim_start_matches(is_css_whitespace));
    trimmed
}

/// Does `name` end with a **live** CSS hex escape (`\` + 1..=6 hex digits)?
///
/// Such an escape consumes a single following whitespace as its *terminator*, and §4.3.7
/// makes that whitespace part of the escape — so it is inside the identifier token, not
/// beside it. A trim that takes it off is reaching inside a token, and the two readers that
/// re-emit such a name have to put it back: a property name against its colon
/// (`\41 : red`), and a function name against its `(` (`c\41 (1px)`, where the ident
/// sequence ends flush with the paren and the whole run is one `<function-token>`).
/// [`trim_end_preserving_escape`] does not cover this — it restores the payload of an
/// *open* escape (a trailing `\`), and a hex escape's backslash is not trailing.
///
/// A literal char after the escape (`ab\44 cd`) or an escaped backslash (`\\41`) does not
/// end with a live escape. **Live** means the escape still *needs* its terminator: one that
/// already carries it (`\41 `) is complete, so re-emitting a separator would double it —
/// hence the final hex-digit check, which answers that case rather than relying on every
/// caller to have trimmed first.
///
/// Walks forward through [`escape_span_at`] rather than scanning backward for hex digits:
/// that is the crate's single definition of how far an escape reaches, so an escaped backslash
/// (`\\41` — a literal `\` followed by the ordinary text `41`) falls out of the walk instead
/// of needing its own parity/lookback rule here.
pub(crate) fn ends_with_live_hex_escape(name: &str) -> bool {
    let bytes = name.as_bytes();
    let mut i = 0;
    let mut ends_with_escape = false;
    while i < bytes.len() {
        if let Some(end) = escape_span_at(name, i) {
            // A hex escape is the one whose first payload byte is a hex digit — and it is
            // still *live* only if it ends ON a hex digit, i.e. `escape_len` found no
            // whitespace terminator to swallow.
            ends_with_escape = bytes[i + 1].is_ascii_hexdigit()
                && end == bytes.len()
                && bytes[end - 1].is_ascii_hexdigit();
            i = end;
            continue;
        }
        ends_with_escape = false;
        i += 1;
    }
    ends_with_escape
}

/// Both ends at once: [`trim_start_css`] then [`trim_end_preserving_escape`] — the CSS
/// counterpart of `str::trim`, and the one spelling every value-boundary trim takes (a
/// value's text, a function's name and arguments, a color channel, a `url()` payload, a
/// selector leaf). A site that reaches for `str::trim` here is a drop site for a non-ASCII
/// space, which is content.
#[inline]
pub(crate) fn trim_css(s: &str) -> &str {
    trim_end_preserving_escape(trim_start_css(s))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_decode_escape_sequences_simple() {
        assert_eq!(decode_escape_sequences(r"\\"), r"\");
        assert_eq!(decode_escape_sequences(r#"\""#), r#"""#);
        assert_eq!(decode_escape_sequences(r"\'"), r"'");
    }

    /// The predicate both name readers ask before restoring an escape's terminator — a
    /// property name against its colon, a function name against its `(`.
    #[test]
    fn a_live_hex_escape_is_one_still_owed_its_terminator() {
        assert!(ends_with_live_hex_escape(r"c\41"));
        assert!(ends_with_live_hex_escape(r"\41"));
        assert!(ends_with_live_hex_escape(r"c\4")); // one digit is a hex escape too
        assert!(ends_with_live_hex_escape(r"c\41abc")); // six digits, all hex
        // Already terminated: the escape carries its whitespace, so nothing is owed.
        assert!(!ends_with_live_hex_escape("c\\41 "));
        // Ended by a literal, so the escape closed on its own.
        assert!(!ends_with_live_hex_escape(r"c\41x"));
        // A literal escape takes exactly one code point and needs no terminator.
        assert!(!ends_with_live_hex_escape(r"c\g"));
        // An escaped backslash is a completed `\\` followed by ordinary text.
        assert!(!ends_with_live_hex_escape(r"c\\41"));
        // No escape at all.
        assert!(!ends_with_live_hex_escape("c41"));
        assert!(!ends_with_live_hex_escape(""));
    }

    #[test]
    fn test_decode_escape_sequences_unicode() {
        // Simple ASCII letter (2 hex digits)
        assert_eq!(decode_escape_sequences(r"\41"), "A");

        // Emoji (5 hex digits)
        assert_eq!(decode_escape_sequences(r"\1F4A9"), "💩");

        // With ONE leading zero (6 hex digits - CSS maximum)
        assert_eq!(decode_escape_sequences(r"\01F4A9"), "💩");
    }

    /// The arena decoder against the reference, over every sequence of up to three
    /// fragments drawn from each escape kind at each edge: hex escapes of one to six
    /// digits and a seventh that is content, every whitespace terminator (CRLF is two
    /// code points, so only its CR is taken), a value past U+10FFFF, a surrogate and
    /// zero, an escaped newline, simple escapes of both quotes, of a letter and of a
    /// non-ASCII character, a lone backslash (trailing when it comes last, which also
    /// makes EOF fall mid-escape), and plain neighbours, ASCII and not.
    #[test]
    fn the_arena_decoder_matches_the_reference_decoder() {
        let fragments = [
            "a",
            "é",
            "0",
            " ",
            "\\",
            "\\\\",
            "\\4",
            "\\41",
            "\\041",
            "\\0041",
            "\\00041",
            "\\000041",
            "\\0000411",
            "\\41 ",
            "\\41\t",
            "\\41\n",
            "\\41\r\n",
            "\\41\x0c",
            "\\110000",
            "\\D800",
            "\\0",
            "\\\n",
            "\\\"",
            "\\'",
            "\\g",
            "\\é",
        ];
        let arena = Bump::new();
        let mut decoded_cases = 0u32;
        let mut check = |source: &str| {
            let reference = decode_escape_sequences(source);
            match (decode_escape_sequences_in(source, &arena), &reference) {
                (None, Cow::Borrowed(_)) => {}
                (Some(decoded), Cow::Owned(reference)) => {
                    assert_eq!(decoded, reference, "{source:?}");
                    decoded_cases += 1;
                }
                (ours, _) => panic!("{source:?}: arena {ours:?}, reference {reference:?}"),
            }
        };
        check("");
        for a in fragments {
            check(a);
            for b in fragments {
                check(&format!("{a}{b}"));
                for c in fragments {
                    check(&format!("{a}{b}{c}"));
                }
            }
        }
        assert!(
            decoded_cases > 10_000,
            "{decoded_cases} escaped sources decoded"
        );
    }

    /// The comparator must return exactly what decoding and then comparing would.
    fn oracle(s: &str, kw: &[u8]) -> bool {
        decode_escape_sequences(s)
            .as_bytes()
            .eq_ignore_ascii_case(kw)
    }

    #[test]
    fn decodes_to_ascii_ignore_case_matches_the_decoder() {
        let cases: &[(&str, &[u8])] = &[
            // the `@import` prelude spellings the printer recognizes
            (r"\75 rl", b"url"),
            (r"\75rl", b"url"),
            (r"\6c ayer", b"layer"),
            (r"\73 upports", b"supports"),
            (r"\55 RL", b"url"),
            (r"\75 rl", b"URL"),
            // simple escapes, a terminator-less hex run, a trailing backslash
            (r"u\rl", b"url"),
            (r"\000075rl", b"url"),
            (r"\000075 rl", b"url"),
            (r"url\", b"url"),
            (r"url\", b"url\\"),
            // a decoded code point that is not the keyword's
            (r"\76 rl", b"url"),
            (r"\41", b"a"),
            (r"\e9", b"\xc3\xa9"),
            (r"\e9", b"e"),
            // an escape naming no scalar value decodes to nothing
            (r"\d800url", b"url"),
            (r"\110000url", b"url"),
            // length mismatches in both directions
            (r"\75 r", b"url"),
            (r"\75 rll", b"url"),
            ("", b""),
            ("", b"a"),
            // `\` + newline is a simple escape to the decoder
            ("u\\\nrl", b"u\nrl"),
        ];
        for &(s, kw) in cases {
            assert_eq!(
                decodes_to_ascii_ignore_case(s, kw),
                oracle(s, kw),
                "{s:?} vs {kw:?}"
            );
        }
        assert!(decodes_to_ascii_ignore_case(r"\75 rl", b"url"));
        assert!(!decodes_to_ascii_ignore_case(r"\76 rl", b"url"));
    }
}

#[cfg(test)]
mod escape_scan_tests {
    use super::{escape_len, trim_end_preserving_escape, trim_start_css};

    #[test]
    fn trim_keeps_an_escaped_space() {
        // The escape's payload survives; ordinary padding past it does not.
        assert_eq!(trim_end_preserving_escape(r"50px\ "), r"50px\ ");
        assert_eq!(trim_end_preserving_escape("50px\\ \t  "), r"50px\ ");
        assert_eq!(trim_end_preserving_escape("a\\\t"), "a\\\t");
    }

    #[test]
    fn trim_drops_ordinary_whitespace() {
        assert_eq!(trim_end_preserving_escape("50px   "), "50px");
        assert_eq!(trim_end_preserving_escape("50px"), "50px");
        // A hex escape's terminator is not a payload — it is trimmable here (whatever
        // follows a trimmed value re-terminates the escape).
        assert_eq!(trim_end_preserving_escape(r"\41 "), r"\41");
    }

    #[test]
    fn trim_counts_the_backslash_run_parity() {
        // Even run = a completed `\\`, so the space after it is ordinary padding.
        assert_eq!(trim_end_preserving_escape(r"a\\ "), r"a\\");
        // Odd run = the last `\` is unescaped and owns the space.
        assert_eq!(trim_end_preserving_escape(r"a\\\ "), r"a\\\ ");
        assert_eq!(trim_end_preserving_escape(r"a\\\\ "), r"a\\\\");
    }

    #[test]
    fn trim_edges() {
        assert_eq!(trim_end_preserving_escape(""), "");
        assert_eq!(trim_end_preserving_escape("   "), "");
        // The backslash run starts at index 0 — nothing precedes it.
        assert_eq!(trim_end_preserving_escape(r"\ "), r"\ ");
        assert_eq!(trim_end_preserving_escape(r"\\ "), r"\\");
        // A lone trailing backslash has no payload to keep.
        assert_eq!(trim_end_preserving_escape(r"a\"), r"a\");
    }

    #[test]
    fn trim_never_keeps_a_newline() {
        // `\` + newline is NOT a valid escape (§4.3.4), so the backslash owns nothing.
        assert_eq!(trim_end_preserving_escape("a\\\n"), "a\\");
        assert_eq!(trim_end_preserving_escape("a\\\r\n"), "a\\");
    }

    #[test]
    fn trim_leaves_non_css_whitespace_alone() {
        // NBSP / ideographic space are CSS *content*, not whitespace (§4.2) — `parseCss`
        // and prettier keep them inside the token, so the trim must never eat one. This is
        // what collapses the payload rule: an escaped NBSP needs no rescue, because nothing
        // takes it in the first place.
        assert_eq!(trim_end_preserving_escape("a\u{a0}"), "a\u{a0}");
        assert_eq!(trim_end_preserving_escape("a\u{3000}"), "a\u{3000}");
        assert_eq!(trim_end_preserving_escape("a\\\u{a0}"), "a\\\u{a0}");
        // …and ordinary CSS whitespace after it still goes.
        assert_eq!(trim_end_preserving_escape("a\u{a0}  "), "a\u{a0}");
    }

    #[test]
    fn trim_start_css_leaves_non_css_whitespace_alone() {
        assert_eq!(trim_start_css("  a"), "a");
        assert_eq!(trim_start_css("\u{a0}a"), "\u{a0}a");
        assert_eq!(trim_start_css(" \u{a0}a"), "\u{a0}a");
    }

    #[test]
    fn escape_len_identity_escapes() {
        assert_eq!(escape_len(r"\ x", 0), Some(2)); // `\ `
        assert_eq!(escape_len(r"\,y", 0), Some(2)); // `\,`
        assert_eq!(escape_len(r"\)b", 0), Some(2)); // `\)`
        // A multi-byte escaped code point is consumed whole.
        assert_eq!(escape_len("\\\u{a0}b", 0), Some(3));
    }

    #[test]
    fn escape_len_hex_escapes_swallow_one_terminator() {
        // Up to SIX hex digits, then optionally ONE whitespace terminator.
        assert_eq!(escape_len(r"\41 2px", 0), Some(4)); // `\41 ` — the space belongs to it
        assert_eq!(escape_len(r"\41b", 0), Some(4)); // `\41b` — `b` is a hex digit
        assert_eq!(escape_len(r"\41z", 0), Some(3)); // `\41` — `z` is not
        assert_eq!(escape_len(r"\0000411", 0), Some(7)); // six digits max, then `1` stops it
        assert_eq!(escape_len(r"\41  x", 0), Some(4)); // only the FIRST space is the terminator
        // A newline can terminate a hex escape (it is CSS whitespace), unlike being a
        // payload.
        assert_eq!(escape_len("\\41\n", 0), Some(4));
    }

    #[test]
    fn escape_len_rejects_a_non_escape() {
        assert_eq!(escape_len("\\\n", 0), None); // `\` + newline is not an escape
        assert_eq!(escape_len("\\\r", 0), None);
        assert_eq!(escape_len(r"\", 0), None); // a lone trailing backslash
    }
}