use once_cell::sync::Lazy;
use regex::Regex;
use std::collections::HashMap;
pub(crate) static REG_REFERENCES_URL: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"\burl\(#([^)]+)\)"#).unwrap());
pub(crate) static REG_REFERENCES_URL_QUOTED: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"\burl\(["']#([^"']+)["']\)"#).unwrap());
pub(crate) static REG_REFERENCES_HREF: Lazy<Regex> = Lazy::new(|| Regex::new(r"^#(.+?)$").unwrap());
pub(crate) static REG_REFERENCES_BEGIN: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(\w+)\.[a-zA-Z]").unwrap());
pub(crate) const GENERATE_ID_CHARS: &[char] = &[
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
];
pub(crate) const REFERENCES_PROPS: &[&str] = &[
"clip-path",
"color-profile",
"fill",
"filter",
"marker-end",
"marker-mid",
"marker-start",
"mask",
"stroke",
"style",
];
#[derive(Debug, Clone)]
pub struct IdGenerator {
current: Vec<usize>,
}
impl Default for IdGenerator {
fn default() -> Self {
Self::new()
}
}
impl IdGenerator {
pub fn new() -> Self {
Self { current: vec![0] }
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> String {
let mut result = String::new();
for &idx in &self.current {
result.push(GENERATE_ID_CHARS[idx]);
}
let mut carry = true;
for i in (0..self.current.len()).rev() {
if carry {
self.current[i] += 1;
if self.current[i] >= GENERATE_ID_CHARS.len() {
self.current[i] = 0;
} else {
carry = false;
}
}
}
if carry {
self.current.push(0);
}
result
}
}
pub(crate) fn find_references(attr_name: &str, attr_value: &str) -> Vec<String> {
let mut ids = Vec::new();
if attr_name == "href" || attr_name.ends_with(":href") {
if let Some(captures) = REG_REFERENCES_HREF.captures(attr_value) {
if let Some(id) = captures.get(1) {
ids.push(id.as_str().to_string());
}
}
return ids;
}
if attr_name == "begin" {
for captures in REG_REFERENCES_BEGIN.captures_iter(attr_value) {
if let Some(id) = captures.get(1) {
ids.push(id.as_str().to_string());
}
}
return ids;
}
if REFERENCES_PROPS.contains(&attr_name) || attr_name == "style" {
for captures in REG_REFERENCES_URL.captures_iter(attr_value) {
if let Some(id) = captures.get(1) {
ids.push(id.as_str().to_string());
}
}
for captures in REG_REFERENCES_URL_QUOTED.captures_iter(attr_value) {
if let Some(id) = captures.get(1) {
ids.push(id.as_str().to_string());
}
}
}
ids
}
pub(crate) fn update_reference_value(value: &str, id_mappings: &HashMap<String, String>) -> String {
let mut result = value.to_string();
result = REG_REFERENCES_HREF
.replace_all(&result, |caps: ®ex::Captures| {
if let Some(id) = caps.get(1) {
if let Some(new_id) = id_mappings.get(id.as_str()) {
return format!("#{}", new_id);
}
}
caps[0].to_string()
})
.to_string();
result = REG_REFERENCES_URL
.replace_all(&result, |caps: ®ex::Captures| {
if let Some(id) = caps.get(1) {
if let Some(new_id) = id_mappings.get(id.as_str()) {
return format!("url(#{})", new_id);
}
}
caps[0].to_string()
})
.to_string();
result = REG_REFERENCES_URL_QUOTED
.replace_all(&result, |caps: ®ex::Captures| {
if let Some(id) = caps.get(1) {
if let Some(new_id) = id_mappings.get(id.as_str()) {
let full_match = &caps[0];
let quote = if full_match.contains('"') { '"' } else { '\'' };
return format!("url({}#{}{})", quote, new_id, quote);
}
}
caps[0].to_string()
})
.to_string();
result = REG_REFERENCES_BEGIN
.replace_all(&result, |caps: ®ex::Captures| {
if let Some(id) = caps.get(1) {
if let Some(new_id) = id_mappings.get(id.as_str()) {
return caps[0].replacen(id.as_str(), new_id, 1);
}
}
caps[0].to_string()
})
.to_string();
result
}