use crate::ast::{self, DiagramMember, GroupMember, LayoutStatement, NodeProperty};
use crate::diagnostic::Span;
use crate::lossless::{self, TokenKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceOrigin {
Authored(Span),
Omitted,
}
impl SourceOrigin {
pub const fn span(self) -> Option<Span> {
match self {
Self::Authored(span) => Some(span),
Self::Omitted => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NodeIconSource {
pub node_id: String,
pub origin: SourceOrigin,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LayoutScope {
Diagram,
Group(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LayoutOrderSource {
pub scope: LayoutScope,
pub origin: SourceOrigin,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceMap {
theme: SourceOrigin,
node_icons: Vec<NodeIconSource>,
layout_orders: Vec<LayoutOrderSource>,
}
impl SourceMap {
pub(crate) fn from_document(document: &ast::Document, lossless: &lossless::Document) -> Self {
let theme = document
.diagram
.members
.iter()
.find_map(|member| match member {
DiagramMember::Theme(theme) => Some(SourceOrigin::Authored(theme.identifier.span)),
_ => None,
})
.unwrap_or(SourceOrigin::Omitted);
let mut node_icons = Vec::new();
let mut layout_orders = vec![LayoutOrderSource {
scope: LayoutScope::Diagram,
origin: layout_order_origin(
document
.diagram
.members
.iter()
.find_map(diagram_member_layout),
lossless,
),
}];
for member in &document.diagram.members {
match member {
DiagramMember::Node(node) => node_icons.push(node_icon_source(node)),
DiagramMember::Group(group) => {
append_group_sources(group, lossless, &mut node_icons, &mut layout_orders);
}
DiagramMember::Edge(_) | DiagramMember::Theme(_) | DiagramMember::Layout(_) => {}
}
}
Self {
theme,
node_icons,
layout_orders,
}
}
pub const fn theme(&self) -> SourceOrigin {
self.theme
}
pub fn node_icons(&self) -> &[NodeIconSource] {
&self.node_icons
}
pub fn node_icon(&self, node_id: &str) -> Option<SourceOrigin> {
self.node_icons
.iter()
.find(|entry| entry.node_id == node_id)
.map(|entry| entry.origin)
}
pub fn layout_orders(&self) -> &[LayoutOrderSource] {
&self.layout_orders
}
pub fn diagram_order(&self) -> SourceOrigin {
self.layout_orders
.first()
.map_or(SourceOrigin::Omitted, |entry| entry.origin)
}
pub fn group_order(&self, group_id: &str) -> Option<SourceOrigin> {
self.layout_orders
.iter()
.find_map(|entry| match &entry.scope {
LayoutScope::Group(identifier) if identifier == group_id => Some(entry.origin),
LayoutScope::Diagram | LayoutScope::Group(_) => None,
})
}
}
fn diagram_member_layout(member: &DiagramMember) -> Option<&ast::Layout> {
match member {
DiagramMember::Layout(layout) => Some(layout),
_ => None,
}
}
fn group_member_layout(member: &GroupMember) -> Option<&ast::Layout> {
match member {
GroupMember::Layout(layout) => Some(layout),
_ => None,
}
}
fn node_icon_source(node: &ast::Node) -> NodeIconSource {
let origin = node
.properties
.iter()
.find_map(|property| match property {
NodeProperty::Icon(icon) => Some(SourceOrigin::Authored(icon.span)),
NodeProperty::Kind(_) | NodeProperty::Detail(_) => None,
})
.unwrap_or(SourceOrigin::Omitted);
NodeIconSource {
node_id: node.identifier.value.clone(),
origin,
}
}
fn append_group_sources(
group: &ast::Group,
lossless: &lossless::Document,
node_icons: &mut Vec<NodeIconSource>,
layout_orders: &mut Vec<LayoutOrderSource>,
) {
layout_orders.push(LayoutOrderSource {
scope: LayoutScope::Group(group.identifier.value.clone()),
origin: layout_order_origin(group.members.iter().find_map(group_member_layout), lossless),
});
for member in &group.members {
match member {
GroupMember::Node(node) => node_icons.push(node_icon_source(node)),
GroupMember::Group(child) => {
append_group_sources(child, lossless, node_icons, layout_orders);
}
GroupMember::Layout(_) => {}
}
}
}
fn layout_order_origin(
layout: Option<&ast::Layout>,
lossless: &lossless::Document,
) -> SourceOrigin {
let Some(list) = layout.and_then(|layout| {
layout
.statements
.iter()
.find_map(|statement| match statement {
LayoutStatement::Order(list) => Some(list),
LayoutStatement::Direction(_) | LayoutStatement::RankSame(_) => None,
})
}) else {
return SourceOrigin::Omitted;
};
SourceOrigin::Authored(order_statement_span(list.span, lossless.tokens()))
}
fn order_statement_span(list_span: Span, tokens: &[lossless::Token]) -> Span {
let keyword = tokens
.iter()
.take_while(|token| token.span.end.byte_offset <= list_span.start.byte_offset)
.filter(|token| !matches!(token.kind, TokenKind::Whitespace | TokenKind::LineComment))
.last();
keyword.map_or(list_span, |keyword| Span::covering(keyword.span, list_span))
}
#[cfg(test)]
mod tests {
use super::SourceOrigin;
#[test]
fn source_origin_returns_only_authored_spans() {
let span = crate::diagnostic::Span::point(crate::diagnostic::SourcePosition::start());
assert_eq!(SourceOrigin::Authored(span).span(), Some(span));
assert_eq!(SourceOrigin::Omitted.span(), None);
}
}