///|
/// The sequence model of a Mermaid AST and the conversion diagnostics.
pub struct Conversion {
sequence : @model.Sequence
diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
}
///|
/// All semantic diagnostic codes. Reading has its own syntax diagnostic codes.
pub let diagnostic_codes : ReadOnlyArray[String] = [
"mermaid/inactive-deactivation", "mermaid/destroyed-participant",
"mermaid/conflicting-participant-kind", "mermaid/invalid-participant-config",
"mermaid/missing-destroying-message", "mermaid/invalid-links",
"mermaid/conflicting-box-group", "mermaid/duplicate-creation",
]
///|
/// Convert a typed AST into a format-independent sequence. All state belongs
/// to this call. Frames retain their sections and recursively ordered events.
pub fn to_sequence(diagram : @ast.Diagram) -> Conversion {
let participants : Array[@model.Participant] = []
let diagnostics : Array[@diagrams.Diagnostic] = []
let declared : Map[String, @model.ParticipantKind] = Map([])
let active : Map[String, Int] = Map([])
let destroyed : Map[String, Bool] = Map([])
let pending : Array[(String, @diagrams.Span)] = []
let mut title = None
let mut accessibility_title = None
let mut accessibility_description = None
let mut numbering = false
let mut next_number = 1.0
let mut step = 1.0
let mut box_index = 0
fn error(code : String, message : String, span : @diagrams.Span) {
diagnostics.push(@diagrams.Diagnostic::error(code, message, span~))
}
fn index(id : String) -> Int {
for i, p in participants {
if p.id == id { return i }
}
participants.push(@model.Participant::new(id))
participants.length() - 1
}
fn use_participant(id : String, span : @diagrams.Span) {
ignore(index(id))
if destroyed.get(id).unwrap_or(false) {
error("mermaid/destroyed-participant", "Participant \{id} was destroyed", span)
}
}
fn activation(id : String, start : Bool, events : Array[@model.Event], span : @diagrams.Span) {
use_participant(id, span)
let depth = active.get(id).unwrap_or(0)
if start {
active[id] = depth + 1
events.push(Activate(id))
} else {
if depth == 0 {
error("mermaid/inactive-deactivation", "Participant \{id} is not active", span)
} else {
active[id] = depth - 1
}
events.push(Deactivate(id))
}
}
fn walk(list : ReadOnlyArray[@ast.Statement], group : @model.BoxGroup?) -> Array[@model.Event] {
let events : Array[@model.Event] = []
for statement in list {
let span = statement.span
match statement.kind {
Participant(p) => {
let existed = participants.iter().any(old => old.id == p.id)
let i = index(p.id)
let old = participants[i]
let metadata = participant_metadata(p.config)
let (kind, shown_label) = match metadata {
Some((kind, shown_label)) => (kind.unwrap_or(if p.keyword == Actor { Actor } else { Participant }), shown_label)
None => {
error("mermaid/invalid-participant-config", "Invalid configuration for \{p.id}", span)
(if p.keyword == Actor { Actor } else { Participant }, None)
}
}
if declared.get(p.id) is Some(previous) && previous != kind {
error("mermaid/conflicting-participant-kind", "Participant \{p.id} has a different declared kind", span)
} else {
declared[p.id] = kind
if group is Some(g) && old.group is Some(previous) && g != previous {
error("mermaid/conflicting-box-group", "Participant \{p.id} belongs to a different box", span)
}
participants[i] = @model.Participant::new(
p.id, label=p.label.unwrap_or(shown_label.unwrap_or(old.label)), kind~,
group?=match old.group { Some(g) => Some(g); None => group }, link?=old.link, properties?=old.properties,
)
}
if p.created {
if existed {
error("mermaid/duplicate-creation", "Participant \{p.id} already exists", span)
}
events.push(Create(p.id))
} else if destroyed.get(p.id).unwrap_or(false) {
error("mermaid/destroyed-participant", "Participant \{p.id} was destroyed", span)
}
}
Message(m) => {
use_participant(m.sender, span)
if m.receiver != m.sender { use_participant(m.receiver, span) }
let (line, sender_head, receiver_head) = arrow_ends(m)
let number = if numbering { Some(next_number) } else { None }
next_number = ((next_number + step) * 100.0 + 0.5).floor() / 100.0
events.push(Message(@model.Message::new(
m.sender, m.receiver, m.text, line~, sender_head~, receiver_head~,
link?=message_link(m.text), number?,
)))
match m.activation {
Some(Activate) => activation(m.receiver, true, events, span)
Some(Deactivate) => activation(m.sender, false, events, span)
None => ()
}
for (id, destroy_span) in pending {
if id != m.sender && id != m.receiver {
error("mermaid/missing-destroying-message", "Next message does not destroy \{id}", destroy_span)
}
destroyed[id] = true
active[id] = 0
events.push(Destroy(id))
}
pending.clear()
}
Destroy(id) => {
use_participant(id, span)
pending.push((id, span))
}
Activate(id) => activation(id, true, events, span)
Deactivate(id) => activation(id, false, events, span)
Note(n) => {
for id in n.participants { use_participant(id, span) }
let placement : @model.Placement = match n.placement { LeftOf => LeftOf; RightOf => RightOf; Over => Over }
events.push(Note(@model.Note::new(placement, n.participants[:], n.text)))
}
Frame(f) => {
let kind : @model.FrameKind = match f.kind {
Loop => Loop; Alt => Alt; Opt => Opt; Par => Par; ParOver => ParOver;
Critical => Critical; Break => Break; Rect => Rect
}
let sections = f.sections.map(s => @model.Section::new(
walk(s.statements, group),
label?=if s.label == "" || f.kind == Rect { None } else { Some(s.label) },
)).iter().to_array()
if sections.length() > 0 {
events.push(Frame(@model.Frame::new(kind, sections[0], rest=sections[1:], colour?=if f.kind == Rect { Some(f.sections[0].label) } else { None })))
}
}
Box(b) => {
let (label, colour) = box_data(b.label)
let g = @model.BoxGroup::new("box:\{box_index}", label, colour?)
box_index += 1
events.append(walk(b.statements, Some(g)))
}
Title(text) => title = Some(text)
AccTitle(text) => accessibility_title = Some(text)
AccDescr(text) => accessibility_description = Some(text)
Autonumber(n) => match n {
Off => { numbering = false; events.push(Autonumber(Off)) }
On(start~, step=new_step) => {
let start = start.bind(s => try Some(@string.parse_double(s)) catch { _ => None })
let new_step = new_step.bind(s => try Some(@string.parse_double(s)) catch { _ => None })
numbering = true
// Omitted values resume the current counter and step.
if start is Some(n) && n != 0.0 { next_number = n }
if new_step is Some(n) && n != 0.0 { step = n }
events.push(Autonumber(On(start~, step=new_step)))
}
}
Link(l) => {
let i = index(l.participant)
if l.kind == Links && !(json_value(l.text) is Some(Object(_))) {
error("mermaid/invalid-links", "Links for \{l.participant} must be a JSON object", span)
}
if statement_link(l) is Some(link) {
let p = participants[i]
participants[i] = @model.Participant::new(p.id, label=p.label, kind=p.kind, group?=p.group, link~, properties?=p.properties)
}
}
}
}
events
}
let events = walk(diagram.statements, None)
for (id, span) in pending {
error("mermaid/missing-destroying-message", "No destroying message for \{id}", span)
}
{
sequence: @model.Sequence::new(participants, events, title?, accessibility_title?, accessibility_description?),
diagnostics: ReadOnlyArray::from_array(diagnostics),
}
}