///|
pub(all) enum WriteFormat { Compact; Pretty } derive(Eq, Debug)
///|
pub extend WriteFormat with Eq::{equal, not_equal}
///|
pub extend WriteFormat with @debug.Debug::{to_repr}
///|
pub struct WriteResult { output : String?; diagnostics : ReadOnlyArray[Diagnostic] } derive(Eq, Debug)
///|
pub extend WriteResult with Eq::{equal, not_equal}
///|
pub extend WriteResult with @debug.Debug::{to_repr}
///|
pub(all) suberror WriteError { Failed(ReadOnlyArray[Diagnostic]) } derive(Eq, Debug)
///|
pub extend WriteError with Eq::{equal, not_equal}
///|
pub extend WriteError with @debug.Debug::{to_repr}
///|
priv struct XmlOutput {
buffer : StringBuilder
limits : Limits
diagnostics : Array[Diagnostic]
uris : Map[String, String]
reserved : Map[String,Bool]
mut size : Int
mut input_records : Int
mut output_records : Int
mut stopped : Bool
}
///|
fn XmlOutput::fail(self : XmlOutput, code : String, message : String, span : Span?) -> Unit {
if !self.stopped { self.diagnostics.push(finding(code,message,span?)) }
self.stopped = true
}
///|
fn XmlOutput::chunk(self : XmlOutput, text : String) -> Unit {
if self.stopped { return }
if text.length() > self.limits.output_units - self.size { self.fail("bpmn/output-limit","XML output exceeds its UTF-16 limit",None); return }
self.buffer.write_string(text); self.size += text.length()
}
///|
fn XmlOutput::records(self : XmlOutput, count : Int, output : Bool, span : Span?) -> Unit {
if self.stopped { return }
let previous = if output { self.output_records } else { self.input_records }
if count > self.limits.nodes - previous { self.fail("bpmn/write-node-limit","Writer node/attribute limit exceeded",span); return }
if output { self.output_records += count } else { self.input_records += count }
}
///|
fn XmlOutput::text_valid(self : XmlOutput, text : String, span : Span?) -> Bool {
if text.length() > self.limits.token_units { self.fail("bpmn/write-token-limit","Writer token limit exceeded",span); return false }
let mut i=0
while i < text.length() {
let (c,width)=codepoint(text,i)
if !xml_char(c) { self.fail("bpmn/forbidden-text","Constructed text contains a forbidden XML character",span); return false }
i+=width
}
true
}
///|
fn XmlOutput::name_valid(self : XmlOutput, name : ExpandedName, span : Span?) -> Bool {
if !self.text_valid(name.local_name,span) || !self.text_valid(name.namespace_uri,span) { return false }
if !ncname(name.local_name) { self.fail("bpmn/invalid-name","Constructed name is not an XML local name",span); return false }
if name.namespace_uri == "http://www.w3.org/2000/xmlns/" { self.fail("bpmn/namespace-conflict","The namespace declaration namespace cannot name a model value",span); return false }
if name.namespace_uri != "" && name.namespace_uri != "http://www.w3.org/XML/1998/namespace" { self.uris[name.namespace_uri] = "" }
true
}
///|
fn XmlOutput::scalar_valid(self : XmlOutput, value : Scalar, expected : ExpandedName?, span : Span?) -> Bool {
if !self.text_valid(value.token(),span) { return false }
match value {
Invalid(..) => { self.fail("bpmn/invalid-scalar","Cannot write an invalid scalar value",span); return false }
Reference(r) => {
guard r.name is Some(name) else { self.fail("bpmn/unbound-qname","Cannot write a QName with an unbound prefix or invalid local name",span); return false }
if !self.name_valid(name,span) { return false }
if r.kind == Id && name.namespace_uri != "" { self.fail("bpmn/invalid-scalar","An ID reference cannot have a namespace",span); return false }
}
Identifier(id) => if !ncname(id) { self.fail("bpmn/invalid-scalar","Identifier is not an XML NCName",span); return false }
_ => ()
}
if expected is Some(t) {
let parsed = parse_scalar(t,value.token(),[])
let matches = match (value,parsed) {
(Reference(a),Reference(b)) => a.kind == b.kind
_ => value == parsed
}
if !matches { self.fail("bpmn/scalar-type-mismatch","Scalar alternative does not match its schema field",span); return false }
}
true
}
///|
fn XmlOutput::binding_valid(self : XmlOutput, bindings : ReadOnlyArray[(String,String)], span : Span?) -> Unit {
let seen : Map[String,Bool] = Map([])
for (prefix,uri) in bindings {
if self.stopped { break }
if !self.text_valid(prefix,span) || !self.text_valid(uri,span) { break }
if seen.contains(prefix) || (prefix != "" && !ncname(prefix)) || prefix == "xmlns" ||
uri == "http://www.w3.org/2000/xmlns/" || (prefix != "" && uri == "") ||
(prefix == "xml" && uri != "http://www.w3.org/XML/1998/namespace") ||
(prefix != "xml" && uri == "http://www.w3.org/XML/1998/namespace") {
self.fail("bpmn/namespace-conflict","Conflicting or reserved namespace binding",span); break
}
seen[prefix] = true
}
}
///|
fn XmlOutput::content_valid(self : XmlOutput, content : Content, outside : Bool, span : Span?) -> Unit {
if self.stopped { return }
self.records(1,false,span)
match content {
Text(text) => {
self.text_valid(text,span) |> ignore
if outside && !text.iter().all(c => space(c.to_int())) { self.fail("bpmn/outside-root","Only literal whitespace is allowed outside the document element",span) }
}
Comment(text) => {
if text.length() > self.limits.token_units-7 { self.fail("bpmn/write-token-limit","Comment token exceeds its limit",span); return }
self.text_valid(text,span) |> ignore
if text.contains("--") || text.has_suffix("-") || text.contains("\r") { self.fail("bpmn/invalid-comment","Comment cannot contain '--', a final '-', or a non-normalized CR",span) }
}
ProcessingInstruction(target~,data~) => {
let extra = if data.is_empty() { 4 } else { 5 }
if target.length() > self.limits.token_units-extra || data.length() > self.limits.token_units-extra-target.length() { self.fail("bpmn/write-token-limit","Processing instruction token exceeds its limit",span); return }
self.text_valid(target,span) |> ignore; self.text_valid(data,span) |> ignore
if !ncname(target) || ascii_lower(target) == "xml" || data.contains("?>") || data.contains("\r") || (!data.is_empty() && space(unit(data,0))) {
self.fail("bpmn/invalid-pi","Processing instruction target or data is not representable without changing its meaning",span)
}
}
Element(_) => if outside { self.fail("bpmn/definitions-required","A document has only one definitions root",span) }
}
}
///|
priv struct WalkFrame { node : Element; depth : Int; mut entered : Bool; mut next : Int; preserve : Bool; expected_type : ExpandedName?; parent_payload : Bool; scope : @namespaces.HashMap[String,String] }
///|
fn XmlOutput::inspect(self : XmlOutput, document : Document) -> Unit {
if document.root.kind.name() != Kind::Definitions.name() { self.fail("bpmn/definitions-required","A BPMN document needs a definitions root",document.root.span); return }
self.records(1,false,None)
for c in document.prolog { self.content_valid(c,true,None) }
for c in document.epilog { self.content_valid(c,true,None) }
let stack=[WalkFrame::{node:document.root,depth:0,entered:false,next:0,preserve:false,expected_type:Kind::Definitions.declared_type(),parent_payload:false,scope:@namespaces.HashMap::new()}]
while !stack.is_empty() && !self.stopped {
let f=stack[stack.length()-1]; let node=f.node
let scope=namespace_context(node,f.scope)
let payload=opaque_kind(node.kind)
if !f.entered {
f.entered=true; self.records(1,false,node.span); self.records(node.values.length(),false,node.span); self.records(node.bindings.length(),false,node.span)
if self.stopped { break }
if f.depth >= self.limits.depth { self.fail("bpmn/write-depth-limit","Writer element depth limit exceeded",node.span); break }
if f.parent_payload && !payload { self.fail("bpmn/invalid-content","Preserved XML children use XmlElement values",node.span); break }
if payload {
if node.value_type != None || node.text_value != None { self.fail("bpmn/type-conflict","Preserved XML cannot claim a typed schema scalar",node.span); break }
if !f.parent_payload && kind_from_name(node.kind.name()) != None && !opaque_unknown_type(node,scope) { self.fail("bpmn/type-conflict","A known standard element outside a payload needs its typed model representation",node.span); break }
} else {
if node.value_type is Some(t) && t.namespace_uri != "http://www.w3.org/2001/XMLSchema" && schema_type(t) == None { self.fail("bpmn/unsupported-type","Unknown typed values must use preserved XML",node.span); break }
let type_name=ExpandedName::new("http://www.w3.org/2001/XMLSchema-instance","type")
if node.value_type != f.expected_type && !node.values.iter().any(a => a.name == Some(type_name)) { self.fail("bpmn/type-conflict","Constructed type differs from its containment type without an explicit xsi:type",node.span); break }
}
self.name_valid(node.kind.name(),node.span) |> ignore
self.binding_valid(node.bindings,node.span)
let seen : Map[(String,String),Bool] = Map([])
for attr in node.values {
if self.stopped { break }
guard attr.name is Some(name) else { self.fail("bpmn/invalid-name","Attribute has an unbound prefix or invalid name",attr.span); break }
if name.namespace_uri == "" && name.local_name == "xmlns" { self.fail("bpmn/namespace-conflict","Namespace declarations belong in bindings, not model attributes",attr.span); break }
self.name_valid(name,attr.span) |> ignore
if seen.contains((name.namespace_uri,name.local_name)) { self.fail("bpmn/duplicate-attribute","Duplicate expanded attribute name",attr.span); break }
seen[(name.namespace_uri,name.local_name)]=true
if opaque_attribute(node,name) {
if !(attr.value is Text(_)) { self.fail("bpmn/scalar-type-mismatch","Opaque XML attributes are string values",attr.span); break }
self.scalar_valid(attr.value,None,attr.span) |> ignore
} else {
self.scalar_valid(attr.value,Some(field_type(node.value_type,name)),attr.span) |> ignore
if name.namespace_uri == "http://www.w3.org/2001/XMLSchema-instance" && name.local_name == "type" {
match attr.value { Reference(r) if r.name != node.value_type => self.fail("bpmn/type-conflict","xsi:type does not match the model's type identity",attr.span); _ => () }
}
}
}
for text in opaque_strings(node) {
for prefix in text_prefixes(text) { if namespace_uri(scope,prefix) == None { self.reserved[prefix]=true } }
}
if node.text_value is Some(value) {
if !simple_type(node.value_type) { self.fail("bpmn/invalid-content","Typed scalar content needs a simple schema type",node.span); break }
self.scalar_valid(value,node.value_type,node.span) |> ignore
if !node.children.is_empty() { self.fail("bpmn/invalid-content","A scalar element cannot contain element children",node.span) }
}
}
if f.next >= node.contents.length() { stack.pop() |> ignore; continue }
let part=node.contents[f.next]; f.next+=1
match part {
Element(child) => {
if self.input_records >= self.limits.nodes { self.fail("bpmn/write-node-limit","Writer node limit exceeded",child.span); break }
if f.depth >= self.limits.depth-1 { self.fail("bpmn/write-depth-limit","Writer element depth limit exceeded",child.span); break }
stack.push({node:child,depth:f.depth+1,entered:false,next:0,preserve:space_policy(node,f.preserve),expected_type:child_type(node.value_type,child.kind),parent_payload:payload,scope})
}
_ => self.content_valid(part,false,node.span)
}
}
if self.stopped { return }
let uris=self.uris.keys().to_array(); uris.sort_by((a,b) => a.lexical_compare(b))
let used : Map[String,Bool] = Map([])
let mut next=1
for uri in uris {
let preferred=namespace_prefix(uri)
let prefix=if preferred != "" && !self.reserved.contains(preferred) { preferred } else {
while self.reserved.contains("ns\{next}") || used.contains("ns\{next}") { next+=1 }
let prefix="ns\{next}"; next+=1; prefix
}
self.uris[uri]=prefix; used[prefix]=true
}
}
///|
fn XmlOutput::escaped(self : XmlOutput, text : String, attribute : Bool) -> Unit {
let mut units = 0
for c in text {
if self.stopped { break }
let escaped = match c {
'<' => "<"; '>' => ">"; '&' => "&"; '"' if attribute => """
'\r' => "
"; '\n' if attribute => "
"; '\t' if attribute => " "
_ => c.to_string()
}
if escaped.length() > self.limits.token_units-units { self.fail("bpmn/write-token-limit","Escaped XML token exceeds its limit",None); break }
units += escaped.length(); self.chunk(escaped)
}
}
///|
fn XmlOutput::line(self : XmlOutput, depth : Int) -> Unit {
if depth > (self.limits.token_units-1)/2 { self.fail("bpmn/write-token-limit","Pretty indentation exceeds its token limit",None); return }
self.chunk("\n")
for _ in 0.. Unit {
self.records(1,true,None)
match content {
Text(text) => self.escaped(text,false)
Comment(text) => { self.chunk("") }
ProcessingInstruction(target~,data~) => { self.chunk(""); self.chunk(target); if !data.is_empty() { self.chunk(" "); self.chunk(data) }; self.chunk("?>") }
Element(_) => self.fail("bpmn/invalid-content","Unexpected element in inert content",None)
}
}
///|
priv struct EmitFrame { node : Element; depth : Int; preserve : Bool; parent_scope : @namespaces.HashMap[String,String]; parent_names : XmlNames?; mut names : XmlNames?; mut entered : Bool; mut next : Int; mut block : Bool; mut any : Bool }
///|
fn XmlOutput::emit_document(self : XmlOutput, document : Document, format : WriteFormat) -> Unit {
self.records(1,true,None)
for part in document.prolog { self.inert_content(part); if format == Pretty { self.line(0) } }
let base=@namespaces.HashMap::new().add("xml","http://www.w3.org/XML/1998/namespace").add("","")
let stack=[EmitFrame::{node:document.root,depth:0,preserve:false,parent_scope:base,parent_names:None,names:None,entered:false,next:0,block:false,any:false}]
while !stack.is_empty() && !self.stopped {
let f=stack[stack.length()-1]; let node=f.node
let preserve=space_policy(node,f.preserve)
let mixed=opaque_kind(node.kind) || (match node.value_type { Some(t) => schema_type(t).map(t => t.has_mixed_content()).unwrap_or(false); None => false })
if !f.entered {
f.entered=true; self.records(1,true,node.span)
let names=self.names_for(node,f.parent_scope,f.parent_names,f.depth == 0)
if self.stopped { break }; f.names=Some(names)
self.chunk("<"); self.qualified_names(names,node.kind.name(),false)
self.namespace_declarations(names,f.parent_scope)
let values=node.values.iter().to_array()
values.sort_by((a,b) => { let a=a.name.unwrap(); let b=b.name.unwrap(); let order=a.namespace_uri.lexical_compare(b.namespace_uri); if order == 0 { a.local_name.lexical_compare(b.local_name) } else { order } })
for attr in values { self.records(1,true,attr.span); self.chunk(" "); self.qualified_names(names,attr.name.unwrap(),true); self.chunk("=\""); self.scalar_names(names,attr.value,true); self.chunk("\"") }
let has_content=node.contents.iter().any(c => match c { Text(s) => node.text_value == None && (mixed || preserve || !s.iter().all(c => space(c.to_int()))); _ => true })
let has_scalar=node.text_value.map(v => !v.token().is_empty()).unwrap_or(false)
if !has_content && !has_scalar { self.chunk("/>"); stack.pop() |> ignore; continue }
self.chunk(">")
f.block=format == Pretty && !mixed && !preserve && node.text_value == None && !node.contents.iter().any(c => match c { Text(s) => !s.iter().all(c => space(c.to_int())); _ => false })
if node.text_value is Some(value) { self.records(1,true,node.span); self.scalar_names(names,value,false) }
}
let names=f.names.unwrap()
if f.next >= node.contents.length() {
if f.block && f.any { self.line(f.depth) }
self.chunk(""); self.qualified_names(names,node.kind.name(),false); self.chunk(">")
stack.pop() |> ignore; continue
}
let part=node.contents[f.next]; f.next+=1
if part is Text(text) { if node.text_value != None || (!mixed && !preserve && text.iter().all(c => space(c.to_int()))) { continue } }
if f.block { self.line(f.depth+1) }; f.any=true
match part {
Element(child) => stack.push({node:child,depth:f.depth+1,preserve,parent_scope:names.scope,parent_names:Some(names),names:None,entered:false,next:0,block:false,any:false})
_ => self.inert_content(part)
}
}
for part in document.epilog { if format == Pretty { self.line(0) }; self.inert_content(part) }
if format == Pretty { self.chunk("\n") }
}
///|
/// Canonical XML with deterministic namespaces and expanded-name attribute order.
/// A failed result never exposes the partially assembled buffer as output.
pub fn try_write(document : Document, format? : WriteFormat = Compact, limits? : Limits = Limits::new()) -> WriteResult {
let output = XmlOutput::{ buffer: StringBuilder(),limits,diagnostics:[],uris:Map([]),reserved:Map([]),size:0,input_records:0,output_records:0,stopped:false }
if limits.source_units <= 0 || limits.output_units <= 0 || limits.nodes <= 0 || limits.depth <= 0 || limits.token_units <= 0 || limits.diagnostics <= 0 { output.fail("bpmn/write-invalid-limits","All processing limits must be positive",None) }
if !output.stopped { output.inspect(document) }
if !output.stopped { output.emit_document(document,format) }
{ output: if output.stopped { None } else { Some(output.buffer.to_string()) }, diagnostics: ReadOnlyArray::from_array(output.diagnostics) }
}
///|
/// Convenience writer. Handle WriteError for invalid constructed values or use
/// try_write when an explicit result is more convenient.
pub fn write(document : Document, format? : WriteFormat = Compact, limits? : Limits = Limits::new()) -> String raise WriteError {
let result=try_write(document,format~,limits~)
match result.output { Some(output) => output; None => raise WriteError::Failed(result.diagnostics) }
}