Skip to main content

pln_parse/
parser.rs

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                // Groups may carry a size from inside (single-item) or outside (split)
27                let mut item = self.parse_group()?;
28                // A group can also have an outer size: ((a|b)=3fr / c)
29                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(); // consume '('
52
53        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        // Check for operator
69        match self.peek() {
70            Some(')') => {
71                // Single-item group — collapse to the inner item (preserving its size)
72                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(); // consume operator
122            children.push(self.parse_item()?);
123            self.skip_whitespace();
124        }
125
126        // Check for mixed operators
127        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(); // consume '='
251        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        // Integer part
264        while let Some(character) = self.peek() {
265            if character.is_ascii_digit() {
266                self.advance();
267            } else {
268                break;
269            }
270        }
271
272        // Decimal part
273        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        // Special case: %
297        if self.peek() == Some('%') {
298            self.advance();
299            return Ok(Unit::Percent);
300        }
301
302        // Read alphabetic unit name
303        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    // -- Helpers --
337
338    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    // -- Basic panels --
387
388    #[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    // -- Splits --
420
421    #[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    // -- Single-item groups --
457
458    #[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    // -- Sizes --
486
487    #[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    // -- Nesting --
565
566    #[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    // -- Whitespace --
613
614    #[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    // -- Spec examples --
627
628    #[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    // -- Error cases --
679
680    #[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}