///|
/// A syntax-only tree adapter for the generated SVG profile. DOCTYPE is
/// rejected before parsing, so entity expansion cannot exceed caller budgets.
priv enum SvgXmlKind {
Document
Element
Text
} derive(Eq)
///|
priv struct SvgXmlAttribute {
raw_name : String
value : String
}
///|
priv struct SvgXmlNode {
kind : SvgXmlKind
raw_name : String
attributes : Array[SvgXmlAttribute]
children : Array[Int32]
value : String
}
///|
fn parse_svg_xml(
source : String,
max_nodes~ : Int32 = 200_000,
) -> Array[SvgXmlNode] raise RasterFailure {
guard source.length() <= 16_777_216 && max_nodes > 0 else {
raise raster_unsupported("XML source/node budget")
}
guard !source.contains(" raise raster_unsupported("malformed XML: " + e.to_string())
}
if event.kind is Eof {
break
}
let node : SvgXmlNode = match event.kind {
Start(e) | Empty(e) => {
units += e.attributes.length()
{
kind: Element,
raw_name: e.name,
attributes: e.attributes.map(a => {
raw_name: a.name,
value: a.value,
}),
children: [],
value: "",
}
}
Text(value) | CData(value) =>
{ kind: Text, raw_name: "", attributes: [], children: [], value, }
End(_) => {
let _ = stack.pop()
continue
}
Comment(_) | PI(..) | Decl(..) => continue
_ => raise raster_unsupported("XML declaration outside generated SVG")
}
units += 1
guard units <= max_nodes && stack.length() <= 256 else {
raise raster_unsupported("XML node/depth budget")
}
let index = nodes.length()
nodes[stack[stack.length() - 1]].children.push(index)
nodes.push(node)
if event.kind is Start(_) {
stack.push(index)
}
}
guard stack.length() == 1 else { raise raster_unsupported("unclosed SVG") }
let roots = nodes[0].children.filter(i => nodes[i].kind == Element)
guard roots.length() == 1 && nodes[roots[0]].raw_name == "svg" else {
raise raster_unsupported("expected one SVG root")
}
nodes
}
///|
fn svg_xml_escape(s : String) -> String {
s
.replace(old="&", new="&")
.replace(old="\"", new=""")
.replace(old="<", new="<")
.replace(old=">", new=">")
}
///|
fn svg_xml_write(
nodes : Array[SvgXmlNode],
index : Int32,
output : StringBuilder,
additions : Map[String, String],
matched : Map[String, Bool],
) -> Unit {
let node = nodes[index]
if node.kind == Text {
output.write_string(svg_xml_escape(node.value))
return
}
if node.kind == Element {
output.write_string("<" + node.raw_name)
for attr in node.attributes {
output.write_string(
" " + attr.raw_name + "=\"" + svg_xml_escape(attr.value) + "\"",
)
}
output.write_string(">")
}
for child in node.children {
svg_xml_write(nodes, child, output, additions, matched)
}
if node.kind == Element {
output.write_string("" + node.raw_name + ">")
for attr in node.attributes {
if attr.raw_name == "id" {
if additions.get(attr.value) is Some(markup) {
output.write_string(markup)
matched[attr.value] = true
}
}
}
}
}