use anyhow::Result;
use once_cell::sync::Lazy;
use regex::Regex;
use std::collections::HashMap;
use tracing::{debug, trace};
static REFERENCE_TAG: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"<([a-zA-Z]+[0-9]+[a-zA-Z0-9_]*)>").expect("valid reference tag pattern")
});
pub fn extract_references(text: &str) -> Result<(String, HashMap<String, String>)> {
let mut refs = HashMap::new();
let mut cleaned = text.to_string();
let opening_regex = &*REFERENCE_TAG;
let mut blocks_to_remove: Vec<(usize, usize)> = Vec::new();
let mut scan_position = 0;
let mut json_depth = 0usize;
let mut in_json_string = false;
let mut escaped = false;
for opening_cap in opening_regex.captures_iter(text) {
let tag_name = &opening_cap[1];
let opening_match = opening_cap.get(0).unwrap();
let opening_start = opening_match.start();
let opening_end = opening_match.end();
if blocks_to_remove
.last()
.is_some_and(|&(_, end)| opening_start < end)
{
continue;
}
if refs.contains_key(tag_name) {
continue;
}
for &byte in &text.as_bytes()[scan_position..opening_start] {
if in_json_string {
if escaped {
escaped = false;
} else if byte == b'\\' {
escaped = true;
} else if byte == b'"' {
in_json_string = false;
}
} else {
match byte {
b'{' | b'[' => json_depth += 1,
b'}' | b']' => json_depth = json_depth.saturating_sub(1),
b'"' if json_depth > 0 => in_json_string = true,
_ => {}
}
}
}
scan_position = opening_start;
if in_json_string {
continue;
}
let closing_pattern = format!("</{}>", tag_name);
if let Some(closing_pos) = text[opening_end..].find(&closing_pattern) {
let abs_closing_pos = opening_end + closing_pos;
let closing_end = abs_closing_pos + closing_pattern.len();
let content = text[opening_end..abs_closing_pos].trim().to_string();
debug!(
"Extracted reference (standard): <{}> ({} chars)",
tag_name,
content.len()
);
refs.insert(tag_name.to_string(), content);
blocks_to_remove.push((opening_start, closing_end));
scan_position = closing_end;
continue;
}
let simplified_pattern = format!("<{}>", tag_name);
if let Some(closing_pos) = text[opening_end..].find(&simplified_pattern) {
let abs_closing_pos = opening_end + closing_pos;
let closing_end = abs_closing_pos + simplified_pattern.len();
let content = text[opening_end..abs_closing_pos].trim().to_string();
debug!(
"Extracted reference (simplified): <{}> ({} chars)",
tag_name,
content.len()
);
refs.insert(tag_name.to_string(), content);
blocks_to_remove.push((opening_start, closing_end));
scan_position = closing_end;
}
}
blocks_to_remove.sort_by(|a, b| b.0.cmp(&a.0));
for (start, end) in blocks_to_remove {
cleaned.replace_range(start..end, "");
}
cleaned = cleaned.trim().to_string();
trace!(
"Reference extraction complete: {} refs extracted, cleaned text {} chars",
refs.len(),
cleaned.len()
);
Ok((cleaned, refs))
}
pub fn resolve_references(json_str: &str, refs: &HashMap<String, String>) -> String {
REFERENCE_TAG
.replace_all(json_str, |capture: ®ex::Captures<'_>| {
refs.get(&capture[1])
.map(|content| escape_json_string(content))
.unwrap_or_else(|| capture[0].to_string())
})
.into_owned()
}
fn escape_json_string(s: &str) -> String {
let quoted = serde_json::to_string(s).expect("serializing a string cannot fail");
quoted[1..quoted.len() - 1].to_string()
}
pub fn contains_references(text: &str) -> bool {
REFERENCE_TAG.is_match(text)
}