///|
pub struct Conversion { diagram : @render.Diagram?; diagnostics : ReadOnlyArray[@diagrams.Diagnostic] } derive(Eq, Debug)
///|
pub extend Conversion with Eq::{equal, not_equal}
///|
pub extend Conversion with @debug.Debug::{to_repr}
///|
priv struct Instance { resource : Int; root : Int; tag : String; pool : String?; container : String?; members : Map[Int,Bool] }
///|
priv struct Projection {
converter : Converter; instances : Array[Instance]; nodes : Array[@model.Node]; flows : Array[@model.Flow]
visuals : Map[(Int,Int,String),Int]; selected : Map[(Int,Int),Bool]
di : Map[(Int,Int),Array[Int]]; di_order : Array[(Int,Int)]; used_di : Map[Int,Bool]; shapes : Map[Int,Array[Int]]; visual_order : Array[(Int,Int,String)]
}
///|
fn Projection::include_scope(self : Projection, resource : Int, root : Int, tag : String, pool : String?, container? : String) -> Unit {
let c=self.converter
let index=c.sources[resource].index
let members=Map([(root,true)])
if !c.charge(index.nodes.length()) { return }
for n in index.nodes { if n.parent.map(p => members.contains(p)).unwrap_or(false) { members[n.ordinal]=true } }
self.instances.push({resource,root,tag,pool,container,members})
for ordinal in members.keys() { self.selected[(resource,ordinal)]=true }
}
///|
fn Projection::selected_di(self : Projection, selected : Selected) -> Unit {
let c=self.converter
if selected.plane is Some(plane) {
self.selected[(0,plane)]=true
let index=c.sources[0].index
let members=Map([(plane,true)])
for n in index.nodes { if n.parent.map(p => members.contains(p)).unwrap_or(false) { members[n.ordinal]=true; self.selected[(0,n.ordinal)]=true } }
for ordinal in index.nodes[plane].children {
let e=index.nodes[ordinal].element
if actual(e) != Some(BpmnBPMNShape) && actual(e) != Some(BpmnBPMNEdge) {
c.add("bpmn/unsupported-element","Selected plane contains an unsupported graphical element",e~); continue
}
let targets=c.resolve_field(0,ordinal,"bpmnElement")
if targets.length() != 1 { c.add("bpmn/invalid-reference","Selected DI must identify one graphical element",e~); continue }
let key=targets[0]
if !self.di.contains(key) { self.di_order.push(key) }
match self.di.get(key) { Some(values) => values.push(ordinal); None => self.di[key]=[ordinal] }
}
}
}
///|
fn Projection::geometry_for(self : Projection, key : (Int,Int), shape : Bool) -> Int? {
let candidates=self.di.get(key).unwrap_or([])
let expected=if shape { @bpmn.TypeId::BpmnBPMNShape } else { BpmnBPMNEdge }
let values=candidates.filter(i => actual(self.converter.sources[0].index.nodes[i].element) == Some(expected))
if values.length() > 1 { self.converter.add("bpmn/ambiguous-di","Multiple DI elements depict one source identity; select an unambiguous plane",e=self.converter.sources[key.0].index.nodes[key.1].element); return None }
values.get(0)
}
///|
fn Projection::add_node(self : Projection, instance : Instance, ordinal : Int, kind : @model.NodeKind) -> Unit {
let c=self.converter; let e=c.sources[instance.resource].index.nodes[ordinal].element
let id=c.render_id(instance.resource,ordinal,instance.tag)
let geometry_ordinal=self.geometry_for((instance.resource,ordinal),true)
let geometry=geometry_ordinal.map(i => c.shape(i))
let ownership=@model.Ownership::new(pool?=instance.pool)
let mut label=if kind == TextAnnotation { e.children.iter().find_first(c => actual(c) == Some(Text)).map(c => c.content).unwrap_or("") } else { e.attribute("name").unwrap_or("") }
let label_ref=if kind == Group { "categoryValueRef" } else if kind == DataObjectReference { "dataObjectRef" } else if kind == DataStoreReference { "dataStoreRef" } else { "" }
if label_ref != "" && label.is_empty() {
for key in c.resolve_field(instance.resource,ordinal,label_ref) { let element=c.sources[key.0].index.nodes[key.1].element; label=element.attribute(if kind == Group { "value" } else { "name" }).unwrap_or("") }
}
let markers=c.markers(instance.resource,ordinal)
self.visuals[(instance.resource,ordinal,instance.tag)]=self.nodes.length()
self.visual_order.push((instance.resource,ordinal,instance.tag))
if geometry_ordinal is Some(i) { self.used_di[i]=true; match self.shapes.get(i) { Some(values) => values.push(self.nodes.length()); None => self.shapes[i]=[self.nodes.length()] } }
self.nodes.push(@model.Node::new(id,kind,bounds=integer_bounds(geometry),source=c.identity(instance.resource,ordinal),label~,geometry?,ownership~,markers~,expanded?=geometry.bind(g => g.expanded),black_box=kind == Participant && e.attribute("processRef") == None))
}
///|
fn Projection::collect(self : Projection, instance : Instance) -> Unit {
let c=self.converter; let index=c.sources[instance.resource].index
let referenced_data=Map([])
for n in index.nodes {
if !instance.members.contains(n.ordinal) { continue }
if actual(n.element) == Some(DataObjectReference) { for key in c.resolve_field(instance.resource,n.ordinal,"dataObjectRef") { referenced_data[key]=true } }
}
for n in index.nodes {
if !instance.members.contains(n.ordinal) || (n.ordinal == instance.root && !(is_type(n.element,SubProcess) && self.di.contains((instance.resource,n.ordinal)))) { continue }
let e=n.element
if e.kind is Unknown(_) { c.add("bpmn/unsupported-element","Selected standard element is unknown",e~); continue }
if n.payload { continue }
if actual(e) == Some(ImplicitThrowEvent) {
if self.di.contains((instance.resource,n.ordinal)) { c.add("bpmn/unsupported-element","Implicit throw events are non-graphical",e~) }
continue
}
if is_type(e,ChoreographyActivity) || is_type(e,ConversationNode) || actual(e) == Some(ConversationLink) || actual(e) == Some(ConversationAssociation) {
c.add("bpmn/unsupported-view","Choreography and conversation rendering is deferred; the document remains intact",e~); continue
}
if node_kind(e) is Some(kind) {
let has_shape=self.di.contains((instance.resource,n.ordinal))
// Data definitions can have explicit depictions; references carry the usual
// data symbol. Activity-local IO stays a semantic endpoint, never a shape.
if kind == DataObject && referenced_data.contains((instance.resource,n.ordinal)) && !has_shape { continue }
if kind == DataInput || kind == DataOutput {
let parent=n.parent.map(p => index.nodes[p])
let process_io=parent.bind(p => p.parent).map(p => p == instance.root && actual(index.nodes[p].element) == Some(Process)).unwrap_or(false)
if !process_io {
if has_shape { c.add("bpmn/unsupported-element","Only process data inputs and outputs have graphical depictions",e~) }
continue
}
}
self.add_node(instance,n.ordinal,kind)
} else if connection_kind(e) == None && (e.kind is Unknown(_) || is_type(e,FlowNode) || is_type(e,Artifact)) {
c.add("bpmn/unsupported-element","Selected graphical kind \{e.kind.local_name()} has no conversion",e~)
}
}
}
///|
fn Projection::visual(self : Projection, key : (Int,Int), instance : String, owner : @bpmn.Element) -> Int? {
if self.visuals.get((key.0,key.1,instance)) is Some(i) { return Some(i) }
let candidates=[]
for ((resource,ordinal,_),i) in self.visuals { if resource == key.0 && ordinal == key.1 { candidates.push(i) } }
if candidates.length() == 1 { return Some(candidates[0]) }
if candidates.length() > 1 { self.converter.add("bpmn/ambiguous-endpoint","Endpoint has multiple visible participant instances; supply an explicit DI endpoint",e=owner); return None }
let index=self.converter.sources[key.0].index
let mut parent=index.nodes[key.1].parent
while parent is Some(ordinal) {
if self.visuals.get((key.0,ordinal,instance)) is Some(i) { return Some(i) }
parent=index.nodes[ordinal].parent
}
self.converter.add("bpmn/invalid-reference","Selected endpoint has no graphical owner",e=owner); None
}
///|
fn Projection::display_endpoint(self : Projection, semantic : (Int,Int), tag : String, di : Int?, field : String, owner : @bpmn.Element) -> Int? {
if di is Some(di) {
let refs=self.converter.resolve_field(0,di,field)
if refs.length() == 1 {
let key=refs[0]
if key.0 != 0 { self.converter.add("bpmn/invalid-reference","DI endpoint must belong to the selected plane",e=owner); return None }
let candidates=self.shapes.get(key.1).unwrap_or([])
if candidates.length() != 1 { self.converter.add("bpmn/ambiguous-endpoint","DI endpoint does not identify one selected shape",e=owner); return None }
return Some(candidates[0])
}
}
let visual=self.visual(semantic,tag,owner)
if actual(owner) == Some(MessageFlow) && visual is Some(i) && self.nodes[i].ownership.pool is Some(pool) {
if !self.converter.charge(self.nodes.length()) { return None }
for index,node in self.nodes { if node.id == pool && node.black_box { return Some(index) } }
}
visual
}
///|
fn Projection::ownership(self : Projection) -> Unit {
let c=self.converter
for instance in self.instances {
let index=c.sources[instance.resource].index
let lane_members : Map[Int,Array[String]]=Map([])
let category_groups : Map[(Int,Int),Array[String]]=Map([])
for (resource,ordinal,tag) in self.visual_order {
let node=self.nodes[self.visuals[(resource,ordinal,tag)]]
if resource != instance.resource || tag != instance.tag || node.kind != Group { continue }
for key in c.resolve_field(resource,ordinal,"categoryValueRef") {
match category_groups.get(key) { Some(groups) => groups.push(node.id); None => category_groups[key]=[node.id] }
}
}
for n in index.nodes {
if !instance.members.contains(n.ordinal) || actual(n.element) != Some(Lane) { continue }
let mut lanes=[c.render_id(instance.resource,n.ordinal,instance.tag)]
let mut parent=n.parent
while parent is Some(p) { if actual(index.nodes[p].element) == Some(Lane) { lanes.insert(0,c.render_id(instance.resource,p,instance.tag)) }; parent=index.nodes[p].parent }
for key in c.resolve_field(instance.resource,n.ordinal,"flowNodeRef") {
if key.0 != instance.resource || !instance.members.contains(key.1) { c.add("bpmn/invalid-reference","Lane member is outside its participant process",e=n.element); continue }
match lane_members.get(key.1) { Some(existing) => for lane in lanes { if !existing.contains(lane) { existing.push(lane) } }; None => lane_members[key.1]=lanes.copy() }
}
lanes=[]
}
for (resource,ordinal,tag) in self.visual_order {
let i=self.visuals[(resource,ordinal,tag)]
if resource != instance.resource || tag != instance.tag || !instance.members.contains(ordinal) { continue }
let n=index.nodes[ordinal]; let node=self.nodes[i]
let mut subprocess=None; let mut parent=n.parent
let lanes=lane_members.get(ordinal).unwrap_or([])
while parent is Some(p) {
if p == instance.root && !self.visuals.contains((resource,p,tag)) { break }
if subprocess == None && is_type(index.nodes[p].element,SubProcess) { subprocess=Some(c.render_id(resource,p,tag)) }
if node.kind == Lane && actual(index.nodes[p].element) == Some(Lane) { let lane=c.render_id(resource,p,tag); if !lanes.contains(lane) { lanes.insert(0,lane) } }
if p == instance.root { break }
parent=index.nodes[p].parent
}
if subprocess == None { subprocess=instance.container }
let attached=c.resolve_field(resource,ordinal,"attachedToRef").get(0).bind(key => self.visual(key,tag,n.element)).map(i => self.nodes[i].id)
let groups : Array[String]=[]
if node.kind != Group {
for key in c.resolve_field(resource,ordinal,"categoryValueRef") {
for group in category_groups.get(key).unwrap_or([]) { if !groups.contains(group) { groups.push(group) } }
}
}
self.nodes[i]=node.with_ownership(@model.Ownership::new(pool?=instance.pool,lanes~,subprocess?,attached_to?=attached,groups~))
}
}
}
///|
fn Projection::connections(self : Projection, instance : Instance) -> Unit {
let c=self.converter; let index=c.sources[instance.resource].index
for n in index.nodes {
if !instance.members.contains(n.ordinal) || n.payload { continue }
let e=n.element
guard connection_kind(e) is Some(kind) else { continue }
let sources=c.resolve_field(instance.resource,n.ordinal,"sourceRef")
let targets=c.resolve_field(instance.resource,n.ordinal,"targetRef")
guard targets.length() == 1 else { c.add("bpmn/invalid-reference","Connection requires one target",e~); continue }
let data=kind == DataAssociation || kind == DataInputAssociation || kind == DataOutputAssociation
if !data && sources.length() != 1 { c.add("bpmn/invalid-reference","Connection requires one source",e~); continue }
let di=self.geometry_for((instance.resource,n.ordinal),false)
let geometry=di.map(i => { self.used_di[i]=true; c.edge(i) })
guard self.display_endpoint(targets[0],instance.tag,di,"targetElement",e) is Some(target) else { continue }
let semantic_target=c.identity(targets[0].0,targets[0].1)
let condition=e.children.iter().find_first(c => c.kind == ConditionExpression).map(c => c.content)
let count=Int::max(1,sources.length())
for j in 0.. self.display_endpoint(key,instance.tag,di,"sourceElement",e))
if source_key != None && source == None { continue }
let identity=c.identity(instance.resource,n.ordinal)
let base=c.render_id(instance.resource,n.ordinal,instance.tag)
let id=if count == 1 { base } else { base+"@source-\{j}" }
let is_default=source_key.map(key => c.resolve_field(key.0,key.1,"default").contains((instance.resource,n.ordinal))).unwrap_or(false)
let direction : @model.AssociationDirection=match e.attribute("associationDirection") { Some("One") => One; Some("Both") => Both; _ => None }
self.flows.push(@model.Flow::new(id,source.map(i => self.nodes[i].id).unwrap_or(""),self.nodes[target].id,points=integer_points(geometry),identity~,kind~,has_source=source_key != None,semantic_source?=source_key.map(key => c.identity(key.0,key.1)),semantic_target~,geometry?,label=e.attribute("name").unwrap_or(""),condition?,is_default~,direction~))
}
}
}
///|
/// Pure view conversion over caller-supplied resources. Geometry remains optional;
/// the SVG renderer owns complete-DI versus whole-view layout policy.
pub fn to_diagram(document : @bpmn.Document, selector? : ViewSelector = Automatic, resources? : @bpmn.ResourceSet = @bpmn.ResourceSet::new(), resource_id? : String, limits? : @bpmn.Limits = @bpmn.Limits::new(), geometry_policy? : @model.GeometryPolicy = PreferDi) -> Conversion {
let documents=resources.documents.iter().to_array()
if resource_id is Some(id) && !documents.iter().any(d => d.location == id) { documents.push(@bpmn.DocumentResource::new(id,document)) }
let resources=@bpmn.ResourceSet::new(documents~,external=resources.external)
let c=Converter::{ignore_di:geometry_policy == @model.GeometryPolicy::Automatic,sources:[],dependencies:Map([]),resources,limits,diagnostics:[],failed:false,work:0}
fn result(diagram : @render.Diagram?) -> Conversion { {diagram:if c.failed { None } else { diagram },diagnostics:ReadOnlyArray::from_array(c.diagnostics)} }
if limits.nodes <= 0 || limits.depth <= 0 || limits.diagnostics <= 0 || limits.token_units <= 0 || limits.source_units <= 0 || limits.output_units <= 0 { return {diagram:None,diagnostics:[@diagrams.Diagnostic::error("bpmn/conversion-limit","All limits must be positive")]} }
let root=make_source(document,resource_id,resources,limits)
c.sources.push(root)
if !c.charge(root.references.references.length()) { return result(None) }
if !root.index.complete || !root.references.complete { c.add("bpmn/conversion-limit","View validation exceeded its limits"); return result(None) }
guard c.select(selector) is Some(selected) else { return result(None) }
let view=c.sources[selected.resource].index.nodes[selected.node].element
if actual(view) != Some(Process) && !is_type(view,SubProcess) && actual(view) != Some(Collaboration) { c.add("bpmn/unsupported-view","Choreography and conversation rendering is deferred",e=view); return result(None) }
let p=Projection::{converter:c,instances:[],nodes:[],flows:[],visuals:Map([]),selected:Map([]),di:Map([]),di_order:[],used_di:Map([]),shapes:Map([]),visual_order:[]}
if !c.ignore_di { p.selected_di(selected) }
if c.failed { return result(None) }
p.include_scope(selected.resource,selected.node,"",None)
if c.failed { return result(None) }
guard p.instances.get(0) is Some(root_instance) else {
c.add("bpmn/conversion-limit","Root view scope was not completed"); return result(None)
}
if actual(view) == Some(Collaboration) {
let index=c.sources[selected.resource].index
for n in index.nodes {
if !root_instance.members.contains(n.ordinal) || actual(n.element) != Some(Participant) { continue }
let pool=c.render_id(selected.resource,n.ordinal,"")
let processes=c.resolve_field(selected.resource,n.ordinal,"processRef")
if processes.length() > 1 { c.add("bpmn/ambiguous-endpoint","Participant has multiple process targets",e=n.element) }
for key in processes {
if actual(c.sources[key.0].index.nodes[key.1].element) != Some(Process) { c.add("bpmn/invalid-reference","Participant must identify a process",e=n.element); continue }
p.include_scope(key.0,key.1,pool,Some(pool))
}
}
}
let mut instance_index=0
while instance_index < p.instances.length() && !c.failed {
let instance=p.instances[instance_index]; instance_index+=1
p.collect(instance)
let index=c.sources[instance.resource].index
for n in index.nodes {
if !instance.members.contains(n.ordinal) || actual(n.element) != Some(CallActivity) { continue }
let visual=p.visuals.get((instance.resource,n.ordinal,instance.tag))
if visual.map(i => p.nodes[i].expanded == Some(true)).unwrap_or(false) {
for key in c.resolve_field(instance.resource,n.ordinal,"calledElement") {
if actual(c.sources[key.0].index.nodes[key.1].element) == Some(Process) {
let call=p.nodes[visual.unwrap()].id
p.include_scope(key.0,key.1,call,instance.pool,container=call)
}
}
}
}
}
// Root-level messages and stores can have explicit selected-plane shapes.
for key in p.di_order {
if p.selected.contains(key) { continue }
let element=c.sources[key.0].index.nodes[key.1].element
if actual(element) == Some(Message) || actual(element) == Some(DataStore) {
p.selected[key]=true
p.add_node({resource:key.0,root:key.1,tag:"",pool:None,container:None,members:Map([(key.1,true)])},key.1,node_kind(element).unwrap())
} else { c.add("bpmn/invalid-reference","DI element is outside the selected view",e=element) }
}
if selected.plane != None {
for instance in p.instances {
if instance.pool is Some(pool) {
let has_internal_di=p.di_order.iter().any(key => key.0 == instance.resource && instance.members.contains(key.1))
if !has_internal_di { for i,node in p.nodes { if node.id == pool && node.geometry != None { p.nodes[i]=node.with_black_box(true) } } }
}
}
}
p.ownership()
for instance in p.instances { p.connections(instance) }
for key in p.di_order { for di in p.di[key] { if !p.used_di.contains(di) { c.add("bpmn/unsupported-element","Selected DI has no graphical projection",e=c.sources[0].index.nodes[di].element) } } }
let seen : Array[@bpmn.ValidationFinding]=[]
for source in c.sources {
for finding in source.findings {
if seen.contains(finding) { continue }; seen.push(finding)
let mut resource : Int?=None
for r,s in c.sources { if s.id == finding.resource_id { resource=Some(r); break } }
let selected=resource.map(r => finding.node.map(n => p.selected.contains((r,n)) || c.dependencies.contains((r,n)) || n == 0).unwrap_or(false)).unwrap_or(false)
let fatal=selected && finding.diagnostic.code != "bpmn/unresolved-import"
let diagnostic=map_diagnostic(finding.diagnostic)
if c.diagnostics.length() < limits.diagnostics { c.diagnostics.push(if fatal { diagnostic } else { {..diagnostic,severity:Warning} }) } else { c.add("bpmn/conversion-limit","Combined diagnostic limit exceeded") }
if fatal { c.failed=true }
}
}
let ids=Map([])
for node in p.nodes { if ids.contains(node.id) { c.add("bpmn/duplicate-id","Duplicate generated render identity") }; ids[node.id]=true }
for flow in p.flows { if ids.contains(flow.id) { c.add("bpmn/duplicate-id","Duplicate generated render identity") }; ids[flow.id]=true }
let id=c.render_id(selected.resource,selected.node,"")
let bpmn=@model.Bpmn::new(id,p.nodes,p.flows,label=view.attribute("name").unwrap_or(id),source=c.identity(selected.resource,selected.node),kind=if actual(view) == Some(Collaboration) { Collaboration } else { Process },plane?=selected.plane.map(i => c.identity(0,i)))
result(Some(Bpmn(bpmn)))
}
///|
/// Read, select, convert and render without performing filesystem/network I/O.
pub fn render_svg(source : String, selector? : ViewSelector = Automatic, resources? : @bpmn.ResourceSet = @bpmn.ResourceSet::new(), geometry_policy? : @model.GeometryPolicy = PreferDi) -> @render.Rendered {
let read=@bpmn.read(source)
guard read.document is Some(document) else { return @render.Rendered::failed("image/svg+xml",read.diagnostics.map(map_diagnostic)) }
let converted=to_diagram(document,selector~,resources~,geometry_policy~)
// A successful selected view has already checked its semantic fields. Lowering
// findings in other views or explicitly ignored DI remain visible as warnings.
let diagnostics=read.diagnostics.map(d => { let mapped=map_diagnostic(d); if converted.diagram != None { {..mapped,severity:Warning} } else { mapped } }).iter().to_array()
diagnostics.append(converted.diagnostics)
guard converted.diagram is Some(diagram) else { return @render.Rendered::failed("image/svg+xml",diagnostics) }
let rendered=@svg.SvgRenderer::new(bpmn_policy=geometry_policy).render(diagram)
diagnostics.append(rendered.diagnostics)
match rendered.output { Some(output) => @render.Rendered::ok(output,rendered.media_type,diagnostics~); None => @render.Rendered::failed(rendered.media_type,diagnostics) }
}