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