Standalone SVG rendering for diagrams.mbt models, in the visual style of archify
///|
test {
let s = @sequence.Sequence::new(
[@sequence.Participant::new("Client"), @sequence.Participant::new("API")],
[Message(@sequence.Message::new("Client", "API", "GET /forecast"))],
)
let r = @render.Renderer::render(@svg.SvgRenderer::new(), Sequence(s))
assert_eq(r.media_type, "image/svg+xml")
assert_eq(r.diagnostics.length(), 0)
let svg = r.output.unwrap()
assert_true(svg.contains("@media (prefers-color-scheme: light)"))
assert_true(svg.contains("data-id=\"Client\""))
}///|
test {
let s = @sequence.Sequence::new([], [], title="Login flow")
let svg = @svg.render_sequence(s, mode=Fixed(Light))
assert_false(svg.contains("<style"))
assert_false(svg.contains("var("))
assert_true(svg.contains("fill=\"#f4f5f7\""))
}| Value | Geometry |
|---|---|
| participants | Boxes, labels, optional sublabels, source participants and each participant's lifeline start and end |
| messages | Chips, labels, optional sublabels, source messages and ordered path segments, including all three segments of a self-message |
| notes | A measured note box and its text; an over note spans its anchors and places text clear of activation bars |
| activations | Bars 10 px wide; each nesting depth moves 4 px right; an open activation ends at destruction or diagram end |
| frames | Frames in preorder, parent indices, header chips and ordered sections with their labels and divider segments |
| groups | Source groups, enclosing boxes and title chips; titles use separate member gaps when group extents interleave |
| title and title_box | The measured diagram title and its reserved area |
///|
test {
let s = @sequence.Sequence::new(
[@sequence.Participant::new("A"), @sequence.Participant::new("B")],
[Message(@sequence.Message::new("A", "B", "hello"))],
)
let g = @layout.layout(s)
let theme = @theme.Theme::classic()
assert_eq(@layout.check(g, theme.dark), [])
assert_eq(@layout.check(g, theme.light), [])
}///|
test "canonical scene SVG equals the ordinary static renderer" {
let sequence = @sequence.Sequence::new(
[@sequence.Participant::new("A"), @sequence.Participant::new("B")],
[Message(@sequence.Message::new("A", "B", "hello"))],
)
let renderer = @svg.SvgRenderer::new()
let scene = renderer.scene(Sequence(sequence)).scene.unwrap()
assert_eq(scene.canonical_svg, @svg.render_sequence(sequence))
assert_false(scene.canonical_svg.contains("data-motion-paths"))
assert_true(scene.display_svg.contains("data-motion-paths"))
assert_eq(scene.paths.length(), 1)
}///|
test "swimlane SVG and scene share one actual handoff path" {
let view = @swimlane.Swimlane::new(
"Request",
[
@swimlane.Node::new("Ask", lane="Customer", shape="stadium"),
@swimlane.Node::new("Review", lane="Support", shape="diamond"),
],
[@swimlane.Connection::new("handoff", "Ask", "Review", label="request")],
lanes=[@swimlane.Lane::new("Customer"), @swimlane.Lane::new("Support")],
direction=LR,
)
let renderer = @svg.SvgRenderer::new(id_prefix="request")
let result = renderer.render(Swimlane(view))
assert_eq(result.diagnostics, [])
let scene = renderer.scene(Swimlane(view)).scene.unwrap()
assert_eq(scene.canonical_svg, result.output.unwrap())
assert_eq(scene.paths.length(), 1)
assert_true(scene.canonical_svg.contains("d=\"" + scene.paths[0].path + "\""))
}///|
test "a regex railroad has one path for each topology connector" {
let model = @regex.Regex::new("digits", "s", [
@regex.Node::new("s", Sequence(["a", "r"])),
@regex.Node::new("a", Anchor(LineStart)),
@regex.Node::new("r", Repeat(@regex.Bounds::new(1), "d")),
@regex.Node::new("d", Class(Shorthand('d'))),
])
let result = @svg.render_regex(model, mode=Fixed(Light))
assert_eq(result.diagnostics, [])
let svg = result.output.unwrap()
let topology = @railroad_trace.Topology::regex(model).topology.unwrap()
assert_eq(
svg.split("data-connector=\"").count() - 1,
topology.connectors.length(),
)
}///|
test "a failure terminal can be shown without its prefix" {
let model = @regex.Regex::new("ab", "s", [
@regex.Node::new("s", Sequence(["a", "b"])),
@regex.Node::new("a", Literal("a")),
@regex.Node::new("b", Class(Shorthand('d'))),
])
let request = @railroad_trace.TraceRequest::regex(model, "ax")
let topology = request.topology().topology.unwrap()
let start = topology.connector_of("ab", Start).unwrap()
let trace = @railroad_trace.Trace::new(request, "example", [
@railroad_trace.Attempt::new(
"a0",
NotMatched,
[
Traverse(start.id, false),
Visit("a", Some(@railroad_trace.InputSpan::new(0, 1))),
],
failure=@railroad_trace.FailurePoint::new(Terminal("b"), position=1),
),
])
let only_failure = @svg.TraceHighlight::new(
trace,
matched=@svg.ChannelStyle::hidden(),
not_matched=@svg.ChannelStyle::hidden(),
)
let svg = @svg.render_regex(model, trace=only_failure).output.unwrap()
assert_true(svg.contains("rr-trace-failure"))
assert_false(svg.contains("rr-trace-path"))
let plain = @svg.render_regex(model).output.unwrap()
assert_false(plain.contains("rr-trace"))
}///|
test "an inline scalar row has no connector" {
let tree = @data_tree.DataTree::new("order", "root", [
@data_tree.Node::object("root", [
@data_tree.Member::named("m0", "id", "v0"),
@data_tree.Member::named("m1", "tags", "v1"),
]),
@data_tree.Node::new("v0", Number("12345678901234567890")),
@data_tree.Node::array("v1", [@data_tree.Member::index("e0", 0, "v2")]),
@data_tree.Node::new("v2", Text("gift")),
])
let svg = @svg.render_data_tree(tree, mode=Fixed(Dark)).output.unwrap()
assert_true(svg.contains("12345678901234567890"))
assert_eq(svg.split("class=\"data-connector\"").count() - 1, 1)
}///|
test "typed styles expose the same immutable final geometry as SVG" {
let view = @flowchart.Flowchart::new(
"connectors",
[
@flowchart.Node::new("a", label="Request"),
@flowchart.Node::new("b", label="Done"),
],
[@flowchart.Connection::new("ab", "a", "b")],
)
let renderer = @svg.SvgRenderer::new(
connectors=@svg.ConnectorOptions::new(
style=Curved,
crossing=Bridge,
minimum_approach=32.0,
),
)
let model = @render.Diagram::Flowchart(view)
let output = renderer.render(model).output.unwrap()
let paths = @svg.inspect_connectors(output)
assert_eq(paths.length(), 1)
assert_eq(paths[0].id, "ab")
assert_eq(paths[0].source, "a")
assert_eq(paths[0].target, "b")
assert_eq(paths, renderer.connector_geometry(model).paths)
}fn ChannelStyle::new(visible? : Bool, color? : Color, width? : Double, dashed? : Bool) -> ChannelStylepub(all) enum ConnectorCommand {
Move(ConnectorPoint)
Line(ConnectorPoint)
Quadratic(ConnectorPoint, ConnectorPoint)
Cubic(ConnectorPoint, ConnectorPoint, ConnectorPoint)
} derive(Eq, Debug)pub struct ConnectorGeometry {
paths : ReadOnlyArray[ResolvedConnector]
diagnostics : ReadOnlyArray[Diagnostic]
} derive(Eq, Debug)fn ConnectorLimits::new(candidates? : Int, subdivisions? : Int, segments? : Int, crossings? : Int, output_units? : Int) -> ConnectorLimitspub struct ConnectorOptions {
style : ConnectorStyle?
crossing : CrossingMode
minimum_approach : Double
corner_radius : Double
bridge_radius : Double
style_supplied_bpmn : Bool
limits : ConnectorLimits
} derive(Eq, Debug)fn ConnectorOptions::new(style? : ConnectorStyle, crossing? : CrossingMode, minimum_approach? : Double, corner_radius? : Double, bridge_radius? : Double, style_supplied_bpmn? : Bool, limits? : ConnectorLimits) -> ConnectorOptionspub struct ResolvedConnector {
id : String
source : String
target : String
route : ConnectorPath
path : ConnectorPath
mask : String?
} derive(Eq, Debug)pub struct Scene {
canonical_svg : String
display_svg : String
catalog : Catalog
paths : ReadOnlyArray[PathBinding]
nodes : ReadOnlyArray[NodeBinding]
}pub struct SvgRenderer {
theme : Theme
mode : Mode
id_prefix : String?
bpmn_policy : GeometryPolicy
bpmn_limits : Limits
architecture_limits : Limits
swimlane_limits : Limits
flowchart_limits : Limits
railroad_limits : Limits
data_tree_limits : Limits
connectors : ConnectorOptions
trace : TraceHighlight?
}impl Renderer for SvgRendererfn SvgRenderer::new(theme? : Theme, mode? : Mode, id_prefix? : String, bpmn_policy? : GeometryPolicy, bpmn_limits? : Limits, architecture_limits? : Limits, swimlane_limits? : Limits, flowchart_limits? : Limits, railroad_limits? : Limits, data_tree_limits? : Limits, trace? : TraceHighlight, connectors? : ConnectorOptions) -> SvgRendererfn[P : RailroadTraceProvider] SvgRenderer::render_traced(self : SvgRenderer, diagram : Diagram, provider : P, input : String, start_rule? : String, limits? : TraceLimits, attempts? : ArrayView[String], matched? : ChannelStyle, not_matched? : ChannelStyle, taken? : ChannelStyle, failure? : ChannelStyle) -> TracedRenderfn SvgRenderer::scene(self : SvgRenderer, diagram : Diagram, sequence_view_id? : String, limits? : Limits, output_units? : Int) -> SceneResultpub struct TraceHighlight {
trace : Trace
attempts : ReadOnlyArray[String]?
matched : ChannelStyle
not_matched : ChannelStyle
taken : ChannelStyle
failure : ChannelStyle
limits : TraceLimits
} derive(Eq, Debug)fn TraceHighlight::new(trace : Trace, attempts? : ArrayView[String], matched? : ChannelStyle, not_matched? : ChannelStyle, taken? : ChannelStyle, failure? : ChannelStyle, limits? : TraceLimits) -> TraceHighlightfn check_connectors(paths : ArrayView[ResolvedConnector], obstacles : ArrayView[ConnectorObstacle], width~ : Double, height~ : Double, limits? : ConnectorLimits) -> ReadOnlyArray[Diagnostic]fn render_architecture(model : Architecture, theme? : Theme, mode? : Mode, id_prefix? : String, limits? : Limits, connectors? : ConnectorOptions) -> Renderedfn render_ebnf(grammar : Grammar, theme? : Theme, mode? : Mode, id_prefix? : String, limits? : Limits, trace? : TraceHighlight, connectors? : ConnectorOptions) -> Renderedfn render_regex(model : Regex, theme? : Theme, mode? : Mode, id_prefix? : String, limits? : Limits, trace? : TraceHighlight, connectors? : ConnectorOptions) -> Renderedfn render_sequence(sequence : Sequence, theme? : Theme, mode? : Mode, id_prefix? : String, connectors? : ConnectorOptions) -> String