///|
fn ascii_lower(s : String) -> String {
let b = StringBuilder()
for c in s { let n = c.to_int(); b.write_char(if n >= 65 && n <= 90 { (n+32).unsafe_to_char() } else { c }) }
b.to_string()
}
///|
fn pseudo_attributes(s : String) -> Array[(String, String)]? {
let out : Array[(String, String)] = []
let mut i = 0
while i < s.length() {
let before = i
while space(unit(s, i)) { i += 1 }
if i == s.length() { break }
if i == before { return None }
let start = i
while name_char(unit(s, i)) { i += 1 }
let name = s[start:i].to_owned()
if !ncname(name) { return None }
while space(unit(s, i)) { i += 1 }
if unit(s, i) != 61 { return None }
i += 1
while space(unit(s, i)) { i += 1 }
let quote = unit(s, i)
if quote != 34 && quote != 39 { return None }
i += 1
let start = i
while i < s.length() && unit(s, i) != quote { i += 1 }
if i == s.length() { return None }
out.push((name, s[start:i].to_owned())); i += 1
}
Some(out)
}
///|
fn XmlParser::declaration(self : XmlParser, body : String, start : Int, end : Int) -> Unit {
let first = if unit(self.source, 0) == 0xfeff { 1 } else { 0 }
if start != first { self.error("xml/invalid-declaration", "XML declaration must be first", start, end) }
guard pseudo_attributes(body) is Some(attrs) && !attrs.is_empty() && attrs[0] == ("version", "1.0") else {
self.error("xml/invalid-declaration", "Expected an XML 1.0 declaration", start, end); return
}
let mut previous = 0
for i in 1.. 1; "standalone" => 2; _ => -1 }
if order <= previous || order < 0 { self.error("xml/invalid-declaration", "Invalid declaration attribute order or duplicate", start, end) }
previous = order
if key == "standalone" && value != "yes" && value != "no" { self.error("xml/invalid-declaration", "Standalone must be yes or no", start, end) }
if key == "encoding" {
let encoding = ascii_lower(value)
if encoding != "utf-8" && encoding != "us-ascii" { self.error("xml/encoding", "Decoded text accepts UTF-8 or US-ASCII declarations", start, end) }
else if encoding == "us-ascii" && !self.ascii_only { self.error("xml/encoding", "US-ASCII declaration conflicts with non-ASCII text", start, end) }
}
}
}
///|
fn XmlParser::block(self : XmlParser, kind : XmlNodeKind, opener : String, closer : String, unclosed : String) -> Unit {
let start = self.pos
guard self.new_node(kind, start) is Some(index) else { return }
let before = self.diagnostics.length()
self.pos += opener.length()
let begin = self.pos
guard self.source[self.pos:].find(closer) is Some(length) else {
self.pos = self.source.length()
self.error(unclosed, "Unclosed XML content block", start, self.pos)
self.emit(Error, self.pos)
self.nodes[index].kind = Error; self.nodes[index].end = self.pos
return
}
let end = self.pos + length
self.pos = end + closer.length()
if self.pos - start > self.limits.token_units { self.error("xml/token-limit", "Content token limit exceeded", start, self.pos); self.halted = true; self.nodes[index].kind = Error; self.nodes[index].end = self.pos; return }
let raw = self.source[begin:end].to_owned()
let token_kind = match kind { Comment => TokenKind::Comment; CData => CData; _ => ProcessingInstruction }
// Emit the complete raw lexical block once; decode never emits overlapping tokens.
self.emit(token_kind, self.pos)
let value = self.decode(begin, end, false, false, token_kind)
self.nodes[index].value = value
if kind == Comment && (raw.contains("--") || raw.has_suffix("-")) { self.error("xml/invalid-comment", "A comment cannot contain '--' or end with '-'", begin, end) }
if kind == CData && self.stack.is_empty() { self.error("xml/outside-root", "CDATA is only allowed inside the document element", start, self.pos) }
if kind == ProcessingInstruction {
let mut split = 0
while split < raw.length() && !space(unit(raw, split)) { split += 1 }
let target = raw[:split].to_owned()
self.nodes[index].raw_name = target
let mut data_start = split
while space(unit(raw, data_start)) { data_start += 1 }
self.nodes[index].value = self.decode(begin+data_start, end, false, false, token_kind)
if target == "xml" {
self.nodes[index].kind = Declaration
let token = self.tokens[self.tokens.length()-1]
self.tokens[self.tokens.length()-1] = { kind: Declaration, text: token.text, span: token.span }
self.declaration(raw[split:].to_owned(), start, self.pos)
} else if !ncname(target) || ascii_lower(target) == "xml" { self.error("xml/invalid-pi", "Invalid or reserved processing-instruction target", begin, begin+split) }
}
self.nodes[index].end = self.pos
self.nodes[index].complete = before == self.diagnostics.length() && !self.halted
if !self.nodes[index].complete { self.nodes[index].kind = Error }
}
///|
fn XmlParser::prohibited(self : XmlParser) -> Unit {
let start = self.pos
guard self.new_node(Declaration, start) is Some(index) else { return }
let mut quote = 0
let mut brackets = 0
self.pos += 2
while self.pos < self.source.length() {
let c = unit(self.source, self.pos)
self.pos += 1
if quote != 0 { if c == quote { quote = 0 } }
else if c == 34 || c == 39 { quote = c }
else if c == 91 { brackets += 1 }
else if c == 93 && brackets > 0 { brackets -= 1 }
else if c == 62 && brackets == 0 { break }
if self.pos - start > self.limits.token_units { self.error("xml/token-limit", "Declaration token limit exceeded", start, self.pos); self.halted = true; break }
}
self.error("xml/prohibited-declaration", "DTD, entity and other markup declarations are prohibited", start, self.pos)
self.emit(Declaration, self.pos)
self.nodes[index].kind = Error; self.nodes[index].end = self.pos
}
///|
fn XmlParser::text(self : XmlParser) -> Unit {
let start = self.pos
guard self.new_node(Text, start) is Some(index) else { return }
while self.pos < self.source.length() && !self.at("<") {
self.pos += 1
if self.pos - start > self.limits.token_units { self.error("xml/token-limit", "Text token limit exceeded", start, self.pos); self.halted = true; self.nodes[index].kind = Error; self.nodes[index].end = self.pos; return }
}
let before = self.diagnostics.length()
let value = self.decode(start, self.pos, false, true, Text)
if self.stack.is_empty() && !self.source[start:self.pos].iter().all(c => space(c.to_int())) { self.error("xml/outside-root", "Non-whitespace text outside the document element", start, self.pos) }
self.nodes[index].value = value
self.nodes[index].end = self.pos
self.nodes[index].complete = before == self.diagnostics.length() && !self.halted
if !self.nodes[index].complete { self.nodes[index].kind = Error }
}
///|
fn parse_xml(source : String, map : SourceMap, limits : Limits) -> (Cst, Array[Diagnostic]) {
let root = XmlBuildNode::{ kind: Document, raw_name: "", name: None, attributes: [], bindings: [], children: [], parent: None, start: 0, end: source.length(), value: "", complete: false }
let parser = XmlParser::{ source, map, limits, ascii_only: source.iter().all(c => c.is_ascii()), tokens: [], nodes: [root], stack: [], namespaces: { "xml": "http://www.w3.org/XML/1998/namespace" }, diagnostics: [], pos: 0, emitted: 0, units: 1, roots: 0, failed: false, halted: false }
if unit(source, 0) == 0xfeff { parser.pos = 1; parser.emit(Whitespace, 1) }
while parser.pos < source.length() && !parser.halted {
if parser.at("", "xml/unclosed-comment") }
else if parser.at("", "xml/unclosed-cdata") }
else if parser.at("") { parser.block(ProcessingInstruction, "", "?>", "xml/unclosed-pi") }
else if parser.at(" XmlNode::{ kind: n.kind, raw_name: n.raw_name, name: n.name, attributes: ReadOnlyArray::from_array(n.attributes), bindings: ReadOnlyArray::from_array(n.bindings), children: ReadOnlyArray::from_array(n.children), parent: n.parent, span: { start: n.start, end: n.end }, value: n.value, complete: n.complete })
({ tokens: ReadOnlyArray::from_array(parser.tokens), nodes: ReadOnlyArray::from_array(nodes), complete: !parser.failed }, parser.diagnostics)
}