use std::collections::{BTreeMap, BTreeSet, VecDeque};
use super::{
DiagramClassRelationMeta, DiagramErRelationshipMeta, DiagramGanttSectionMeta, DiagramMeta,
DiagramSequenceBlockMeta, DiagramStableIdMap,
};
use crate::model::seq_ast::{
SequenceBlock, SequenceBlockKind, SequenceSection, SequenceSectionKind,
};
use crate::model::{
ClassAst, ClassRelation, DiagramAst, ErAst, ErRelationship, FlowEdge, FlowchartAst, GanttAst,
GanttSection, GanttTask, GanttTaskStart, ObjectId, SequenceAst, SequenceMessage,
SequenceMessageKind,
};
pub(crate) fn stable_id_map_from_ast(ast: &DiagramAst) -> DiagramStableIdMap {
match ast {
DiagramAst::Sequence(seq_ast) => {
let mut by_name = BTreeMap::new();
for (participant_id, participant) in seq_ast.participants() {
let mermaid_name = participant.mermaid_name();
if mermaid_name.is_empty() {
continue;
}
by_name
.entry(mermaid_name.to_owned())
.or_insert_with(|| participant_id.to_string());
}
DiagramStableIdMap {
by_mermaid_id: BTreeMap::new(),
by_name,
by_fingerprint: BTreeMap::new(),
}
}
DiagramAst::Flowchart(flow_ast) => {
let mut by_mermaid_id = BTreeMap::new();
for (node_id, node) in flow_ast.nodes() {
let Some(mermaid_id) = flow_node_mermaid_id(node_id, node.mermaid_id()) else {
continue;
};
by_mermaid_id.entry(mermaid_id).or_insert_with(|| node_id.to_string());
}
DiagramStableIdMap {
by_mermaid_id,
by_name: BTreeMap::new(),
by_fingerprint: BTreeMap::new(),
}
}
DiagramAst::Class(class_ast) => {
let mut by_name = BTreeMap::new();
for (class_id, class) in class_ast.classes() {
if class.name().is_empty() {
continue;
}
by_name.entry(class.name().to_owned()).or_insert_with(|| class_id.to_string());
}
DiagramStableIdMap {
by_mermaid_id: BTreeMap::new(),
by_name,
by_fingerprint: BTreeMap::new(),
}
}
DiagramAst::Er(er_ast) => {
let mut by_name = BTreeMap::new();
for (id, e) in er_ast.entities() {
if !e.name().is_empty() {
by_name.entry(e.name().to_owned()).or_insert_with(|| id.to_string());
}
}
DiagramStableIdMap {
by_mermaid_id: BTreeMap::new(),
by_name,
by_fingerprint: BTreeMap::new(),
}
}
DiagramAst::Gantt(gantt_ast) => {
let mut by_mermaid_id = BTreeMap::new();
let mut by_name = BTreeMap::new();
let mut by_fingerprint = BTreeMap::new();
let mut name_counts = BTreeMap::<&str, usize>::new();
for task in gantt_ast.tasks().values() {
*name_counts.entry(task.name()).or_default() += 1;
}
for (task_id, task) in gantt_ast.tasks() {
if let Some(tag) = task.mermaid_tag().filter(|t| !t.is_empty()) {
by_mermaid_id.entry(tag.to_owned()).or_insert_with(|| task_id.to_string());
}
if !task.name().is_empty() && name_counts.get(task.name()) == Some(&1) {
by_name.entry(task.name().to_owned()).or_insert_with(|| task_id.to_string());
}
for fingerprint in gantt_task_fingerprints(gantt_ast, task_id, task) {
by_fingerprint.entry(fingerprint).or_insert_with(|| task_id.to_string());
}
}
DiagramStableIdMap { by_mermaid_id, by_name, by_fingerprint }
}
}
}
fn gantt_task_base_fingerprint(ast: &GanttAst, task: &GanttTask) -> String {
let start = match task.start() {
GanttTaskStart::Date(date) => format!("date:{date}"),
GanttTaskStart::After(dependency_id) => {
let dependency = ast.tasks().get(dependency_id);
let semantic_dependency = dependency
.and_then(|task| task.mermaid_tag())
.map(|tag| format!("tag:{tag}"))
.or_else(|| dependency.map(|task| format!("name:{:?}", task.name())))
.unwrap_or_else(|| format!("id:{dependency_id}"));
format!("after:{semantic_dependency}")
}
GanttTaskStart::Unspecified => "unspecified".to_owned(),
};
format!(
"name={:?};start={start};duration_days={};raw_duration={:?}",
task.name(),
task.duration_days(),
task.raw_duration()
)
}
fn gantt_task_fingerprints(ast: &GanttAst, task_id: &ObjectId, task: &GanttTask) -> Vec<String> {
let base = gantt_task_base_fingerprint(ast, task);
let section_contexts = ast
.sections()
.iter()
.enumerate()
.find(|(_, section)| section.task_ids().contains(task_id))
.map(|(section_index, section)| {
let section_occurrence = ast.sections()[..section_index]
.iter()
.filter(|candidate| candidate.name() == section.name())
.count();
let occurrence = section
.task_ids()
.iter()
.take_while(|candidate| *candidate != task_id)
.filter(|candidate| {
ast.tasks().get(*candidate).is_some_and(|candidate| {
gantt_task_base_fingerprint(ast, candidate) == base
})
})
.count();
vec![
format!(
"section={:?};section_occurrence={section_occurrence};occurrence={occurrence}",
section.name()
),
format!("section={:?};occurrence={occurrence}", section.name()),
]
})
.unwrap_or_else(|| vec!["section=<unsectioned>;occurrence=0".to_owned()]);
let mut fingerprints =
section_contexts.into_iter().map(|context| format!("{base};{context}")).collect::<Vec<_>>();
fingerprints.push(base);
fingerprints
}
fn flow_node_mermaid_id(node_id: &ObjectId, mermaid_id: Option<&str>) -> Option<String> {
mermaid_id.filter(|id| !id.is_empty()).map(ToOwned::to_owned).or_else(|| {
node_id.as_str().strip_prefix("n:").filter(|id| !id.is_empty()).map(ToOwned::to_owned)
})
}
fn parse_stable_object_id(raw: &str) -> Option<ObjectId> {
ObjectId::new(raw.to_owned()).ok()
}
fn remap_id(remap: &BTreeMap<ObjectId, ObjectId>, id: &ObjectId) -> ObjectId {
remap.get(id).cloned().unwrap_or_else(|| id.clone())
}
fn allocate_reconciled_object_id(
base_id: &ObjectId,
assigned_ids: &BTreeSet<ObjectId>,
) -> ObjectId {
let mut suffix = 1_u64;
loop {
let candidate = ObjectId::new(format!("{}:reconcile:{suffix:04}", base_id.as_str()))
.expect("reconciled object id should be valid");
if !assigned_ids.contains(&candidate) {
return candidate;
}
suffix = suffix.saturating_add(1);
}
}
pub(crate) fn reconcile_sequence_participants(ast: &mut SequenceAst, sidecar: &DiagramMeta) {
if sidecar.stable_id_map.by_name.is_empty() {
return;
}
let mut remap = BTreeMap::<ObjectId, ObjectId>::new();
let mut next_participants = BTreeMap::<ObjectId, crate::model::SequenceParticipant>::new();
let mut assigned_ids = BTreeSet::<ObjectId>::new();
for (parsed_participant_id, participant) in ast.participants() {
let mut mapped_id = sidecar
.stable_id_map
.by_name
.get(participant.mermaid_name())
.and_then(|raw| parse_stable_object_id(raw))
.filter(|stable_id| !assigned_ids.contains(stable_id))
.unwrap_or_else(|| parsed_participant_id.clone());
if assigned_ids.contains(&mapped_id) {
mapped_id = if !assigned_ids.contains(parsed_participant_id) {
parsed_participant_id.clone()
} else {
allocate_reconciled_object_id(parsed_participant_id, &assigned_ids)
};
}
assigned_ids.insert(mapped_id.clone());
if &mapped_id != parsed_participant_id {
remap.insert(parsed_participant_id.clone(), mapped_id.clone());
}
next_participants.insert(mapped_id, participant.clone());
}
if remap.is_empty() {
return;
}
*ast.participants_mut() = next_participants;
let next_messages = ast
.messages()
.iter()
.map(|msg| {
let mut updated = SequenceMessage::new(
msg.message_id().clone(),
remap_id(&remap, msg.from_participant_id()),
remap_id(&remap, msg.to_participant_id()),
msg.kind(),
msg.text().to_owned(),
msg.order_key(),
);
updated.set_raw_arrow(msg.raw_arrow().map(ToOwned::to_owned));
updated
})
.collect();
*ast.messages_mut() = next_messages;
}
pub(crate) fn reconcile_flowchart_nodes(ast: &mut FlowchartAst, sidecar: &DiagramMeta) {
if sidecar.stable_id_map.by_mermaid_id.is_empty() {
return;
}
let mut remap = BTreeMap::<ObjectId, ObjectId>::new();
let mut next_nodes = BTreeMap::<ObjectId, crate::model::FlowNode>::new();
let mut assigned_ids = BTreeSet::<ObjectId>::new();
for (parsed_node_id, node) in ast.nodes() {
let mut mapped_id = flow_node_mermaid_id(parsed_node_id, node.mermaid_id())
.and_then(|mermaid_id| sidecar.stable_id_map.by_mermaid_id.get(&mermaid_id).cloned())
.and_then(|raw| parse_stable_object_id(&raw))
.filter(|stable_id| !assigned_ids.contains(stable_id))
.unwrap_or_else(|| parsed_node_id.clone());
if assigned_ids.contains(&mapped_id) {
mapped_id = if !assigned_ids.contains(parsed_node_id) {
parsed_node_id.clone()
} else {
allocate_reconciled_object_id(parsed_node_id, &assigned_ids)
};
}
assigned_ids.insert(mapped_id.clone());
if &mapped_id != parsed_node_id {
remap.insert(parsed_node_id.clone(), mapped_id.clone());
}
next_nodes.insert(mapped_id, node.clone());
}
if remap.is_empty() {
return;
}
*ast.nodes_mut() = next_nodes;
let next_edges = ast
.edges()
.iter()
.map(|(edge_id, edge)| {
let mut updated = FlowEdge::new_with(
remap_id(&remap, edge.from_node_id()),
remap_id(&remap, edge.to_node_id()),
edge.label().map(ToOwned::to_owned),
edge.style().map(ToOwned::to_owned),
);
updated.set_connector(edge.connector().map(ToOwned::to_owned));
(edge_id.clone(), updated)
})
.collect();
*ast.edges_mut() = next_edges;
if ast.node_groups().is_empty() {
return;
}
let mut next_node_groups = BTreeMap::<ObjectId, ObjectId>::new();
for (node_id, group_id) in ast.node_groups() {
let mapped_node_id = remap_id(&remap, node_id);
next_node_groups.entry(mapped_node_id).or_insert(group_id.clone());
}
*ast.node_groups_mut() = next_node_groups;
}
pub(crate) fn reconcile_class_nodes(ast: &mut ClassAst, sidecar: &DiagramMeta) {
if sidecar.stable_id_map.by_name.is_empty() {
return;
}
let mut remap = BTreeMap::<ObjectId, ObjectId>::new();
let mut next_classes = BTreeMap::new();
let mut assigned_ids = BTreeSet::new();
let reserved_ids = sidecar
.stable_id_map
.by_name
.values()
.filter_map(|raw| parse_stable_object_id(raw))
.collect::<BTreeSet<_>>();
for (parsed_id, class) in ast.classes() {
let stable_id = sidecar
.stable_id_map
.by_name
.get(class.name())
.and_then(|raw| parse_stable_object_id(raw));
let mapped_id = if let Some(stable_id) = stable_id.filter(|id| !assigned_ids.contains(id)) {
stable_id
} else if !reserved_ids.contains(parsed_id) && !assigned_ids.contains(parsed_id) {
parsed_id.clone()
} else {
let unavailable =
reserved_ids.iter().chain(&assigned_ids).cloned().collect::<BTreeSet<_>>();
allocate_reconciled_object_id(parsed_id, &unavailable)
};
assigned_ids.insert(mapped_id.clone());
if &mapped_id != parsed_id {
remap.insert(parsed_id.clone(), mapped_id.clone());
}
next_classes.insert(mapped_id, class.clone());
}
if remap.is_empty() {
return;
}
*ast.classes_mut() = next_classes;
*ast.relations_mut() = ast
.relations()
.iter()
.map(|(relation_id, relation)| {
let updated = ClassRelation::new(
remap_id(&remap, relation.from_class_id()),
remap_id(&remap, relation.to_class_id()),
relation.kind(),
)
.with_label(relation.label().map(ToOwned::to_owned))
.with_raw_connector(relation.raw_connector().map(ToOwned::to_owned));
(relation_id.clone(), updated)
})
.collect();
}
pub(crate) fn reconcile_er_entities(ast: &mut ErAst, sidecar: &DiagramMeta) {
if sidecar.stable_id_map.by_name.is_empty() {
return;
}
let mut remap = BTreeMap::<ObjectId, ObjectId>::new();
let mut next_entities = BTreeMap::new();
let mut assigned_ids = BTreeSet::new();
let reserved_ids = sidecar
.stable_id_map
.by_name
.values()
.filter_map(|raw| parse_stable_object_id(raw))
.collect::<BTreeSet<_>>();
for (parsed_id, entity) in ast.entities() {
let stable_id = sidecar
.stable_id_map
.by_name
.get(entity.name())
.and_then(|raw| parse_stable_object_id(raw));
let mapped_id = if let Some(stable_id) = stable_id.filter(|id| !assigned_ids.contains(id)) {
stable_id
} else if !reserved_ids.contains(parsed_id) && !assigned_ids.contains(parsed_id) {
parsed_id.clone()
} else {
let unavailable =
reserved_ids.iter().chain(&assigned_ids).cloned().collect::<BTreeSet<_>>();
allocate_reconciled_object_id(parsed_id, &unavailable)
};
assigned_ids.insert(mapped_id.clone());
if &mapped_id != parsed_id {
remap.insert(parsed_id.clone(), mapped_id.clone());
}
next_entities.insert(mapped_id, entity.clone());
}
if remap.is_empty() {
return;
}
*ast.entities_mut() = next_entities;
*ast.relationships_mut() = ast
.relationships()
.iter()
.map(|(relationship_id, relationship)| {
let updated = ErRelationship::new(
remap_id(&remap, relationship.from_entity_id()),
remap_id(&remap, relationship.to_entity_id()),
relationship.from_card(),
relationship.to_card(),
)
.with_stroke(relationship.stroke())
.with_label(relationship.label().map(ToOwned::to_owned))
.with_raw_connector(relationship.raw_connector().map(ToOwned::to_owned));
(relationship_id.clone(), updated)
})
.collect();
}
pub(crate) fn reconcile_gantt_tasks(ast: &mut GanttAst, sidecar: &DiagramMeta) {
if sidecar.stable_id_map.by_mermaid_id.is_empty()
&& sidecar.stable_id_map.by_name.is_empty()
&& sidecar.stable_id_map.by_fingerprint.is_empty()
{
return;
}
let mut remap = BTreeMap::<ObjectId, ObjectId>::new();
let mut assigned_ids = BTreeSet::new();
let reserved_ids = sidecar
.stable_id_map
.by_mermaid_id
.values()
.chain(sidecar.stable_id_map.by_name.values())
.chain(sidecar.stable_id_map.by_fingerprint.values())
.filter_map(|raw| parse_stable_object_id(raw))
.collect::<BTreeSet<_>>();
let mut name_counts = BTreeMap::<&str, usize>::new();
for task in ast.tasks().values() {
*name_counts.entry(task.name()).or_default() += 1;
}
for (parsed_id, task) in ast.tasks() {
let fingerprints = gantt_task_fingerprints(ast, parsed_id, task);
let strong_count = fingerprints.len().saturating_sub(1);
let stable_id =
task.mermaid_tag()
.and_then(|tag| sidecar.stable_id_map.by_mermaid_id.get(tag))
.or_else(|| {
fingerprints.into_iter().take(strong_count).find_map(|fingerprint| {
sidecar.stable_id_map.by_fingerprint.get(&fingerprint)
})
})
.or_else(|| {
(name_counts.get(task.name()) == Some(&1))
.then(|| sidecar.stable_id_map.by_name.get(task.name()))
.flatten()
})
.and_then(|raw| parse_stable_object_id(raw));
if let Some(stable_id) = stable_id.filter(|id| !assigned_ids.contains(id)) {
assigned_ids.insert(stable_id.clone());
remap.insert(parsed_id.clone(), stable_id);
}
}
for (parsed_id, task) in ast.tasks() {
if remap.contains_key(parsed_id) {
continue;
}
let stable_id = gantt_task_fingerprints(ast, parsed_id, task)
.into_iter()
.next_back()
.and_then(|fingerprint| sidecar.stable_id_map.by_fingerprint.get(&fingerprint))
.and_then(|raw| parse_stable_object_id(raw))
.filter(|id| !assigned_ids.contains(id));
let mapped_id = if let Some(stable_id) = stable_id {
stable_id
} else if !reserved_ids.contains(parsed_id) && !assigned_ids.contains(parsed_id) {
parsed_id.clone()
} else {
let unavailable =
reserved_ids.iter().chain(&assigned_ids).cloned().collect::<BTreeSet<_>>();
allocate_reconciled_object_id(parsed_id, &unavailable)
};
assigned_ids.insert(mapped_id.clone());
remap.insert(parsed_id.clone(), mapped_id);
}
if remap.iter().all(|(from, to)| from == to) {
return;
}
let next_tasks = ast
.tasks()
.iter()
.map(|(parsed_id, task)| {
let task_id = remap_id(&remap, parsed_id);
let start = match task.start() {
GanttTaskStart::Date(date) => GanttTaskStart::Date(date.clone()),
GanttTaskStart::After(dependency_id) => {
GanttTaskStart::After(remap_id(&remap, dependency_id))
}
GanttTaskStart::Unspecified => GanttTaskStart::Unspecified,
};
let mut updated = GanttTask::new(
task_id.clone(),
task.name(),
start,
task.duration_days(),
task.raw_duration(),
)
.with_mermaid_tag(task.mermaid_tag().map(ToOwned::to_owned));
updated.set_note(task.note().map(ToOwned::to_owned));
(task_id, updated)
})
.collect();
*ast.tasks_mut() = next_tasks;
for section in ast.sections_mut() {
for task_id in section.task_ids_mut() {
*task_id = remap_id(&remap, task_id);
}
}
}
pub(crate) fn reconcile_flowchart_edges(ast: &mut FlowchartAst, sidecar: &DiagramMeta) {
if sidecar.flow_edges.is_empty() {
return;
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct FlowEdgeFingerprint {
from_node_id: ObjectId,
to_node_id: ObjectId,
label: Option<String>,
connector: Option<String>,
}
fn normalize_flow_connector(connector: Option<&str>) -> Option<String> {
match connector {
None | Some("-->") => None,
Some(other) => Some(other.to_owned()),
}
}
fn numeric_edge_id(edge_id: &ObjectId) -> Option<u64> {
let raw = edge_id.as_str().strip_prefix("e:")?;
if raw.is_empty() || !raw.chars().all(|ch| ch.is_ascii_digit()) {
return None;
}
raw.parse().ok()
}
fn allocate_edge_id(next_numeric: &mut u64, taken_ids: &mut BTreeSet<ObjectId>) -> ObjectId {
loop {
let candidate = ObjectId::new(format!("e:{next_numeric:04}")).expect("valid edge id");
*next_numeric = next_numeric.saturating_add(1);
if taken_ids.insert(candidate.clone()) {
return candidate;
}
}
}
let mut by_fingerprint: BTreeMap<FlowEdgeFingerprint, VecDeque<(ObjectId, Option<String>)>> =
BTreeMap::new();
let mut taken_ids: BTreeSet<ObjectId> = BTreeSet::new();
for entry in &sidecar.flow_edges {
taken_ids.insert(entry.edge_id.clone());
let fingerprint = FlowEdgeFingerprint {
from_node_id: entry.from_node_id.clone(),
to_node_id: entry.to_node_id.clone(),
label: entry.label.clone(),
connector: normalize_flow_connector(entry.connector.as_deref()),
};
by_fingerprint
.entry(fingerprint)
.or_default()
.push_back((entry.edge_id.clone(), entry.style.clone()));
}
let mut max_numeric = 0u64;
for edge_id in taken_ids.iter().chain(ast.edges().keys()) {
if let Some(value) = numeric_edge_id(edge_id) {
max_numeric = max_numeric.max(value);
}
}
let mut next_numeric = max_numeric.saturating_add(1);
let mut next_edges: BTreeMap<ObjectId, FlowEdge> = BTreeMap::new();
for (parsed_edge_id, edge) in ast.edges() {
let fingerprint = FlowEdgeFingerprint {
from_node_id: edge.from_node_id().clone(),
to_node_id: edge.to_node_id().clone(),
label: edge.label().map(ToOwned::to_owned),
connector: normalize_flow_connector(edge.connector()),
};
if let Some(queue) = by_fingerprint.get_mut(&fingerprint) {
if let Some((stable_id, style)) = queue.pop_front() {
let mut updated = edge.clone();
updated.set_style(style.clone());
let target_id = if next_edges.contains_key(&stable_id) {
allocate_edge_id(&mut next_numeric, &mut taken_ids)
} else {
stable_id
};
taken_ids.insert(target_id.clone());
next_edges.insert(target_id, updated);
continue;
}
}
let updated = edge.clone();
let target_id =
if taken_ids.contains(parsed_edge_id) || next_edges.contains_key(parsed_edge_id) {
allocate_edge_id(&mut next_numeric, &mut taken_ids)
} else {
taken_ids.insert(parsed_edge_id.clone());
parsed_edge_id.clone()
};
next_edges.insert(target_id, updated);
}
*ast.edges_mut() = next_edges;
}
pub(crate) fn reconcile_class_relations(ast: &mut ClassAst, sidecar: &DiagramMeta) {
if sidecar.class_relations.is_empty() {
return;
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Fingerprint {
from_id: ObjectId,
to_id: ObjectId,
kind: u8,
connector: u8,
label: Option<String>,
}
fn kind_key(kind: crate::model::ClassRelationKind) -> u8 {
match kind {
crate::model::ClassRelationKind::Inheritance => 0,
crate::model::ClassRelationKind::Composition => 1,
crate::model::ClassRelationKind::Aggregation => 2,
crate::model::ClassRelationKind::Association => 3,
crate::model::ClassRelationKind::Dependency => 4,
crate::model::ClassRelationKind::Realization => 5,
crate::model::ClassRelationKind::Link => 6,
}
}
fn from_meta(meta: &DiagramClassRelationMeta) -> Fingerprint {
Fingerprint {
from_id: meta.from_class_id.clone(),
to_id: meta.to_class_id.clone(),
kind: kind_key(meta.kind),
connector: connector_key(meta.kind, meta.raw_connector.as_deref()),
label: meta.label.clone(),
}
}
fn connector_key(kind: crate::model::ClassRelationKind, raw: Option<&str>) -> u8 {
let raw = raw.unwrap_or("");
match kind {
crate::model::ClassRelationKind::Inheritance
| crate::model::ClassRelationKind::Realization => u8::from(!raw.starts_with("<|")),
crate::model::ClassRelationKind::Composition => {
u8::from(raw.is_empty() || raw.starts_with('*'))
}
crate::model::ClassRelationKind::Aggregation => {
u8::from(raw.is_empty() || raw.starts_with('o'))
}
crate::model::ClassRelationKind::Association => u8::from(!raw.starts_with('<')),
crate::model::ClassRelationKind::Dependency => {
if raw.is_empty() || raw.contains('>') {
2
} else if raw.starts_with('<') {
1
} else {
0
}
}
crate::model::ClassRelationKind::Link => u8::from(raw == "<-->"),
}
}
let mut by_fingerprint = BTreeMap::<Fingerprint, VecDeque<ObjectId>>::new();
let mut taken_ids = BTreeSet::new();
for meta in &sidecar.class_relations {
taken_ids.insert(meta.relation_id.clone());
by_fingerprint.entry(from_meta(meta)).or_default().push_back(meta.relation_id.clone());
}
let mut next_relations = BTreeMap::new();
for (parsed_id, relation) in ast.relations() {
let fingerprint = Fingerprint {
from_id: relation.from_class_id().clone(),
to_id: relation.to_class_id().clone(),
kind: kind_key(relation.kind()),
connector: connector_key(relation.kind(), relation.raw_connector()),
label: relation.label().map(ToOwned::to_owned),
};
let matched_id = by_fingerprint.get_mut(&fingerprint).and_then(VecDeque::pop_front);
let target_id = match matched_id {
Some(stable_id) if !next_relations.contains_key(&stable_id) => stable_id,
_ if !taken_ids.contains(parsed_id) && !next_relations.contains_key(parsed_id) => {
parsed_id.clone()
}
_ => {
let unavailable =
taken_ids.iter().chain(next_relations.keys()).cloned().collect::<BTreeSet<_>>();
allocate_reconciled_object_id(parsed_id, &unavailable)
}
};
taken_ids.insert(target_id.clone());
next_relations.insert(target_id, relation.clone());
}
*ast.relations_mut() = next_relations;
}
pub(crate) fn reconcile_er_relationships(ast: &mut ErAst, sidecar: &DiagramMeta) {
if sidecar.er_relationships.is_empty() {
return;
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Fingerprint {
from_id: ObjectId,
to_id: ObjectId,
from_card: u8,
to_card: u8,
stroke: u8,
label: Option<String>,
}
fn card_key(card: crate::model::ErCardinality) -> u8 {
match card {
crate::model::ErCardinality::ExactlyOne => 0,
crate::model::ErCardinality::ZeroOrOne => 1,
crate::model::ErCardinality::OneOrMore => 2,
crate::model::ErCardinality::ZeroOrMore => 3,
}
}
fn stroke_key(stroke: crate::model::ErStroke) -> u8 {
match stroke {
crate::model::ErStroke::Identifying => 0,
crate::model::ErStroke::NonIdentifying => 1,
}
}
fn from_meta(meta: &DiagramErRelationshipMeta) -> Fingerprint {
Fingerprint {
from_id: meta.from_entity_id.clone(),
to_id: meta.to_entity_id.clone(),
from_card: card_key(meta.from_card),
to_card: card_key(meta.to_card),
stroke: stroke_key(meta.stroke),
label: meta.label.clone(),
}
}
let mut by_fingerprint = BTreeMap::<Fingerprint, VecDeque<ObjectId>>::new();
let mut taken_ids = BTreeSet::new();
for meta in &sidecar.er_relationships {
taken_ids.insert(meta.relationship_id.clone());
by_fingerprint.entry(from_meta(meta)).or_default().push_back(meta.relationship_id.clone());
}
let mut next_relationships = BTreeMap::new();
for (parsed_id, relationship) in ast.relationships() {
let fingerprint = Fingerprint {
from_id: relationship.from_entity_id().clone(),
to_id: relationship.to_entity_id().clone(),
from_card: card_key(relationship.from_card()),
to_card: card_key(relationship.to_card()),
stroke: stroke_key(relationship.stroke()),
label: relationship.label().map(ToOwned::to_owned),
};
let matched_id = by_fingerprint.get_mut(&fingerprint).and_then(VecDeque::pop_front);
let target_id = match matched_id {
Some(stable_id) if !next_relationships.contains_key(&stable_id) => stable_id,
_ if !taken_ids.contains(parsed_id) && !next_relationships.contains_key(parsed_id) => {
parsed_id.clone()
}
_ => {
let unavailable = taken_ids
.iter()
.chain(next_relationships.keys())
.cloned()
.collect::<BTreeSet<_>>();
allocate_reconciled_object_id(parsed_id, &unavailable)
}
};
taken_ids.insert(target_id.clone());
next_relationships.insert(target_id, relationship.clone());
}
*ast.relationships_mut() = next_relationships;
}
pub(crate) fn reconcile_gantt_sections(ast: &mut GanttAst, sidecar: &DiagramMeta) {
if sidecar.gantt_sections.is_empty() {
return;
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Fingerprint {
name: String,
task_ids: Vec<ObjectId>,
}
fn from_meta(meta: &DiagramGanttSectionMeta) -> Fingerprint {
Fingerprint { name: meta.name.clone(), task_ids: meta.task_ids.clone() }
}
let mut by_fingerprint = BTreeMap::<Fingerprint, VecDeque<ObjectId>>::new();
let mut old_name_counts = BTreeMap::<&str, usize>::new();
let mut current_name_counts = BTreeMap::<&str, usize>::new();
for meta in &sidecar.gantt_sections {
*old_name_counts.entry(meta.name.as_str()).or_default() += 1;
}
for section in ast.sections() {
*current_name_counts.entry(section.name()).or_default() += 1;
}
let mut by_unique_name = BTreeMap::<&str, ObjectId>::new();
let mut taken_ids = BTreeSet::new();
for meta in &sidecar.gantt_sections {
taken_ids.insert(meta.section_id.clone());
by_fingerprint.entry(from_meta(meta)).or_default().push_back(meta.section_id.clone());
if old_name_counts.get(meta.name.as_str()) == Some(&1) {
by_unique_name.insert(meta.name.as_str(), meta.section_id.clone());
}
}
let mut assigned_ids = BTreeSet::new();
let mut next_sections = Vec::with_capacity(ast.sections().len());
for section in ast.sections() {
let fingerprint =
Fingerprint { name: section.name().to_owned(), task_ids: section.task_ids().to_vec() };
let matched_id =
by_fingerprint.get_mut(&fingerprint).and_then(VecDeque::pop_front).or_else(|| {
(current_name_counts.get(section.name()) == Some(&1))
.then(|| by_unique_name.get(section.name()).cloned())
.flatten()
});
let target_id = match matched_id {
Some(stable_id) if !assigned_ids.contains(&stable_id) => stable_id,
_ if !taken_ids.contains(section.section_id())
&& !assigned_ids.contains(section.section_id()) =>
{
section.section_id().clone()
}
_ => {
let unavailable =
taken_ids.iter().chain(&assigned_ids).cloned().collect::<BTreeSet<_>>();
allocate_reconciled_object_id(section.section_id(), &unavailable)
}
};
taken_ids.insert(target_id.clone());
assigned_ids.insert(target_id.clone());
let mut updated = GanttSection::new(target_id, section.name());
*updated.task_ids_mut() = section.task_ids().to_vec();
next_sections.push(updated);
}
*ast.sections_mut() = next_sections;
}
pub(crate) fn reconcile_sequence_messages(ast: &mut SequenceAst, sidecar: &DiagramMeta) {
if sidecar.sequence_messages.is_empty() {
return;
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct MessageFingerprint {
from_participant_id: ObjectId,
to_participant_id: ObjectId,
kind: u8,
text: String,
}
fn kind_key(kind: SequenceMessageKind) -> u8 {
match kind {
SequenceMessageKind::Sync => 0,
SequenceMessageKind::Async => 1,
SequenceMessageKind::Return => 2,
}
}
fn numeric_message_id(message_id: &ObjectId) -> Option<u64> {
let raw = message_id.as_str().strip_prefix("m:")?;
if raw.is_empty() || !raw.chars().all(|ch| ch.is_ascii_digit()) {
return None;
}
raw.parse().ok()
}
fn allocate_message_id(next_numeric: &mut u64, taken_ids: &mut BTreeSet<ObjectId>) -> ObjectId {
loop {
let candidate =
ObjectId::new(format!("m:{next_numeric:04}")).expect("valid message id");
*next_numeric = next_numeric.saturating_add(1);
if taken_ids.insert(candidate.clone()) {
return candidate;
}
}
}
let mut by_fingerprint: BTreeMap<MessageFingerprint, VecDeque<ObjectId>> = BTreeMap::new();
let mut taken_ids: BTreeSet<ObjectId> = BTreeSet::new();
for entry in &sidecar.sequence_messages {
taken_ids.insert(entry.message_id.clone());
let fingerprint = MessageFingerprint {
from_participant_id: entry.from_participant_id.clone(),
to_participant_id: entry.to_participant_id.clone(),
kind: kind_key(entry.kind),
text: entry.text.clone(),
};
by_fingerprint.entry(fingerprint).or_default().push_back(entry.message_id.clone());
}
let mut max_numeric = 0u64;
for message_id in taken_ids.iter().chain(ast.messages().iter().map(|msg| msg.message_id())) {
if let Some(value) = numeric_message_id(message_id) {
max_numeric = max_numeric.max(value);
}
}
let mut next_numeric = max_numeric.saturating_add(1);
let mut next_messages = Vec::with_capacity(ast.messages().len());
let mut assigned_ids: BTreeSet<ObjectId> = BTreeSet::new();
let mut remap: BTreeMap<ObjectId, ObjectId> = BTreeMap::new();
for msg in ast.messages() {
let original_message_id = msg.message_id().clone();
let fingerprint = MessageFingerprint {
from_participant_id: msg.from_participant_id().clone(),
to_participant_id: msg.to_participant_id().clone(),
kind: kind_key(msg.kind()),
text: msg.text().to_owned(),
};
let message_id = match by_fingerprint.get_mut(&fingerprint) {
Some(queue) => match queue.pop_front() {
Some(stable_id) => {
if assigned_ids.contains(&stable_id) {
allocate_message_id(&mut next_numeric, &mut taken_ids)
} else {
stable_id
}
}
None => {
if taken_ids.contains(msg.message_id())
|| assigned_ids.contains(msg.message_id())
{
allocate_message_id(&mut next_numeric, &mut taken_ids)
} else {
taken_ids.insert(msg.message_id().clone());
msg.message_id().clone()
}
}
},
None => {
if taken_ids.contains(msg.message_id()) || assigned_ids.contains(msg.message_id()) {
allocate_message_id(&mut next_numeric, &mut taken_ids)
} else {
taken_ids.insert(msg.message_id().clone());
msg.message_id().clone()
}
}
};
assigned_ids.insert(message_id.clone());
remap.insert(original_message_id, message_id.clone());
let mut updated = SequenceMessage::new(
message_id,
msg.from_participant_id().clone(),
msg.to_participant_id().clone(),
msg.kind(),
msg.text().to_owned(),
msg.order_key(),
);
updated.set_raw_arrow(msg.raw_arrow().map(ToOwned::to_owned));
next_messages.push(updated);
}
*ast.messages_mut() = next_messages;
remap_sequence_block_message_ids(ast.blocks_mut(), &remap);
}
fn remap_sequence_block_message_ids(
blocks: &mut [crate::model::seq_ast::SequenceBlock],
remap: &BTreeMap<ObjectId, ObjectId>,
) {
if remap.is_empty() {
return;
}
for block in blocks {
for section in block.sections_mut() {
for message_id in section.message_ids_mut() {
if let Some(mapped) = remap.get(message_id) {
*message_id = mapped.clone();
}
}
}
remap_sequence_block_message_ids(block.blocks_mut(), remap);
}
}
pub(crate) fn reconcile_flowchart_notes(ast: &mut FlowchartAst, sidecar: &DiagramMeta) {
if sidecar.flow_node_notes.is_empty() {
return;
}
for (node_id, note) in &sidecar.flow_node_notes {
if let Some(node) = ast.nodes_mut().get_mut(node_id) {
node.set_note(Some(note.clone()));
}
}
}
pub(crate) fn reconcile_flowchart_symbols(ast: &mut FlowchartAst, sidecar: &DiagramMeta) {
if sidecar.flow_node_symbols.is_empty() {
return;
}
for (node_id, symbol) in &sidecar.flow_node_symbols {
if let Some(node) = ast.nodes_mut().get_mut(node_id) {
node.set_symbol(Some(symbol.clone()));
}
}
}
pub(crate) fn reconcile_sequence_participant_notes(ast: &mut SequenceAst, sidecar: &DiagramMeta) {
if sidecar.sequence_participant_notes.is_empty() {
return;
}
for (participant_id, note) in &sidecar.sequence_participant_notes {
if let Some(participant) = ast.participants_mut().get_mut(participant_id) {
participant.set_note(Some(note.clone()));
}
}
}
pub(crate) fn reconcile_class_notes(ast: &mut crate::model::ClassAst, sidecar: &DiagramMeta) {
if sidecar.class_node_notes.is_empty() {
return;
}
for (class_id, note) in &sidecar.class_node_notes {
if let Some(class) = ast.classes_mut().get_mut(class_id) {
class.set_note(Some(note.clone()));
}
}
}
pub(crate) fn reconcile_er_notes(ast: &mut crate::model::ErAst, sidecar: &DiagramMeta) {
if sidecar.er_entity_notes.is_empty() {
return;
}
for (entity_id, note) in &sidecar.er_entity_notes {
if let Some(entity) = ast.entities_mut().get_mut(entity_id) {
entity.set_note(Some(note.clone()));
}
}
}
pub(crate) fn reconcile_gantt_notes(ast: &mut crate::model::GanttAst, sidecar: &DiagramMeta) {
let tag_to_current: BTreeMap<String, ObjectId> = ast
.tasks()
.iter()
.filter_map(|(id, task)| {
task.mermaid_tag().filter(|t| !t.is_empty()).map(|t| (t.to_owned(), id.clone()))
})
.collect();
let mut old_id_to_tag = BTreeMap::<ObjectId, String>::new();
for (tag, raw_id) in &sidecar.stable_id_map.by_mermaid_id {
if let Some(old_id) = parse_stable_object_id(raw_id) {
old_id_to_tag.entry(old_id).or_insert_with(|| tag.clone());
}
}
for (note_task_id, note) in &sidecar.gantt_task_notes {
if let Some(task) = ast.tasks_mut().get_mut(note_task_id) {
task.set_note(Some(note.clone()));
continue;
}
if let Some(tag) = old_id_to_tag.get(note_task_id) {
if let Some(current_id) = tag_to_current.get(tag) {
if let Some(task) = ast.tasks_mut().get_mut(current_id) {
task.set_note(Some(note.clone()));
}
}
}
}
for (lane_id, note) in &sidecar.gantt_lane_notes {
let migrated_id = lane_id
.as_str()
.strip_prefix("lane:")
.filter(|suffix| suffix.len() == 4 && suffix.chars().all(|ch| ch.is_ascii_digit()))
.and_then(|suffix| suffix.parse::<u32>().ok())
.and_then(|day| ast.lane_id_for_relative_day(day))
.unwrap_or_else(|| lane_id.clone());
ast.set_lane_note(migrated_id, Some(note.clone()));
}
}
pub(crate) fn reconcile_sequence_participant_symbols(ast: &mut SequenceAst, sidecar: &DiagramMeta) {
if sidecar.sequence_participant_symbols.is_empty() {
return;
}
for (participant_id, symbol) in &sidecar.sequence_participant_symbols {
if let Some(participant) = ast.participants_mut().get_mut(participant_id) {
participant.set_symbol(Some(symbol.clone()));
}
}
}
pub(crate) fn reconcile_sequence_blocks(ast: &mut SequenceAst, sidecar: &DiagramMeta) {
if sidecar.sequence_blocks.is_empty() || ast.blocks().is_empty() {
return;
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct SectionFingerprint {
kind: u8,
header: Option<String>,
message_ids: Vec<ObjectId>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct BlockFingerprint {
parent_block_id: Option<ObjectId>,
kind: u8,
header: Option<String>,
sections: Vec<SectionFingerprint>,
}
fn block_kind_key(kind: SequenceBlockKind) -> u8 {
match kind {
SequenceBlockKind::Alt => 0,
SequenceBlockKind::Opt => 1,
SequenceBlockKind::Loop => 2,
SequenceBlockKind::Par => 3,
}
}
fn section_kind_key(kind: SequenceSectionKind) -> u8 {
match kind {
SequenceSectionKind::Main => 0,
SequenceSectionKind::Else => 1,
SequenceSectionKind::And => 2,
}
}
fn fingerprint_from_meta(entry: &DiagramSequenceBlockMeta) -> BlockFingerprint {
BlockFingerprint {
parent_block_id: entry.parent_block_id.clone(),
kind: block_kind_key(entry.kind),
header: entry.header.clone(),
sections: entry
.sections
.iter()
.map(|section| SectionFingerprint {
kind: section_kind_key(section.kind),
header: section.header.clone(),
message_ids: section.message_ids.clone(),
})
.collect(),
}
}
fn fingerprint_from_block(
block: &SequenceBlock,
parent_block_id: Option<ObjectId>,
) -> BlockFingerprint {
BlockFingerprint {
parent_block_id,
kind: block_kind_key(block.kind()),
header: block.header().map(ToOwned::to_owned),
sections: block
.sections()
.iter()
.map(|section| SectionFingerprint {
kind: section_kind_key(section.kind()),
header: section.header().map(ToOwned::to_owned),
message_ids: section.message_ids().to_vec(),
})
.collect(),
}
}
let mut by_fingerprint: BTreeMap<BlockFingerprint, VecDeque<&DiagramSequenceBlockMeta>> =
BTreeMap::new();
for entry in &sidecar.sequence_blocks {
by_fingerprint.entry(fingerprint_from_meta(entry)).or_default().push_back(entry);
}
let mut block_remap = BTreeMap::<ObjectId, ObjectId>::new();
let mut section_remap = BTreeMap::<ObjectId, ObjectId>::new();
let mut assigned_block_ids = BTreeSet::<ObjectId>::new();
let mut assigned_section_ids = BTreeSet::<ObjectId>::new();
for entry in &sidecar.sequence_blocks {
assigned_block_ids.insert(entry.block_id.clone());
for section in &entry.sections {
assigned_section_ids.insert(section.section_id.clone());
}
}
fn allocate_unique_id(
preferred: &ObjectId,
assigned: &mut BTreeSet<ObjectId>,
prefix: &str,
) -> ObjectId {
if assigned.insert(preferred.clone()) {
return preferred.clone();
}
let mut suffix = 1_u64;
loop {
let candidate = ObjectId::new(format!("{prefix}:reconcile:{suffix:04}"))
.expect("reconciled object id should be valid");
if assigned.insert(candidate.clone()) {
return candidate;
}
suffix = suffix.saturating_add(1);
}
}
fn walk_match(
blocks: &[SequenceBlock],
parent_stable_id: Option<ObjectId>,
by_fingerprint: &mut BTreeMap<BlockFingerprint, VecDeque<&DiagramSequenceBlockMeta>>,
block_remap: &mut BTreeMap<ObjectId, ObjectId>,
section_remap: &mut BTreeMap<ObjectId, ObjectId>,
assigned_block_ids: &mut BTreeSet<ObjectId>,
assigned_section_ids: &mut BTreeSet<ObjectId>,
) {
for block in blocks {
let fingerprint = fingerprint_from_block(block, parent_stable_id.clone());
if let Some(queue) = by_fingerprint.get_mut(&fingerprint) {
if let Some(entry) = queue.pop_front() {
assigned_block_ids.remove(&entry.block_id);
let target_block_id = if assigned_block_ids.contains(&entry.block_id) {
allocate_unique_id(block.block_id(), assigned_block_ids, "b")
} else {
assigned_block_ids.insert(entry.block_id.clone());
entry.block_id.clone()
};
block_remap.insert(block.block_id().clone(), target_block_id.clone());
for (section, entry_section) in
block.sections().iter().zip(entry.sections.iter())
{
assigned_section_ids.remove(&entry_section.section_id);
let target_section_id = if assigned_section_ids
.contains(&entry_section.section_id)
{
allocate_unique_id(section.section_id(), assigned_section_ids, "sec")
} else {
assigned_section_ids.insert(entry_section.section_id.clone());
entry_section.section_id.clone()
};
section_remap.insert(section.section_id().clone(), target_section_id);
}
walk_match(
block.blocks(),
Some(target_block_id),
by_fingerprint,
block_remap,
section_remap,
assigned_block_ids,
assigned_section_ids,
);
continue;
}
}
let target_block_id = allocate_unique_id(block.block_id(), assigned_block_ids, "b");
if &target_block_id != block.block_id() {
block_remap.insert(block.block_id().clone(), target_block_id.clone());
}
for section in block.sections() {
let target_section_id =
allocate_unique_id(section.section_id(), assigned_section_ids, "sec");
if &target_section_id != section.section_id() {
section_remap.insert(section.section_id().clone(), target_section_id);
}
}
walk_match(
block.blocks(),
Some(target_block_id),
by_fingerprint,
block_remap,
section_remap,
assigned_block_ids,
assigned_section_ids,
);
}
}
walk_match(
ast.blocks(),
None,
&mut by_fingerprint,
&mut block_remap,
&mut section_remap,
&mut assigned_block_ids,
&mut assigned_section_ids,
);
if block_remap.is_empty() && section_remap.is_empty() {
return;
}
fn apply_remaps(
blocks: &mut [SequenceBlock],
block_remap: &BTreeMap<ObjectId, ObjectId>,
section_remap: &BTreeMap<ObjectId, ObjectId>,
) {
for block in blocks {
let old_id = block.block_id().clone();
let new_id = block_remap.get(&old_id).cloned().unwrap_or(old_id);
let kind = block.kind();
let header = block.header().map(ToOwned::to_owned);
let mut sections = std::mem::take(block.sections_mut());
for section in sections.iter_mut() {
let old_section_id = section.section_id().clone();
let new_section_id =
section_remap.get(&old_section_id).cloned().unwrap_or(old_section_id);
let section_kind = section.kind();
let section_header = section.header().map(ToOwned::to_owned);
let message_ids = section.message_ids().to_vec();
*section =
SequenceSection::new(new_section_id, section_kind, section_header, message_ids);
}
let mut nested = std::mem::take(block.blocks_mut());
apply_remaps(&mut nested, block_remap, section_remap);
*block = SequenceBlock::new(new_id, kind, header, sections, nested);
}
}
apply_remaps(ast.blocks_mut(), &block_remap, §ion_remap);
}
pub(crate) fn reconcile_diagram_ast(ast: &mut DiagramAst, sidecar: &DiagramMeta) {
match ast {
DiagramAst::Flowchart(flow_ast) => {
reconcile_flowchart_nodes(flow_ast, sidecar);
reconcile_flowchart_edges(flow_ast, sidecar);
reconcile_flowchart_notes(flow_ast, sidecar);
reconcile_flowchart_symbols(flow_ast, sidecar);
}
DiagramAst::Sequence(seq_ast) => {
reconcile_sequence_participants(seq_ast, sidecar);
reconcile_sequence_messages(seq_ast, sidecar);
reconcile_sequence_blocks(seq_ast, sidecar);
reconcile_sequence_participant_notes(seq_ast, sidecar);
reconcile_sequence_participant_symbols(seq_ast, sidecar);
}
DiagramAst::Class(class_ast) => {
reconcile_class_nodes(class_ast, sidecar);
reconcile_class_relations(class_ast, sidecar);
reconcile_class_notes(class_ast, sidecar);
}
DiagramAst::Er(er_ast) => {
reconcile_er_entities(er_ast, sidecar);
reconcile_er_relationships(er_ast, sidecar);
reconcile_er_notes(er_ast, sidecar);
}
DiagramAst::Gantt(gantt_ast) => {
reconcile_gantt_tasks(gantt_ast, sidecar);
reconcile_gantt_sections(gantt_ast, sidecar);
reconcile_gantt_notes(gantt_ast, sidecar);
}
}
}
pub(crate) fn identity_sidecar_from_diagram(diagram: &crate::model::Diagram) -> DiagramMeta {
use super::{sequence_blocks_meta_from_ast, DiagramFlowEdgeMeta, DiagramSequenceMessageMeta};
use std::path::PathBuf;
let (
flow_edges,
class_relations,
er_relationships,
gantt_sections,
sequence_messages,
sequence_blocks,
flow_node_notes,
sequence_participant_notes,
class_node_notes,
er_entity_notes,
gantt_task_notes,
gantt_lane_notes,
flow_node_symbols,
sequence_participant_symbols,
) = match diagram.ast() {
DiagramAst::Flowchart(ast) => (
ast.edges()
.iter()
.map(|(edge_id, edge)| DiagramFlowEdgeMeta {
edge_id: edge_id.clone(),
from_node_id: edge.from_node_id().clone(),
to_node_id: edge.to_node_id().clone(),
label: edge.label().map(ToOwned::to_owned),
connector: edge.connector().map(ToOwned::to_owned),
style: edge.style().map(ToOwned::to_owned),
})
.collect(),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
ast.nodes()
.iter()
.filter_map(|(node_id, node)| {
node.note().map(|note| (node_id.clone(), note.to_owned()))
})
.collect(),
BTreeMap::new(),
BTreeMap::new(),
BTreeMap::new(),
BTreeMap::new(),
BTreeMap::new(),
ast.nodes()
.iter()
.filter_map(|(node_id, node)| {
node.symbol().map(|symbol| (node_id.clone(), symbol.clone()))
})
.collect(),
BTreeMap::new(),
),
DiagramAst::Sequence(ast) => (
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
ast.messages_in_order()
.into_iter()
.map(|msg| DiagramSequenceMessageMeta {
message_id: msg.message_id().clone(),
from_participant_id: msg.from_participant_id().clone(),
to_participant_id: msg.to_participant_id().clone(),
kind: msg.kind(),
text: msg.text().to_owned(),
})
.collect(),
sequence_blocks_meta_from_ast(ast),
BTreeMap::new(),
ast.participants()
.iter()
.filter_map(|(participant_id, participant)| {
participant.note().map(|note| (participant_id.clone(), note.to_owned()))
})
.collect(),
BTreeMap::new(),
BTreeMap::new(),
BTreeMap::new(),
BTreeMap::new(),
BTreeMap::new(),
ast.participants()
.iter()
.filter_map(|(participant_id, participant)| {
participant.symbol().map(|symbol| (participant_id.clone(), symbol.clone()))
})
.collect(),
),
DiagramAst::Class(ast) => (
Vec::new(),
super::class_relations_meta_from_ast(ast),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
BTreeMap::new(),
BTreeMap::new(),
ast.classes()
.iter()
.filter_map(|(class_id, class)| {
class.note().map(|note| (class_id.clone(), note.to_owned()))
})
.collect(),
BTreeMap::new(),
BTreeMap::new(),
BTreeMap::new(),
BTreeMap::new(),
BTreeMap::new(),
),
DiagramAst::Er(ast) => (
Vec::new(),
Vec::new(),
super::er_relationships_meta_from_ast(ast),
Vec::new(),
Vec::new(),
Vec::new(),
BTreeMap::new(),
BTreeMap::new(),
BTreeMap::new(),
ast.entities()
.iter()
.filter_map(|(entity_id, entity)| {
entity.note().map(|note| (entity_id.clone(), note.to_owned()))
})
.collect(),
BTreeMap::new(),
BTreeMap::new(),
BTreeMap::new(),
BTreeMap::new(),
),
DiagramAst::Gantt(ast) => (
Vec::new(),
Vec::new(),
Vec::new(),
super::gantt_sections_meta_from_ast(ast),
Vec::new(),
Vec::new(),
BTreeMap::new(),
BTreeMap::new(),
BTreeMap::new(),
BTreeMap::new(),
ast.tasks()
.iter()
.filter_map(|(task_id, task)| {
task.note().map(|note| (task_id.clone(), note.to_owned()))
})
.collect(),
ast.lane_notes().clone(),
BTreeMap::new(),
BTreeMap::new(),
),
};
DiagramMeta {
diagram_id: diagram.diagram_id().clone(),
mmd_path: PathBuf::new(),
stable_id_map: stable_id_map_from_ast(diagram.ast()),
xrefs: Vec::new(),
flow_edges,
class_relations,
er_relationships,
gantt_sections,
sequence_messages,
sequence_blocks,
default_symbol_repository_id: diagram.default_symbol_repository_id().map(ToOwned::to_owned),
flow_node_notes,
sequence_participant_notes,
class_node_notes,
er_entity_notes,
gantt_task_notes,
gantt_lane_notes,
flow_node_symbols,
sequence_participant_symbols,
}
}