1use crate::Problem;
4
5#[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 Theme(String, Pos),
46}
47
48#[derive(Debug, Clone, PartialEq)]
49pub struct Attr {
50 pub name: String,
51 pub value: Value,
52 pub pos: Pos,
53}
54
55#[derive(Debug, Clone, PartialEq)]
56pub enum Value {
57 Str(String, Pos),
60 Num(f64),
61 Bool(bool),
62 Array(Vec<Value>),
63 Object(Vec<(String, Value)>),
64 Prop(String),
65 Theme(String, Pos),
68 Themed {
74 light: String,
75 dark: String,
76 written: String,
77 },
78}
79
80pub fn parse(source: &str) -> Result<File, Problem> {
82 let mut p = Parser {
83 chars: source.chars().collect(),
84 at: 0,
85 line: 1,
86 col: 1,
87 depth: 0,
88 };
89 let mut imports = Vec::new();
90 p.skip_space();
91 while p.peek_word("import") {
92 let pos = p.pos();
93 p.expect_word("import")?;
94 p.skip_space();
95 let name = p.ident()?;
96 p.skip_space();
97 p.expect_word("from")?;
98 p.skip_space();
99 let path = p.string()?;
100 imports.push(Import { name, path, pos });
101 p.skip_space();
102 }
103 if p.peek() != Some('<') {
104 return Err(p.problem("expected an import or the root element"));
105 }
106 let root = p.element()?;
107 p.skip_space();
108 if p.peek().is_some() {
109 return Err(p.problem("expected nothing after the root element"));
110 }
111 Ok(File { imports, root })
112}
113
114struct Parser {
115 chars: Vec<char>,
116 at: usize,
117 line: u32,
118 col: u32,
119 depth: usize,
121}
122
123pub const MAX_DEPTH: usize = 256;
126
127pub const THEME: &str = "theme";
130
131pub const MAX_NUMBER: f64 = 1_000_000.0;
134
135impl Parser {
136 fn pos(&self) -> Pos {
137 Pos {
138 line: self.line,
139 col: self.col,
140 }
141 }
142
143 fn problem(&self, message: &str) -> Problem {
144 Problem {
145 file: String::new(),
146 pos: self.pos(),
147 message: message.to_string(),
148 }
149 }
150
151 fn peek(&self) -> Option<char> {
152 self.chars.get(self.at).copied()
153 }
154
155 fn peek_at(&self, n: usize) -> Option<char> {
156 self.chars.get(self.at + n).copied()
157 }
158
159 fn bump(&mut self) -> Option<char> {
160 let c = self.peek()?;
161 self.at += 1;
162 if c == '\n' {
163 self.line += 1;
164 self.col = 1;
165 } else {
166 self.col += 1;
167 }
168 Some(c)
169 }
170
171 fn skip_space(&mut self) {
173 loop {
174 match self.peek() {
175 Some(c) if c.is_whitespace() => {
176 self.bump();
177 }
178 Some('/') if self.peek_at(1) == Some('/') => {
179 while !matches!(self.peek(), None | Some('\n')) {
180 self.bump();
181 }
182 }
183 _ => return,
184 }
185 }
186 }
187
188 fn peek_word(&self, word: &str) -> bool {
189 word.chars()
190 .enumerate()
191 .all(|(i, c)| self.peek_at(i) == Some(c))
192 && !self
193 .peek_at(word.len())
194 .is_some_and(|c| c.is_alphanumeric() || c == '_')
195 }
196
197 fn expect_word(&mut self, word: &str) -> Result<(), Problem> {
198 if !self.peek_word(word) {
199 return Err(self.problem(&format!("expected `{word}`")));
200 }
201 for _ in 0..word.len() {
202 self.bump();
203 }
204 Ok(())
205 }
206
207 fn expect(&mut self, c: char) -> Result<(), Problem> {
208 if self.peek() != Some(c) {
209 return Err(self.problem(&format!("expected `{c}`")));
210 }
211 self.bump();
212 Ok(())
213 }
214
215 fn ident(&mut self) -> Result<String, Problem> {
216 let mut s = String::new();
217 while let Some(c) = self.peek() {
218 if c.is_alphanumeric() || c == '_' || (c == '-' && !s.is_empty()) {
219 s.push(c);
220 self.bump();
221 } else {
222 break;
223 }
224 }
225 if s.is_empty() || s.starts_with(|c: char| c.is_ascii_digit()) {
226 return Err(self.problem("expected a name"));
227 }
228 Ok(s)
229 }
230
231 fn deeper(&mut self) -> Result<(), Problem> {
233 self.depth += 1;
234 if self.depth > MAX_DEPTH {
235 return Err(self.problem(&format!("nested more than {MAX_DEPTH} deep")));
236 }
237 Ok(())
238 }
239
240 fn string(&mut self) -> Result<String, Problem> {
243 let quote = match self.peek() {
244 Some(q @ ('"' | '\'')) => q,
245 _ => return Err(self.problem("expected a string")),
246 };
247 let open = self.problem("unterminated string");
249 self.bump();
250 let mut s = String::new();
251 loop {
252 match self.bump() {
253 None | Some('\n') => return Err(open),
254 Some(c) if c == quote => return Ok(s),
255 Some(c) => s.push(c),
256 }
257 }
258 }
259
260 fn element(&mut self) -> Result<Element, Problem> {
261 let pos = self.pos();
262 self.deeper()?;
263 let element = self.element_inner(pos);
264 self.depth -= 1;
265 element
266 }
267
268 fn element_inner(&mut self, pos: Pos) -> Result<Element, Problem> {
269 self.expect('<')?;
270 let name = self.ident()?;
271 let mut attrs = Vec::new();
272 loop {
273 self.skip_space();
274 match self.peek() {
275 Some('/') => {
276 self.bump();
277 self.expect('>')?;
278 return Ok(Element {
279 name,
280 attrs,
281 children: Vec::new(),
282 pos,
283 });
284 }
285 Some('>') => {
286 self.bump();
287 break;
288 }
289 _ => {
290 let pos = self.pos();
291 let attr = self.ident()?;
292 self.skip_space();
293 let value = if self.peek() == Some('=') {
294 self.bump();
295 self.skip_space();
296 self.attr_value()?
297 } else {
298 Value::Bool(true)
299 };
300 attrs.push(Attr {
301 name: attr,
302 value,
303 pos,
304 });
305 }
306 }
307 }
308 let children = self.children(&name)?;
309 Ok(Element {
310 name,
311 attrs,
312 children,
313 pos,
314 })
315 }
316
317 fn children(&mut self, parent: &str) -> Result<Vec<Child>, Problem> {
318 let mut children = Vec::new();
319 let mut text: Vec<TextPart> = Vec::new();
320 let mut text_pos = self.pos();
321 let mut literal = String::new();
322 loop {
323 match self.peek() {
324 None => return Err(self.problem(&format!("`<{parent}>` is never closed"))),
325 Some('<') => {
326 flush_text(&mut literal, &mut text, &mut children, text_pos);
327 if self.peek_at(1) == Some('/') {
328 self.bump();
329 self.bump();
330 let name = self.ident()?;
331 if name != parent {
332 return Err(self.problem(&format!("`</{name}>` closes `<{parent}>`")));
333 }
334 self.skip_space();
335 self.expect('>')?;
336 return Ok(children);
337 }
338 children.push(Child::Element(self.element()?));
339 text_pos = self.pos();
340 }
341 Some('{') => {
342 if text.is_empty() && literal.trim().is_empty() {
343 text_pos = self.pos();
344 }
345 if !literal.is_empty() {
346 text.push(TextPart::Literal(std::mem::take(&mut literal)));
347 }
348 self.bump();
349 self.skip_space();
350 let pos = self.pos();
351 let name = self.ident()?;
352 let part = if name == THEME && self.peek() == Some('.') {
353 self.bump();
354 TextPart::Theme(self.ident()?, pos)
355 } else {
356 TextPart::Prop(name)
357 };
358 self.skip_space();
359 self.expect('}')?;
360 text.push(part);
361 }
362 Some(c) => {
363 if text.is_empty() && literal.trim().is_empty() && !c.is_whitespace() {
364 text_pos = self.pos();
365 }
366 literal.push(c);
367 self.bump();
368 }
369 }
370 }
371 }
372
373 fn attr_value(&mut self) -> Result<Value, Problem> {
374 match self.peek() {
375 Some('"' | '\'') => {
376 let pos = self.pos();
377 Ok(Value::Str(self.string()?, pos))
378 }
379 Some('{') => {
380 self.bump();
381 self.skip_space();
382 let value = self.value()?;
383 self.skip_space();
384 self.expect('}')?;
385 Ok(value)
386 }
387 _ => Err(self.problem("expected a string or a value in braces")),
388 }
389 }
390
391 fn value(&mut self) -> Result<Value, Problem> {
392 let nests = matches!(self.peek(), Some('[' | '{'));
394 if nests {
395 self.deeper()?;
396 }
397 let value = self.value_inner();
398 if nests {
399 self.depth -= 1;
400 }
401 value
402 }
403
404 fn value_inner(&mut self) -> Result<Value, Problem> {
405 match self.peek() {
406 Some('"' | '\'') => {
407 let pos = self.pos();
408 Ok(Value::Str(self.string()?, pos))
409 }
410 Some('[') => {
411 self.bump();
412 let mut items = Vec::new();
413 loop {
414 self.skip_space();
415 if self.peek() == Some(']') {
416 self.bump();
417 return Ok(Value::Array(items));
418 }
419 items.push(self.value()?);
420 self.skip_space();
421 match self.peek() {
422 Some(',') => {
423 self.bump();
424 }
425 Some(']') => {}
426 _ => return Err(self.problem("expected `,` or `]`")),
427 }
428 }
429 }
430 Some('{') => {
431 self.bump();
432 let mut fields = Vec::new();
433 loop {
434 self.skip_space();
435 if self.peek() == Some('}') {
436 self.bump();
437 return Ok(Value::Object(fields));
438 }
439 let key = self.ident()?;
440 self.skip_space();
441 self.expect(':')?;
442 self.skip_space();
443 fields.push((key, self.value()?));
444 self.skip_space();
445 match self.peek() {
446 Some(',') => {
447 self.bump();
448 }
449 Some('}') => {}
450 _ => return Err(self.problem("expected `,` or `}`")),
451 }
452 }
453 }
454 Some(c) if c.is_ascii_digit() || c == '-' || c == '.' => {
455 let mut s = String::new();
456 while let Some(c) = self.peek() {
457 if c.is_ascii_digit() || c == '-' || c == '.' {
458 s.push(c);
459 self.bump();
460 } else {
461 break;
462 }
463 }
464 number(&s)
465 .map(Value::Num)
466 .map_err(|message| self.problem(&message))
467 }
468 _ => {
469 let pos = self.pos();
470 let name = self.ident()?;
471 Ok(match name.as_str() {
472 "true" => Value::Bool(true),
473 "false" => Value::Bool(false),
474 THEME if self.peek() == Some('.') => {
475 self.bump();
476 Value::Theme(self.ident()?, pos)
477 }
478 _ => Value::Prop(name),
479 })
480 }
481 }
482 }
483}
484
485pub fn number(s: &str) -> Result<f64, String> {
489 if !s
490 .chars()
491 .all(|c| c.is_ascii_digit() || c == '-' || c == '.')
492 {
493 return Err(format!("`{s}` is not a number"));
494 }
495 in_range(
496 s,
497 s.parse::<f64>()
498 .map_err(|_| format!("`{s}` is not a number"))?,
499 )
500}
501
502pub fn in_range(s: &str, n: f64) -> Result<f64, String> {
504 if n.abs() <= MAX_NUMBER {
505 Ok(n)
506 } else if n.is_nan() {
507 Err(format!("`{s}` is not a number"))
508 } else if n < 0.0 {
509 Err(format!(
510 "`{s}` is too small: numbers go down to -{MAX_NUMBER}"
511 ))
512 } else {
513 Err(format!("`{s}` is too large: numbers go up to {MAX_NUMBER}"))
514 }
515}
516
517fn flush_text(literal: &mut String, text: &mut Vec<TextPart>, children: &mut Vec<Child>, pos: Pos) {
521 if !literal.is_empty() {
522 text.push(TextPart::Literal(std::mem::take(literal)));
523 }
524 let mut parts = Vec::new();
525 for part in text.drain(..) {
526 match part {
527 TextPart::Literal(s) => {
528 let s = collapse(&s);
529 if !s.is_empty() {
530 parts.push(TextPart::Literal(s));
531 }
532 }
533 prop => parts.push(prop),
534 }
535 }
536 if !parts.is_empty() {
537 children.push(Child::Text(parts, pos));
538 }
539}
540
541fn collapse(s: &str) -> String {
542 let lines: Vec<&str> = s.split('\n').collect();
543 if lines.len() == 1 {
544 return s.to_string();
545 }
546 lines
547 .iter()
548 .map(|l| l.trim())
549 .filter(|l| !l.is_empty())
550 .collect::<Vec<_>>()
551 .join(" ")
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557
558 #[test]
559 fn imports_attributes_and_text_with_props() {
560 let file = parse(
561 r##"// a comment
562import Button from "./button.design"
563
564<Frame width={390} padding={[12, 20]} shadow={{ y: 4, color: "#0002" }} clip>
565 <Text size={16}>Hi, {name}</Text>
566 <Button label="Pay" />
567</Frame>"##,
568 )
569 .unwrap();
570 assert_eq!(file.imports[0].name, "Button");
571 assert_eq!(file.imports[0].path, "./button.design");
572 let root = &file.root;
573 assert_eq!(root.attrs[0].value, Value::Num(390.0));
574 assert_eq!(
575 root.attrs[1].value,
576 Value::Array(vec![Value::Num(12.0), Value::Num(20.0)])
577 );
578 assert!(matches!(root.attrs[2].value, Value::Object(ref f) if f.len() == 2));
579 assert_eq!(root.attrs[3].value, Value::Bool(true));
580 let Child::Element(text) = &root.children[0] else {
581 panic!()
582 };
583 assert_eq!(
584 text.children,
585 vec![Child::Text(
586 vec![
587 TextPart::Literal("Hi, ".into()),
588 TextPart::Prop("name".into())
589 ],
590 Pos { line: 5, col: 19 }
591 )]
592 );
593 assert_eq!(root.children.len(), 2);
594 }
595
596 #[test]
597 fn a_problem_has_its_place() {
598 let problem = parse("<Frame>\n <Text>hi</Frame>").unwrap_err();
599 assert_eq!(problem.pos, Pos { line: 2, col: 18 });
600 assert_eq!(problem.message, "`</Frame>` closes `<Text>`");
601 }
602
603 #[test]
604 fn every_syntax_error_has_its_place() {
605 let cases = [
606 ("", "1:1: expected an import or the root element"),
607 (
608 "<Frame />\n<Frame />",
609 "2:1: expected nothing after the root element",
610 ),
611 ("import \"./a.design\"", "1:8: expected a name"),
612 ("import A \"./a.design\"", "1:10: expected `from`"),
613 ("import A from ./a.design", "1:15: expected a string"),
614 ("<Frame name=\"x\n/>", "1:13: unterminated string"),
615 (
616 "<Frame>\n <Text>hi</Text>",
617 "2:18: `<Frame>` is never closed",
618 ),
619 (
620 "<Frame gap=4 />",
621 "1:12: expected a string or a value in braces",
622 ),
623 ("<Frame padding={[1 2]} />", "1:20: expected `,` or `]`"),
624 ("<Frame shadow={{ y 4 }} />", "1:20: expected `:`"),
625 (
626 "<Frame shadow={{ y: 4 x: 1 }} />",
627 "1:23: expected `,` or `}`",
628 ),
629 ("<Frame gap={1.2.3} />", "1:18: `1.2.3` is not a number"),
630 ("<Frame gap={4 />", "1:15: expected `}`"),
631 ("<Frame><Text>{ }</Text></Frame>", "1:16: expected a name"),
632 (
633 "<Frame><Text>hi</Frame>",
634 "1:23: `</Frame>` closes `<Text>`",
635 ),
636 ("<Frame / >", "1:9: expected `>`"),
637 ];
638 for (source, expected) in cases {
639 let problem = parse(source).unwrap_err();
640 assert_eq!(
641 format!(
642 "{}:{}: {}",
643 problem.pos.line, problem.pos.col, problem.message
644 ),
645 expected,
646 "{source:?}"
647 );
648 }
649 }
650
651 #[test]
652 fn strings_bare_attributes_and_names_follow_jsx() {
653 let file = parse(r#"<Frame name='it\s "quoted"' aria-label="a\nb" clip data-x={false} />"#)
654 .unwrap();
655 let attrs = &file.root.attrs;
656 assert_eq!(
658 attrs[0].value,
659 Value::Str(r#"it\s "quoted""#.into(), Pos { line: 1, col: 13 })
660 );
661 assert_eq!(attrs[1].name, "aria-label");
662 assert_eq!(
663 attrs[1].value,
664 Value::Str(r"a\nb".into(), Pos { line: 1, col: 40 })
665 );
666 assert_eq!(attrs[2].value, Value::Bool(true));
668 assert_eq!(attrs[3].name, "data-x");
669 assert_eq!(attrs[3].value, Value::Bool(false));
670 }
671
672 #[test]
673 fn nesting_and_numbers_have_limits() {
674 let deep = "<Frame>".repeat(MAX_DEPTH + 1) + &"</Frame>".repeat(MAX_DEPTH + 1);
675 let problem = parse(&deep).unwrap_err();
676 assert_eq!(
677 problem.message,
678 format!("nested more than {MAX_DEPTH} deep")
679 );
680 let deep = format!("<Frame gap={{{}}} />", "[".repeat(100_000));
681 let problem = parse(&deep).unwrap_err();
682 assert_eq!(
683 problem.message,
684 format!("nested more than {MAX_DEPTH} deep")
685 );
686 let problem = parse(&format!("<Frame width={{{}}} />", "9".repeat(400))).unwrap_err();
687 assert!(
688 problem
689 .message
690 .ends_with("is too large: numbers go up to 1000000"),
691 "{}",
692 problem.message
693 );
694 assert!(parse("<Frame width={1000000} />").is_ok());
695 let fine = "<Frame>".repeat(MAX_DEPTH) + &"</Frame>".repeat(MAX_DEPTH);
696 assert!(parse(&fine).is_ok());
697 }
698
699 #[test]
700 fn arrays_and_objects_count_toward_the_limit_as_elements_do() {
701 let at = |n: usize| format!("<Frame gap={{{}1{}}} />", "[".repeat(n), "]".repeat(n));
703 assert!(parse(&at(MAX_DEPTH - 1)).is_ok());
704 assert_eq!(
705 parse(&at(MAX_DEPTH)).unwrap_err().message,
706 format!("nested more than {MAX_DEPTH} deep")
707 );
708 let objects = |n: usize| {
709 format!(
710 "<Frame shadow={{{}1{}}} />",
711 "{ a: ".repeat(n),
712 "}".repeat(n)
713 )
714 };
715 assert!(parse(&objects(MAX_DEPTH - 1)).is_ok());
716 assert!(parse(&objects(MAX_DEPTH)).is_err());
717 }
718
719 #[test]
720 fn a_number_below_the_limit_is_too_small() {
721 assert_eq!(
722 parse("<Frame x={-1000001} />").unwrap_err().message,
723 "`-1000001` is too small: numbers go down to -1000000"
724 );
725 assert!(parse("<Frame x={-1000000} />").is_ok());
726 }
727
728 #[test]
729 fn a_theme_color_is_a_value_with_its_place() {
730 let file = parse(
731 "<Frame fill={theme.surface} shadow={{ color: theme.shadow-dark }}>\n <Text>{theme.text}</Text>\n</Frame>",
732 )
733 .unwrap();
734 let root = &file.root;
735 assert_eq!(
736 root.attrs[0].value,
737 Value::Theme("surface".into(), Pos { line: 1, col: 14 })
738 );
739 assert_eq!(
740 root.attrs[1].value,
741 Value::Object(vec![(
742 "color".into(),
743 Value::Theme("shadow-dark".into(), Pos { line: 1, col: 46 })
744 )])
745 );
746 let Child::Element(text) = &root.children[0] else {
747 panic!()
748 };
749 assert_eq!(
750 text.children,
751 vec![Child::Text(
752 vec![TextPart::Theme("text".into(), Pos { line: 2, col: 10 })],
753 Pos { line: 2, col: 9 }
754 )]
755 );
756 assert_eq!(
758 parse("<Frame fill={theme} />").unwrap().root.attrs[0].value,
759 Value::Prop("theme".into())
760 );
761 assert_eq!(
762 parse("<Frame fill={theme.} />").unwrap_err().message,
763 "expected a name"
764 );
765 }
766
767 #[test]
768 fn text_follows_jsx_whitespace() {
769 let file = parse("<Text>\n Free shipping\n on orders\n </Text>").unwrap();
770 assert_eq!(
771 file.root.children,
772 vec![Child::Text(
773 vec![TextPart::Literal("Free shipping on orders".into())],
774 Pos { line: 2, col: 5 }
775 )]
776 );
777 }
778}