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"));
}
}