Skip to main content

devpage_design/
parse.rs

1//! Text → syntax tree: the JSX subset a `.design` file is written in.
2
3use crate::Problem;
4
5/// A line and column, both from 1.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub struct Pos {
8    pub line: u32,
9    pub col: u32,
10}
11
12#[derive(Debug, Clone, PartialEq)]
13pub struct File {
14    pub imports: Vec<Import>,
15    pub root: Element,
16}
17
18#[derive(Debug, Clone, PartialEq)]
19pub struct Import {
20    pub name: String,
21    pub path: String,
22    pub pos: Pos,
23}
24
25#[derive(Debug, Clone, PartialEq)]
26pub struct Element {
27    pub name: String,
28    pub attrs: Vec<Attr>,
29    pub children: Vec<Child>,
30    pub pos: Pos,
31}
32
33#[derive(Debug, Clone, PartialEq)]
34pub enum Child {
35    Element(Element),
36    Text(Vec<TextPart>, Pos),
37}
38
39#[derive(Debug, Clone, PartialEq)]
40pub enum TextPart {
41    Literal(String),
42    Prop(String),
43}
44
45#[derive(Debug, Clone, PartialEq)]
46pub struct Attr {
47    pub name: String,
48    pub value: Value,
49    pub pos: Pos,
50}
51
52#[derive(Debug, Clone, PartialEq)]
53pub enum Value {
54    Str(String),
55    Num(f64),
56    Bool(bool),
57    Array(Vec<Value>),
58    Object(Vec<(String, Value)>),
59    Prop(String),
60}
61
62/// Parses a whole file, or gives the one problem where it first went wrong.
63pub fn parse(source: &str) -> Result<File, Problem> {
64    let mut p = Parser {
65        chars: source.chars().collect(),
66        at: 0,
67        line: 1,
68        col: 1,
69        depth: 0,
70    };
71    let mut imports = Vec::new();
72    p.skip_space();
73    while p.peek_word("import") {
74        let pos = p.pos();
75        p.expect_word("import")?;
76        p.skip_space();
77        let name = p.ident()?;
78        p.skip_space();
79        p.expect_word("from")?;
80        p.skip_space();
81        let path = p.string()?;
82        imports.push(Import { name, path, pos });
83        p.skip_space();
84    }
85    if p.peek() != Some('<') {
86        return Err(p.problem("expected an import or the root element"));
87    }
88    let root = p.element()?;
89    p.skip_space();
90    if p.peek().is_some() {
91        return Err(p.problem("expected nothing after the root element"));
92    }
93    Ok(File { imports, root })
94}
95
96struct Parser {
97    chars: Vec<char>,
98    at: usize,
99    line: u32,
100    col: u32,
101    /// How many elements, arrays and objects enclose the current place.
102    depth: usize,
103}
104
105/// How deep elements, arrays and objects may nest in one file: far beyond any
106/// real design, and well inside the stack the parser recurses on.
107pub const MAX_DEPTH: usize = 256;
108
109/// The largest number a design may write, in either direction: far beyond any
110/// real design, and small enough that layout and painting stay finite.
111pub const MAX_NUMBER: f64 = 1_000_000.0;
112
113impl Parser {
114    fn pos(&self) -> Pos {
115        Pos {
116            line: self.line,
117            col: self.col,
118        }
119    }
120
121    fn problem(&self, message: &str) -> Problem {
122        Problem {
123            file: String::new(),
124            pos: self.pos(),
125            message: message.to_string(),
126        }
127    }
128
129    fn peek(&self) -> Option<char> {
130        self.chars.get(self.at).copied()
131    }
132
133    fn peek_at(&self, n: usize) -> Option<char> {
134        self.chars.get(self.at + n).copied()
135    }
136
137    fn bump(&mut self) -> Option<char> {
138        let c = self.peek()?;
139        self.at += 1;
140        if c == '\n' {
141            self.line += 1;
142            self.col = 1;
143        } else {
144            self.col += 1;
145        }
146        Some(c)
147    }
148
149    /// Skips whitespace and `//` comments.
150    fn skip_space(&mut self) {
151        loop {
152            match self.peek() {
153                Some(c) if c.is_whitespace() => {
154                    self.bump();
155                }
156                Some('/') if self.peek_at(1) == Some('/') => {
157                    while !matches!(self.peek(), None | Some('\n')) {
158                        self.bump();
159                    }
160                }
161                _ => return,
162            }
163        }
164    }
165
166    fn peek_word(&self, word: &str) -> bool {
167        word.chars()
168            .enumerate()
169            .all(|(i, c)| self.peek_at(i) == Some(c))
170            && !self
171                .peek_at(word.len())
172                .is_some_and(|c| c.is_alphanumeric() || c == '_')
173    }
174
175    fn expect_word(&mut self, word: &str) -> Result<(), Problem> {
176        if !self.peek_word(word) {
177            return Err(self.problem(&format!("expected `{word}`")));
178        }
179        for _ in 0..word.len() {
180            self.bump();
181        }
182        Ok(())
183    }
184
185    fn expect(&mut self, c: char) -> Result<(), Problem> {
186        if self.peek() != Some(c) {
187            return Err(self.problem(&format!("expected `{c}`")));
188        }
189        self.bump();
190        Ok(())
191    }
192
193    fn ident(&mut self) -> Result<String, Problem> {
194        let mut s = String::new();
195        while let Some(c) = self.peek() {
196            if c.is_alphanumeric() || c == '_' || (c == '-' && !s.is_empty()) {
197                s.push(c);
198                self.bump();
199            } else {
200                break;
201            }
202        }
203        if s.is_empty() || s.starts_with(|c: char| c.is_ascii_digit()) {
204            return Err(self.problem("expected a name"));
205        }
206        Ok(s)
207    }
208
209    /// Enters one more level of nesting, or says the file nests too deep.
210    fn deeper(&mut self) -> Result<(), Problem> {
211        self.depth += 1;
212        if self.depth > MAX_DEPTH {
213            return Err(self.problem(&format!("nested more than {MAX_DEPTH} deep")));
214        }
215        Ok(())
216    }
217
218    /// A quoted string, JSX's way: double or single quotes, and no escapes —
219    /// a backslash is itself.
220    fn string(&mut self) -> Result<String, Problem> {
221        let quote = match self.peek() {
222            Some(q @ ('"' | '\'')) => q,
223            _ => return Err(self.problem("expected a string")),
224        };
225        // An unterminated string is reported where it opened.
226        let open = self.problem("unterminated string");
227        self.bump();
228        let mut s = String::new();
229        loop {
230            match self.bump() {
231                None | Some('\n') => return Err(open),
232                Some(c) if c == quote => return Ok(s),
233                Some(c) => s.push(c),
234            }
235        }
236    }
237
238    fn element(&mut self) -> Result<Element, Problem> {
239        let pos = self.pos();
240        self.deeper()?;
241        let element = self.element_inner(pos);
242        self.depth -= 1;
243        element
244    }
245
246    fn element_inner(&mut self, pos: Pos) -> Result<Element, Problem> {
247        self.expect('<')?;
248        let name = self.ident()?;
249        let mut attrs = Vec::new();
250        loop {
251            self.skip_space();
252            match self.peek() {
253                Some('/') => {
254                    self.bump();
255                    self.expect('>')?;
256                    return Ok(Element {
257                        name,
258                        attrs,
259                        children: Vec::new(),
260                        pos,
261                    });
262                }
263                Some('>') => {
264                    self.bump();
265                    break;
266                }
267                _ => {
268                    let pos = self.pos();
269                    let attr = self.ident()?;
270                    self.skip_space();
271                    let value = if self.peek() == Some('=') {
272                        self.bump();
273                        self.skip_space();
274                        self.attr_value()?
275                    } else {
276                        Value::Bool(true)
277                    };
278                    attrs.push(Attr {
279                        name: attr,
280                        value,
281                        pos,
282                    });
283                }
284            }
285        }
286        let children = self.children(&name)?;
287        Ok(Element {
288            name,
289            attrs,
290            children,
291            pos,
292        })
293    }
294
295    fn children(&mut self, parent: &str) -> Result<Vec<Child>, Problem> {
296        let mut children = Vec::new();
297        let mut text: Vec<TextPart> = Vec::new();
298        let mut text_pos = self.pos();
299        let mut literal = String::new();
300        loop {
301            match self.peek() {
302                None => return Err(self.problem(&format!("`<{parent}>` is never closed"))),
303                Some('<') => {
304                    flush_text(&mut literal, &mut text, &mut children, text_pos);
305                    if self.peek_at(1) == Some('/') {
306                        self.bump();
307                        self.bump();
308                        let name = self.ident()?;
309                        if name != parent {
310                            return Err(self.problem(&format!("`</{name}>` closes `<{parent}>`")));
311                        }
312                        self.skip_space();
313                        self.expect('>')?;
314                        return Ok(children);
315                    }
316                    children.push(Child::Element(self.element()?));
317                    text_pos = self.pos();
318                }
319                Some('{') => {
320                    if text.is_empty() && literal.trim().is_empty() {
321                        text_pos = self.pos();
322                    }
323                    if !literal.is_empty() {
324                        text.push(TextPart::Literal(std::mem::take(&mut literal)));
325                    }
326                    self.bump();
327                    self.skip_space();
328                    let name = self.ident()?;
329                    self.skip_space();
330                    self.expect('}')?;
331                    text.push(TextPart::Prop(name));
332                }
333                Some(c) => {
334                    if text.is_empty() && literal.trim().is_empty() && !c.is_whitespace() {
335                        text_pos = self.pos();
336                    }
337                    literal.push(c);
338                    self.bump();
339                }
340            }
341        }
342    }
343
344    fn attr_value(&mut self) -> Result<Value, Problem> {
345        match self.peek() {
346            Some('"' | '\'') => Ok(Value::Str(self.string()?)),
347            Some('{') => {
348                self.bump();
349                self.skip_space();
350                let value = self.value()?;
351                self.skip_space();
352                self.expect('}')?;
353                Ok(value)
354            }
355            _ => Err(self.problem("expected a string or a value in braces")),
356        }
357    }
358
359    fn value(&mut self) -> Result<Value, Problem> {
360        // Only arrays and objects nest, so only they count toward the limit.
361        let nests = matches!(self.peek(), Some('[' | '{'));
362        if nests {
363            self.deeper()?;
364        }
365        let value = self.value_inner();
366        if nests {
367            self.depth -= 1;
368        }
369        value
370    }
371
372    fn value_inner(&mut self) -> Result<Value, Problem> {
373        match self.peek() {
374            Some('"' | '\'') => Ok(Value::Str(self.string()?)),
375            Some('[') => {
376                self.bump();
377                let mut items = Vec::new();
378                loop {
379                    self.skip_space();
380                    if self.peek() == Some(']') {
381                        self.bump();
382                        return Ok(Value::Array(items));
383                    }
384                    items.push(self.value()?);
385                    self.skip_space();
386                    match self.peek() {
387                        Some(',') => {
388                            self.bump();
389                        }
390                        Some(']') => {}
391                        _ => return Err(self.problem("expected `,` or `]`")),
392                    }
393                }
394            }
395            Some('{') => {
396                self.bump();
397                let mut fields = Vec::new();
398                loop {
399                    self.skip_space();
400                    if self.peek() == Some('}') {
401                        self.bump();
402                        return Ok(Value::Object(fields));
403                    }
404                    let key = self.ident()?;
405                    self.skip_space();
406                    self.expect(':')?;
407                    self.skip_space();
408                    fields.push((key, self.value()?));
409                    self.skip_space();
410                    match self.peek() {
411                        Some(',') => {
412                            self.bump();
413                        }
414                        Some('}') => {}
415                        _ => return Err(self.problem("expected `,` or `}`")),
416                    }
417                }
418            }
419            Some(c) if c.is_ascii_digit() || c == '-' || c == '.' => {
420                let mut s = String::new();
421                while let Some(c) = self.peek() {
422                    if c.is_ascii_digit() || c == '-' || c == '.' {
423                        s.push(c);
424                        self.bump();
425                    } else {
426                        break;
427                    }
428                }
429                number(&s)
430                    .map(Value::Num)
431                    .map_err(|message| self.problem(&message))
432            }
433            _ => {
434                let name = self.ident()?;
435                Ok(match name.as_str() {
436                    "true" => Value::Bool(true),
437                    "false" => Value::Bool(false),
438                    _ => Value::Prop(name),
439                })
440            }
441        }
442    }
443}
444
445/// A number as a design writes one — digits, a `-` and a `.` — held to
446/// ±[`MAX_NUMBER`]: in braces, and inside a string that holds numbers, like a
447/// gradient's angle or a view box.
448pub fn number(s: &str) -> Result<f64, String> {
449    if !s
450        .chars()
451        .all(|c| c.is_ascii_digit() || c == '-' || c == '.')
452    {
453        return Err(format!("`{s}` is not a number"));
454    }
455    in_range(
456        s,
457        s.parse::<f64>()
458            .map_err(|_| format!("`{s}` is not a number"))?,
459    )
460}
461
462/// `n`, written `s`, if it lies within ±[`MAX_NUMBER`].
463pub fn in_range(s: &str, n: f64) -> Result<f64, String> {
464    if n.abs() <= MAX_NUMBER {
465        Ok(n)
466    } else if n.is_nan() {
467        Err(format!("`{s}` is not a number"))
468    } else if n < 0.0 {
469        Err(format!(
470            "`{s}` is too small: numbers go down to -{MAX_NUMBER}"
471        ))
472    } else {
473        Err(format!("`{s}` is too large: numbers go up to {MAX_NUMBER}"))
474    }
475}
476
477/// Ends a run of text. Text with a line break in it follows JSX: each line
478/// is trimmed, blank lines go, and the rest join with one space. Text on one
479/// line is kept as written, runs of spaces included, as JSX keeps it.
480fn flush_text(literal: &mut String, text: &mut Vec<TextPart>, children: &mut Vec<Child>, pos: Pos) {
481    if !literal.is_empty() {
482        text.push(TextPart::Literal(std::mem::take(literal)));
483    }
484    let mut parts = Vec::new();
485    for part in text.drain(..) {
486        match part {
487            TextPart::Literal(s) => {
488                let s = collapse(&s);
489                if !s.is_empty() {
490                    parts.push(TextPart::Literal(s));
491                }
492            }
493            prop => parts.push(prop),
494        }
495    }
496    if !parts.is_empty() {
497        children.push(Child::Text(parts, pos));
498    }
499}
500
501fn collapse(s: &str) -> String {
502    let lines: Vec<&str> = s.split('\n').collect();
503    if lines.len() == 1 {
504        return s.to_string();
505    }
506    lines
507        .iter()
508        .map(|l| l.trim())
509        .filter(|l| !l.is_empty())
510        .collect::<Vec<_>>()
511        .join(" ")
512}
513
514#[cfg(test)]
515mod tests {
516    use super::*;
517
518    #[test]
519    fn imports_attributes_and_text_with_props() {
520        let file = parse(
521            r##"// a comment
522import Button from "./button.design"
523
524<Frame width={390} padding={[12, 20]} shadow={{ y: 4, color: "#0002" }} clip>
525  <Text size={16}>Hi, {name}</Text>
526  <Button label="Pay" />
527</Frame>"##,
528        )
529        .unwrap();
530        assert_eq!(file.imports[0].name, "Button");
531        assert_eq!(file.imports[0].path, "./button.design");
532        let root = &file.root;
533        assert_eq!(root.attrs[0].value, Value::Num(390.0));
534        assert_eq!(
535            root.attrs[1].value,
536            Value::Array(vec![Value::Num(12.0), Value::Num(20.0)])
537        );
538        assert!(matches!(root.attrs[2].value, Value::Object(ref f) if f.len() == 2));
539        assert_eq!(root.attrs[3].value, Value::Bool(true));
540        let Child::Element(text) = &root.children[0] else {
541            panic!()
542        };
543        assert_eq!(
544            text.children,
545            vec![Child::Text(
546                vec![
547                    TextPart::Literal("Hi, ".into()),
548                    TextPart::Prop("name".into())
549                ],
550                Pos { line: 5, col: 19 }
551            )]
552        );
553        assert_eq!(root.children.len(), 2);
554    }
555
556    #[test]
557    fn a_problem_has_its_place() {
558        let problem = parse("<Frame>\n  <Text>hi</Frame>").unwrap_err();
559        assert_eq!(problem.pos, Pos { line: 2, col: 18 });
560        assert_eq!(problem.message, "`</Frame>` closes `<Text>`");
561    }
562
563    #[test]
564    fn every_syntax_error_has_its_place() {
565        let cases = [
566            ("", "1:1: expected an import or the root element"),
567            (
568                "<Frame />\n<Frame />",
569                "2:1: expected nothing after the root element",
570            ),
571            ("import \"./a.design\"", "1:8: expected a name"),
572            ("import A \"./a.design\"", "1:10: expected `from`"),
573            ("import A from ./a.design", "1:15: expected a string"),
574            ("<Frame name=\"x\n/>", "1:13: unterminated string"),
575            (
576                "<Frame>\n  <Text>hi</Text>",
577                "2:18: `<Frame>` is never closed",
578            ),
579            (
580                "<Frame gap=4 />",
581                "1:12: expected a string or a value in braces",
582            ),
583            ("<Frame padding={[1 2]} />", "1:20: expected `,` or `]`"),
584            ("<Frame shadow={{ y 4 }} />", "1:20: expected `:`"),
585            (
586                "<Frame shadow={{ y: 4 x: 1 }} />",
587                "1:23: expected `,` or `}`",
588            ),
589            ("<Frame gap={1.2.3} />", "1:18: `1.2.3` is not a number"),
590            ("<Frame gap={4 />", "1:15: expected `}`"),
591            ("<Frame><Text>{ }</Text></Frame>", "1:16: expected a name"),
592            (
593                "<Frame><Text>hi</Frame>",
594                "1:23: `</Frame>` closes `<Text>`",
595            ),
596            ("<Frame / >", "1:9: expected `>`"),
597        ];
598        for (source, expected) in cases {
599            let problem = parse(source).unwrap_err();
600            assert_eq!(
601                format!(
602                    "{}:{}: {}",
603                    problem.pos.line, problem.pos.col, problem.message
604                ),
605                expected,
606                "{source:?}"
607            );
608        }
609    }
610
611    #[test]
612    fn strings_bare_attributes_and_names_follow_jsx() {
613        let file = parse(r#"<Frame name='it\s "quoted"' aria-label="a\nb" clip data-x={false} />"#)
614            .unwrap();
615        let attrs = &file.root.attrs;
616        // No escapes: a backslash is itself, in either quote.
617        assert_eq!(attrs[0].value, Value::Str(r#"it\s "quoted""#.into()));
618        assert_eq!(attrs[1].name, "aria-label");
619        assert_eq!(attrs[1].value, Value::Str(r"a\nb".into()));
620        // A bare attribute is true.
621        assert_eq!(attrs[2].value, Value::Bool(true));
622        assert_eq!(attrs[3].name, "data-x");
623        assert_eq!(attrs[3].value, Value::Bool(false));
624    }
625
626    #[test]
627    fn nesting_and_numbers_have_limits() {
628        let deep = "<Frame>".repeat(MAX_DEPTH + 1) + &"</Frame>".repeat(MAX_DEPTH + 1);
629        let problem = parse(&deep).unwrap_err();
630        assert_eq!(
631            problem.message,
632            format!("nested more than {MAX_DEPTH} deep")
633        );
634        let deep = format!("<Frame gap={{{}}} />", "[".repeat(100_000));
635        let problem = parse(&deep).unwrap_err();
636        assert_eq!(
637            problem.message,
638            format!("nested more than {MAX_DEPTH} deep")
639        );
640        let problem = parse(&format!("<Frame width={{{}}} />", "9".repeat(400))).unwrap_err();
641        assert!(
642            problem
643                .message
644                .ends_with("is too large: numbers go up to 1000000"),
645            "{}",
646            problem.message
647        );
648        assert!(parse("<Frame width={1000000} />").is_ok());
649        let fine = "<Frame>".repeat(MAX_DEPTH) + &"</Frame>".repeat(MAX_DEPTH);
650        assert!(parse(&fine).is_ok());
651    }
652
653    #[test]
654    fn arrays_and_objects_count_toward_the_limit_as_elements_do() {
655        // One element and 255 arrays are 256 deep: fine. One more is not.
656        let at = |n: usize| format!("<Frame gap={{{}1{}}} />", "[".repeat(n), "]".repeat(n));
657        assert!(parse(&at(MAX_DEPTH - 1)).is_ok());
658        assert_eq!(
659            parse(&at(MAX_DEPTH)).unwrap_err().message,
660            format!("nested more than {MAX_DEPTH} deep")
661        );
662        let objects = |n: usize| {
663            format!(
664                "<Frame shadow={{{}1{}}} />",
665                "{ a: ".repeat(n),
666                "}".repeat(n)
667            )
668        };
669        assert!(parse(&objects(MAX_DEPTH - 1)).is_ok());
670        assert!(parse(&objects(MAX_DEPTH)).is_err());
671    }
672
673    #[test]
674    fn a_number_below_the_limit_is_too_small() {
675        assert_eq!(
676            parse("<Frame x={-1000001} />").unwrap_err().message,
677            "`-1000001` is too small: numbers go down to -1000000"
678        );
679        assert!(parse("<Frame x={-1000000} />").is_ok());
680    }
681
682    #[test]
683    fn text_follows_jsx_whitespace() {
684        let file = parse("<Text>\n    Free shipping\n    on orders\n  </Text>").unwrap();
685        assert_eq!(
686            file.root.children,
687            vec![Child::Text(
688                vec![TextPart::Literal("Free shipping on orders".into())],
689                Pos { line: 2, col: 5 }
690            )]
691        );
692    }
693}