Skip to main content

svugy_parse/d2/
ast.rs

1//! Represent the v1 subset of a d2 file as a tree of shapes, edges and fields, each carrying its source span.
2
3use std::fmt;
4use std::ops::Range;
5
6use strum::{EnumString, IntoStaticStr};
7use svugy_model::model::ByteRange;
8
9/// Hold the top-level statements of a d2 file that fall inside the v1 subset.
10#[derive(Debug, Clone, PartialEq, Default)]
11pub struct D2Document {
12    pub statements: Vec<Statement>,
13}
14
15/// Hold one statement: a shape, an edge chain, or a field on a shape.
16#[derive(Debug, Clone, PartialEq)]
17pub enum Statement {
18    Shape(ShapeDeclaration),
19    Edge(EdgeDeclaration),
20    Field(FieldAssignment),
21}
22
23/// Declare the shape at `path`, optionally labelled, with the statements of its nested map.
24#[derive(Debug, Clone, PartialEq)]
25pub struct ShapeDeclaration {
26    pub path: KeyPath,
27    pub label: Option<Scalar>,
28    pub body: Vec<Statement>,
29    pub span: Span,
30}
31
32/// Declare the edges of a chain `source -> target -> …`, sharing one optional label and the fields of its map.
33#[derive(Debug, Clone, PartialEq)]
34pub struct EdgeDeclaration {
35    pub source: KeyPath,
36    pub links: Vec<EdgeLink>,
37    pub label: Option<Scalar>,
38    pub fields: Vec<Field>,
39    pub span: Span,
40}
41
42/// Link the previous endpoint of an edge chain to `target` through `operator`.
43#[derive(Debug, Clone, PartialEq)]
44pub struct EdgeLink {
45    pub operator: EdgeOperator,
46    pub operator_span: Span,
47    pub target: KeyPath,
48}
49
50/// Name the direction of an edge operator: `->`, `<-`, `<->` or `--` (any number of dashes).
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum EdgeOperator {
53    Forward,
54    Backward,
55    Bidirectional,
56    Undirected,
57}
58
59/// Assign a field to the shape at `target`; an empty target means the enclosing shape (or the root).
60#[derive(Debug, Clone, PartialEq)]
61pub struct FieldAssignment {
62    pub target: KeyPath,
63    pub field: Field,
64    pub span: Span,
65}
66
67/// Hold the value of one reserved field of the v1 subset; `style.x: v` and `style: { x: v }` both yield `Style`, `vars: { svugy: { … } }` yields `Svugy`.
68#[derive(Debug, Clone, PartialEq)]
69pub enum Field {
70    Shape(Scalar),
71    Label(Scalar),
72    Direction(Scalar),
73    Link(Scalar),
74    Tooltip(Scalar),
75    Style(Vec<StyleProperty>),
76    Svugy(Vec<SvugyProperty>),
77}
78
79/// Set one `style` property, e.g. `fill: "#f4a261"`.
80#[derive(Debug, Clone, PartialEq)]
81pub struct StyleProperty {
82    pub name: KeySegment,
83    pub value: Scalar,
84    pub span: Span,
85}
86
87/// Carry one attribute d2 cannot say through the d2 variable `svugy`, addressed by the key segments below it, e.g. `["position", "x"]` for `svugy.position.x: 150`.
88#[derive(Debug, Clone, PartialEq)]
89pub struct SvugyProperty {
90    pub attribute_path: Vec<String>,
91    pub value: SvugyValue,
92    pub span: Span,
93}
94
95/// Carry the value of one `svugy` attribute: a plain value, or a `[ … ]` list of plain values.
96#[derive(Debug, Clone, PartialEq)]
97pub enum SvugyValue {
98    Scalar(Scalar),
99    List { items: Vec<Scalar>, span: Span },
100}
101
102/// Address a shape by the dotted path of its key, e.g. `a.b."c.d"`, after climbing one scope per leading `_`.
103#[derive(Debug, Clone, PartialEq, Default)]
104pub struct KeyPath {
105    pub parent_references: usize,
106    pub segments: Vec<KeySegment>,
107    pub span: Span,
108}
109
110/// Name one segment of a key path, unquoted and unescaped, so a quoted `"a.b"` stays one segment.
111#[derive(Debug, Clone, PartialEq)]
112pub struct KeySegment {
113    pub name: String,
114    pub span: Span,
115}
116
117/// Carry a scalar value's decoded text; the model types it against the field it lands in.
118#[derive(Debug, Clone, PartialEq)]
119pub struct Scalar {
120    pub text: String,
121    pub span: Span,
122}
123
124/// Locate a byte range in the d2 source, convertible to both a model span and a miette span.
125#[derive(Clone, Copy, PartialEq, Eq, Default, Hash)]
126pub struct Span {
127    pub start: usize,
128    pub end: usize,
129}
130
131/// Name a d2 reserved keyword outside `style` (d2ast/keywords.go at v0.9.0), recognised case-insensitively as d2 does.
132#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, IntoStaticStr)]
133#[strum(serialize_all = "kebab-case", ascii_case_insensitive)]
134pub(crate) enum Keyword {
135    Shape,
136    Label,
137    Direction,
138    Link,
139    Style,
140    Vars,
141    Class,
142    Classes,
143    Layers,
144    Scenarios,
145    Steps,
146    Icon,
147    Constraint,
148    Tooltip,
149    Near,
150    Width,
151    Height,
152    Top,
153    Left,
154    GridRows,
155    GridColumns,
156    GridGap,
157    VerticalGap,
158    HorizontalGap,
159    SourceArrowhead,
160    TargetArrowhead,
161}
162
163impl Field {
164    /// Name the keyword this field was written under.
165    pub(crate) fn keyword(&self) -> Keyword {
166        match self {
167            Self::Shape(_shape) => Keyword::Shape,
168            Self::Label(_label) => Keyword::Label,
169            Self::Direction(_direction) => Keyword::Direction,
170            Self::Link(_link) => Keyword::Link,
171            Self::Tooltip(_tooltip) => Keyword::Tooltip,
172            Self::Style(_properties) => Keyword::Style,
173            Self::Svugy(_properties) => Keyword::Vars,
174        }
175    }
176}
177
178impl SvugyProperty {
179    /// Name the d2 variable whose entries carry what d2 cannot say, e.g. `vars: { svugy: { caption: … } }`.
180    pub const VARIABLE: &str = "svugy";
181}
182
183impl EdgeOperator {
184    /// Read the operator an arrow spells from its arrowheads, whatever its dash count.
185    pub(crate) fn from_arrow(arrow: &str) -> Self {
186        let has_source_arrowhead = arrow.starts_with('<');
187        let has_target_arrowhead = arrow.ends_with('>');
188        match (has_source_arrowhead, has_target_arrowhead) {
189            (true, true) => Self::Bidirectional,
190            (true, false) => Self::Backward,
191            (false, true) => Self::Forward,
192            (false, false) => Self::Undirected,
193        }
194    }
195
196    /// Spell the operator's shortest d2 arrow.
197    pub fn arrow(self) -> &'static str {
198        match self {
199            Self::Forward => "->",
200            Self::Backward => "<-",
201            Self::Bidirectional => "<->",
202            Self::Undirected => "--",
203        }
204    }
205}
206
207impl Span {
208    /// Create a new span from byte `start` up to byte `end`.
209    pub fn new(start: usize, end: usize) -> Self {
210        Self { start, end }
211    }
212
213    /// Return the byte range the span covers.
214    pub fn range(self) -> Range<usize> {
215        self.start..self.end
216    }
217}
218
219impl fmt::Debug for Span {
220    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
221        write!(formatter, "{}..{}", self.start, self.end)
222    }
223}
224
225impl From<Range<usize>> for Span {
226    fn from(range: Range<usize>) -> Self {
227        Self::new(range.start, range.end)
228    }
229}
230
231impl From<Span> for Range<usize> {
232    fn from(span: Span) -> Self {
233        span.range()
234    }
235}
236
237impl From<Span> for ByteRange {
238    fn from(span: Span) -> Self {
239        ByteRange::new(span.start, span.end)
240    }
241}
242
243impl From<Span> for miette::SourceSpan {
244    fn from(span: Span) -> Self {
245        span.range().into()
246    }
247}
248
249impl chumsky::span::Span for Span {
250    type Context = ();
251    type Offset = usize;
252
253    fn new(_context: (), range: Range<usize>) -> Self {
254        range.into()
255    }
256
257    fn context(&self) {}
258
259    fn start(&self) -> usize {
260        self.start
261    }
262
263    fn end(&self) -> usize {
264        self.end
265    }
266}
267
268impl Statement {
269    /// Return the span of the whole statement.
270    pub fn span(&self) -> Span {
271        match self {
272            Self::Shape(shape) => shape.span,
273            Self::Edge(edge) => edge.span,
274            Self::Field(assignment) => assignment.span,
275        }
276    }
277}