use std::collections::HashMap;
use super::{Dom, NodeId, XDeclaration, XName, XNamespace};
pub fn parse_xdocument(dom: &mut Dom, xml: &str) -> NodeId {
let xml = xml.strip_prefix('\u{feff}').unwrap_or(xml);
let chars: Vec<char> = xml.chars().collect();
let mut p = Parser {
dom,
c: chars,
pos: 0,
};
p.parse_document()
}
struct Parser<'a> {
dom: &'a mut Dom,
c: Vec<char>,
pos: usize,
}
impl<'a> Parser<'a> {
fn len(&self) -> usize {
self.c.len()
}
fn cur(&self) -> char {
if self.pos < self.len() {
self.c[self.pos]
} else {
'\0'
}
}
fn starts_with(&self, s: &str) -> bool {
for (off, ch) in s.chars().enumerate() {
let i = self.pos + off;
if i >= self.len() || self.c[i] != ch {
return false;
}
}
true
}
fn index_of(&self, s: &str, from: usize) -> Option<usize> {
if s.is_empty() || from > self.len() {
return None;
}
let needle_chars: usize = s.chars().count();
let end = self.len().saturating_sub(needle_chars);
for i in from..=end {
if i + needle_chars > self.len() {
break;
}
let mut ok = true;
for (off, ch) in s.chars().enumerate() {
if self.c[i + off] != ch {
ok = false;
break;
}
}
if ok {
return Some(i);
}
}
None
}
fn slice(&self, start: usize, end: usize) -> String {
let end = end.min(self.len());
if start >= end {
return String::new();
}
self.c[start..end].iter().collect()
}
fn skip_ws(&mut self) {
while self.pos < self.len() && self.cur().is_whitespace() {
self.pos += 1;
}
}
fn read_name(&mut self) -> String {
let start = self.pos;
while self.pos < self.len() {
let ch = self.cur();
if ch.is_whitespace() || ch == '/' || ch == '>' || ch == '=' {
break;
}
self.pos += 1;
}
self.slice(start, self.pos)
}
fn parse_document(&mut self) -> NodeId {
let doc = self.dom.new_document();
let len = self.len();
while self.pos < len {
self.skip_ws();
if self.starts_with("<?xml") {
let after = self.pos + 5;
let is_declaration = after >= len
|| self
.c
.get(after)
.is_none_or(|&ch| ch.is_whitespace() || ch == '?');
if is_declaration {
let end = self.index_of("?>", self.pos);
let decl_str = self.slice(self.pos, end.unwrap_or(len));
let decl = parse_declaration(&decl_str);
self.dom.set_declaration(doc, Some(decl));
self.pos = end.map(|e| e + 2).unwrap_or(len);
continue;
}
}
if self.starts_with("<?") {
let pi = self.parse_pi_node();
self.dom.add(doc, pi);
continue;
}
if self.starts_with("<!--") {
let end = self.index_of("-->", self.pos);
let body = self.slice(self.pos + 4, end.unwrap_or(len));
let cm = self.dom.new_comment(&body);
self.dom.add(doc, cm);
self.pos = end.map(|e| e + 3).unwrap_or(len);
continue;
}
if self.starts_with("<!") {
let end = self.index_of(">", self.pos);
self.pos = end.map(|e| e + 1).unwrap_or(len);
continue;
}
if self.cur() == '<' {
let scope: HashMap<String, String> = HashMap::new();
let el = self.parse_element(&scope);
self.dom.add(doc, el);
continue;
}
break;
}
doc
}
fn parse_element(&mut self, ns_scope: &HashMap<String, String>) -> NodeId {
self.pos += 1; let raw_name = self.read_name();
let mut raw_attrs: Vec<(String, String)> = Vec::new();
loop {
self.skip_ws();
if self.cur() == '/' || self.cur() == '>' {
break;
}
let aname = self.read_name();
self.skip_ws();
let mut avalue = String::new();
if self.cur() == '=' {
self.pos += 1; self.skip_ws();
let quote = self.cur();
self.pos += 1; let vstart = self.pos;
while self.pos < self.len() && self.cur() != quote {
self.pos += 1;
}
avalue = self.slice(vstart, self.pos);
self.pos += 1; }
raw_attrs.push((aname, unescape_xml_text(&avalue)));
}
let mut local_scope_owned: Option<HashMap<String, String>> = None;
for (name, value) in &raw_attrs {
if name == "xmlns" {
local_scope_owned
.get_or_insert_with(|| ns_scope.clone())
.insert(String::new(), value.clone());
} else if let Some(prefix) = name.strip_prefix("xmlns:") {
local_scope_owned
.get_or_insert_with(|| ns_scope.clone())
.insert(prefix.to_string(), value.clone());
}
}
let local_scope: &HashMap<String, String> = local_scope_owned.as_ref().unwrap_or(ns_scope);
let el_name = resolve(&raw_name, false, local_scope);
let el = self.dom.new_element(el_name);
for (name, value) in &raw_attrs {
let an = resolve(name, true, local_scope);
self.dom.set_attribute_value(el, &an, Some(value));
}
self.skip_ws();
if self.cur() == '/' {
self.pos += 2; return el;
}
self.pos += 1;
let len = self.len();
while self.pos < len {
if self.starts_with("</") {
self.pos += 2;
self.read_name();
self.skip_ws();
if self.cur() == '>' {
self.pos += 1;
}
break;
}
if self.starts_with("<!--") {
let end = self.index_of("-->", self.pos);
let body = self.slice(self.pos + 4, end.unwrap_or(len));
let cm = self.dom.new_comment(&body);
self.dom.add(el, cm);
self.pos = end.map(|e| e + 3).unwrap_or(len);
continue;
}
if self.starts_with("<![CDATA[") {
let end = self.index_of("]]>", self.pos);
let body = self.slice(self.pos + 9, end.unwrap_or(len));
let t = self.dom.new_text(&body);
self.dom.add(el, t);
self.pos = end.map(|e| e + 3).unwrap_or(len);
continue;
}
if self.starts_with("<?") {
let pi = self.parse_pi_node();
self.dom.add(el, pi);
continue;
}
if self.cur() == '<' {
let child = self.parse_element(local_scope);
self.dom.add(el, child);
continue;
}
let tstart = self.pos;
while self.pos < len && self.cur() != '<' {
self.pos += 1;
}
let raw = self.slice(tstart, self.pos);
if !raw.is_empty() {
let t = self.dom.new_text(&unescape_xml_text(&raw));
self.dom.add(el, t);
}
}
el
}
fn parse_pi_node(&mut self) -> NodeId {
let len = self.len();
self.pos += 2; let target_start = self.pos;
while self.pos < len {
let ch = self.cur();
if ch.is_whitespace() || ch == '?' || ch == '>' {
break;
}
self.pos += 1;
}
let target = self.slice(target_start, self.pos);
let end = self.index_of("?>", self.pos);
let data = self.slice(self.pos, end.unwrap_or(len));
self.pos = end.map(|e| e + 2).unwrap_or(len);
self.dom.new_pi(&target, &data)
}
}
fn resolve(qn: &str, is_attr: bool, local_scope: &HashMap<String, String>) -> XName {
if let Some(colon) = qn.find(':') {
let prefix = &qn[..colon];
let local = &qn[colon + 1..];
if prefix == "xmlns" {
return XNamespace::xmlns().name(local);
}
if prefix == "xml" {
return XNamespace::xml().name(local);
}
return match local_scope.get(prefix) {
Some(ns) => XNamespace::get(ns).name(local),
None => XName::get(local, ""),
};
}
if qn == "xmlns" {
return XName::get("xmlns", "");
}
let def_ns: Option<&String> = if is_attr { None } else { local_scope.get("") };
match def_ns {
Some(ns) if !ns.is_empty() => XNamespace::get(ns).name(qn),
_ => XName::get(qn, ""),
}
}
fn parse_declaration(decl: &str) -> XDeclaration {
XDeclaration {
version: Some(extract_pseudo_attr(decl, "version").unwrap_or_else(|| "1.0".to_string())),
encoding: extract_pseudo_attr(decl, "encoding"),
standalone: extract_pseudo_attr(decl, "standalone"),
}
}
fn extract_pseudo_attr(s: &str, key: &str) -> Option<String> {
let idx = s.find(key)?;
let rest = &s[idx + key.len()..];
let eq = rest.find('=')?;
let after = rest[eq + 1..].trim_start();
let mut chars = after.chars();
let quote = chars.next()?;
if quote != '"' && quote != '\'' {
return None;
}
let end = after[1..].find(quote)?;
Some(after[1..1 + end].to_string())
}
pub fn unescape_xml_text(s: &str) -> String {
if !s.contains('&') {
return s.to_string();
}
let chars: Vec<char> = s.chars().collect();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < chars.len() {
if chars[i] == '&' {
if let Some(semi_rel) = chars[i + 1..].iter().position(|&c| c == ';') {
let body: String = chars[i + 1..i + 1 + semi_rel].iter().collect();
if let Some(decoded) = decode_entity(&body) {
out.push_str(&decoded);
i = i + 1 + semi_rel + 1;
continue;
}
}
out.push('&');
i += 1;
} else {
out.push(chars[i]);
i += 1;
}
}
out
}
fn decode_entity(body: &str) -> Option<String> {
let b: Vec<char> = body.chars().collect();
if b.is_empty() {
return None;
}
if b[0] == '#' {
let code = if b.len() > 1 && (b[1] == 'x' || b[1] == 'X') {
u32::from_str_radix(&body[2..], 16).ok()?
} else {
body[1..].parse::<u32>().ok()?
};
return char::from_u32(code).map(|c| c.to_string());
}
if !b.iter().all(|c| c.is_ascii_alphabetic()) {
return None;
}
match body {
"lt" => Some("<".to_string()),
"gt" => Some(">".to_string()),
"amp" => Some("&".to_string()),
"quot" => Some("\"".to_string()),
"apos" => Some("'".to_string()),
_ => None,
}
}