stack_compiler/
source_map.rs1use crate::ast::{self, DiagramMember, GroupMember, LayoutStatement, NodeProperty};
4use crate::diagnostic::Span;
5use crate::lossless::{self, TokenKind};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum SourceOrigin {
10 Authored(Span),
12 Omitted,
14}
15
16impl SourceOrigin {
17 pub const fn span(self) -> Option<Span> {
19 match self {
20 Self::Authored(span) => Some(span),
21 Self::Omitted => None,
22 }
23 }
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct NodeIconSource {
29 pub node_id: String,
31 pub origin: SourceOrigin,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum LayoutScope {
38 Diagram,
40 Group(String),
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct LayoutOrderSource {
47 pub scope: LayoutScope,
49 pub origin: SourceOrigin,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct SourceMap {
56 theme: SourceOrigin,
57 node_icons: Vec<NodeIconSource>,
58 layout_orders: Vec<LayoutOrderSource>,
59}
60
61impl SourceMap {
62 pub(crate) fn from_document(document: &ast::Document, lossless: &lossless::Document) -> Self {
63 let theme = document
64 .diagram
65 .members
66 .iter()
67 .find_map(|member| match member {
68 DiagramMember::Theme(theme) => Some(SourceOrigin::Authored(theme.identifier.span)),
69 _ => None,
70 })
71 .unwrap_or(SourceOrigin::Omitted);
72
73 let mut node_icons = Vec::new();
74 let mut layout_orders = vec![LayoutOrderSource {
75 scope: LayoutScope::Diagram,
76 origin: layout_order_origin(
77 document
78 .diagram
79 .members
80 .iter()
81 .find_map(diagram_member_layout),
82 lossless,
83 ),
84 }];
85
86 for member in &document.diagram.members {
87 match member {
88 DiagramMember::Node(node) => node_icons.push(node_icon_source(node)),
89 DiagramMember::Group(group) => {
90 append_group_sources(group, lossless, &mut node_icons, &mut layout_orders);
91 }
92 DiagramMember::Edge(_) | DiagramMember::Theme(_) | DiagramMember::Layout(_) => {}
93 }
94 }
95
96 Self {
97 theme,
98 node_icons,
99 layout_orders,
100 }
101 }
102
103 pub const fn theme(&self) -> SourceOrigin {
105 self.theme
106 }
107
108 pub fn node_icons(&self) -> &[NodeIconSource] {
110 &self.node_icons
111 }
112
113 pub fn node_icon(&self, node_id: &str) -> Option<SourceOrigin> {
115 self.node_icons
116 .iter()
117 .find(|entry| entry.node_id == node_id)
118 .map(|entry| entry.origin)
119 }
120
121 pub fn layout_orders(&self) -> &[LayoutOrderSource] {
123 &self.layout_orders
124 }
125
126 pub fn diagram_order(&self) -> SourceOrigin {
128 self.layout_orders
129 .first()
130 .map_or(SourceOrigin::Omitted, |entry| entry.origin)
131 }
132
133 pub fn group_order(&self, group_id: &str) -> Option<SourceOrigin> {
135 self.layout_orders
136 .iter()
137 .find_map(|entry| match &entry.scope {
138 LayoutScope::Group(identifier) if identifier == group_id => Some(entry.origin),
139 LayoutScope::Diagram | LayoutScope::Group(_) => None,
140 })
141 }
142}
143
144fn diagram_member_layout(member: &DiagramMember) -> Option<&ast::Layout> {
145 match member {
146 DiagramMember::Layout(layout) => Some(layout),
147 _ => None,
148 }
149}
150
151fn group_member_layout(member: &GroupMember) -> Option<&ast::Layout> {
152 match member {
153 GroupMember::Layout(layout) => Some(layout),
154 _ => None,
155 }
156}
157
158fn node_icon_source(node: &ast::Node) -> NodeIconSource {
159 let origin = node
160 .properties
161 .iter()
162 .find_map(|property| match property {
163 NodeProperty::Icon(icon) => Some(SourceOrigin::Authored(icon.span)),
164 NodeProperty::Kind(_) | NodeProperty::Detail(_) => None,
165 })
166 .unwrap_or(SourceOrigin::Omitted);
167
168 NodeIconSource {
169 node_id: node.identifier.value.clone(),
170 origin,
171 }
172}
173
174fn append_group_sources(
175 group: &ast::Group,
176 lossless: &lossless::Document,
177 node_icons: &mut Vec<NodeIconSource>,
178 layout_orders: &mut Vec<LayoutOrderSource>,
179) {
180 layout_orders.push(LayoutOrderSource {
181 scope: LayoutScope::Group(group.identifier.value.clone()),
182 origin: layout_order_origin(group.members.iter().find_map(group_member_layout), lossless),
183 });
184
185 for member in &group.members {
186 match member {
187 GroupMember::Node(node) => node_icons.push(node_icon_source(node)),
188 GroupMember::Group(child) => {
189 append_group_sources(child, lossless, node_icons, layout_orders);
190 }
191 GroupMember::Layout(_) => {}
192 }
193 }
194}
195
196fn layout_order_origin(
197 layout: Option<&ast::Layout>,
198 lossless: &lossless::Document,
199) -> SourceOrigin {
200 let Some(list) = layout.and_then(|layout| {
201 layout
202 .statements
203 .iter()
204 .find_map(|statement| match statement {
205 LayoutStatement::Order(list) => Some(list),
206 LayoutStatement::Direction(_) | LayoutStatement::RankSame(_) => None,
207 })
208 }) else {
209 return SourceOrigin::Omitted;
210 };
211
212 SourceOrigin::Authored(order_statement_span(list.span, lossless.tokens()))
213}
214
215fn order_statement_span(list_span: Span, tokens: &[lossless::Token]) -> Span {
216 let keyword = tokens
217 .iter()
218 .take_while(|token| token.span.end.byte_offset <= list_span.start.byte_offset)
219 .filter(|token| !matches!(token.kind, TokenKind::Whitespace | TokenKind::LineComment))
220 .last();
221
222 keyword.map_or(list_span, |keyword| Span::covering(keyword.span, list_span))
223}
224
225#[cfg(test)]
226mod tests {
227 use super::SourceOrigin;
228
229 #[test]
230 fn source_origin_returns_only_authored_spans() {
231 let span = crate::diagnostic::Span::point(crate::diagnostic::SourcePosition::start());
232 assert_eq!(SourceOrigin::Authored(span).span(), Some(span));
233 assert_eq!(SourceOrigin::Omitted.span(), None);
234 }
235}