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: Scalar,
92    pub span: Span,
93}
94
95/// Address a shape by the dotted path of its key, e.g. `a.b."c.d"`, after climbing one scope per leading `_`.
96#[derive(Debug, Clone, PartialEq, Default)]
97pub struct KeyPath {
98    pub parent_references: usize,
99    pub segments: Vec<KeySegment>,
100    pub span: Span,
101}
102
103/// Name one segment of a key path, unquoted and unescaped, so a quoted `"a.b"` stays one segment.
104#[derive(Debug, Clone, PartialEq)]
105pub struct KeySegment {
106    pub name: String,
107    pub span: Span,
108}
109
110/// Carry a scalar value's decoded text; the model types it against the field it lands in.
111#[derive(Debug, Clone, PartialEq)]
112pub struct Scalar {
113    pub text: String,
114    pub span: Span,
115}
116
117/// Locate a byte range in the d2 source, convertible to both a model span and a miette span.
118#[derive(Clone, Copy, PartialEq, Eq, Default, Hash)]
119pub struct Span {
120    pub start: usize,
121    pub end: usize,
122}
123
124/// Name a d2 reserved keyword outside `style` (d2ast/keywords.go at v0.9.0), recognised case-insensitively as d2 does.
125#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, IntoStaticStr)]
126#[strum(serialize_all = "kebab-case", ascii_case_insensitive)]
127pub(crate) enum Keyword {
128    Shape,
129    Label,
130    Direction,
131    Link,
132    Style,
133    Vars,
134    Class,
135    Classes,
136    Layers,
137    Scenarios,
138    Steps,
139    Icon,
140    Constraint,
141    Tooltip,
142    Near,
143    Width,
144    Height,
145    Top,
146    Left,
147    GridRows,
148    GridColumns,
149    GridGap,
150    VerticalGap,
151    HorizontalGap,
152    SourceArrowhead,
153    TargetArrowhead,
154}
155
156impl Field {
157    /// Name the keyword this field was written under.
158    pub(crate) fn keyword(&self) -> Keyword {
159        match self {
160            Self::Shape(_shape) => Keyword::Shape,
161            Self::Label(_label) => Keyword::Label,
162            Self::Direction(_direction) => Keyword::Direction,
163            Self::Link(_link) => Keyword::Link,
164            Self::Tooltip(_tooltip) => Keyword::Tooltip,
165            Self::Style(_properties) => Keyword::Style,
166            Self::Svugy(_properties) => Keyword::Vars,
167        }
168    }
169}
170
171impl SvugyProperty {
172    /// Name the d2 variable whose entries carry what d2 cannot say, e.g. `vars: { svugy: { caption: … } }`.
173    pub const VARIABLE: &str = "svugy";
174}
175
176impl EdgeOperator {
177    /// Read the operator an arrow spells from its arrowheads, whatever its dash count.
178    pub(crate) fn from_arrow(arrow: &str) -> Self {
179        let has_source_arrowhead = arrow.starts_with('<');
180        let has_target_arrowhead = arrow.ends_with('>');
181        match (has_source_arrowhead, has_target_arrowhead) {
182            (true, true) => Self::Bidirectional,
183            (true, false) => Self::Backward,
184            (false, true) => Self::Forward,
185            (false, false) => Self::Undirected,
186        }
187    }
188
189    /// Spell the operator's shortest d2 arrow.
190    pub fn arrow(self) -> &'static str {
191        match self {
192            Self::Forward => "->",
193            Self::Backward => "<-",
194            Self::Bidirectional => "<->",
195            Self::Undirected => "--",
196        }
197    }
198}
199
200impl Span {
201    /// Create a new span from byte `start` up to byte `end`.
202    pub fn new(start: usize, end: usize) -> Self {
203        Self { start, end }
204    }
205
206    /// Return the byte range the span covers.
207    pub fn range(self) -> Range<usize> {
208        self.start..self.end
209    }
210}
211
212impl fmt::Debug for Span {
213    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
214        write!(formatter, "{}..{}", self.start, self.end)
215    }
216}
217
218impl From<Range<usize>> for Span {
219    fn from(range: Range<usize>) -> Self {
220        Self::new(range.start, range.end)
221    }
222}
223
224impl From<Span> for Range<usize> {
225    fn from(span: Span) -> Self {
226        span.range()
227    }
228}
229
230impl From<Span> for ByteRange {
231    fn from(span: Span) -> Self {
232        ByteRange::new(span.start, span.end)
233    }
234}
235
236impl From<Span> for miette::SourceSpan {
237    fn from(span: Span) -> Self {
238        span.range().into()
239    }
240}
241
242impl chumsky::span::Span for Span {
243    type Context = ();
244    type Offset = usize;
245
246    fn new(_context: (), range: Range<usize>) -> Self {
247        range.into()
248    }
249
250    fn context(&self) {}
251
252    fn start(&self) -> usize {
253        self.start
254    }
255
256    fn end(&self) -> usize {
257        self.end
258    }
259}
260
261impl Statement {
262    /// Return the span of the whole statement.
263    pub fn span(&self) -> Span {
264        match self {
265            Self::Shape(shape) => shape.span,
266            Self::Edge(edge) => edge.span,
267            Self::Field(assignment) => assignment.span,
268        }
269    }
270}