diff --git a/content/about-treehouse.tree b/content/about-treehouse.tree index 6e7f4c7..2644613 100644 --- a/content/about-treehouse.tree +++ b/content/about-treehouse.tree @@ -224,18 +224,18 @@ % id = "01H8V55APD5686J8GTXP118V8E" - ``` - \% id = "root" - \- this is a branch + % id = "root" + - this is a branch - \% id = "child" - \- this is a child branch + % id = "child" + - this is a child branch - \+ this is a branch that is collapsed + + this is a branch that is collapsed - \- and this is a child of that branch + - and this is a child of that branch - \% content.link = "some-other-tree" - \- and this branch links to another tree + % content.link = "some-other-tree" + - and this branch links to another tree ``` % id = "01H8V55APDB49SPHPPMV2BCMW3" diff --git a/crates/treehouse-format/src/lib.rs b/crates/treehouse-format/src/lib.rs index 9cb4927..6afdf89 100644 --- a/crates/treehouse-format/src/lib.rs +++ b/crates/treehouse-format/src/lib.rs @@ -13,6 +13,9 @@ pub enum ParseErrorKind { #[error("at least {expected} spaces of indentation were expected, but got {got}")] InconsistentIndentation { got: usize, expected: usize }, + + #[error("unterminated code block")] + UnterminatedCodeBlock, } #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] diff --git a/crates/treehouse-format/src/pull.rs b/crates/treehouse-format/src/pull.rs index 66cacbe..0060b73 100644 --- a/crates/treehouse-format/src/pull.rs +++ b/crates/treehouse-format/src/pull.rs @@ -40,11 +40,21 @@ pub struct Parser<'a> { pub position: usize, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum AllowCodeBlocks { + No, + Yes, +} + impl<'a> Parser<'a> { fn current(&self) -> Option { self.input[self.position..].chars().next() } + fn current_starts_with(&self, s: &str) -> bool { + self.input[self.position..].starts_with(s) + } + fn advance(&mut self) { self.position += self.current().map(|c| c.len_utf8()).unwrap_or(0); } @@ -58,6 +68,12 @@ impl<'a> Parser<'a> { count } + fn eat_while(&mut self, cond: impl Fn(char) -> bool) { + while self.current().map(&cond).is_some_and(|x| x) { + self.advance(); + } + } + fn eat_until(&mut self, cond: impl Fn(char) -> bool) { while self.current().map(&cond).is_some_and(|x| !x) { self.advance(); @@ -76,21 +92,46 @@ impl<'a> Parser<'a> { &mut self, indent_level: usize, cond: impl Fn(char) -> bool, + allow_code_blocks: AllowCodeBlocks, ) -> Result<(), ParseError> { + let mut code_block: Option> = None; loop { - self.eat_until(|c| c == '\n'); - let before_indentation = self.position; - let line_indent_level = self.eat_as_long_as(' '); - let after_indentation = self.position; - if self.current().map(&cond).is_some_and(identity) || self.current().is_none() { - self.position = before_indentation; - break; - } else if !matches!(self.current(), Some('\n')) && line_indent_level < indent_level { - return Err(ParseErrorKind::InconsistentIndentation { - got: line_indent_level, - expected: indent_level, + if let Some(range) = &code_block { + self.eat_while(|c| c == ' '); + if self.current_starts_with("```") { + code_block = None; + self.position += 3; + self.eat_until(|c| c == '\n'); + continue; + } + self.eat_until(|c| c == '\n'); + + if self.current().is_none() { + return Err(ParseErrorKind::UnterminatedCodeBlock.at(range.clone())); + } + } else { + self.eat_while(|c| c == ' '); + if allow_code_blocks == AllowCodeBlocks::Yes && self.current_starts_with("```") { + code_block = Some(self.position..self.position + 3); + self.position += 3; + continue; + } + + self.eat_until(|c| c == '\n'); + let before_indentation = self.position; + let line_indent_level = self.eat_as_long_as(' '); + let after_indentation = self.position; + if self.current().map(&cond).is_some_and(identity) || self.current().is_none() { + self.position = before_indentation; + break; + } else if !matches!(self.current(), Some('\n')) && line_indent_level < indent_level + { + return Err(ParseErrorKind::InconsistentIndentation { + got: line_indent_level, + expected: indent_level, + } + .at(before_indentation..after_indentation)); } - .at(before_indentation..after_indentation)); } } Ok(()) @@ -107,7 +148,11 @@ impl<'a> Parser<'a> { let start = self.position; self.advance(); let after_percent = self.position; - self.eat_indented_lines_until(indent_level, |c| c == '-' || c == '+')?; + self.eat_indented_lines_until( + indent_level, + |c| c == '-' || c == '+', + AllowCodeBlocks::No, + )?; self.eat_as_long_as(' '); let end = self.position; Some(Attributes { @@ -128,7 +173,11 @@ impl<'a> Parser<'a> { let kind_end = self.position; let content_start = self.position; - self.eat_indented_lines_until(indent_level, |c| c == '-' || c == '+' || c == '%')?; + self.eat_indented_lines_until( + indent_level, + |c| c == '-' || c == '+' || c == '%', + AllowCodeBlocks::Yes, + )?; let content_end = self.position; Ok(Some(BranchEvent {