use std::collections::{BTreeMap, BTreeSet};
use std::ops::Bound;
use svugy_model::builder::{ElementRef, ModelBuilder, WorkflowRef};
use svugy_model::model::{ByteRange, Edge, Element, PlacementHint, Shape};
use svugy_model::path::ElementPath;
use svugy_model::validate::{ModelAdmission, ModelReport};
use svugy_model::workflow::{ElementOverride, Step, StepId, Workflow, WorkflowId};
use super::D2Import;
use super::D2Parse;
use super::ast::{
EdgeDeclaration, EdgeLink, EdgeOperator, Field, FieldAssignment, KeyPath, KeySegment, Scalar,
ShapeDeclaration, Span, Statement,
};
use super::carrier::{CarrierReader, ElementAttribute};
use super::diagnostic::{Construct, Diagnostics};
const MAIN_WORKFLOW_ID: &str = "main";
const EVERY_EDGE_STEP_ID: &str = "main";
const EVERY_EDGE_STEP_TITLE: &str = "Every edge";
const NO_SHAPE: &str = "the diagram declares no shape, so there is nothing to draw";
const LAYERED_DIRECTION: &str = "right";
const MAPPED_D2_SHAPES: [(&str, Option<Shape>); 5] = [
("rectangle", None),
("square", None),
("cylinder", Some(Shape::Cylinder)),
("circle", Some(Shape::Disc)),
("oval", Some(Shape::Disc)),
];
const SPECIAL_D2_SHAPES: [&str; 4] = ["sql_table", "sequence_diagram", "class", "hierarchy"];
struct Lowering {
title: Option<String>,
subtitle: Option<String>,
elements: ElementDrafts,
edges: Vec<EdgeDraft>,
diagnostics: Diagnostics,
}
#[derive(Default)]
struct ElementDrafts {
drafts: BTreeMap<ElementPath, ElementDraft>,
}
struct ElementDraft {
declaration_index: usize,
name: String,
element: Element,
main_override: Option<ElementOverride>,
d2_shape: Option<Shape>,
svugy_shape: Option<Shape>,
badge_mark_span: Option<Span>,
}
struct EdgeDraft {
from_key: ElementPath,
to_key: ElementPath,
edge: Edge,
}
impl D2Parse {
pub fn lower(self) -> D2Import {
let D2Parse {
document,
errors,
warnings,
} = self;
let diagnostics = Diagnostics::from_parts(errors, warnings);
let mut lowering = Lowering::new(diagnostics);
let root_key = ElementPath::root();
lowering.lower_statements(&root_key, document.statements);
lowering.into_import()
}
}
impl Lowering {
fn new(diagnostics: Diagnostics) -> Self {
let elements = ElementDrafts::default();
let edges = Vec::new();
Self {
title: None,
subtitle: None,
elements,
edges,
diagnostics,
}
}
fn lower_statements(&mut self, scope_key: &ElementPath, statements: Vec<Statement>) {
for statement in statements {
match statement {
Statement::Shape(shape) => self.lower_shape(scope_key, shape),
Statement::Edge(edge) => self.lower_edge(scope_key, edge),
Statement::Field(assignment) => self.lower_field_assignment(scope_key, assignment),
}
}
}
fn lower_shape(&mut self, scope_key: &ElementPath, shape: ShapeDeclaration) {
let ShapeDeclaration {
path,
label,
body,
span: _declaration_span,
} = shape;
let Some(element_key) = self
.elements
.declare_path(scope_key, &path, &mut self.diagnostics)
else {
return;
};
if let Some(label) = label {
self.elements
.edit_element(&element_key, |element| element.relabeled(label.text));
}
self.lower_statements(&element_key, body);
}
fn lower_edge(&mut self, scope_key: &ElementPath, edge: EdgeDeclaration) {
let EdgeDeclaration {
source,
links,
label,
fields,
span: _declaration_span,
} = edge;
let shared_edge = self.build_shared_edge_or_warn(label, fields);
let Some(mut tail_key) =
self.elements
.declare_path(scope_key, &source, &mut self.diagnostics)
else {
return;
};
let mut tail_start = source.span.start;
for link in links {
let EdgeLink {
operator,
operator_span,
target,
} = link;
let Some(head_key) =
self.elements
.declare_path(scope_key, &target, &mut self.diagnostics)
else {
return;
};
let link_head_key = head_key.clone();
let (from_key, to_key) =
self.orient_or_warn(operator, operator_span, tail_key, link_head_key);
let link_span = Span::new(tail_start, target.span.end);
let link_range = ByteRange::from(link_span);
let edge = shared_edge.clone().with_span(link_range);
let edge_draft = EdgeDraft {
from_key,
to_key,
edge,
};
self.push_edge_unless_lineage_or_warn(edge_draft, link_span);
tail_key = head_key;
tail_start = target.span.start;
}
}
fn build_shared_edge_or_warn(&mut self, label: Option<Scalar>, fields: Vec<Field>) -> Edge {
let mut edge = Edge::new();
if let Some(label) = label {
edge = edge.with_label(label.text);
}
for field in fields {
edge = self.apply_edge_field_or_warn(edge, field);
}
edge
}
fn apply_edge_field_or_warn(&mut self, edge: Edge, field: Field) -> Edge {
let field_keyword = field.keyword();
let keyword_name: &str = field_keyword.into();
match field {
Field::Label(label) => edge.with_label(label.text),
Field::Tooltip(tooltip) => edge.with_description(tooltip.text),
Field::Style(properties) => properties.into_iter().fold(edge, |edge, property| {
edge.with_style(property.name.name, property.value.text)
}),
Field::Shape(value) | Field::Direction(value) | Field::Link(value) => {
let construct = Construct::FieldOnEdge(keyword_name);
self.diagnostics.record_warning(construct, value.span);
edge
}
Field::Svugy(_rejected_carrier) => edge,
}
}
fn orient_or_warn(
&mut self,
operator: EdgeOperator,
operator_span: Span,
tail_key: ElementPath,
head_key: ElementPath,
) -> (ElementPath, ElementPath) {
match operator {
EdgeOperator::Forward => (tail_key, head_key),
EdgeOperator::Backward => (head_key, tail_key),
EdgeOperator::Bidirectional | EdgeOperator::Undirected => {
let construct = Construct::ArrowheadForm(operator);
self.diagnostics.record_warning(construct, operator_span);
(tail_key, head_key)
}
}
}
fn push_edge_unless_lineage_or_warn(&mut self, edge_draft: EdgeDraft, link_span: Span) {
let is_lineage_edge = edge_draft.from_key.shares_lineage_with(&edge_draft.to_key);
if is_lineage_edge {
self.diagnostics
.record_warning(Construct::LineageEdge, link_span);
return;
}
self.edges.push(edge_draft);
}
fn lower_field_assignment(&mut self, scope_key: &ElementPath, assignment: FieldAssignment) {
let FieldAssignment {
target,
field,
span,
} = assignment;
let Some(target_key) =
self.elements
.declare_path(scope_key, &target, &mut self.diagnostics)
else {
return;
};
let is_root = target_key.is_root();
if is_root {
self.lower_root_field(field, span);
return;
}
match field {
Field::Direction(direction) => self.lower_direction_field(&target_key, direction),
Field::Shape(shape) => self.lower_shape_field(&target_key, shape),
Field::Label(label) => {
self.elements
.edit_element(&target_key, |element| element.relabeled(label.text));
}
Field::Tooltip(tooltip) => {
self.elements.edit_element(&target_key, |element| {
element.with_description(tooltip.text)
});
}
Field::Link(link) => {
let link_range = ByteRange::from(link.span);
self.elements.edit_element(&target_key, |element| {
element.with_link_written_at(link.text, Some(link_range))
});
}
Field::Style(properties) => self.elements.edit_element(&target_key, |element| {
properties.into_iter().fold(element, |element, property| {
element.with_style(property.name.name, property.value.text)
})
}),
Field::Svugy(properties) => {
let mut carrier_reader = CarrierReader::new(&mut self.diagnostics);
let element_attributes = carrier_reader.read_element(properties, span);
self.merge_element_carrier(&target_key, element_attributes, span);
}
}
}
fn lower_root_field(&mut self, field: Field, span: Span) {
let root_key = ElementPath::root();
let field_keyword = field.keyword();
let keyword_name: &str = field_keyword.into();
match field {
Field::Label(label) => self.title = Some(label.text),
Field::Svugy(properties) => {
let mut carrier_reader = CarrierReader::new(&mut self.diagnostics);
let subtitle = carrier_reader.read_root_subtitle(properties);
if let Some(subtitle) = subtitle {
self.subtitle = Some(subtitle);
}
}
Field::Direction(direction) => self.lower_direction_field(&root_key, direction),
_element_only_field => {
let construct = Construct::FieldOnRoot(keyword_name);
self.diagnostics.record_warning(construct, span);
}
}
}
fn lower_direction_field(&mut self, element_key: &ElementPath, direction: Scalar) {
let is_diff_layered_direction = direction.text != LAYERED_DIRECTION;
if is_diff_layered_direction {
let construct = Construct::UnmappedDirection(direction.text);
self.diagnostics.record_warning(construct, direction.span);
return;
}
let is_root = element_key.is_root();
if is_root {
return;
}
self.elements
.edit_main_override(element_key, direction.span, |element_override| {
element_override.with_child_placement(PlacementHint::Layered)
});
}
fn lower_shape_field(&mut self, element_key: &ElementPath, shape: Scalar) {
let lowercase_shape = shape.text.to_ascii_lowercase();
let lowercase_shape_name = lowercase_shape.as_str();
let is_special_shape = SPECIAL_D2_SHAPES.contains(&lowercase_shape_name);
if is_special_shape {
let construct = Construct::SpecialShape(shape.text);
self.diagnostics.record_warning(construct, shape.span);
return;
}
let mut mapped_shapes = MAPPED_D2_SHAPES.iter();
let mapped_shape =
mapped_shapes.find(|(d2_shape, _primitive)| *d2_shape == lowercase_shape);
let Some((_d2_shape, primitive)) = mapped_shape else {
let construct = Construct::UnmappedShape(shape.text);
self.diagnostics.record_warning(construct, shape.span);
return;
};
self.elements.draft_mut(element_key).d2_shape = *primitive;
}
fn merge_element_carrier(
&mut self,
element_key: &ElementPath,
attributes: Vec<ElementAttribute>,
carrier_span: Span,
) {
for attribute in attributes {
match attribute {
ElementAttribute::Caption(caption) => {
self.elements
.edit_element(element_key, |element| element.with_caption(caption));
}
ElementAttribute::Primitive(shape) => {
self.elements.draft_mut(element_key).svugy_shape = Some(shape);
}
ElementAttribute::Height(height) => {
self.elements
.edit_element(element_key, |element| element.with_height(height));
}
ElementAttribute::BadgeMark { is_marked, span } => {
let badge_mark_span = is_marked.then_some(span);
self.elements.draft_mut(element_key).badge_mark_span = badge_mark_span;
}
ElementAttribute::Position(position) => {
self.elements.edit_main_override(
element_key,
carrier_span,
|element_override| element_override.with_position(position),
);
}
}
}
}
fn into_import(self) -> D2Import {
let Self {
title,
subtitle,
elements,
edges,
mut diagnostics,
} = self;
let declares_no_shape = elements.drafts.is_empty();
if declares_no_shape {
let file_start = Span::new(0, 0);
diagnostics.record_error(NO_SHAPE, file_start);
let withheld_admission = ModelAdmission {
model: None,
report: ModelReport::default(),
};
let (errors, warnings) = diagnostics.into_sorted_parts();
return D2Import {
admission: withheld_admission,
errors,
warnings,
};
}
elements.warn_container_badge_marks(&mut diagnostics);
let main_workflow_id = WorkflowId::new(MAIN_WORKFLOW_ID);
let main_title = title.unwrap_or_else(|| MAIN_WORKFLOW_ID.to_owned());
let mut main_workflow = Workflow::new(main_workflow_id, main_title);
if let Some(subtitle) = subtitle {
main_workflow = main_workflow.with_subtitle(subtitle);
}
let admission = Self::build_model(main_workflow, elements, edges);
let (errors, warnings) = diagnostics.into_sorted_parts();
D2Import {
admission,
errors,
warnings,
}
}
fn build_model(
main_workflow: Workflow,
elements: ElementDrafts,
edges: Vec<EdgeDraft>,
) -> ModelAdmission {
let badged_edges: Vec<EdgeDraft> = edges
.into_iter()
.map(|edge_draft| edge_draft.badged_if_from_leaf_into_badge_leaf(&elements))
.collect();
let untouched_leaf_keys = elements.leaf_keys_no_edge_touches(&badged_edges);
let mut builder = ModelBuilder::new();
let main_workflow_ref = builder.add_workflow(main_workflow);
let element_refs = elements.add_to(&mut builder, &main_workflow_ref);
let every_edge_step_id = StepId::new(EVERY_EDGE_STEP_ID);
let every_edge_step = Step::new(every_edge_step_id, EVERY_EDGE_STEP_TITLE);
let every_edge_step_ref = builder.add_step(&main_workflow_ref, every_edge_step);
for edge_draft in badged_edges {
let EdgeDraft {
from_key,
to_key,
edge,
} = edge_draft;
let from_ref = &element_refs[&from_key];
let to_ref = &element_refs[&to_key];
builder.add_edge(&every_edge_step_ref, from_ref, to_ref, edge);
}
for leaf_key in untouched_leaf_keys {
let leaf_ref = &element_refs[&leaf_key];
builder.list_element(&every_edge_step_ref, leaf_ref);
}
builder.build()
}
}
impl ElementDrafts {
fn declare_path(
&mut self,
scope_key: &ElementPath,
path: &KeyPath,
diagnostics: &mut Diagnostics,
) -> Option<ElementPath> {
let mut element_key = scope_key.clone();
for _parent_reference in 0..path.parent_references {
let Some(parent_key) = element_key.parent() else {
let message = "`_` climbs above the root, which has no parent scope";
diagnostics.record_error(message, path.span);
return None;
};
element_key = parent_key;
}
for segment in &path.segments {
let prefix_span = Span::new(path.span.start, segment.span.end);
element_key = self.declare_child(&element_key, segment, prefix_span);
}
Some(element_key)
}
fn declare_child(
&mut self,
parent_key: &ElementPath,
segment: &KeySegment,
span: Span,
) -> ElementPath {
let element_key = Self::child_key(parent_key, &segment.name);
let is_declared = self.drafts.contains_key(&element_key);
if is_declared {
return element_key;
}
let declaration_index = self.drafts.len();
let name = segment.name.clone();
let draft = ElementDraft::new(declaration_index, name, span);
let drafted_key = element_key.clone();
self.drafts.insert(drafted_key, draft);
element_key
}
fn child_key(parent_key: &ElementPath, name: &str) -> ElementPath {
let folded_name = name.to_lowercase();
parent_key.child(folded_name)
}
fn edit_element(&mut self, element_key: &ElementPath, edit: impl FnOnce(Element) -> Element) {
let draft = self.draft_mut(element_key);
let element = draft.element.clone();
draft.element = edit(element);
}
fn edit_main_override(
&mut self,
element_key: &ElementPath,
span: Span,
edit: impl FnOnce(ElementOverride) -> ElementOverride,
) {
let draft = self.draft_mut(element_key);
let main_override = draft.main_override.take().unwrap_or_default();
let field_range = ByteRange::from(span);
let edited_override = edit(main_override).with_span(field_range);
draft.main_override = Some(edited_override);
}
fn leaf_keys_no_edge_touches(&self, edges: &[EdgeDraft]) -> Vec<ElementPath> {
let touched_keys: BTreeSet<&ElementPath> = edges
.iter()
.flat_map(|edge_draft| [&edge_draft.from_key, &edge_draft.to_key])
.collect();
let mut untouched_leaf_drafts: Vec<(&ElementPath, &ElementDraft)> = self
.drafts
.iter()
.filter(|(element_key, _draft)| {
let is_leaf = !self.is_container(element_key);
let is_untouched = !touched_keys.contains(element_key);
is_leaf && is_untouched
})
.collect();
untouched_leaf_drafts.sort_by_key(|(_element_key, draft)| draft.declaration_index);
untouched_leaf_drafts
.into_iter()
.map(|(element_key, _draft)| element_key.clone())
.collect()
}
fn add_to(
self,
builder: &mut ModelBuilder,
workflow: &WorkflowRef,
) -> BTreeMap<ElementPath, ElementRef> {
let mut element_refs: BTreeMap<ElementPath, ElementRef> = BTreeMap::new();
for (element_key, draft) in self.drafts {
let parent_key = element_key.parent();
let parent_ref = parent_key.and_then(|parent_key| element_refs.get(&parent_key));
let (name, element, main_override) = draft.into_parts();
let element_ref = match parent_ref {
Some(parent_ref) => builder.add_child(parent_ref, &name, element),
None => builder.add_top_level(&name, element),
};
if let Some(main_override) = main_override {
builder.override_element(workflow, &element_ref, main_override);
}
element_refs.insert(element_key, element_ref);
}
element_refs
}
fn warn_container_badge_marks(&self, diagnostics: &mut Diagnostics) {
for (element_key, draft) in &self.drafts {
let Some(badge_mark_span) = draft.badge_mark_span else {
continue;
};
let is_leaf = !self.is_container(element_key);
if is_leaf {
continue;
}
diagnostics.record_warning(Construct::ContainerBadge, badge_mark_span);
}
}
fn is_badge_target(&self, element_key: &ElementPath) -> bool {
let draft = self.drafts.get(element_key);
draft.is_some_and(|draft| draft.badge_mark_span.is_some())
}
fn is_container(&self, element_key: &ElementPath) -> bool {
let after_element_key = (Bound::Excluded(element_key), Bound::Unbounded);
let mut later_drafts = self.drafts.range::<ElementPath, _>(after_element_key);
let next_draft = later_drafts.next();
next_draft.is_some_and(|(next_key, _draft)| element_key.is_ancestor_of(next_key))
}
fn draft_mut(&mut self, element_key: &ElementPath) -> &mut ElementDraft {
self.drafts
.get_mut(element_key)
.expect("declare_path declares every element it returns")
}
}
impl ElementDraft {
fn new(declaration_index: usize, name: String, span: Span) -> Self {
let source_range = ByteRange::from(span);
let label = name.clone();
let element = Element::new(label).with_span(source_range);
Self {
declaration_index,
name,
element,
main_override: None,
d2_shape: None,
svugy_shape: None,
badge_mark_span: None,
}
}
fn into_parts(self) -> (String, Element, Option<ElementOverride>) {
let Self {
declaration_index: _declaration_index,
name,
element,
main_override,
d2_shape,
svugy_shape,
badge_mark_span: _badge_mark_span,
} = self;
let shape = svugy_shape.or(d2_shape);
let Some(shape) = shape else {
return (name, element, main_override);
};
let shaped_element = element.with_shape(shape);
(name, shaped_element, main_override)
}
}
impl EdgeDraft {
fn badged_if_from_leaf_into_badge_leaf(self, elements: &ElementDrafts) -> Self {
let is_from_container = elements.is_container(&self.from_key);
if is_from_container {
return self;
}
let is_into_container = elements.is_container(&self.to_key);
if is_into_container {
return self;
}
let is_into_unmarked_element = !elements.is_badge_target(&self.to_key);
if is_into_unmarked_element {
return self;
}
let badged_edge = self.edge.with_badge();
Self {
edge: badged_edge,
..self
}
}
}