///|
/// Namespace-resolved XML representation consumed by the SVG parser.
/// `Milky2018/xml` owns syntax validation and source ranges; this representation
/// owns the SVG-specific namespace and attribute semantics used downstream.
priv struct SVGXmlElement {
name : String
attributes : Array[(String, String)]
}
///|
priv enum SVGXmlEvent {
XmlStart(SVGXmlElement)
XmlEnd(String)
XmlEmpty(SVGXmlElement)
XmlText(String)
XmlCData(String)
XmlPI(target~ : String, data~ : String)
XmlEof
}
///|
let svg_namespace_uri = "http://www.w3.org/2000/svg"
///|
let xml_namespace_uri = "http://www.w3.org/XML/1998/namespace"
///|
let xlink_namespace_uri = "http://www.w3.org/1999/xlink"
///|
let xlink_href_attribute_name : String = "{" + xlink_namespace_uri + "}href"
///|
priv struct XmlNamespaceScope {
default_uri : String?
default_declared : Bool
prefixes : Map[String, String]
}
///|
fn XmlNamespaceScope::root() -> XmlNamespaceScope {
{
default_uri: None,
default_declared: false,
prefixes: { "xml": xml_namespace_uri },
}
}
///|
fn XmlNamespaceScope::with_declarations(
self : XmlNamespaceScope,
attributes : Array[@xml.XmlAttribute],
) -> XmlNamespaceScope {
let mut default_uri = self.default_uri
let mut default_declared = self.default_declared
let prefixes = self.prefixes.copy()
for attribute in attributes {
let name = attribute.name
let value = attribute.value
if name == "xmlns" {
default_declared = true
default_uri = if value == "" { None } else { Some(value) }
} else if name.has_prefix("xmlns:") {
let prefix = name[6:].to_owned()
if value == "" {
prefixes.remove(prefix)
} else {
prefixes[prefix] = value
}
}
}
{ default_uri, default_declared, prefixes }
}
///|
fn XmlNamespaceScope::normalize_element_name(
self : XmlNamespaceScope,
qualified_name : String,
) -> String {
let (local_name, namespace_uri, prefix_bound, legacy_unqualified) = match
qualified_name.find(":") {
Some(index) => {
let prefix = qualified_name[:index].to_owned()
let local_name = qualified_name[index + 1:].to_owned()
match self.prefixes.get(prefix) {
Some(uri) => (local_name, Some(uri), true, false)
None => (local_name, None, false, false)
}
}
None => (qualified_name, self.default_uri, true, !self.default_declared)
}
if !prefix_bound {
// An unbound prefix must never gain SVG semantics merely from its local
// name. Curly braces cannot occur in an XML name, so this marker cannot
// collide with an authored SVG element name.
"{unbound-prefix}" + local_name
} else {
match namespace_uri {
// Preserve the library's historical support for unqualified SVG
// fragments such as `` while respecting any declared namespace.
None if legacy_unqualified => local_name
None => "{no-namespace}" + local_name
Some(uri) if uri == svg_namespace_uri => local_name
Some(uri) => "{" + uri + "}" + local_name
}
}
}
///|
fn XmlNamespaceScope::normalize_attribute_name(
self : XmlNamespaceScope,
qualified_name : String,
) -> String {
if qualified_name == "xmlns" || qualified_name.has_prefix("xmlns:") {
return qualified_name
}
match qualified_name.find(":") {
None => qualified_name
Some(index) => {
let prefix = qualified_name[:index].to_owned()
let local_name = qualified_name[index + 1:].to_owned()
match self.prefixes.get(prefix) {
Some(uri) => "{" + uri + "}" + local_name
None => "{unbound-prefix}" + local_name
}
}
}
}
///|
fn XmlNamespaceScope::normalize_attributes(
self : XmlNamespaceScope,
attributes : Array[@xml.XmlAttribute],
) -> Array[(String, String)] {
attributes.map(fn(attribute) {
(self.normalize_attribute_name(attribute.name), attribute.value)
})
}
///|
fn read_xml_events(svg_str : String) -> Array[SVGXmlEvent]? {
let reader = @xml.Reader::from_string(svg_str)
try {
let events : Array[SVGXmlEvent] = []
let scopes : Array[XmlNamespaceScope] = [XmlNamespaceScope::root()]
while true {
let current = scopes[scopes.length() - 1]
let event : SVGXmlEvent = match reader.read_event().kind {
Start(element) => {
let scope = current.with_declarations(element.attributes)
scopes.push(scope)
XmlStart({
name: scope.normalize_element_name(element.name),
attributes: scope.normalize_attributes(element.attributes),
})
}
Empty(element) => {
let scope = current.with_declarations(element.attributes)
XmlEmpty({
name: scope.normalize_element_name(element.name),
attributes: scope.normalize_attributes(element.attributes),
})
}
End(name) => {
let normalized = XmlEnd(current.normalize_element_name(name))
if scopes.length() > 1 {
let _ = scopes.pop()
}
normalized
}
Text(text) => XmlText(text)
CData(text) => XmlCData(text)
PI(target~, data~) => XmlPI(target~, data~)
Eof => XmlEof
Comment(_) | Decl(..) | DocType(_) => continue
}
events.push(event)
if event is XmlEof {
break
}
}
Some(events)
} catch {
_ => None
}
}