///|
priv struct BpmnPaint {
paint : Paint
limit : Int32
diagnostics : Array[@diagrams.Diagnostic]
mut used : Int32
mut failed : Bool
mut work : Int32
work_limit : Int32
}
///|
fn BpmnPaint::fail(self : BpmnPaint, code : String, message : String) -> Unit {
if !self.failed {
self.diagnostics.push(@diagrams.Diagnostic::error(code, message))
}
self.failed = true
}
///|
fn BpmnPaint::reserve(self : BpmnPaint, units : Int32) -> Bool {
if self.failed {
return false
}
if units < 0 || units > self.limit - self.used {
self.fail("bpmn/render-limit", "BPMN SVG output limit exceeded")
return false
}
self.used += units
true
}
///|
/// Validate XML 1.0 characters and account for the writer's escaped UTF-16 size.
fn BpmnPaint::escaped(
self : BpmnPaint,
value : String,
attribute : Bool,
) -> Bool {
let mut i = 0
while i < value.length() {
let u = value.code_unit_at(i).to_int()
i += 1
if u >= 0xd800 && u <= 0xdbff {
if i >= value.length() ||
value.code_unit_at(i).to_int() < 0xdc00 ||
value.code_unit_at(i).to_int() > 0xdfff {
self.fail(
"bpmn/invalid-xml-character", "BPMN text contains an unpaired surrogate",
)
return false
}
i += 1
if !self.reserve(2) {
return false
}
continue
}
if (u < 32 && u != 9 && u != 10 && u != 13) ||
(u >= 0xdc00 && u <= 0xdfff) ||
u == 0xfffe ||
u == 0xffff {
self.fail(
"bpmn/invalid-xml-character", "BPMN text contains a forbidden XML character",
)
return false
}
let units = if u == 38 {
5
} else if u == 60 || u == 62 {
4
} else if u == 13 || (attribute && (u == 9 || u == 10)) {
5
} else if attribute && u == 34 {
6
} else {
1
}
if !self.reserve(units) {
return false
}
}
true
}
///|
fn BpmnPaint::text(self : BpmnPaint, value : String) -> @writer.Node {
if !self.escaped(value, false) {
return @writer.text("")
}
@writer.text(value)
}
///|
fn BpmnPaint::element(
self : BpmnPaint,
name : String,
attrs~ : ArrayView[(String, String)] = [],
children~ : ArrayView[@writer.Node] = [],
) -> @writer.Node {
if !self.reserve(name.length() * 2 + 64) {
return @writer.element("g")
}
for (key, value) in attrs {
if !self.reserve(key.length() + 4) || !self.escaped(value, true) {
return @writer.element("g")
}
}
@writer.element(name, attrs~, children~)
}
///|
fn BpmnPaint::path(
self : BpmnPaint,
d : String,
stroke : String,
fill~ : String = "none",
width~ : String = "1.5",
dash? : String,
) -> @writer.Node {
let attrs = [
("d", d),
("fill", fill),
("stroke", stroke),
("stroke-width", width),
("stroke-linejoin", "round"),
("stroke-linecap", "round"),
]
if dash is Some(dash) {
attrs.push(("stroke-dasharray", dash))
}
self.element("path", attrs~)
}
///|
fn BpmnPaint::circle(
self : BpmnPaint,
x : String,
y : String,
radius : String,
stroke : String,
fill~ : String = "none",
width~ : String = "1.5",
dash? : String,
) -> @writer.Node {
let attrs = [
("cx", x),
("cy", y),
("r", radius),
("fill", fill),
("stroke", stroke),
("stroke-width", width),
]
if dash is Some(dash) {
attrs.push(("stroke-dasharray", dash))
}
self.element("circle", attrs~)
}
///|
fn BpmnPaint::rect(
self : BpmnPaint,
x : String,
y : String,
width : String,
height : String,
stroke : String,
fill~ : String = "none",
radius~ : String = "0",
weight~ : String = "1.5",
dash? : String,
) -> @writer.Node {
let attrs = [
("x", x),
("y", y),
("width", width),
("height", height),
("rx", radius),
("fill", fill),
("stroke", stroke),
("stroke-width", weight),
]
if dash is Some(dash) {
attrs.push(("stroke-dasharray", dash))
}
self.element("rect", attrs~)
}
///|
fn BpmnPaint::metadata(
self : BpmnPaint,
id : String,
identity : @bpmn.SourceIdentity,
properties : @bpmn.RenderProperties?,
) -> Array[(String, String)] {
let attrs = [("data-id", id)]
if identity.source_id is Some(id) {
attrs.push(("data-source-id", id))
}
if identity.resource_id is Some(id) {
attrs.push(("data-source-resource", id))
}
let path = StringBuilder()
let mut units = 0
for i, index in identity.path {
let part = (if i == 0 { "" } else { "/" }) + index.to_string()
if part.length() > self.limit - self.used - units {
self.fail(
"bpmn/render-limit", "Source identity metadata exceeds the output budget",
)
break
}
units += part.length()
path.write_string(part)
}
attrs.push(("data-source-path", path.to_string()))
if properties.bind(p => p.link) is Some(link) {
attrs.push(("data-link", link.target))
}
if properties.bind(p => p.description) is Some(description) {
attrs.push(("data-description", description))
}
attrs
}
///|
fn bpmn_kind(kind : @bpmn.NodeKind) -> String {
match kind {
StartEvent => "startEvent"
EndEvent => "endEvent"
IntermediateCatchEvent => "intermediateCatchEvent"
IntermediateThrowEvent => "intermediateThrowEvent"
BoundaryEvent => "boundaryEvent"
Task => "task"
UserTask => "userTask"
ServiceTask => "serviceTask"
SendTask => "sendTask"
ReceiveTask => "receiveTask"
ManualTask => "manualTask"
BusinessRuleTask => "businessRuleTask"
ScriptTask => "scriptTask"
SubProcess => "subProcess"
AdHocSubProcess => "adHocSubProcess"
Transaction => "transaction"
CallActivity => "callActivity"
ExclusiveGateway => "exclusiveGateway"
InclusiveGateway => "inclusiveGateway"
ParallelGateway => "parallelGateway"
ComplexGateway => "complexGateway"
EventBasedGateway => "eventBasedGateway"
Participant => "participant"
Lane => "lane"
DataObject => "dataObject"
DataObjectReference => "dataObjectReference"
DataStore => "dataStore"
DataStoreReference => "dataStoreReference"
DataInput => "dataInput"
DataOutput => "dataOutput"
Group => "group"
TextAnnotation => "textAnnotation"
Message => "message"
}
}
///|
fn bpmn_connection(kind : @bpmn.ConnectionKind) -> String {
match kind {
SequenceFlow => "sequenceFlow"
MessageFlow => "messageFlow"
Association => "association"
DataInputAssociation => "dataInputAssociation"
DataOutputAssociation => "dataOutputAssociation"
DataAssociation => "dataAssociation"
}
}
///|
fn bpmn_event(kind : @bpmn.EventKind) -> String {
match kind {
Message => "message"
Timer => "timer"
Conditional => "conditional"
Link => "link"
Signal => "signal"
Error => "error"
Escalation => "escalation"
Cancel => "cancel"
Compensation => "compensation"
Terminate => "terminate"
}
}
///|
fn bpmn_detail(node : @bpmn.Node, key : String) -> String? {
node.markers.details.iter().find_first(p => p.0 == "@" + key).map(p => p.1)
}
///|
fn BpmnPaint::charge(self : BpmnPaint, amount : Int32) -> Bool {
if amount < 0 || amount > self.work_limit - self.work {
self.fail("bpmn/render-limit", "BPMN painting work limit exceeded")
return false
}
self.work += amount
true
}