1use crate::ast::{Item, Node, Size, Span, Unit};
2use crate::error::{ErrorKind, ParseError};
3
4pub fn parse(input: &str) -> Result<Item, ParseError> {
5 let mut parser = Parser { input, pos: 0 };
6 let item = parser.parse_item()?;
7 parser.skip_whitespace();
8 if parser.pos < parser.input.len() {
9 return Err(parser.error(ErrorKind::TrailingInput, "unexpected input after layout"));
10 }
11 Ok(item)
12}
13
14struct Parser<'a> {
15 input: &'a str,
16 pos: usize,
17}
18
19impl<'a> Parser<'a> {
20 fn parse_item(&mut self) -> Result<Item, ParseError> {
21 self.skip_whitespace();
22 let start = self.pos;
23
24 match self.peek() {
25 Some('(') => {
26 let mut item = self.parse_group()?;
28 if let Some(outer_size) = self.try_parse_size()? {
30 item.size = Some(outer_size);
31 }
32 item.span.start = start;
33 item.span.end = self.pos;
34 Ok(item)
35 }
36 Some(_) => {
37 let node = self.parse_panel()?;
38 let size = self.try_parse_size()?;
39 let span = Span {
40 start,
41 end: self.pos,
42 };
43 Ok(Item { node, size, span })
44 }
45 None => Err(self.error(ErrorKind::UnexpectedEof, "expected a panel or group")),
46 }
47 }
48
49 fn parse_group(&mut self) -> Result<Item, ParseError> {
50 let group_start = self.pos;
51 self.advance(); self.skip_whitespace();
54 if self.peek() == Some(')') {
55 return Err(ParseError {
56 message: "empty group".into(),
57 span: Span {
58 start: group_start,
59 end: self.pos + 1,
60 },
61 kind: ErrorKind::EmptyGroup,
62 });
63 }
64
65 let first = self.parse_item()?;
66 self.skip_whitespace();
67
68 match self.peek() {
70 Some(')') => {
71 self.advance();
73 Ok(first)
74 }
75 Some('|') => {
76 let node = self.parse_split_tail(first, '|', group_start)?;
77 Ok(Item {
78 node,
79 size: None,
80 span: Span {
81 start: group_start,
82 end: self.pos,
83 },
84 })
85 }
86 Some('/') => {
87 let node = self.parse_split_tail(first, '/', group_start)?;
88 Ok(Item {
89 node,
90 size: None,
91 span: Span {
92 start: group_start,
93 end: self.pos,
94 },
95 })
96 }
97 Some(_) => Err(self.error(
98 ErrorKind::UnexpectedChar(self.input[self.pos..].chars().next().unwrap()),
99 "expected '|', '/', or ')'",
100 )),
101 None => Err(ParseError {
102 message: "unclosed group — expected ')'".into(),
103 span: Span {
104 start: group_start,
105 end: self.pos,
106 },
107 kind: ErrorKind::UnclosedGroup,
108 }),
109 }
110 }
111
112 fn parse_split_tail(
113 &mut self,
114 first: Item,
115 operator: char,
116 group_start: usize,
117 ) -> Result<Node, ParseError> {
118 let mut children = vec![first];
119
120 while self.peek() == Some(operator) {
121 self.advance(); children.push(self.parse_item()?);
123 self.skip_whitespace();
124 }
125
126 self.skip_whitespace();
128 let other_operator = if operator == '|' { '/' } else { '|' };
129 if self.peek() == Some(other_operator) {
130 return Err(self.error(
131 ErrorKind::MixedOperators,
132 &format!(
133 "mixed operators — group uses '{}' but found '{}'",
134 operator, other_operator
135 ),
136 ));
137 }
138
139 match self.peek() {
140 Some(')') => {
141 self.advance();
142 }
143 Some(_) => {
144 return Err(self.error(
145 ErrorKind::UnexpectedChar(self.input[self.pos..].chars().next().unwrap()),
146 "expected operator or ')'",
147 ));
148 }
149 None => {
150 return Err(ParseError {
151 message: "unclosed group — expected ')'".into(),
152 span: Span {
153 start: group_start,
154 end: self.pos,
155 },
156 kind: ErrorKind::UnclosedGroup,
157 });
158 }
159 }
160
161 match operator {
162 '|' => Ok(Node::HSplit { children }),
163 '/' => Ok(Node::VSplit { children }),
164 _ => unreachable!(),
165 }
166 }
167
168 fn parse_panel(&mut self) -> Result<Node, ParseError> {
169 self.skip_whitespace();
170 match self.peek() {
171 Some('"') | Some('\'') => self.parse_quoted(),
172 Some(_) => self.parse_word(),
173 None => Err(self.error(ErrorKind::UnexpectedEof, "expected a panel name")),
174 }
175 }
176
177 fn parse_word(&mut self) -> Result<Node, ParseError> {
178 let start = self.pos;
179 while let Some(character) = self.peek() {
180 if character.is_whitespace() || "|/()=\"'".contains(character) {
181 break;
182 }
183 self.advance();
184 }
185 if self.pos == start {
186 return Err(self.error(ErrorKind::UnexpectedEof, "expected a panel name"));
187 }
188 let name = self.input[start..self.pos].to_string();
189 Ok(Node::Panel { name })
190 }
191
192 fn parse_quoted(&mut self) -> Result<Node, ParseError> {
193 let start = self.pos;
194 let quote = self.advance();
195 let mut name = String::new();
196
197 loop {
198 match self.peek() {
199 None => {
200 return Err(ParseError {
201 message: format!("unclosed string — expected closing {}", quote),
202 span: Span {
203 start,
204 end: self.pos,
205 },
206 kind: ErrorKind::UnclosedString,
207 });
208 }
209 Some('\\') => {
210 self.advance();
211 match self.peek() {
212 Some(character) if character == quote => {
213 name.push(character);
214 self.advance();
215 }
216 Some(character) => {
217 name.push('\\');
218 name.push(character);
219 self.advance();
220 }
221 None => {
222 return Err(ParseError {
223 message: format!("unclosed string — expected closing {}", quote),
224 span: Span {
225 start,
226 end: self.pos,
227 },
228 kind: ErrorKind::UnclosedString,
229 });
230 }
231 }
232 }
233 Some(character) if character == quote => {
234 self.advance();
235 return Ok(Node::Panel { name });
236 }
237 Some(character) => {
238 name.push(character);
239 self.advance();
240 }
241 }
242 }
243 }
244
245 fn try_parse_size(&mut self) -> Result<Option<Size>, ParseError> {
246 self.skip_whitespace();
247 if self.peek() != Some('=') {
248 return Ok(None);
249 }
250 self.advance(); self.skip_whitespace();
252
253 let number_start = self.pos;
254 let value = self.parse_number()?;
255 let unit = self.parse_unit(number_start)?;
256
257 Ok(Some(Size { value, unit }))
258 }
259
260 fn parse_number(&mut self) -> Result<f64, ParseError> {
261 let start = self.pos;
262
263 while let Some(character) = self.peek() {
265 if character.is_ascii_digit() {
266 self.advance();
267 } else {
268 break;
269 }
270 }
271
272 if self.peek() == Some('.') {
274 self.advance();
275 while let Some(character) = self.peek() {
276 if character.is_ascii_digit() {
277 self.advance();
278 } else {
279 break;
280 }
281 }
282 }
283
284 if self.pos == start {
285 return Err(self.error(ErrorKind::InvalidNumber, "expected a number"));
286 }
287
288 self.input[start..self.pos]
289 .parse::<f64>()
290 .map_err(|_| self.error_at(start, ErrorKind::InvalidNumber, "invalid number"))
291 }
292
293 fn parse_unit(&mut self, value_start: usize) -> Result<Unit, ParseError> {
294 let unit_start = self.pos;
295
296 if self.peek() == Some('%') {
298 self.advance();
299 return Ok(Unit::Percent);
300 }
301
302 while let Some(character) = self.peek() {
304 if character.is_ascii_alphabetic() {
305 self.advance();
306 } else {
307 break;
308 }
309 }
310
311 let unit_str = &self.input[unit_start..self.pos];
312 match unit_str {
313 "fr" => Ok(Unit::Fr),
314 "col" => Ok(Unit::Col),
315 "row" => Ok(Unit::Row),
316 "px" => Ok(Unit::Px),
317 "" => Err(ParseError {
318 message: "size value requires a unit (fr, col, row, px, %)".into(),
319 span: Span {
320 start: value_start,
321 end: self.pos,
322 },
323 kind: ErrorKind::InvalidUnit(String::new()),
324 }),
325 other => Err(ParseError {
326 message: format!("unknown unit '{}' — expected fr, col, row, px, or %", other),
327 span: Span {
328 start: unit_start,
329 end: self.pos,
330 },
331 kind: ErrorKind::InvalidUnit(other.to_string()),
332 }),
333 }
334 }
335
336 fn peek(&self) -> Option<char> {
339 self.input[self.pos..].chars().next()
340 }
341
342 fn advance(&mut self) -> char {
343 let character = self.input[self.pos..].chars().next().unwrap();
344 self.pos += character.len_utf8();
345 character
346 }
347
348 fn skip_whitespace(&mut self) {
349 while let Some(character) = self.peek() {
350 if character.is_whitespace() {
351 self.advance();
352 } else {
353 break;
354 }
355 }
356 }
357
358 fn error(&self, kind: ErrorKind, message: &str) -> ParseError {
359 self.error_at(self.pos, kind, message)
360 }
361
362 fn error_at(&self, position: usize, kind: ErrorKind, message: &str) -> ParseError {
363 ParseError {
364 message: message.to_string(),
365 span: Span {
366 start: position,
367 end: (position + 1).min(self.input.len()),
368 },
369 kind,
370 }
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 fn parse_ok(input: &str) -> Item {
379 parse(input).unwrap_or_else(|error| panic!("parse failed for '{}': {}", input, error))
380 }
381
382 fn parse_err(input: &str) -> ParseError {
383 parse(input).unwrap_err()
384 }
385
386 #[test]
389 fn bare_panel() {
390 let item = parse_ok("editor");
391 assert!(matches!(item.node, Node::Panel { ref name } if name == "editor"));
392 assert!(item.size.is_none());
393 }
394
395 #[test]
396 fn double_quoted_panel() {
397 let item = parse_ok("\"Left Panel\"");
398 assert!(matches!(item.node, Node::Panel { ref name } if name == "Left Panel"));
399 }
400
401 #[test]
402 fn single_quoted_panel() {
403 let item = parse_ok("'Right Panel'");
404 assert!(matches!(item.node, Node::Panel { ref name } if name == "Right Panel"));
405 }
406
407 #[test]
408 fn quoted_with_escape() {
409 let item = parse_ok(r#""it\"s a panel""#);
410 assert!(matches!(item.node, Node::Panel { ref name } if name == "it\"s a panel"));
411 }
412
413 #[test]
414 fn single_quoted_with_escape() {
415 let item = parse_ok(r"'it\'s a panel'");
416 assert!(matches!(item.node, Node::Panel { ref name } if name == "it's a panel"));
417 }
418
419 #[test]
422 fn horizontal_split() {
423 let item = parse_ok("(left|right)");
424 match &item.node {
425 Node::HSplit { children } => {
426 assert_eq!(children.len(), 2);
427 assert!(matches!(&children[0].node, Node::Panel { name } if name == "left"));
428 assert!(matches!(&children[1].node, Node::Panel { name } if name == "right"));
429 }
430 other => panic!("expected HSplit, got {:?}", other),
431 }
432 }
433
434 #[test]
435 fn vertical_split() {
436 let item = parse_ok("(top/bottom)");
437 match &item.node {
438 Node::VSplit { children } => {
439 assert_eq!(children.len(), 2);
440 assert!(matches!(&children[0].node, Node::Panel { name } if name == "top"));
441 assert!(matches!(&children[1].node, Node::Panel { name } if name == "bottom"));
442 }
443 other => panic!("expected VSplit, got {:?}", other),
444 }
445 }
446
447 #[test]
448 fn three_way_split() {
449 let item = parse_ok("(a|b|c)");
450 match &item.node {
451 Node::HSplit { children } => assert_eq!(children.len(), 3),
452 other => panic!("expected HSplit, got {:?}", other),
453 }
454 }
455
456 #[test]
459 fn single_item_group() {
460 let item = parse_ok("(panel)");
461 assert!(matches!(item.node, Node::Panel { ref name } if name == "panel"));
462 }
463
464 #[test]
465 fn sized_single_item_group() {
466 let item = parse_ok("(panel=2fr)");
467 assert!(matches!(item.node, Node::Panel { ref name } if name == "panel"));
468 let size = item.size.unwrap();
469 assert_eq!(size.value, 2.0);
470 assert_eq!(size.unit, Unit::Fr);
471 }
472
473 #[test]
474 fn redundant_parens() {
475 let item = parse_ok("((left)|right)");
476 match &item.node {
477 Node::HSplit { children } => {
478 assert_eq!(children.len(), 2);
479 assert!(matches!(&children[0].node, Node::Panel { name } if name == "left"));
480 }
481 other => panic!("expected HSplit, got {:?}", other),
482 }
483 }
484
485 #[test]
488 fn size_fr() {
489 let item = parse_ok("(left=2fr|right)");
490 match &item.node {
491 Node::HSplit { children } => {
492 let size = children[0].size.as_ref().unwrap();
493 assert_eq!(size.value, 2.0);
494 assert_eq!(size.unit, Unit::Fr);
495 }
496 other => panic!("expected HSplit, got {:?}", other),
497 }
498 }
499
500 #[test]
501 fn size_col() {
502 let item = parse_ok("(sidebar=80col|main)");
503 match &item.node {
504 Node::HSplit { children } => {
505 let size = children[0].size.as_ref().unwrap();
506 assert_eq!(size.value, 80.0);
507 assert_eq!(size.unit, Unit::Col);
508 }
509 other => panic!("expected HSplit, got {:?}", other),
510 }
511 }
512
513 #[test]
514 fn size_row() {
515 let item = parse_ok("(header=5row/body)");
516 match &item.node {
517 Node::VSplit { children } => {
518 let size = children[0].size.as_ref().unwrap();
519 assert_eq!(size.value, 5.0);
520 assert_eq!(size.unit, Unit::Row);
521 }
522 other => panic!("expected VSplit, got {:?}", other),
523 }
524 }
525
526 #[test]
527 fn size_px() {
528 let item = parse_ok("(sidebar=200px|main)");
529 match &item.node {
530 Node::HSplit { children } => {
531 let size = children[0].size.as_ref().unwrap();
532 assert_eq!(size.value, 200.0);
533 assert_eq!(size.unit, Unit::Px);
534 }
535 other => panic!("expected HSplit, got {:?}", other),
536 }
537 }
538
539 #[test]
540 fn size_percent() {
541 let item = parse_ok("(left=30%|right)");
542 match &item.node {
543 Node::HSplit { children } => {
544 let size = children[0].size.as_ref().unwrap();
545 assert_eq!(size.value, 30.0);
546 assert_eq!(size.unit, Unit::Percent);
547 }
548 other => panic!("expected HSplit, got {:?}", other),
549 }
550 }
551
552 #[test]
553 fn size_decimal() {
554 let item = parse_ok("(left=1.5fr|right)");
555 match &item.node {
556 Node::HSplit { children } => {
557 let size = children[0].size.as_ref().unwrap();
558 assert_eq!(size.value, 1.5);
559 }
560 other => panic!("expected HSplit, got {:?}", other),
561 }
562 }
563
564 #[test]
567 fn nested_split() {
568 let item = parse_ok("(editor|(terminal/files))");
569 match &item.node {
570 Node::HSplit { children } => {
571 assert_eq!(children.len(), 2);
572 assert!(
573 matches!(&children[1].node, Node::VSplit { children } if children.len() == 2)
574 );
575 }
576 other => panic!("expected HSplit, got {:?}", other),
577 }
578 }
579
580 #[test]
581 fn nested_with_sizes() {
582 let item = parse_ok("(editor=3fr|(terminal=2fr/files))");
583 match &item.node {
584 Node::HSplit { children } => {
585 assert_eq!(children[0].size.as_ref().unwrap().value, 3.0);
586 match &children[1].node {
587 Node::VSplit { children } => {
588 assert_eq!(children[0].size.as_ref().unwrap().value, 2.0);
589 assert!(children[1].size.is_none());
590 }
591 other => panic!("expected VSplit, got {:?}", other),
592 }
593 }
594 other => panic!("expected HSplit, got {:?}", other),
595 }
596 }
597
598 #[test]
599 fn sized_group() {
600 let item = parse_ok("((files=20%|editor1|editor2)=3fr/terminal)");
601 match &item.node {
602 Node::VSplit { children } => {
603 assert_eq!(children[0].size.as_ref().unwrap().value, 3.0);
604 assert!(
605 matches!(&children[0].node, Node::HSplit { children } if children.len() == 3)
606 );
607 }
608 other => panic!("expected VSplit, got {:?}", other),
609 }
610 }
611
612 #[test]
615 fn whitespace_ignored() {
616 let item = parse_ok("( left | right )");
617 assert!(matches!(&item.node, Node::HSplit { children } if children.len() == 2));
618 }
619
620 #[test]
621 fn leading_trailing_whitespace() {
622 let item = parse_ok(" (left|right) ");
623 assert!(matches!(&item.node, Node::HSplit { children } if children.len() == 2));
624 }
625
626 #[test]
629 fn spec_equal_horizontal() {
630 parse_ok("(left|right)");
631 }
632
633 #[test]
634 fn spec_equal_vertical() {
635 parse_ok("(top/bottom)");
636 }
637
638 #[test]
639 fn spec_nested() {
640 parse_ok("(editor|(terminal/files))");
641 }
642
643 #[test]
644 fn spec_unequal() {
645 parse_ok("(left=2fr|right)");
646 }
647
648 #[test]
649 fn spec_fixed_sidebar() {
650 parse_ok("(sidebar=80col|main)");
651 }
652
653 #[test]
654 fn spec_three_way_fixed() {
655 parse_ok("(nav=60col|content|panel=40col)");
656 }
657
658 #[test]
659 fn spec_percentage() {
660 parse_ok("(left=30%|right)");
661 }
662
663 #[test]
664 fn spec_nested_with_sizes() {
665 parse_ok("(editor=3fr|(terminal=2fr/files))");
666 }
667
668 #[test]
669 fn spec_ide_layout() {
670 parse_ok("((files=20% | editor1 | editor2)=3fr / terminal)");
671 }
672
673 #[test]
674 fn spec_quoted_names() {
675 parse_ok("(\"Left Panel\"=2fr | \"Right Panel\")");
676 }
677
678 #[test]
681 fn error_mixed_operators() {
682 let error = parse_err("(a|b/c)");
683 assert_eq!(error.kind, ErrorKind::MixedOperators);
684 }
685
686 #[test]
687 fn error_unclosed_group() {
688 let error = parse_err("(a|b");
689 assert_eq!(error.kind, ErrorKind::UnclosedGroup);
690 }
691
692 #[test]
693 fn error_unclosed_string() {
694 let error = parse_err("\"hello");
695 assert_eq!(error.kind, ErrorKind::UnclosedString);
696 }
697
698 #[test]
699 fn error_invalid_unit() {
700 let error = parse_err("(a=2xx|b)");
701 assert!(matches!(error.kind, ErrorKind::InvalidUnit(_)));
702 }
703
704 #[test]
705 fn error_missing_unit() {
706 let error = parse_err("(a=2|b)");
707 assert!(matches!(error.kind, ErrorKind::InvalidUnit(_)));
708 }
709
710 #[test]
711 fn error_empty_input() {
712 let error = parse_err("");
713 assert_eq!(error.kind, ErrorKind::UnexpectedEof);
714 }
715
716 #[test]
717 fn error_trailing_input() {
718 let error = parse_err("(a|b) extra");
719 assert_eq!(error.kind, ErrorKind::TrailingInput);
720 }
721
722 #[test]
723 fn error_empty_group() {
724 let error = parse_err("()");
725 assert_eq!(error.kind, ErrorKind::EmptyGroup);
726 }
727}