use std::collections::HashMap;
pub fn expand_marked_sections(content: &str) -> Result<String, String> {
if !content.contains("<![%") {
return Ok(content.to_string());
}
let entities = parse_parameter_entities(content);
expand_conditional_sections(content, &entities)
}
pub fn expand_marked_sections_with_entities(
content: &str,
external_entities: &HashMap<String, String>,
) -> Result<String, String> {
if !content.contains("<![%") {
return Ok(content.to_string());
}
let mut entities = external_entities.clone();
let local_entities = parse_parameter_entities(content);
entities.extend(local_entities);
expand_conditional_sections(content, &entities)
}
pub fn extract_parameter_entities(content: &str) -> HashMap<String, String> {
parse_parameter_entities(content)
}
fn parse_parameter_entities(content: &str) -> HashMap<String, String> {
let mut entities = HashMap::new();
let mut pos = 0;
while let Some(start) = content[pos..].find("<!ENTITY") {
let start = pos + start;
if let Some(end) = content[start..].find('>') {
let decl = &content[start..start + end + 1];
if let Some((name, value)) = parse_entity_decl(decl) {
entities.insert(name, value);
}
pos = start + end + 1;
} else {
break;
}
}
entities
}
fn parse_entity_decl(decl: &str) -> Option<(String, String)> {
let percent_pos = decl.find('%')?;
let after_percent = &decl[percent_pos + 1..];
let name_part = after_percent.split('"').next()?.trim();
let parts: Vec<&str> = decl.split('"').collect();
if parts.len() >= 2 {
let value = parts[1].trim();
Some((name_part.to_string(), value.to_string()))
} else {
None
}
}
fn expand_conditional_sections(
content: &str,
entities: &HashMap<String, String>,
) -> Result<String, String> {
let mut result = String::with_capacity(content.len());
let mut pos = 0;
while pos < content.len() {
if let Some(start) = content[pos..].find("<![%") {
let start = pos + start;
result.push_str(&content[pos..start]);
if let Some((entity_name, section_content, end)) = parse_marked_section(&content[start..]) {
let end = start + end;
match entities.get(&entity_name).map(|s| s.as_str()) {
Some("INCLUDE") => {
result.push_str(§ion_content);
}
Some("IGNORE") | None => {
}
_ => {
}
}
pos = end;
} else {
result.push_str(&content[start..start + 4]);
pos = start + 4;
}
} else {
result.push_str(&content[pos..]);
break;
}
}
Ok(result)
}
fn parse_marked_section(text: &str) -> Option<(String, String, usize)> {
if !text.starts_with("<![%") {
return None;
}
let after_percent = &text[4..]; let bracket_pos = after_percent.find('[')?;
let entity_name_raw = &after_percent[..bracket_pos];
let entity_name = entity_name_raw.trim().trim_end_matches(';').to_string();
let content_start = 4 + bracket_pos + 1;
let closing = text.find("]]>")?;
if closing < content_start {
return None;
}
let content = text[content_start..closing].to_string();
let total_length = closing + 3;
Some((entity_name, content, total_length))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_entity_decl() {
let decl = r#"<!ENTITY % l10n-en "INCLUDE">"#;
let result = parse_entity_decl(decl);
assert_eq!(result, Some(("l10n-en".to_string(), "INCLUDE".to_string())));
let decl2 = r#"<!ENTITY % feature "IGNORE">"#;
let result2 = parse_entity_decl(decl2);
assert_eq!(result2, Some(("feature".to_string(), "IGNORE".to_string())));
}
#[test]
fn test_parse_marked_section() {
let text = r#"<![%l10n-en[ (("en") foo) ]]>"#;
let result = parse_marked_section(text);
assert_eq!(
result,
Some(("l10n-en".to_string(), " ((\"en\") foo) ".to_string(), 29))
);
}
#[test]
fn test_parse_marked_section_with_semicolon() {
let text = r#"<![%l10n-af;[<!ENTITY dbl1af "foo">]]>"#;
let result = parse_marked_section(text);
assert_eq!(
result,
Some(("l10n-af".to_string(), "<!ENTITY dbl1af \"foo\">".to_string(), 38))
);
}
#[test]
fn test_expand_include() {
let content = r#"
<!ENTITY % feature "INCLUDE">
(define test
<![%feature[ (included-code) ]]>
(other-code))
"#;
let result = expand_marked_sections(content).unwrap();
assert!(result.contains("(included-code)"));
assert!(!result.contains("<![%"));
}
#[test]
fn test_expand_ignore() {
let content = r#"
<!ENTITY % feature "IGNORE">
(define test
<![%feature[ (ignored-code) ]]>
(other-code))
"#;
let result = expand_marked_sections(content).unwrap();
assert!(!result.contains("(ignored-code)"));
assert!(!result.contains("<![%"));
assert!(result.contains("(other-code)"));
}
#[test]
fn test_multiple_sections() {
let content = r#"
<!ENTITY % en "INCLUDE">
<!ENTITY % de "IGNORE">
<![%en[ (english) ]]>
<![%de[ (deutsch) ]]>
"#;
let result = expand_marked_sections(content).unwrap();
assert!(result.contains("(english)"));
assert!(!result.contains("(deutsch)"));
}
#[test]
fn test_no_marked_sections() {
let content = "(define test (code))";
let result = expand_marked_sections(content).unwrap();
assert_eq!(result, content);
}
#[test]
fn test_case_clause_pattern() {
let content = r#"
<!ENTITY % l10n-en "INCLUDE">
<!ENTITY % l10n-de "IGNORE">
(define (test-function lang)
(case lang
<![%l10n-en[ (("en") "English") ]]>
<![%l10n-de[ (("de") "German") ]]>
(else "Unknown")))
"#;
let result = expand_marked_sections(content).unwrap();
println!("==== RESULT ====");
println!("{}", result);
println!("================");
assert!(result.contains(r#"(("en") "English")"#));
assert!(!result.contains(r#"(("de") "German")"#));
assert!(result.contains("(else"));
}
}