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
// CSS value parsing
//
// Parses CSS values into structured AST (CssValue enum).
// Handles identifiers, strings, numbers/dimensions, colors, functions, and lists.

pub(super) mod colors;
pub(crate) mod cursor;
pub(super) mod dimensions;
pub(super) mod functions;
pub(super) mod lists;
pub(crate) mod operators;
pub(crate) mod parser;
pub(crate) mod scan;
pub(super) mod strings;

use crate::ast::CssValue;
use crate::escapes::{ends_with_live_hex_escape, trim_end_preserving_escape, trim_start_css};
use crate::parser::value::lists::ValueSeparator;
use bumpalo::Bump;
use tsv_lang::Span;

// The per-kind parsers the dispatch below calls. Internal to the value parser — like
// `classify_separators`, none of them is part of the crate's surface.
use colors::{parse_color, parse_color_function};

use dimensions::parse_dimension;
use functions::parse_function_arguments;
use strings::parse_string_literal;

/// Parse a CSS value into a structured CssValue
///
/// Extracts the value directly from source using the provided span, then parses
/// it using ValueParser for accurate span tracking with same-source recursion.
///
/// This ensures that nested value spans are accurate even with multiline formatting,
/// since we're working with the actual source text rather than reconstructed tokens.
///
/// # Arguments
/// * `source` - The CSS source text (may be a substring of the full document)
/// * `source_relative_span` - The span of the value relative to `source` (positions within source)
/// * `base_offset` - Offset to add to spans for absolute positions in full document
/// * `class` - The value's top-level separator class if the declaration's boundary scan
///   already derived it (`crate::parser::decl_scan`), which lets `ValueParser` skip its own
///   classifying pass over the very same bytes. `None` means "not known", never "no
///   separator" — that is `Some(ValueSeparator::None)`.
/// * `group_closes_value` - whether that scan saw the value's first parenthesized group
///   close on the value's last byte, which is the function leaf's own paren scan
///   answered in advance (see [`plain_function_parts`]). `false` means "not known".
/// * `colon_is_operator` - whether a top-level `:` in this value is a value operator,
///   which it is in a **custom property's** value and nowhere else at the top level (see
///   `operators::split_value_run`). The caller knows it from the property name.
/// * `room` - the function levels the parse may enter (the depth limit's count, one level
///   a function or parenthesized group — [`FunctionLevels`]).
///
/// # Errors
///
/// The host offset of the `(` whose function would enter past `room`, the parse
/// abandoned there.
#[expect(clippy::too_many_arguments)]
pub(super) fn parse_value_from_source<'arena>(
    source: &str,
    source_relative_span: Span,
    base_offset: u32,
    class: Option<ValueSeparator>,
    group_closes_value: bool,
    colon_is_operator: bool,
    room: u32,
    arena: &'arena Bump,
) -> Result<CssValue<'arena>, u32> {
    let value_str = source_relative_span.extract(source);
    let absolute_start = base_offset + source_relative_span.start;

    // An all-whitespace (or empty) value keeps the span it was handed.
    let Some((trimmed, leading)) = locate_value(value_str) else {
        return Ok(CssValue::Identifier {
            span: Span {
                start: absolute_start,
                end: base_offset + source_relative_span.end,
            },
        });
    };

    // The value's own span: where it starts, plus how long it is. The end needs
    // no separate trailing-whitespace offset — a span that begins at the value
    // and runs its length already excludes what follows it.
    let start = absolute_start + leading as u32;
    let absolute_span = Span {
        start,
        end: start + trimmed.len() as u32,
    };

    // The class was derived over the span the declaration scan measured, so it describes
    // this text only when the trim took nothing off either end. It normally does not (a real
    // stylesheet puts no whitespace at a value's ends, and the scan's own `value_end` is
    // already trimmed); when it does, the class is dropped and the fused pass runs as usual.
    // The group's close describes the same text and is dropped with it.
    let (class, group_closes_text) = if leading == 0 && trimmed.len() == value_str.len() {
        (class, group_closes_value)
    } else {
        (None, false)
    };

    // ValueParser re-parses the same source text, so nested value spans stay
    // accurate through its same-source recursion.
    let parser = if colon_is_operator {
        parser::ValueParser::new_custom_property(trimmed, absolute_span)
    } else {
        parser::ValueParser::new(trimmed, absolute_span)
    };
    // Every level the parse enters is a function's or a group's `(`, a byte of the text,
    // so a text no longer than the room cannot run out of it: the count is skipped, and the
    // parse runs unbounded, as it does outside any value's parse (`FunctionLevels::within`
    // never nests — no value parse reaches the declaration parser).
    if trimmed.len() <= room as usize {
        return Ok(parser.parse_scanned(arena, class, group_closes_text));
    }
    FunctionLevels::within(room, || {
        parser.parse_scanned(arena, class, group_closes_text)
    })
}

/// The function levels a value's parse may still enter, and the `(` where it ran out — the
/// depth limit's count for the one recursion the declaration parser does not see: a
/// function's argument list is parsed by re-reading its text
/// ([`parse_function_arguments`], which reaches [`parse_function_leaf`] again for a
/// function or group nested in it), and every walk of the tree recurses the same way, a
/// level a function. The value parser cannot fail, so the count rides beside it (a
/// thread-local, set for one value's parse by [`FunctionLevels::within`] — skipped for a
/// value too short to run out of the room), and a function past the room parses as
/// nothing — the declaration then refuses at the `(` it recorded.
///
/// Counting the functions the parse enters, rather than the parens the declaration's
/// boundary scan steps over, is what makes the count the walks': that scan reads a url-token
/// whole (`url(((x)))` is one token to the lexer), the value parser does not.
#[derive(Clone, Copy)]
struct FunctionLevels {
    room: u32,
    /// The host offset of the `(` the parse ran out of room at; `u32::MAX` while it has not.
    refused_at: u32,
}

thread_local! {
    static FUNCTION_LEVELS: std::cell::Cell<FunctionLevels> = const {
        std::cell::Cell::new(FunctionLevels::UNBOUNDED)
    };
}

impl FunctionLevels {
    /// Outside [`FunctionLevels::within`] (a unit test's own value parse): no bound.
    const UNBOUNDED: FunctionLevels = FunctionLevels {
        room: u32::MAX,
        refused_at: u32::MAX,
    };

    /// Run `parse` with `room` function levels to enter: its value, or the `(` where the
    /// room ran out. Every exit — a panic unwinding through `parse` included, which a
    /// binding catches and lives on past on the same thread — restores the count it found
    /// (outside any parse, unbounded).
    fn within<T>(room: u32, parse: impl FnOnce() -> T) -> Result<T, u32> {
        /// Restores the count `within` displaced when it goes out of scope.
        struct Restore(FunctionLevels);
        impl Drop for Restore {
            fn drop(&mut self) {
                FUNCTION_LEVELS.set(self.0);
            }
        }
        let _restore = Restore(FUNCTION_LEVELS.replace(FunctionLevels {
            room,
            refused_at: u32::MAX,
        }));
        let value = parse();
        let refused_at = FUNCTION_LEVELS.get().refused_at;
        if refused_at == u32::MAX {
            Ok(value)
        } else {
            Err(refused_at)
        }
    }

    /// Enter a function whose `(` sits at host offset `open`: `false` (the `(` recorded,
    /// where it is the first) where no room is left.
    #[inline]
    fn enter(open: u32) -> bool {
        let mut levels = FUNCTION_LEVELS.get();
        if levels.room == 0 {
            if levels.refused_at == u32::MAX {
                levels.refused_at = open;
                FUNCTION_LEVELS.set(levels);
            }
            return false;
        }
        levels.room -= 1;
        FUNCTION_LEVELS.set(levels);
        true
    }

    /// Leave a function [`FunctionLevels::enter`] entered.
    #[inline]
    fn leave() {
        let mut levels = FUNCTION_LEVELS.get();
        levels.room += 1;
        FUNCTION_LEVELS.set(levels);
    }
}

/// A value's text with its surrounding **CSS** whitespace removed, and how many
/// bytes of that whitespace preceded it. `None` when the value is entirely
/// whitespace.
///
/// The span a declaration hands over is, in practice, already trimmed — real
/// stylesheets do not put a whitespace byte at either end of a value — so the
/// common case answers from two byte comparisons and never walks the text.
///
/// The trim is [`trim_start_css`] / [`trim_end_preserving_escape`], **not**
/// `str::trim`: CSS whitespace is [`is_css_whitespace`](crate::whitespace::is_css_whitespace)'s five ASCII characters, so NBSP and
/// U+3000 are value *content* (`str::trim` would eat them), and the trailing
/// whitespace a `\` escape owns is content too — `50px\ ;` must keep its escaped
/// space or the backslash strands onto the `;` and the output stops parsing.
/// `trimmed` becomes the `ValueParser`'s source and its length is what the leaf
/// spans are derived from, so cutting an escape's payload here is what does it.
///
/// The fast path's guard is therefore exactly `u8::is_ascii_whitespace` — the
/// same set the trim acts on, so the two arms cannot disagree. A boundary byte
/// that is not ASCII whitespace settles the question outright, **including a
/// non-ASCII one**: no byte of a multi-byte char is ASCII whitespace, and the
/// trim would not have touched that char anyway. ⚠️ The set is
/// `u8::is_ascii_whitespace`, **not** `char::is_whitespace` (which would eat NBSP)
/// and not the lexer's `is_ascii_css_whitespace` (which adds the **vertical tab**
/// to match `parseCss`); the vertical tab is content to this trim.
fn locate_value(value_str: &str) -> Option<(&str, usize)> {
    let bytes = value_str.as_bytes();
    let settled = |b: u8| !b.is_ascii_whitespace();
    if let (Some(&first), Some(&last)) = (bytes.first(), bytes.last())
        && settled(first)
        && settled(last)
    {
        return Some((value_str, 0));
    }

    let after_leading = trim_start_css(value_str);
    let trimmed = trim_end_preserving_escape(after_leading);
    if trimmed.is_empty() {
        return None;
    }
    Some((trimmed, value_str.len() - after_leading.len()))
}

// Old parsing functions removed - replaced by ValueParser with same-source recursion
// - parse_value_string() → use parse_value_from_source() instead
// - parse_value_or_list() → handled internally by ValueParser
// See: parser::ValueParser for the new implementation

/// Parse a single CSS value (no lists).
///
/// `s` is expected already trimmed: the sole caller (`ValueParser::build_scanned_leaf`)
/// forwards a trimmed range — the fast path passes `self.text()` (the boundary
/// check confirmed it is trimmed) and the two-pass fallback passes
/// `self.text().trim()` — so no `str::trim` is repeated here.
///
/// `group_closes_run` is the declaration scan's word that the run's first parenthesized
/// group closes on its last byte ([`plain_function_parts`]); `false` is "not known".
#[inline]
pub(crate) fn parse_single_value<'arena>(
    s: &str,
    span: Span,
    colon_is_operator: bool,
    head_welds: bool,
    group_closes_run: bool,
    arena: &'arena Bump,
) -> CssValue<'arena> {
    // A whole-run function under a PLAIN name (`var(--a)`, `rgba(0, 0, 0, 0.5)`,
    // `-webkit-gradient(…)`) is one closed token to the splitter by construction: the name
    // holds no byte that ends a word, and the run ends at the group's own `)`. So it is read
    // as the leaf straight away, and the group's interior is scanned once — the split walks
    // it to find the `)` and the function arm walks it again. Whatever the arm declines (a
    // `)` closing early, an unbalanced group) takes the split exactly as before.
    if let Some(paren_pos) = plain_function_paren(s.as_bytes())
        && let Some((name, args)) = plain_function_parts(s, paren_pos, group_closes_run)
    {
        debug_assert!(
            operators::split_value_run(s, colon_is_operator, head_welds, arena).is_none(),
            "a plain-name function run splits: {s:?}"
        );
        return parse_function_leaf(s, span, paren_pos, name, args, arena);
    }

    // An operator run is several members, not a leaf — `12px/1.5` is a dimension, a `/`
    // and a dimension, and every classification below then runs on each of them. Asked
    // first because the run's own shape is what the rest of this function would otherwise
    // misread: `(1.5)(2.5)` ends in a `)` without being one function, and `1.5/2.5` starts
    // with a number without being one dimension. It answers `None` for a single-token
    // OPERAND run, which is nearly every value, so the leaf path below is untouched.
    if let Some(tokens) = operators::split_value_run(s, colon_is_operator, head_welds, arena) {
        let mut values = bumpalo::collections::Vec::with_capacity_in(tokens.len(), arena);
        for token in tokens {
            let member = &s[token.start..token.end];
            let member_span = Span {
                start: span.start + token.start as u32,
                end: span.start + token.end as u32,
            };
            values.push(if token.is_operator {
                CssValue::Operator { span: member_span }
            } else {
                // Every operand is re-classified from scratch, which is what gets the
                // number after the operator to its normalizer — but only its LEAF kind is
                // still open: the splitter already said this member is one operand token,
                // and asking it again over the member's own bytes would re-derive that
                // verdict against a position the member no longer sits at (the `-` in
                // `1.5*-` is a whole run of its own here, and a whole-run `-` IS an
                // operator — see `operators::ValueToken::is_operator`).
                parse_leaf_value(member, member_span, arena)
            });
        }
        return CssValue::List {
            values: values.into_bump_slice(),
            span,
        };
    }

    parse_leaf_value(s, span, arena)
}

/// Classify one already-delimited value token — a string, a function or colour function,
/// a colour, a dimension, or the opaque identifier everything else falls back to.
///
/// The half of [`parse_single_value`] that runs once the run's member boundaries are
/// settled, so it asks nothing about position: a member reaches it straight from the
/// splitter, and a single-token value reaches it as the whole run. Every classifier
/// declines an empty `s`, so an empty value is the identifier over its (empty) span.
fn parse_leaf_value<'arena>(s: &str, span: Span, arena: &'arena Bump) -> CssValue<'arena> {
    // String literal
    if let Some(val) = parse_string_literal(s, span, arena) {
        return val;
    }

    // Function call or color function.
    //
    // The last byte decides whether either scan below can pay off at all:
    // `extract_function_parts` accepts a value as a function only when the matching `)` is
    // its final byte — the same postcondition the printer's argument-list bounds are read
    // off — so a value ending in anything else has no function to find, and the two walks
    // that would establish that (the search for the opening `(`, then the matching-paren
    // scan from it) answer a question one byte comparison has already answered. Most of a
    // stylesheet's leaves — `red`, `0`, `1px`, `#fff` — end in something else. It is the
    // same kind of pre-filter the vocabulary sets put in front of their member compare
    // (`crate::keyword_set`): it refuses only what cannot match, so it can skip work but
    // never change an answer.
    //
    // The refusal is graded by the scan it skips, so every CSS fixture re-proves it.
    let bytes = s.as_bytes();
    debug_assert!(
        bytes.last() == Some(&b')')
            || bytes
                .iter()
                .position(|&b| b == b'(')
                .and_then(|paren_pos| extract_function_parts(s, paren_pos))
                .is_none(),
        "a value not ending in `)` was read as a function: {s:?}"
    );
    // The search for the `(` is a byte-position scan rather than `str::find(char)`, whose
    // CharSearcher state machine outweighs a direct byte loop on this hot per-value-token
    // path (equivalent: `(` is ASCII, self-synchronising).
    if bytes.last() == Some(&b')')
        && let Some(paren_pos) = bytes.iter().position(|&b| b == b'(')
        && let Some((name, args)) = extract_function_parts(s, paren_pos)
    {
        return parse_function_leaf(s, span, paren_pos, name, args, arena);
    }

    // Hex or named color
    if let Some(color) = parse_color(s) {
        return CssValue::Color { color, span };
    }

    // Dimension (number with optional unit)
    if let Some(dim) = parse_dimension(s, span) {
        return dim;
    }

    // Default to identifier (text recovered from `span` at print time)
    CssValue::Identifier { span }
}

/// The offset of the `(` after a PLAIN name opening `bytes` — an ASCII letter or `_` (or a
/// `-` before one), then letters, digits, `_` and `-` — when `bytes` also ends in a `)`.
/// `None` for every other shape, which [`parse_single_value`] leaves to the split.
pub(in crate::parser) fn plain_function_paren(bytes: &[u8]) -> Option<usize> {
    if bytes.last() != Some(&b')') {
        return None;
    }
    // The nameless group (`(1.5 - a)`) is the same closed token with an empty name.
    if bytes[0] == b'(' {
        return Some(0);
    }
    let head = usize::from(bytes[0] == b'-');
    if !bytes
        .get(head)
        .is_some_and(|b| b.is_ascii_alphabetic() || *b == b'_')
    {
        return None;
    }
    for (i, &b) in bytes.iter().enumerate().skip(head + 1) {
        match b {
            b'(' => return Some(i),
            b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-' => {}
            _ => return None,
        }
    }
    None
}

/// [`extract_function_parts`] for a run [`plain_function_paren`] accepted: its name is the
/// whole of `s[..paren_pos]` and already a valid one (plain ASCII word content, no
/// whitespace, no escape), so only the paren scan is left to ask.
///
/// And for a declaration's whole value the boundary scan has already asked it.
/// `group_closes_run` is `ValueFacts::group_closes_value` — the value's first group closes
/// on its last byte — handed over only with a stated leaf class, so the run is the text
/// that scan walked, holding no comment, no escape outside a string and no url-token.
/// `plain_function_paren` puts nothing but name bytes ahead of `paren_pos`, so the `(`
/// there is the first one the scan nested on, and its close is the run's last byte without
/// [`scan::matching_close_paren`] walking the group again. `false` claims nothing, and the
/// scan runs as it would for any other leaf.
///
/// The two scans' models differ in ways the stated class rules out — the boundary scan
/// steps a comment and a url-token whole and declines on an escape, where this one reads
/// a `/*` as content and steps an escape — and agree on what is left: parens nest, a
/// string is opaque, `[]` and `{}` move no paren depth. The claim is held to the scan it
/// replaces in every debug build.
fn plain_function_parts(s: &str, paren_pos: usize, group_closes_run: bool) -> Option<(&str, &str)> {
    let close = if group_closes_run {
        debug_assert_eq!(
            scan::matching_close_paren(s, paren_pos),
            Some(s.len() - 1),
            "the declaration scan closed {s:?}'s group at its end, the paren scan did not"
        );
        s.len() - 1
    } else {
        scan::matching_close_paren(s, paren_pos)?
    };
    let parts = (close == s.len() - 1).then(|| (&s[..paren_pos], &s[paren_pos + 1..close]));
    debug_assert_eq!(
        parts,
        extract_function_parts(s, paren_pos),
        "a plain-name function read two ways: {s:?}"
    );
    parts
}

/// The function or colour-function leaf `s` is, given the `name` and `args` its
/// `(` at `paren_pos` splits it into.
fn parse_function_leaf<'arena>(
    s: &str,
    span: Span,
    paren_pos: usize,
    name: &str,
    args: &str,
    arena: &'arena Bump,
) -> CssValue<'arena> {
    // Try color function first
    if let Some(color) = parse_color_function(name, args) {
        return CssValue::Color { color, span };
    }
    // Fall back to generic function
    // Calculate accurate span for arguments (inside parens)
    // The args string starts at: paren_pos + 1 (after opening paren)
    // The args string ends at: paren_pos + 1 + args.len()
    let args_start = paren_pos + 1;
    let args_span = Span {
        start: span.start + args_start as u32,
        end: span.start + args_start as u32 + args.len() as u32,
    };
    // A comma **closing** the argument list (`var(--a,)`, `rgb(1, 2, 3,)`) terminated
    // no argument, so it is not one — CSS Syntax 3's comma-split stops once the input
    // is empty. It is still authored content the printer must spell back, and it reads
    // that off the source between the last argument and the `)`
    // (`printer::declarations::list_has_closing_comma`) rather than from a synthesized
    // empty argument here: an *escaped* comma (`var(--b, x\,)`) is content inside the
    // last argument, and a synthesized one would double it.
    //
    // The name is a slice of `s`, so it is a slice of the source: hand the printer
    // its span rather than a copy of its bytes (span-for-verbatim). `name` itself
    // still answers `parse_color_function` above, at parse time.
    //
    // Its offset is read off the two heads rather than threaded out of the split,
    // which keeps that tuple at four words (returning it as a fifth cost ~288 bytes
    // of `.text` and a little of the lever).
    //
    // ⚠️ It does NOT buy back the frame. This is the CSS value parser's own
    // recursion, and the name's location has to live across `parse_function_arguments`
    // below, so `build_scanned_leaf`'s frame grows 16 bytes and `calc(calc(…))` loses ~1,157 levels of
    // its depth budget. Three spellings were measured — the fifth tuple element, this
    // one, and hoisting the recursive call above the struct expression — and all three
    // read exactly that number. It is the shape's price, not a spelling's; don't spend
    // a session re-spelling it.
    let name_start = name.as_ptr() as usize - s.as_ptr() as usize;
    let name_span = Span {
        start: span.start + name_start as u32,
        end: span.start + (name_start + name.len()) as u32,
    };
    // one level of the depth limit's count, the argument list's recursion; past the room
    // the function parses as nothing, the declaration refusing it ([`FunctionLevels`])
    if !FunctionLevels::enter(span.start + paren_pos as u32) {
        return CssValue::Identifier { span };
    }
    let args = parse_function_arguments(args, args_span, arena);
    FunctionLevels::leave();
    CssValue::Function {
        name_span,
        args,
        span,
    }
}

/// Extract function name and arguments, validating balanced parentheses.
/// Both returned strings borrow from `s`, and neither is kept: the caller reads the name's
/// **offset** off the two heads and stores a `name_span` (span-for-verbatim, so an escaped
/// spelling survives), and `args` is re-parsed.
///
/// `Some` means the whole of `s` is the function — the matching close paren is its last
/// byte — which is what lets the printer bound the argument list at `span.end - 1`
/// (`build_value_function_doc`'s closing-comma read). It is also why `parse_leaf_value`
/// can refuse on `s`'s last byte alone: the close paren this returns on *is* that byte, so a
/// value ending in anything else is never a function.
///
/// ⚠️ **`paren_pos` must address a `(`** — `parse_leaf_value` derives it from
/// `position(|&b| b == b'(')` and `plain_function_parts` hands on the one
/// `plain_function_paren` found, so the first byte [`scan::matching_close_paren`] reads
/// always opens the run, which is what lets its unsigned depth start at zero. A violated precondition trips that walk's
/// `debug_assert`, rather than quietly returning a wrong span.
///
/// ⚠️ **That leading search is quote- and escape-blind, and [`is_function_name`] is what keeps
/// it sound.** A `(` can be *content* in the name region — inside a glued string (`'x'fn(a)`)
/// or an escape (`a\(b(c)`) — and the search stops on it either way. The name validation
/// refuses both, for two different reasons, and neither is a special case:
///
/// - a quote is not word content — the oracle tokenizes a string on its own and prints it as
///   a separate member (`'x' fn(1.5)`), so `'x'fn` is no one word;
/// - the byte before an escaped `(` is the escape's own `\`, so `name_part` ends in a
///   **dangling** `\` — not a valid escape (§4.3.7 needs a code point after it), and so not
///   word content either.
///
/// So a mis-located `(` always yields a name the validation rejects, and the value falls back
/// to an opaque identifier printed verbatim. That is lossless, and on both shapes it is what
/// prettier emits too (it unbalances on the escaped paren and splits the glued string).
/// ⚠️ The dangling-`\` half is **wider than the agreement**: where that escaped `(` happens to
/// *balance* at the value's end, prettier reads its own one-byte word `\` as the name and
/// normalizes inside (`a\(2.50)` → `a\(2.5)`). Keeping the search blind is worth that cell;
/// `css/values/functions/escaped_delimiter_name_prettier_divergence` records it.
fn extract_function_parts(s: &str, paren_pos: usize) -> Option<(&str, &str)> {
    // CSS whitespace only (CSS Syntax 3 §4.2), like every other value-boundary trim: a
    // Unicode `str::trim` would cut a non-ASCII space (an NBSP) out of the name's span, and
    // the printer emits the name from that span, so the character would be dropped. Kept in
    // `name_part`, it fails the validation below and the value stays an opaque identifier.
    let name_region = trim_start_css(&s[..paren_pos]);
    let mut name_part = trim_end_preserving_escape(name_region);

    // ⚠️ A hex escape's optional whitespace **terminator** belongs to the escape (§4.3.7),
    // so a name ending in a live one owns the character the trim just took — the space in
    // `c\41 (1px)` is inside the ident sequence, which therefore ends flush with the `(` and
    // makes the whole run one `<function-token>`. Give it back, the same restore the
    // property name against its colon already makes (`\41 : red`). The printer emits the
    // name from this span, so without it the author's spelling is rewritten to `c\41(1px)`.
    // Not folded into `trim_end_preserving_escape`, which restores an *open* escape's
    // payload (a trailing `\`): at every other site it serves, what follows the trimmed text
    // is the value's own end, where prettier drops the terminator and tsv matches it.
    //
    // Only one character, and only the escape's own: any further whitespace is a real gap,
    // which would have split the value in two before this scan ever saw it.
    if name_part.len() < name_region.len()
        && matches!(name_region.as_bytes()[name_part.len()], b' ' | b'\t')
        && ends_with_live_hex_escape(name_part)
    {
        name_part = &name_region[..=name_part.len()];
    }

    // An **empty** name is the parenthesized group (`(1.5)`, `(100vw - 1.5px)`) — CSS Syntax 3
    // §"component value" calls it a `()`-block, a *simple block* whose associated token is the
    // `(`, and gives it the same payload a function has ("a value consisting of a list of
    // component values"): only the name/token tells the two apart. So its members parse and
    // reach the ordinary number / colour / string / dimension printers, which is also
    // prettier's model — `parseValue` gives a bare `(…)` a `function` node whose `value` is
    // `''`. Read as an opaque identifier instead, the group switched every value-level rule
    // off for its whole interior. The name validation is asked only when there is a name to
    // ask about, so nothing else widens.
    //
    // ⚠️ An empty name can only ever BE the group's own opener, which is what keeps the
    // escape-blind search for `paren_pos` above sound in this arm too: `s` reaches here
    // trimmed, so `s[..paren_pos]` trims to nothing only when `paren_pos` is 0. A `(` found
    // inside an escape or a string always has bytes before it, so it always yields a
    // *non-empty* name — and one the validation rejects, per the ⚠️ on the search.
    //
    // The other two associated tokens are deliberately NOT read this way: `[a]` and `{a}` are
    // left opaque, which is where prettier's own value parser leaves them (`[1.50]` keeps its
    // number on both sides, and a `{}`-block is the whole-value form css-syntax-3 restricts to
    // custom properties — `css/values/variables/block_value_svelte_prettier_divergence`).
    if !name_part.is_empty() && !is_function_name(name_part) {
        return None;
    }

    // Where the run closes is the value scanners' shared question, not this one's: a paren
    // inside a quoted string or inside an escape closes nothing, and a walk that counted
    // them would balance the function somewhere other than its real end. See
    // [`scan::matching_close_paren`], which also carries the hop's rung note, the refused
    // pre-test, and why a block comment is deliberately NOT stepped over.
    let close_pos = scan::matching_close_paren(s, paren_pos)?;

    // Closing paren must be at end of string
    if close_pos != s.len() - 1 {
        return None;
    }

    let args = &s[paren_pos + 1..close_pos];
    Some((name_part, args))
}

/// Whether `name` is a function NAME — the **word** postcss-values-parser reads ahead of a
/// `(`, which is this layer's oracle rather than CSS Syntax 3's.
///
/// ⚠️ Deliberately **not** the spec's ident sequence. A `<function-token>`'s name is one
/// (§"Consume an ident-like token") and an ident code point is only a letter, a digit, `-`,
/// `_` or the non-ASCII set (§"ident code point"), so `aa/`, `50%` and `aa!` are no names to
/// the spec at all — they are to postcss-values-parser, the parser behind prettier's CSS
/// printer, whose `<function-token>` is a `word` token glued to a `(`. The value layer
/// already reads that oracle rather than the spec's tokenizer: the operator run (`12px/1.5`)
/// and the nameless `()`-block are the same transcription. Reading the spec's production here
/// instead dropped every such value onto the verbatim [`CssValue::Identifier`] path, which
/// switches **every** value normalization off for the whole declaration (`aa/(2.50)` kept its
/// `2.50` where `aaf(2.50)` normalized). [`is_value_word_code_point`] holds the class.
///
/// An escape is word content to both readings (§"Consume an ident sequence"), so `\66 n`,
/// `v\61 r` and `a\28 b` are the names `fn`, `var` and `a(b` and each is a real function —
/// the printer then reads the name from its span, so the author's spelling survives. A `\`
/// that starts no valid escape (a dangling one at the name's end, or one before a newline) is
/// not word content and refuses, which is what keeps [`extract_function_parts`]'s
/// escape-blind search for the opening `(` sound — see the ⚠️ there.
///
/// ⚠️ A word whose text **is a number** is a name here and a `value-number` to the oracle,
/// which prints it spaced from the group beside it — a cataloged divergence
/// (`css/values/functions/number_shaped_name_prettier_divergence`), not a miss.
fn is_function_name(name: &str) -> bool {
    // A welded `+` heads the word it welded onto (`+a(2.5)` is the function `+a` to
    // postcss, and the split hands it here whole); an interior `+` never reaches this
    // position, since the split ends a word at it.
    let name = match name.strip_prefix('+') {
        Some(rest) if !rest.is_empty() => rest,
        _ => name,
    };
    // The escape-free name, which is every name a stylesheet really holds: `\` is not word
    // content, so this pass already stops on the first one and the walk below is entered
    // only for a name that has one (or is genuinely not a name at all).
    if name.chars().all(is_value_word_code_point) {
        return true;
    }
    escaped_name_is_value_word(name)
}

/// A code point that is **word content** to the value oracle. Named once so the fast pass
/// above and the escape walk below cannot drift — a name accepted by one and refused by the
/// other would be a function whose recognition depended on whether it carried an escape.
///
/// Stated as what it **excludes**, because that is the finite half and the only half the
/// oracle fixes. Everything else is word content — whether it continues
/// postcss-values-parser's own word (`wordEndRe`'s complement: `#`, `$`, `%`, `.`, `/`, `<`,
/// `=`, `?`, `^`) or becomes a word token of its own that `splitWord` glues back on (`!`,
/// `&`, `>`, `|`, `~`, `` ` ``). The two are indistinguishable here, because tsv takes the
/// whole region as one name and emits it from its span, and prettier prints the glued run
/// back the same way. `[` and `]` are word content for the same reason plus one more: a
/// `[…]` block's interior is word TEXT to the oracle, not a descended-into group
/// (`a[1.50]c(2.50)` → `a[1.50]c(2.5)` — the bracketed number is kept and only the argument
/// normalizes), so the whole region really is the name. Three exclusions, each measured:
///
/// - **the oracle reads across it** — whitespace (`<whitespace-token>`), `,` (comma), `:`
///   (colon), `@` (atword), `*` and `+` (operator), `'` and `"` (string), `{` and `}` (their
///   own token kinds). The first eight put a space or a separate member in prettier's output
///   (`aa @(1.5)`, `aa * (1.5)`, `'x' fn(1.5)`), so a name holding one would print glued
///   against it. The braces are the subtler pair: prettier glues them, but it tokenizes the
///   word *between* them on its own and canonicalizes it, so taking the region whole would
///   freeze content the oracle rewrites (`a{1.50}c(2.50)` → `a{1.5}c(2.5)`, reachable on a
///   custom property). That is a run-split question, not a name-grammar one.
/// - **it cannot occur in the region** — `;`, which terminates the declaration on both sides
///   before any value is read, and `(`, which *is* `paren_pos` by construction.
/// - **soundness** — `\` and `)`. A dangling `\` is what makes
///   [`extract_function_parts`]'s escape-blind search for the opening `(` sound (see the ⚠️
///   there), and prettier unbalances on a stray `)` and emits the declaration verbatim
///   (`a)(2.50)`), so tsv refuses it too. ⚠️ The bracket pair opens no third hazard:
///   `matching_close_paren` is quote- and escape-aware but not bracket-aware, yet a `)`
///   inside a `[…]` keeps the refusal above and an *unbalanced* `(` inside one is rejected by
///   both parsers at the declaration level (`a[(]c(2.50)`), so the reachable names are
///   bracket-closed and paren-free.
///
/// ⚠️ The non-ASCII half is the **lexer's** (`is_non_ascii_identifier_codepoint`, every code
/// point at or above U+00A0) rather than the exclusion above, which is wider than anything
/// measured up there. This position re-reads a token the lexer already read, so a narrower
/// class refuses a name the lexer accepted: `a°` is one identifier to the lexer, and reading
/// it as a non-name dropped the whole value onto the verbatim `Identifier` path, silently
/// switching off every value normalization for that declaration (`a°(1.50)` kept its `1.50`
/// where `aé(1.50)` normalized).
fn is_value_word_code_point(c: char) -> bool {
    if c.is_ascii() {
        return !matches!(
            c,
            // the oracle reads across it
            ' ' | '\t' | '\n' | '\r' | '\u{b}' | '\u{c}'
                | ','
                | ':'
                | '@'
                | '*'
                | '+'
                | '\''
                | '"'
                | '{'
                | '}'
                // cannot occur in the region
                | ';'
                | '('
                // soundness
                | '\\'
                | ')'
        );
    }
    crate::lexer::is_non_ascii_identifier_codepoint(c)
}

/// The escaped tail of [`is_function_name`] — outlined and cold, so the escape walk stays off
/// every function value's path, the same split the printer's own escape-spelled-name question
/// takes (`printer::values::function_name_is`).
#[cold]
#[inline(never)]
fn escaped_name_is_value_word(name: &str) -> bool {
    let bytes = name.as_bytes();
    let mut i = 0;
    while i < bytes.len() {
        if bytes[i] == b'\\' {
            // Steps the escape whole, terminator included, so `a\29 b` is one word and not
            // a word, a space and another word. The single definition every value scanner
            // shares (`crate::escapes::escape_len`).
            let Some(len) = crate::escapes::escape_len(name, i) else {
                return false;
            };
            i += len;
            continue;
        }
        // `i` is a char boundary: the loop only ever advances by a whole escape or a whole
        // char, and both lengths are measured in code points.
        let Some(ch) = name[i..].chars().next() else {
            return false;
        };
        if !is_value_word_code_point(ch) {
            return false;
        }
        i += ch.len_utf8();
    }
    true
}

#[cfg(test)]
mod function_name_tests {
    use super::{extract_function_parts, is_function_name};

    /// An escape is word content (CSS Syntax 3 §"Consume an ident sequence" — the one half
    /// the oracle and the spec agree on), whatever it spells.
    #[test]
    fn an_escape_is_word_content() {
        // Escape-free, then the hex spellings, then the literal ones — including the four
        // whose payload byte is what prettier's own tokenizer reads as structure.
        for name in [
            "fn", "a-b_1", "fé", r"\66 n", r"v\61 r", r"a\28 b", r"a\29 b", r"\31 fn", r"a\)b",
            r#"a\"b"#, r"a\'b", r"a\\b", r"a\ b", r"\-fn",
        ] {
            assert!(is_function_name(name), "{name:?} is a word");
        }
    }

    /// The name is postcss-values-parser's **word**, not the spec's ident sequence, and this
    /// is the whole character class in one table: every printable ASCII code point, graded
    /// against the exclusion set `is_value_word_code_point` declares. Exhaustive on purpose —
    /// a hand-written pair of "these are in, these are out" lists leaves whatever neither
    /// mentions ungraded, which is how the first cut of this rule shipped with `[` refused for
    /// a reason that turned out to be false.
    ///
    /// `\` is the one code point this sweep cannot grade, because mid-name it is not a bare
    /// character at all: `aa\bb` holds the valid escape `\b`, which IS word content. Its
    /// exclusion is about the **dangling** spelling and
    /// [`a_dangling_backslash_is_not_word_content`] owns it.
    #[test]
    fn the_word_class_is_exactly_this_exclusion_set() {
        /// Every printable-ASCII code point `is_value_word_code_point` refuses, with the
        /// group it belongs to. Grouped exactly as the predicate's doc groups them, so a
        /// change to one has to move the other.
        const EXCLUDED: &[char] = &[
            // the oracle reads across it
            ' ', ',', ':', '@', '*', '+', '\'', '"', '{', '}',
            // cannot occur in the region
            ';', '(',
            // soundness (`\` is graded by the dangling-backslash test, not here)
            ')',
        ];
        for c in (0x20u8..=0x7e).map(char::from) {
            if c == '\\' {
                continue;
            }
            let want = !EXCLUDED.contains(&c);
            // Three positions, so a positional rule cannot creep into what is a pure
            // character class — save the ONE positional rule the name owns: a `+` at
            // the head is the weld (`+a(2.5)` is the function `+a` to postcss), and
            // the run splitter never hands an interior or trailing `+` here.
            for name in [format!("aa{c}bb"), format!("{c}aa"), format!("aa{c}")] {
                let want = want || (c == '+' && name == "+aa");
                assert_eq!(
                    is_function_name(&name),
                    want,
                    "{name:?}: expected word content = {want}"
                );
            }
        }
        assert!(is_function_name("+aa"), "a welded `+` heads a name");
        assert!(
            !is_function_name("+"),
            "a lone `+` is an operator, never a name"
        );
        // The ASCII whitespace the sweep's printable range does not reach.
        for c in ['\t', '\n', '\r', '\u{b}', '\u{c}'] {
            assert!(
                !is_function_name(&format!("aa{c}bb")),
                "{c:?} is whitespace"
            );
        }
        // Non-ASCII keeps the lexer's threshold rather than the exclusion set, so that a name
        // the lexer read as one identifier is not refused here (`a°(1.50)`).
        assert!(is_function_name("a\u{b0}"), "U+00B0 is at or above U+00A0");
        assert!(!is_function_name("a\u{85}"), "U+0085 is below it");
    }

    /// The *reasons* behind the exclusion set above, one cell each — the table says which
    /// code points, these say why, on the whole-name shapes the oracle was measured at.
    #[test]
    fn the_exclusion_set_reasons() {
        // Read across: the oracle tokenizes a string, a comma, a colon, an atword or an
        // operator on its own and prints it as a separate member (`'x' fn(1.5)`, `aa @(1.5)`).
        for name in ["'x'fn", "\"x\"fn", "a b", "a,b", "a:b", "a@b", "a*b", "a+b"] {
            assert!(!is_function_name(name), "{name:?} is printed separately");
        }
        // The braces are glued but tokenized apart, and the oracle canonicalizes the word
        // between them (`a{1.50}c(2.50)` → `a{1.5}c(2.5)`), so a region holding one would
        // freeze content prettier rewrites. A `;` cannot reach here at all.
        for name in ["a;b", "a{b", "a}b", "a{1.50}c"] {
            assert!(!is_function_name(name), "{name:?} is read across");
        }
        // Soundness: prettier unbalances on a stray `)` and emits the value verbatim.
        assert!(!is_function_name("a)b"), "a stray close paren");
        // A `[…]` block's interior IS word text to the oracle, kept verbatim while only the
        // argument normalizes (`a[1.50]c(2.50)` → `a[1.50]c(2.5)`), so the whole region is
        // the name — the pair's asymmetry with the braces above is the oracle's.
        for name in ["a[b]c", "a[1.50]c", "a[b]"] {
            assert!(is_function_name(name), "{name:?} is a word");
        }
    }

    /// The whole seam: which values read as a function, and where the name and arguments
    /// are cut. `parse_leaf_value` enters on the value's first `(` byte, so the tests do too.
    #[test]
    fn the_first_paren_byte_locates_the_function_or_refuses_it() {
        fn parts(s: &str) -> Option<(&str, &str)> {
            let open = s.bytes().position(|b| b == b'(')?;
            extract_function_parts(s, open)
        }
        // An escape-spelled name is a function like any other.
        assert_eq!(parts(r"\66 n(.10)"), Some((r"\66 n", ".10")));
        assert_eq!(parts(r"a\29 b(.10)"), Some((r"a\29 b", ".10")));
        assert_eq!(parts(r"a\)b(.10)"), Some((r"a\)b", ".10")));
        // ⚠️ An escaped `(` is the one payload the blind search stops on. The name then
        // ends in a dangling `\` and refuses, so the value stays a verbatim identifier —
        // which is also what prettier emits, by unbalancing.
        assert_eq!(parts(r"a\(b(.10)"), None);
        assert_eq!(parts(r"a\(b(c))"), None);
        // A quote is not ident content either, so a glued string refuses the same way.
        assert_eq!(parts("'x'fn(.10)"), None);
        // The close paren must be the value's last byte (the printer bounds the argument
        // list at `span.end - 1` on the strength of it).
        assert_eq!(parts("fn(a)b"), None);
        assert_eq!(parts("fn(a"), None);
    }
}

#[cfg(test)]
mod value_span_tests {
    use super::parse_value_from_source;
    use bumpalo::Bump;
    use tsv_lang::Span;

    /// The span `parse_value_from_source` gives the value it parsed.
    fn value_span(source: &str, start: u32, end: u32) -> Span {
        let arena = Bump::new();
        parse_value_from_source(
            source,
            Span { start, end },
            0,
            None,
            false,
            false,
            256,
            &arena,
        )
        .expect("within the room")
        .span()
    }

    /// The already-trimmed fast path must agree with the trimming path on where
    /// the value starts and ends. No corpus can grade this: real declaration
    /// spans arrive pre-trimmed (200K+ of them without one whitespace byte at
    /// either end), so the trimming path is only ever reached by inputs a
    /// stylesheet does not contain — and a span error inside it would sail
    /// through every fixture and corpus diff in the repo.
    #[test]
    fn trims_the_span_the_same_either_way() {
        // Pre-trimmed (the fast path) — the span comes back untouched.
        assert_eq!(value_span("red", 0, 3), Span { start: 0, end: 3 });
        assert_eq!(value_span("a red b", 2, 5), Span { start: 2, end: 5 });

        // CSS whitespace at either end must come off the span identically.
        for (source, span, want) in [
            (" red", (0, 4), (1, 4)),
            ("red ", (0, 4), (0, 3)),
            ("  red  ", (0, 7), (2, 5)),
            ("\tred\t", (0, 5), (1, 4)),
            ("\nred\n", (0, 5), (1, 4)),
            ("\rred\r", (0, 5), (1, 4)),
            ("\x0cred\x0c", (0, 5), (1, 4)),
        ] {
            assert_eq!(
                value_span(source, span.0, span.1),
                Span {
                    start: want.0,
                    end: want.1
                },
                "value span for {source:?}"
            );
        }
    }

    /// Only [`is_css_whitespace`](crate::whitespace::is_css_whitespace)'s five ASCII characters are whitespace to CSS.
    /// Everything else at a boundary is value **content** and keeps its span — which
    /// is also what lets the fast path settle a non-ASCII byte outright.
    ///
    /// The vertical tab is the sharp edge: `char::is_whitespace` accepts it (so
    /// `str::trim` would eat it) and so does the *lexer*'s `is_ascii_css_whitespace`
    /// (which matches `parseCss`), but this trim is the CSS class and must not.
    #[test]
    fn non_css_whitespace_is_content() {
        for (source, span) in [
            ("\u{a0}red\u{a0}", (0, 7)),     // NBSP
            ("\u{3000}red\u{3000}", (0, 9)), // ideographic space
            ("\x0bred\x0b", (0, 5)),         // vertical tab
            ("é", (0, 2)),
        ] {
            assert_eq!(
                value_span(source, span.0, span.1),
                Span {
                    start: span.0,
                    end: span.1
                },
                "value span for {source:?}"
            );
        }
    }

    /// An ASCII space adjacent to a kept non-ASCII space still comes off — only the
    /// ASCII byte is trimmed, the NBSP stays as content.
    #[test]
    fn ascii_space_trims_beside_a_kept_nbsp() {
        assert_eq!(value_span(" \u{a0}red", 0, 6), Span { start: 1, end: 6 });
    }

    /// The trailing whitespace a `\` escape owns is content: trimming it strands
    /// the backslash onto whatever follows (`50px\ ;` → `50px\;`, the `;` now
    /// escaped) and the declaration no longer parses. Padding past the escaped
    /// character is ordinary whitespace and still goes.
    #[test]
    fn an_escapes_payload_stays_in_the_span() {
        assert_eq!(value_span(r"50px\ ", 0, 6), Span { start: 0, end: 6 });
        assert_eq!(value_span(r"50px\   ", 0, 8), Span { start: 0, end: 6 });
        // An even backslash run is a completed `\\` — the space is just padding.
        assert_eq!(value_span(r"50px\\ ", 0, 7), Span { start: 0, end: 6 });
    }
}

#[cfg(test)]
mod function_levels_tests {
    use super::{FUNCTION_LEVELS, FunctionLevels};

    /// A panic inside a value's parse — which a binding catches, the thread living on —
    /// leaves no room behind: the next parse on the thread starts unbounded.
    #[test]
    fn a_panic_leaves_the_count_unbounded() {
        let caught = std::panic::catch_unwind(|| {
            FunctionLevels::within(3, || {
                assert!(FunctionLevels::enter(0));
                panic!("a parse panicked mid-function");
            })
        });
        assert!(caught.is_err());
        let levels = FUNCTION_LEVELS.get();
        assert_eq!(
            (levels.room, levels.refused_at),
            (u32::MAX, u32::MAX),
            "the count it found is restored"
        );
    }
}