///|
priv struct RailPaint {
paint : Paint
diagnostics : Array[@diagrams.Diagnostic]
limit : Int32
mut used : Int32
mut failed : Bool
}
///|
fn RailPaint::reserve(self : RailPaint, units : Int32) -> Bool {
if units < 0 || units > self.limit - self.used {
if !self.failed {
self.diagnostics.push(
@diagrams.Diagnostic::error(
"railroad/render-limit", "Railroad SVG output limit exceeded",
),
)
}
self.failed = true
return false
}
self.used += units
true
}
///|
fn RailPaint::escaped(
self : RailPaint,
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.diagnostics.push(
@diagrams.Diagnostic::error(
"railroad/invalid-xml-character", "Railroad text contains an unpaired surrogate",
),
)
self.failed = true
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.diagnostics.push(
@diagrams.Diagnostic::error(
"railroad/invalid-xml-character", "Railroad text contains a forbidden XML character",
),
)
self.failed = true
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 RailPaint::element(
self : RailPaint,
name : String,
attrs~ : ArrayView[(String, String)] = [],
children~ : ArrayView[@writer.Node] = [],
) -> @writer.Node {
if !self.reserve(name.length() * 2 + 8) {
return @writer.element("g")
}
for pair in attrs {
if !self.reserve(pair.0.length() + 4) || !self.escaped(pair.1, true) {
return @writer.element("g")
}
}
@writer.element(name, attrs~, children~)
}
///|
fn RailPaint::text(self : RailPaint, value : String) -> @writer.Node {
if !self.escaped(value, false) {
return @writer.text("")
}
@writer.text(value)
}
///|
fn rail_number(value : Double) -> String {
@railroad_layout.svg_number(value)
}
///|
fn RailPaint::label(
self : RailPaint,
label : @railroad_layout.Label,
fill : String,
class : String,
anchor~ : String = "middle",
) -> @writer.Node {
let x = if anchor == "start" { label.box.x } else { label.x }
let children = []
for i, line in label.lines {
children.push(
self.element(
"tspan",
attrs=[
("x", rail_number(x)),
(
"y",
rail_number(
label.box.y +
label.size +
i.to_double() * (label.size + 4.0) -
2.0,
),
),
],
children=[self.text(line)],
),
)
}
self.element(
"text",
attrs=[
("class", class),
("x", rail_number(x)),
("y", rail_number(label.box.y + label.size - 2.0)),
("text-anchor", anchor),
("font-family", font_stack),
("font-size", rail_number(label.size)),
("font-weight", label.weight.to_string()),
("fill", fill),
],
children~,
)
}
///|
fn shape_name(shape : @railroad_layout.Shape) -> String {
match shape {
Pill => "terminal"
Square => "reference"
Class => "class"
Special => "special"
Anchor => "anchor"
Lookaround => "lookaround"
Dot => "empty"
}
}
///|
fn term_family(shape : @railroad_layout.Shape) -> String {
match shape {
Pill => "frontend"
Square => "database"
Class => "cloud"
Special => "messagebus"
Anchor | Lookaround | Dot => "external"
}
}
///|
fn term_role(kind : @railroad_trace.TermKind) -> String {
match kind {
Consumed => "consumed"
Reference(_) => "reference"
Anchor => "anchor"
Assertion => "assertion"
Empty => "empty"
}
}
///|
fn RailPaint::rect(
self : RailPaint,
class : String,
r : @railroad_layout.Rect,
radius : Double,
fill : String,
stroke : String,
dash? : String,
width~ : String = "1.5",
) -> @writer.Node {
let attrs = [
("class", class),
("x", rail_number(r.x)),
("y", rail_number(r.y)),
("width", rail_number(r.width)),
("height", rail_number(r.height)),
("rx", rail_number(radius)),
("fill", fill),
("stroke", stroke),
("stroke-width", width),
]
if dash is Some(d) {
attrs.push(("stroke-dasharray", d))
}
self.element("rect", attrs~)
}
///|
fn RailPaint::term(
self : RailPaint,
box : @railroad_layout.TermBox,
) -> @writer.Node {
let family = term_family(box.shape)
let fill = self.paint.color(family + "-fill")
let stroke = self.paint.color(family + "-stroke")
let ink = self.paint.color("text")
let t = box.term
let attrs = [
("class", "rr-term rr-" + shape_name(box.shape)),
("data-term", t.id),
("data-role", term_role(t.kind)),
("data-rule", t.rule),
]
if t.constraint {
attrs.push(("data-constraint", "true"))
}
let children = []
let mut dash : String? = None
match t.kind {
Reference(Resolved(rule)) => attrs.push(("data-target", rule))
Reference(Unresolved) => {
attrs.push(("data-resolution", "unresolved"))
dash = Some("4 3")
children.push(
self.element("title", children=[self.text("Undefined rule " + t.label)]),
)
}
Reference(Ambiguous(_)) => {
attrs.push(("data-resolution", "ambiguous"))
dash = Some("4 3")
children.push(
self.element("title", children=[
self.text("Rule " + t.label + " has more than one definition"),
]),
)
}
_ => ()
}
let r = box.rect
match box.shape {
Dot =>
children.push(
self.element("circle", attrs=[
("class", "rr-dot"),
("cx", rail_number(r.x + r.width / 2.0)),
("cy", rail_number(r.y + r.height / 2.0)),
("r", "5"),
("fill", stroke),
]),
)
Pill =>
children.push(self.rect("rr-box", r, r.height / 2.0, fill, stroke, dash?))
Square => children.push(self.rect("rr-box", r, 2.0, fill, stroke, dash?))
Class => children.push(self.rect("rr-box", r, 6.0, fill, stroke))
Special =>
children.push(self.rect("rr-box", r, 6.0, fill, stroke, dash="2 3"))
Anchor =>
children.push(self.rect("rr-box", r, 6.0, fill, stroke, dash="4 3"))
Lookaround =>
children.push(self.rect("rr-box", r, 8.0, fill, stroke, dash="6 4"))
}
if box.shape != Dot {
children.push(
self.label(
box.label,
if box.shape == Lookaround {
self.paint.color("muted")
} else {
ink
},
"rr-label",
),
)
}
for note in box.notes {
children.push(self.label(note, self.paint.color("muted"), "rr-note-text"))
}
self.element("g", attrs~, children~)
}
///|
fn RailPaint::wire(
self : RailPaint,
wire : @railroad_layout.Wire,
) -> @writer.Node {
let c = wire.connector
let stroke = if c.constraint {
self.paint.color("muted")
} else {
self.paint.color("arrow")
}
let attrs = [
("class", "rr-connector rr-" + c.role.name()),
("data-connector", c.id),
("data-role", c.role.name()),
("data-owner", c.owner),
("d", wire.path()),
("fill", "none"),
("stroke", stroke),
("stroke-width", "1.5"),
]
if c.constraint {
attrs.push(("data-constraint", "true"))
attrs.push(("stroke-dasharray", "4 3"))
}
if c.role == Return {
attrs.push(("marker-end", "url(#rr-return)"))
}
let path = self.element("path", attrs~)
match wire.label {
Some(label) =>
self.element("g", attrs=[("class", "rr-wire")], children=[
path,
self.label(label, self.paint.color("muted"), "rr-bounds"),
])
None => path
}
}
///|
/// Supplied evidence for one target, by channel.
priv struct Evidence {
matched : Array[String]
not_matched : Array[String]
taken : Array[String]
failure : Array[(String, @railroad_trace.FailurePoint)]
}
///|
priv struct Overlay {
trace : @railroad_trace.Trace
selected : Array[@railroad_trace.Attempt]
terms : Map[String, Evidence]
connectors : Map[String, Evidence]
/// Failures at entry or exit boundaries, by point identity.
boundaries : Map[String, Evidence]
order : Array[String]
}
///|
fn evidence() -> Evidence {
{ matched: [], not_matched: [], taken: [], failure: [], }
}
///|
fn add_unique(list : Array[String], value : String) -> Unit {
if !list.contains(value) {
list.push(value)
}
}
///|
/// Validate a highlight request and collect evidence by target. The
/// trace must belong to this model and view; no target is guessed.
fn resolve_overlay(
highlight : TraceHighlight,
model : @railroad_layout.Model,
) -> Result[Overlay, ReadOnlyArray[@diagrams.Diagnostic]] {
let trace = highlight.trace
let subject : @railroad_trace.Subject = match model {
Regex(m) => Regex(m)
Ebnf(g) =>
match trace.subject.start_rule {
Some(rule) => Ebnf(g, rule)
None =>
return Err([
@diagrams.Diagnostic::error(
"trace/subject-mismatch", "The trace does not describe a grammar",
),
])
}
}
let checked = @railroad_trace.validate_trace(
trace,
subject,
view_id=subject.model_id(),
limits=highlight.limits,
)
guard checked.validated is Some(validated) else {
return Err(checked.diagnostics)
}
let topology = validated.topology
let selected = match highlight.attempts {
None => trace.attempts.iter().collect()
Some(ids) => {
let out = []
for id in ids {
match trace.attempts.iter().find_first(a => a.id == id) {
Some(a) => out.push(a)
None =>
return Err([
@diagrams.Diagnostic::error(
"trace/invalid-attempt",
"The highlight selects missing attempt '\{id}'",
),
])
}
}
out
}
}
let terms : Map[String, Evidence] = Map([])
let connectors : Map[String, Evidence] = Map([])
let boundaries : Map[String, Evidence] = Map([])
let order = []
fn slot(map : Map[String, Evidence], id : String) -> Evidence {
match map.get(id) {
Some(e) => e
None => {
let e = evidence()
map[id] = e
order.push(id)
e
}
}
}
for attempt in selected {
for step in attempt.steps {
let e = match step {
Visit(id, _) => slot(terms, id)
Traverse(id, _) => slot(connectors, id)
}
match attempt.status {
Matched => add_unique(e.matched, attempt.id)
NotMatched => add_unique(e.not_matched, attempt.id)
}
add_unique(e.taken, attempt.id)
}
if attempt.failure is Some(failure) {
let e = match failure.target {
Terminal(id) | Assertion(id) => slot(terms, id)
Entry(id) => slot(boundaries, topology.entry(id).unwrap_or(id))
Exit(id) => slot(boundaries, topology.exit(id).unwrap_or(id))
}
e.failure.push((attempt.id, failure))
}
}
Ok({ trace, selected, terms, connectors, boundaries, order, })
}
///|
fn RailPaint::channel_color(
self : RailPaint,
style : ChannelStyle,
token : String,
) -> String {
match style.color {
Some(c) => c.to_css()
None => self.paint.color(token)
}
}
///|
fn status_text(channel : String, attempts : ArrayView[String]) -> String {
"\{channel} in attempt\{if attempts.length() == 1 { "" } else { "s" }} \{attempts.join(", ")}. Supplied by an external trace provider."
}
///|
/// Draw supplied evidence in four independent layers: matched, not matched,
/// taken path or prefix, then failure points on top. Each layer has a
/// different stroke width, so shared targets keep every classification.
fn RailPaint::overlay(
self : RailPaint,
highlight : TraceHighlight,
overlay : Overlay,
geometry : @railroad_layout.Geometry,
) -> @writer.Node {
let wires : Map[String, @railroad_layout.Wire] = Map([])
for w in geometry.wires {
wires[w.connector.id] = w
}
let boxes : Map[String, @railroad_layout.TermBox] = Map([])
for b in geometry.terms {
boxes[b.term.id] = b
}
let layers = []
let channels : Array[
(String, String, ChannelStyle, Double, (Evidence) -> Array[String]),
] = [
("matched", "backend-ink", highlight.matched, 4.0, e => e.matched),
(
"not-matched",
"messagebus-ink",
highlight.not_matched,
2.0,
e => e.not_matched,
),
("taken", "database-ink", highlight.taken, 0.0, e => e.taken),
]
for channel in channels {
let (name, token, style, extra, pick) = channel
if !style.visible {
continue
}
let color = self.channel_color(style, token)
let items = []
for id in overlay.order {
if overlay.connectors.get(id) is Some(e) &&
!pick(e).is_empty() &&
wires.get(id) is Some(wire) {
let attrs = [
("class", "rr-trace-path rr-trace-" + name),
("data-channel", name),
("data-target", id),
("data-attempts", pick(e).join(" ")),
("d", wire.path()),
("fill", "none"),
("stroke", color),
("stroke-width", rail_number(style.width + extra)),
("stroke-linecap", "round"),
("stroke-opacity", "0.85"),
("pointer-events", "none"),
]
if style.dashed {
attrs.push(("stroke-dasharray", "6 4"))
}
items.push(
self.element("path", attrs~, children=[
self.element("title", children=[
self.text(
status_text(
match name {
"matched" => "Matched"
"not-matched" => "Not matched"
_ => "Taken"
},
pick(e),
),
),
]),
]),
)
}
if overlay.terms.get(id) is Some(e) &&
!pick(e).is_empty() &&
boxes.get(id) is Some(box) {
let grow = 2.0 + extra
let r = box.rect
let attrs = [
("class", "rr-trace-term rr-trace-" + name),
("data-channel", name),
("data-target", id),
("data-attempts", pick(e).join(" ")),
("x", rail_number(r.x - grow)),
("y", rail_number(r.y - grow)),
("width", rail_number(r.width + 2.0 * grow)),
("height", rail_number(r.height + 2.0 * grow)),
("rx", rail_number(r.height / 2.0 + grow)),
("fill", "none"),
("stroke", color),
("stroke-width", rail_number(style.width)),
("stroke-opacity", "0.9"),
("pointer-events", "none"),
]
if style.dashed {
attrs.push(("stroke-dasharray", "6 4"))
}
items.push(
self.element("rect", attrs~, children=[
self.element("title", children=[
self.text(
status_text(
match name {
"matched" => "Matched"
"not-matched" => "Not matched"
_ => "Taken"
},
pick(e),
),
),
]),
]),
)
}
}
if !items.is_empty() {
layers.push(
self.element(
"g",
attrs=[("class", "rr-trace-layer"), ("data-channel", name)],
children=items,
),
)
}
}
if highlight.failure.visible {
let color = self.channel_color(highlight.failure, "security-ink")
let items = []
fn failure_title(e : Evidence) -> String {
let parts = []
for pair in e.failure {
let (attempt, f) = pair
let target = match f.target {
Terminal(_) => "terminal"
Assertion(_) => "assertion"
Entry(_) => "entry boundary"
Exit(_) => "exit boundary"
}
let position = f.position
.map(p => " at input position \{p}")
.unwrap_or("")
let message = f.message.map(m => ": " + m).unwrap_or("")
parts.push(
"Failure in attempt \{attempt} at the \{target}\{position}\{message}",
)
}
parts.join(". ") + ". Supplied by an external trace provider."
}
for id in overlay.order {
match (overlay.terms.get(id), boxes.get(id)) {
(Some(e), Some(box)) if !e.failure.is_empty() => {
let r = box.rect
let cx = r.x + r.width
let cy = r.y
items.push(
self.element(
"g",
attrs=[
("class", "rr-trace-failure"),
("data-channel", "failure"),
("data-target", id),
("data-attempts", e.failure.map(p => p.0).join(" ")),
("pointer-events", "none"),
],
children=[
self.element("title", children=[self.text(failure_title(e))]),
self.element("rect", attrs=[
("x", rail_number(r.x - 2.0)),
("y", rail_number(r.y - 2.0)),
("width", rail_number(r.width + 4.0)),
("height", rail_number(r.height + 4.0)),
("rx", rail_number(r.height / 2.0 + 2.0)),
("fill", "none"),
("stroke", color),
("stroke-width", rail_number(highlight.failure.width)),
]),
self.element("circle", attrs=[
("cx", rail_number(cx)),
("cy", rail_number(cy)),
("r", "7"),
("fill", self.paint.color("bg")),
("stroke", color),
("stroke-width", "2"),
]),
self.element("path", attrs=[
(
"d",
"M\{rail_number(cx - 3.0)} \{rail_number(cy - 3.0)} L\{rail_number(cx + 3.0)} \{rail_number(cy + 3.0)} M\{rail_number(cx + 3.0)} \{rail_number(cy - 3.0)} L\{rail_number(cx - 3.0)} \{rail_number(cy + 3.0)}",
),
("stroke", color),
("stroke-width", "2"),
]),
],
),
)
}
_ => ()
}
match (overlay.boundaries.get(id), geometry.point(id)) {
(Some(e), Some(p)) if !e.failure.is_empty() =>
items.push(
self.element(
"g",
attrs=[
("class", "rr-trace-failure rr-trace-boundary"),
("data-channel", "failure"),
("data-target", id),
("data-attempts", e.failure.map(pair => pair.0).join(" ")),
("pointer-events", "none"),
],
children=[
self.element("title", children=[self.text(failure_title(e))]),
self.element("circle", attrs=[
("cx", rail_number(p.x)),
("cy", rail_number(p.y)),
("r", "8"),
("fill", self.paint.color("bg")),
("stroke", color),
("stroke-width", "2.5"),
]),
self.element("path", attrs=[
(
"d",
"M\{rail_number(p.x - 4.0)} \{rail_number(p.y - 4.0)} L\{rail_number(p.x + 4.0)} \{rail_number(p.y + 4.0)} M\{rail_number(p.x + 4.0)} \{rail_number(p.y - 4.0)} L\{rail_number(p.x - 4.0)} \{rail_number(p.y + 4.0)}",
),
("stroke", color),
("stroke-width", "2"),
]),
],
),
)
_ => ()
}
}
if !items.is_empty() {
layers.push(
self.element(
"g",
attrs=[("class", "rr-trace-layer"), ("data-channel", "failure")],
children=items,
),
)
}
}
let matched = overlay.selected.iter().filter(a => a.status == Matched).count()
self.element(
"g",
attrs=[
("class", "rr-trace"),
("data-provider", overlay.trace.provider),
("data-attempts", overlay.selected.map(a => a.id).join(" ")),
("data-matched", matched.to_string()),
("data-not-matched", (overlay.selected.length() - matched).to_string()),
],
children=layers,
)
}
///|
priv struct RailroadPaint {
rendered : @render.Rendered
geometry : @railroad_layout.Geometry?
}
///|
fn railroad_model(diagram : @render.Diagram) -> @railroad_layout.Model? {
match diagram {
Regex(m) => Some(Regex(m))
Ebnf(g) => Some(Ebnf(g))
_ => None
}
}
///|
fn paint_railroad(
diagram : @render.Diagram,
renderer : SvgRenderer,
) -> RailroadPaint {
fn failed(diagnostics : ArrayView[@diagrams.Diagnostic]) -> RailroadPaint {
{
rendered: @render.Rendered::failed("image/svg+xml", diagnostics),
geometry: None,
}
}
guard railroad_model(diagram) is Some(model) else {
return failed([
@diagrams.Diagnostic::error(
"railroad/unsupported-diagram", "Not a regex or EBNF diagram",
),
])
}
let limits = renderer.railroad_limits.with_minimum_approach(
renderer.connectors.minimum_approach,
)
let result = @railroad_layout.layout(model, limits~)
let diagnostics = result.diagnostics.iter().collect()
guard result.geometry is Some(geometry) else { return failed(diagnostics) }
let overlay = match renderer.trace {
None => None
Some(h) =>
match resolve_overlay(h, model) {
Ok(o) => Some((h, o))
Err(errors) => return failed(errors)
}
}
let theme = renderer.theme
let mode = renderer.mode
let c = RailPaint::{
paint: Paint::{ theme, mode, },
diagnostics,
limit: limits.output_units,
used: 0,
failed: false,
}
let defs = []
if mode == Adaptive {
defs.push(c.element("style", children=[c.text(style_sheet(theme))]))
}
let arrow = c.paint.color("arrow")
defs.push(
c.element(
"marker",
attrs=[
("id", "rr-return"),
("viewBox", "-10 -6 16 12"),
("refX", "0"),
("refY", "0"),
("markerWidth", "12"),
("markerHeight", "10"),
("markerUnits", "userSpaceOnUse"),
("orient", "auto-start-reverse"),
],
children=[
c.element("path", attrs=[
("d", "M-8 -4 L0 0 L-8 4 Z"),
("fill", arrow),
("stroke", "none"),
]),
],
),
)
let kind = if model is Regex(_) { "regex" } else { "ebnf" }
let parts = []
if geometry.title is Some(label) {
parts.push(
c.element("g", attrs=[("class", "rr-title")], children=[
c.label(label, c.paint.color("text"), "rr-title-text"),
]),
)
}
let notes = []
for note in geometry.notes {
notes.push(
c.element("g", attrs=[("class", "rr-note"), ("data-note", note.id)], children=[
c.rect(
"rr-note-box",
note.rect,
4.0,
c.paint.color("external-fill"),
c.paint.color("external-stroke"),
),
c.label(note.label, c.paint.color("text"), "rr-note-text"),
]),
)
}
let tracks = []
for track in geometry.tracks {
let children = [
c.label(
track.label,
c.paint.color("text"),
"rr-rule-name",
anchor="start",
),
]
for note in track.notes {
children.push(
c.label(note, c.paint.color("muted"), "rr-note-text", anchor="start"),
)
}
children.push(
c.element("circle", attrs=[
("class", "rr-start"),
("cx", rail_number(track.start.x - 5.0)),
("cy", rail_number(track.start.y)),
("r", "5"),
("fill", arrow),
]),
)
children.push(
c.element("circle", attrs=[
("class", "rr-end"),
("cx", rail_number(track.end.x + 6.0)),
("cy", rail_number(track.end.y)),
("r", "6"),
("fill", "none"),
("stroke", arrow),
("stroke-width", "1.5"),
]),
)
children.push(
c.element("circle", attrs=[
("cx", rail_number(track.end.x + 6.0)),
("cy", rail_number(track.end.y)),
("r", "3"),
("fill", arrow),
]),
)
tracks.push(
c.element(
"g",
attrs=[("class", "rr-track"), ("data-rule", track.track.id)],
children~,
),
)
}
let frames = []
for frame in geometry.frames {
let role = if frame.role == Except { "except" } else { "group" }
frames.push(
c.element(
"g",
attrs=[("class", "rr-frame rr-" + role), ("data-owner", frame.owner)],
children=[
c.rect(
"rr-frame-box",
frame.rect,
8.0,
"none",
c.paint.color(
if frame.role == Except {
"security-stroke"
} else {
"muted"
},
),
dash="5 4",
width="1.2",
),
c.label(
frame.label,
c.paint.color(
if frame.role == Except {
"security-ink"
} else {
"muted"
},
),
"rr-frame-label",
),
],
),
)
}
let wires = []
for wire in geometry.wires {
if c.failed {
break
}
wires.push(c.wire(wire))
}
let terms = []
for term in geometry.terms {
if c.failed {
break
}
terms.push(c.term(term))
}
let title = match model {
Regex(m) =>
m.accessibility_title
.map(t => Some(t))
.unwrap_or(m.title)
.unwrap_or("Regular expression")
Ebnf(g) =>
g.accessibility_title
.map(t => Some(t))
.unwrap_or(g.title)
.unwrap_or("EBNF grammar")
}
let mut description = match model {
Regex(m) =>
m.accessibility_description.unwrap_or(
"Railroad diagram of the regular expression \{m.pattern.unwrap_or("")} with \{geometry.terms.length()} terms. It shows structure; it does not match input.",
)
Ebnf(g) =>
g.accessibility_description.unwrap_or(
"Railroad diagram of a grammar with \{geometry.tracks.length()} rules and \{geometry.terms.length()} terms. References are not expanded. It shows structure; it does not parse input.",
)
}
let content = [
c.element("g", attrs=[("class", "rr-notes")], children=notes),
c.element("g", attrs=[("class", "rr-frames")], children=frames),
c.element("g", attrs=[("class", "rr-connectors")], children=wires),
c.element("g", attrs=[("class", "rr-terms")], children=terms),
c.element("g", attrs=[("class", "rr-tracks")], children=tracks),
]
if overlay is Some((h, o)) {
let matched = o.selected.iter().filter(a => a.status == Matched).count()
description = description +
" Supplied trace from provider \{o.trace.provider} for input \"\{o.trace.input}\": \{o.selected.length()} attempts, \{matched} matched and \{o.selected.length() - matched} not matched. The library shows this external evidence and does not compute it."
content.push(c.overlay(h, o, geometry))
}
let root = c.element(
"svg",
attrs=[
("xmlns", "http://www.w3.org/2000/svg"),
(
"viewBox",
"0 0 \{rail_number(geometry.width)} \{rail_number(geometry.height)}",
),
("width", rail_number(geometry.width)),
("height", rail_number(geometry.height)),
("role", "img"),
("data-diagram", kind),
],
children=[
c.element("title", children=[c.text(title)]),
c.element("desc", children=[c.text(description)]),
c.element("defs", children=defs),
c.element("rect", attrs=[
("class", "bg"),
("x", "0"),
("y", "0"),
("width", rail_number(geometry.width)),
("height", rail_number(geometry.height)),
("fill", c.paint.color("bg")),
]),
..parts,
..content,
],
)
if c.failed {
return failed(c.diagnostics)
}
let obstacles : Array[ConnectorObstacle] = []
let labels : Array[(String, ConnectorObstacle)] = []
for term in geometry.terms {
let r = term.rect
obstacles.push({
owner: term.term.id,
x: r.x,
y: r.y,
width: r.width,
height: r.height,
})
}
for wire in geometry.wires {
if wire.label is Some(label) {
let r = label.box
labels.push(
(
"",
{
owner: wire.connector.id,
x: r.x,
y: r.y,
width: r.width,
height: r.height,
},
),
)
}
}
for frame in geometry.frames {
let r = frame.label.box
labels.push(
(
"",
{
owner: frame.owner,
x: r.x,
y: r.y,
width: r.width,
height: r.height,
},
),
)
}
for note in geometry.notes {
let r = note.rect
labels.push(
(
"",
{ owner: note.id, x: r.x, y: r.y, width: r.width, height: r.height, },
),
)
}
for track in geometry.tracks {
let r = track.label.box
labels.push(
(
"",
{
owner: track.track.id,
x: r.x,
y: r.y,
width: r.width,
height: r.height,
},
),
)
}
if geometry.title is Some(title) {
let r = title.box
labels.push(
(
"",
{ owner: "title", x: r.x, y: r.y, width: r.width, height: r.height, },
),
)
}
guard finalize_connectors(
root,
renderer.connectors,
c.diagnostics,
obstacles=obstacles[:],
labels=labels[:],
)
is Some(root) else {
return failed(c.diagnostics)
}
let original = @writer.to_string(root)
if original.length() > limits.output_units / 2 {
return failed([
@diagrams.Diagnostic::error(
"railroad/render-limit", "Railroad SVG output limit exceeded",
),
])
}
let prefix = match renderer.id_prefix {
Some(raw) => {
let out = StringBuilder()
out.write_string("rr-u-")
for ch in raw {
out.write_string(ch.to_int().to_string(radix=16) + "-")
}
out.to_string()
}
None => {
let mut hash : UInt64 = 14695981039346656037
for i in 0.. limits.output_units {
return failed([
@diagrams.Diagnostic::error(
"railroad/render-limit", "Scoped railroad SVG exceeds its output limit",
),
])
}
let palettes = match mode {
Adaptive => [("dark", theme.dark), ("light", theme.light)]
Fixed(Dark) => [("dark", theme.dark)]
Fixed(Light) => [("light", theme.light)]
}
let custom = []
if renderer.trace is Some(h) {
for style in [h.matched, h.not_matched, h.taken, h.failure] {
if style.visible && style.color is Some(color) {
custom.push(color)
}
}
}
for pair in palettes {
let (name, palette) = pair
for
finding in @railroad_layout.check(
geometry,
palette,
work_limit=limits.work,
highlight_colors=custom,
) {
if finding.rule == Limit {
return failed([
@diagrams.Diagnostic::error("railroad/layout-limit", finding.message),
])
}
c.diagnostics.push(
@diagrams.Diagnostic::warning(
"railroad/legibility",
name + ": " + finding.message,
),
)
}
}
{
rendered: @render.Rendered::ok(
output,
"image/svg+xml",
diagnostics=c.diagnostics,
),
geometry: Some(geometry),
}
}
///|
/// Scene parts of a railroad: the canonical render, one drawn path per
/// connector occurrence and one node binding per term visit.
fn railroad_scene(
renderer : SvgRenderer,
diagram : @render.Diagram,
catalog : @animation.Catalog,
output_units : Int32,
) -> Result[
(@render.Rendered, Array[(@animation.Occurrence, String)], Array[NodeBinding]),
ReadOnlyArray[@diagrams.Diagnostic],
] {
let painted = paint_railroad(diagram, renderer)
guard painted.geometry is Some(geometry) && painted.rendered.output is Some(_) else {
return Err(painted.rendered.diagnostics)
}
let wires : Map[String, @railroad_layout.Wire] = Map([])
for w in geometry.wires {
wires[w.connector.id] = w
}
let paths = []
let nodes = []
for occurrence in catalog.occurrences {
match (occurrence.railroad_connector, occurrence.railroad_term) {
(Some(connector), _) => {
guard wires.get(connector.id) is Some(wire) else {
return Err([
@diagrams.Diagnostic::error(
"animation/binding-mismatch", "A railroad connector has no drawn path",
),
])
}
let d = wire.path()
if d.length() > output_units {
return Err([
@diagrams.Diagnostic::error(
"animation/scene-limit", "Railroad scene path exceeds its output limit",
),
])
}
paths.push((occurrence, d))
}
(None, Some(term)) =>
nodes.push({ endpoint: occurrence.key, kind: RailroadTerm(term.id), })
_ =>
return Err([
@diagrams.Diagnostic::error(
"animation/binding-mismatch", "A railroad occurrence has no target",
),
])
}
}
Ok((painted.rendered, paths, nodes))
}
///|
/// Render a regex railroad with the same options as `SvgRenderer`. A
/// supplied trace adds static highlights; no provider is invoked.
pub fn render_regex(
model : @regex.Regex,
theme~ : @theme.Theme = @theme.Theme::classic(),
mode~ : Mode = Adaptive,
id_prefix? : String,
limits~ : @railroad_layout.Limits = @railroad_layout.Limits::new(),
trace? : TraceHighlight,
connectors~ : ConnectorOptions = ConnectorOptions::new(),
) -> @render.Rendered {
let renderer = SvgRenderer::new(
connectors~,
theme~,
mode~,
id_prefix?,
railroad_limits=limits,
trace?,
)
paint_railroad(Regex(model), renderer).rendered
}
///|
/// Render a grammar railroad with the same options as `SvgRenderer`.
pub fn render_ebnf(
grammar : @ebnf.Grammar,
theme~ : @theme.Theme = @theme.Theme::classic(),
mode~ : Mode = Adaptive,
id_prefix? : String,
limits~ : @railroad_layout.Limits = @railroad_layout.Limits::new(),
trace? : TraceHighlight,
connectors~ : ConnectorOptions = ConnectorOptions::new(),
) -> @render.Rendered {
let renderer = SvgRenderer::new(
connectors~,
theme~,
mode~,
id_prefix?,
railroad_limits=limits,
trace?,
)
paint_railroad(Ebnf(grammar), renderer).rendered
}
///|
/// The result of the explicit provider-and-render driver.
pub struct TracedRender {
run : @railroad_trace.TraceRun
rendered : @render.Rendered
}
///|
/// Invoke `provider` once for `input` and render its validated trace with
/// the given channels. This is the only renderer entry point that calls a
/// provider. Unsupported, exhausted and error outcomes give diagnostics and
/// no SVG; a completed non-match renders its evidence.
pub fn[P : @railroad_trace.RailroadTraceProvider] SvgRenderer::render_traced(
self : SvgRenderer,
diagram : @render.Diagram,
provider : P,
input : String,
start_rule? : String,
limits~ : @railroad_trace.TraceLimits = @railroad_trace.TraceLimits::new(),
attempts? : ArrayView[String],
matched~ : ChannelStyle = ChannelStyle::new(),
not_matched~ : ChannelStyle = ChannelStyle::new(dashed=true),
taken~ : ChannelStyle = ChannelStyle::hidden(),
failure~ : ChannelStyle = ChannelStyle::new(),
) -> TracedRender {
fn refused(diagnostics : ArrayView[@diagrams.Diagnostic]) -> TracedRender {
{
run: @railroad_trace.TraceRun::refused(diagnostics),
rendered: @render.Rendered::failed("image/svg+xml", diagnostics),
}
}
let request = match diagram {
Regex(m) => @railroad_trace.TraceRequest::regex(m, input, limits~)
Ebnf(g) =>
match start_rule {
Some(rule) =>
@railroad_trace.TraceRequest::ebnf(g, rule, input, limits~)
None =>
return refused([
@diagrams.Diagnostic::error(
"trace/invalid-request", "A grammar trace needs a start rule identity",
),
])
}
_ =>
return refused([
@diagrams.Diagnostic::error(
"trace/subject-mismatch", "Traces apply only to regex and EBNF diagrams",
),
])
}
let run = @railroad_trace.run(provider, request)
guard run.validated is Some(validated) else {
return {
run,
rendered: @render.Rendered::failed("image/svg+xml", run.diagnostics),
}
}
let highlight = TraceHighlight::new(
validated.trace,
attempts?,
matched~,
not_matched~,
taken~,
failure~,
limits~,
)
let rendered = paint_railroad(diagram, { ..self, trace: Some(highlight), }).rendered
{ run, rendered, }
}
///|
/// A traced render that did not reach a provider, with the reasons.
pub fn TracedRender::refused(
diagnostics : ArrayView[@diagrams.Diagnostic],
) -> TracedRender {
{
run: @railroad_trace.TraceRun::refused(diagnostics),
rendered: @render.Rendered::failed("image/svg+xml", diagnostics),
}
}