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}
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
62pub 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 depth: usize,
103}
104
105pub const MAX_DEPTH: usize = 256;
108
109pub 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 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 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 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 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 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
445pub 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
462pub 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
477fn 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 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 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 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}