tsv_css 0.8.0

CSS formatter and parser compatible with Svelte's parseCss — part of tsv
Documentation
use super::identifiers::IDENT_CONTINUE_LUT;
use super::token::{Token, TokenKind};
use crate::number::{continues_unit, exponent_len};

/// Read a CSS number, percentage, or dimension into `*dst`
/// Numbers: 42, 1.5, .5, -42, +1.5
/// Percentages: 50%, -100%
/// Dimensions: 16px, 1.5em, -2.5rem
///
/// Written through the caller's slot rather than returned: a returned `Token` comes back
/// through a stack temporary the caller then copies into its own.
pub(crate) fn read_number_into(source: &str, pos: &mut usize, dst: &mut Token) {
    let start = *pos;
    let bytes = source.as_bytes();
    let len = bytes.len();
    let mut p = start;

    // The mantissa and exponent grammar is entirely ASCII — digits, `.`, `e`/`E`, sign,
    // `%` — so it reads straight off the byte slice. The former per-char `chars().next()`
    // decoded a `char` for every digit of every number in the stylesheet.

    // Read optional sign
    if matches!(bytes.get(p), Some(b'-' | b'+')) {
        p += 1;
    }

    // Read integer part
    let int_start = p;
    while p < len && bytes[p].is_ascii_digit() {
        p += 1;
    }
    let has_integer_digits = p > int_start;

    // Read decimal part
    if bytes.get(p) == Some(&b'.') {
        let peek_char = source[p + 1..].chars().next();
        if peek_char.is_some_and(|ch| ch.is_ascii_digit()) {
            p += 1; // consume '.'
            while p < len && bytes[p].is_ascii_digit() {
                p += 1;
            }
        } else if has_integer_digits
            && (exponent_len(&source[p + 1..]) > 0
                || peek_char.is_none_or(|ch| !continues_unit(ch)))
        {
            // Trailing dot that belongs to the number: before an exponent
            // (`1.e1` → `1e1`) or a terminator (`;`, `)`, `,`, `%`, whitespace,
            // EOF — `1.` → `1`). A following identifier char is left alone, so
            // `1.png` (a url path, or the invalid number-dot-ident sequence) is
            // preserved verbatim rather than merged into a dimension.
            p += 1; // consume '.'
        }
    }

    // Read scientific-notation exponent: [eE][+-]?\d+
    // Consumed as part of the number so it normalizes (and isn't mistaken for a
    // dimension unit). `1em` keeps `em` as a unit because `m` is not a digit.
    if exponent_len(&source[p..]) > 0 {
        p += 1; // 'e' / 'E'
        if matches!(bytes.get(p), Some(b'+' | b'-')) {
            p += 1;
        }
        while p < len && bytes[p].is_ascii_digit() {
            p += 1;
        }
    }

    let num_end = p;
    *pos = p;

    // No validation parse: the dispatch enters here only at a digit, a `.` then a digit, or
    // a sign then a digit or a `.` and a digit, so the scan above always
    // holds a digit in the integer or fraction part, and every string it can accept —
    // `[+-]? (\d+ (\.\d*)? | \.\d+) ([eE][+-]?\d+)?` — is in Rust's `f64` grammar (an
    // out-of-range one parses to infinity rather than failing).
    debug_assert!(
        source[start..num_end].parse::<f64>().is_ok(),
        "read_number_into scanned a non-number: {:?}",
        &source[start..num_end]
    );

    // Check for percentage
    if bytes.get(p) == Some(&b'%') {
        p += 1;
        *pos = p;
        *dst = Token {
            kind: TokenKind::Percentage,
            start: start as u32,
            end: p as u32,
        };
        return;
    }

    // Check for dimension (unit). A unit is an identifier, so its body continues on the
    // same predicate `read_identifier` uses — hence the shared LUT. The *opening* test is
    // Unicode `is_alphabetic()`, so one char is decoded for it; the body then runs the
    // ASCII table and yields to the char arm at the first non-ASCII byte, leaving a
    // non-ASCII unit lexing exactly as before.
    if let Some(ch) = source[p..].chars().next()
        && (ch.is_alphabetic() || ch == '-')
    {
        let unit_start = p;

        loop {
            while p < len && IDENT_CONTINUE_LUT[bytes[p] as usize] {
                p += 1;
            }
            match source[p..].chars().next() {
                Some(ch) if ch.is_alphanumeric() || ch == '-' || ch == '_' => {
                    p += ch.len_utf8();
                }
                _ => break,
            }
        }

        let unit_len = p - unit_start;
        if unit_len > 0 {
            *pos = p;
            *dst = Token {
                kind: TokenKind::Dimension {
                    unit_len: unit_len as u8,
                },
                start: start as u32,
                end: p as u32,
            };
            return;
        }

        // Reset position if we didn't find a valid unit
        p = unit_start;
    }

    // Just a number
    *pos = p;
    *dst = Token {
        kind: TokenKind::Number,
        start: start as u32,
        end: p as u32,
    };
}