svelte-syntax 0.1.5

Lightweight syntax-layer crate for the Rust Svelte toolchain
Documentation
use std::sync::Arc;

pub fn parse_svelte_ignores(comment_data: &str) -> Box<[Arc<str>]> {
    let trimmed = comment_data.trim_start();
    let Some(rest) = trimmed.strip_prefix("svelte-ignore") else {
        return Box::default();
    };

    let mut ignores = Vec::new();
    let mut token = String::new();
    for ch in rest.chars() {
        if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' || ch == '$' {
            token.push(ch);
            continue;
        }
        if !token.is_empty() {
            push_ignore_code_variants(&token, &mut ignores);
            token.clear();
        }
    }
    if !token.is_empty() {
        push_ignore_code_variants(&token, &mut ignores);
    }

    ignores.into_boxed_slice()
}

fn push_ignore_code_variants(code: &str, ignores: &mut Vec<Arc<str>>) {
    push_unique_ignore(code, ignores);

    if let Some(replacement) = legacy_ignore_replacement(code) {
        push_unique_ignore(replacement, ignores);
        return;
    }

    if code.contains('-') {
        let normalized = code.replace('-', "_");
        push_unique_ignore(&normalized, ignores);
    }
}

fn push_unique_ignore(code: &str, ignores: &mut Vec<Arc<str>>) {
    if ignores.iter().any(|existing| existing.as_ref() == code) {
        return;
    }
    ignores.push(Arc::from(code));
}

fn legacy_ignore_replacement(code: &str) -> Option<&'static str> {
    match code {
        "non-top-level-reactive-declaration" => Some("reactive_declaration_invalid_placement"),
        "module-script-reactive-declaration" => Some("reactive_declaration_module_script"),
        "empty-block" => Some("block_empty"),
        "avoid-is" => Some("attribute_avoid_is"),
        "invalid-html-attribute" => Some("attribute_invalid_property_name"),
        "a11y-structure" => Some("a11y_figcaption_parent"),
        "illegal-attribute-character" => Some("attribute_illegal_colon"),
        "invalid-rest-eachblock-binding" => Some("bind_invalid_each_rest"),
        "unused-export-let" => Some("export_let_unused"),
        _ => None,
    }
}

pub(crate) fn find_valid_legacy_closing_tag_start(
    source: &str,
    start: usize,
    end: usize,
    tag_name: &str,
) -> Option<usize> {
    if start >= end || tag_name.is_empty() {
        return None;
    }

    let needle = format!("</{tag_name}");
    let mut search_from = start;

    while search_from < end {
        let rel = source.get(search_from..end)?.find(&needle)?;
        let candidate_start = search_from + rel;
        let next = source
            .as_bytes()
            .get(candidate_start + needle.len())
            .copied();
        if next.is_none_or(|byte| matches!(byte, b'>' | b'/' | b' ' | b'\t' | b'\n' | b'\r')) {
            return Some(candidate_start);
        }
        search_from = candidate_start.saturating_add(1);
    }

    None
}

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

    #[test]
    fn parse_svelte_ignores_keeps_legacy_and_normalized_codes() {
        let ignores = parse_svelte_ignores(
            " svelte-ignore non-top-level-reactive-declaration a11y-something-something ",
        );

        let codes = ignores.iter().map(|code| code.as_ref()).collect::<Vec<_>>();
        assert_eq!(
            codes,
            vec![
                "non-top-level-reactive-declaration",
                "reactive_declaration_invalid_placement",
                "a11y-something-something",
                "a11y_something_something",
            ]
        );
    }
}