svgpack_parser 0.1.0

SVG/XML parsing helpers for svgpack.
Documentation
use anyhow::Context;
use quick_xml::events::Event;
use quick_xml::Reader;
use regex::Regex;

#[derive(Debug, Clone)]
pub struct SvgMeta {
    pub view_box: Option<String>,
    pub inner: String,
}

pub fn parse_meta(input: &str) -> anyhow::Result<SvgMeta> {
    let mut reader = Reader::from_str(input);
    reader.config_mut().trim_text(true);
    let mut buf = Vec::new();
    let mut view_box = None;
    let mut root_seen = false;

    loop {
        match reader
            .read_event_into(&mut buf)
            .context("failed parsing svg document")?
        {
            Event::Start(e) if e.name().as_ref() == b"svg" => {
                root_seen = true;
                for attr in e.attributes().flatten() {
                    if attr.key.as_ref() == b"viewBox" {
                        let value = String::from_utf8_lossy(attr.value.as_ref()).to_string();
                        view_box = Some(value);
                    }
                }
                break;
            }
            Event::Eof => break,
            _ => {}
        }
        buf.clear();
    }

    if !root_seen {
        anyhow::bail!("input is not a valid svg document");
    }

    let inner = extract_inner_svg(input)?;
    Ok(SvgMeta { view_box, inner })
}

pub fn extract_inner_svg(input: &str) -> anyhow::Result<String> {
    let re = Regex::new(r"(?s)<svg\b[^>]*>(.*?)</svg>").context("failed compiling regex")?;
    let captures = re
        .captures(input)
        .ok_or_else(|| anyhow::anyhow!("could not locate <svg> root element"))?;
    Ok(captures
        .get(1)
        .map(|m| m.as_str().trim().to_string())
        .unwrap_or_default())
}

pub fn extract_title(input: &str) -> Option<String> {
    let re = Regex::new(r"(?s)<title\b[^>]*>(.*?)</title>").ok()?;
    re.captures(input)
        .and_then(|c| c.get(1))
        .map(|m| m.as_str().trim().to_string())
}

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

    #[test]
    fn parse_meta_extracts_viewbox_and_inner() {
        let svg = r#"<svg viewBox="0 0 24 24"><title>A</title><path d="M0 0"/></svg>"#;
        let meta = parse_meta(svg).expect("valid svg should parse");
        assert_eq!(meta.view_box.as_deref(), Some("0 0 24 24"));
        assert!(meta.inner.contains("<path"));
    }

    #[test]
    fn parse_meta_rejects_non_svg_input() {
        let err = parse_meta("<div>nope</div>").expect_err("non-svg should fail");
        assert!(err
            .to_string()
            .contains("input is not a valid svg document"));
    }

    #[test]
    fn extract_title_finds_trimmed_content() {
        let svg = r#"<svg><title>  Check icon  </title></svg>"#;
        assert_eq!(extract_title(svg).as_deref(), Some("Check icon"));
    }

    #[test]
    fn parse_meta_handles_declaration_and_doctype() {
        let svg = r#"<?xml version="1.0"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN">
<svg viewBox="0 0 16 16"><path d="M0 0"/></svg>"#;
        let meta = parse_meta(svg).expect("svg with declaration should parse");
        assert_eq!(meta.view_box.as_deref(), Some("0 0 16 16"));
    }
}