omena-transform-passes 0.2.0

Transform pass registry and DAG planner for Omena CSS
Documentation
use std::borrow::Cow;

pub(crate) fn normalize_custom_property_name(name: &str) -> Option<&str> {
    let name = name.trim();
    if name.starts_with("--") && name.len() > 2 {
        return Some(name);
    }
    None
}

pub(crate) fn css_identifier_text_is_plain(text: &str) -> bool {
    text.chars()
        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_'))
}

pub(crate) fn is_css_ident_start(ch: char) -> bool {
    ch == '-' || ch == '_' || ch.is_ascii_alphabetic()
}

pub(crate) fn is_css_ident_continue(ch: char) -> bool {
    is_css_ident_start(ch) || ch.is_ascii_digit()
}

pub(crate) fn css_identifier_names_match(left: &str, right: &str) -> bool {
    left == right || decode_css_identifier_escapes(left) == decode_css_identifier_escapes(right)
}

pub(crate) fn decode_css_identifier_escapes(text: &str) -> Cow<'_, str> {
    if !text.contains('\\') {
        return Cow::Borrowed(text);
    }

    let mut output = String::with_capacity(text.len());
    let mut index = 0usize;
    while index < text.len() {
        let Some(ch) = text[index..].chars().next() else {
            break;
        };
        if ch != '\\' {
            output.push(ch);
            index += ch.len_utf8();
            continue;
        }

        let escape_start = index;
        index += ch.len_utf8();
        let Some(next) = text[index..].chars().next() else {
            output.push('\\');
            break;
        };
        if next == '\n' || next == '\r' || next == '\u{c}' {
            output.push_str(&text[escape_start..index + next.len_utf8()]);
            index += next.len_utf8();
            continue;
        }
        if next.is_ascii_hexdigit() {
            let hex_start = index;
            let mut hex_end = index;
            let mut digit_count = 0usize;
            while hex_end < text.len() && digit_count < 6 {
                let Some(candidate) = text[hex_end..].chars().next() else {
                    break;
                };
                if !candidate.is_ascii_hexdigit() {
                    break;
                }
                hex_end += candidate.len_utf8();
                digit_count += 1;
            }
            let codepoint = u32::from_str_radix(&text[hex_start..hex_end], 16).ok();
            if let Some(decoded) = codepoint.and_then(char::from_u32) {
                output.push(decoded);
            }
            index = hex_end;
            if let Some(terminator) = text[index..].chars().next()
                && terminator.is_ascii_whitespace()
            {
                index += terminator.len_utf8();
            }
            continue;
        }

        output.push(next);
        index += next.len_utf8();
    }

    Cow::Owned(output)
}

pub(crate) fn css_identifier_escape_sequence_end(text: &str, slash_index: usize) -> Option<usize> {
    let slash = text[slash_index..].chars().next()?;
    if slash != '\\' {
        return None;
    }
    let mut index = slash_index + slash.len_utf8();
    let next = text[index..].chars().next()?;
    if !next.is_ascii_hexdigit() {
        return Some(index + next.len_utf8());
    }

    let mut digit_count = 0usize;
    while index < text.len() && digit_count < 6 {
        let Some(candidate) = text[index..].chars().next() else {
            break;
        };
        if !candidate.is_ascii_hexdigit() {
            break;
        }
        index += candidate.len_utf8();
        digit_count += 1;
    }
    if let Some(terminator) = text[index..].chars().next()
        && terminator.is_ascii_whitespace()
    {
        index += terminator.len_utf8();
    }
    Some(index)
}