diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2c233981..56d597a4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1141,6 +1141,20 @@ impl<'a, 'o> Parser<'a, 'o> { &self.options.extension.front_matter_delimiter, ) { if let Some((front_matter, rest)) = split_off_front_matter(s, delimiter) { + let lines = front_matter + .as_bytes() + .iter() + .filter(|b| **b == b'\n') + .count(); + + let mut stripped_front_matter = front_matter.to_string(); + strings::remove_trailing_blank_lines(&mut stripped_front_matter); + let stripped_lines = stripped_front_matter + .as_bytes() + .iter() + .filter(|b| **b == b'\n') + .count(); + let node = self.add_child( self.root, NodeValue::FrontMatter(front_matter.to_string()), @@ -1148,6 +1162,15 @@ impl<'a, 'o> Parser<'a, 'o> { ); s = rest; self.finalize(node).unwrap(); + + node.data.borrow_mut().sourcepos = Sourcepos { + start: nodes::LineColumn { line: 1, column: 1 }, + end: nodes::LineColumn { + line: 1 + stripped_lines, + column: delimiter.len(), + }, + }; + self.line_number += lines; } } diff --git a/src/tests.rs b/src/tests.rs index 575310be..e143709f 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -12,6 +12,7 @@ mod description_lists; mod empty; mod escaped_char_spans; mod footnotes; +mod front_matter; mod fuzz; mod greentext; mod header_ids; @@ -237,10 +238,19 @@ fn asssert_node_eq<'a>(node: &'a AstNode<'a>, location: &[usize], expected: &Nod macro_rules! sourcepos { (($spsl:literal:$spsc:literal-$spel:literal:$spec:literal)) => { - ($spsl, $spsc, $spel, $spec).into() + $crate::nodes::Sourcepos { + start: $crate::nodes::LineColumn { + line: $spsl, + column: $spsc, + }, + end: $crate::nodes::LineColumn { + line: $spel, + column: $spec, + }, + } }; ((XXX)) => { - (0, 1, 0, 1).into() + $crate::tests::sourcepos!((0:1-0:1)) }; } diff --git a/src/tests/front_matter.rs b/src/tests/front_matter.rs new file mode 100644 index 00000000..0e6ba755 --- /dev/null +++ b/src/tests/front_matter.rs @@ -0,0 +1,181 @@ +use crate::{format_commonmark, parse_document, Arena, Options}; + +use super::*; + +#[test] +fn round_trip_one_field() { + let mut options = Options::default(); + options.extension.front_matter_delimiter = Some("---".to_owned()); + let arena = Arena::new(); + let input = "---\nlayout: post\n---\nText\n"; + let root = parse_document(&arena, input, &options); + let mut buf = Vec::new(); + format_commonmark(&root, &options, &mut buf).unwrap(); + assert_eq!(&String::from_utf8(buf).unwrap(), input); +} + +#[test] +fn round_trip_wide_delimiter() { + let mut options = Options::default(); + options.extension.front_matter_delimiter = Some("\u{04fc}".to_owned()); + let arena = Arena::new(); + let input = "\u{04fc}\nlayout: post\n\u{04fc}\nText\n"; + let root = parse_document(&arena, input, &options); + let mut buf = Vec::new(); + format_commonmark(&root, &options, &mut buf).unwrap(); + assert_eq!(&String::from_utf8(buf).unwrap(), input); +} + +#[test] +fn ast_wide_delimiter() { + let input = "\u{04fc}\nlayout: post\n\u{04fc}\nText\n"; + + assert_ast_match_i( + input, + ast!((document (1:1-4:4) [ + (frontmatter (1:1-3:2) []) + (paragraph (4:1-4:4) [ + (text (4:1-4:4) []) + ]) + ])), + |opts| opts.extension.front_matter_delimiter = Some("\u{04fc}".to_owned()), + ); +} + +#[test] +fn ast() { + let input = "q\nlayout: post\nq\nText\n"; + + assert_ast_match_i( + input, + ast!((document (1:1-4:4) [ + (frontmatter (1:1-3:1) []) + (paragraph (4:1-4:4) [ + (text (4:1-4:4) []) + ]) + ])), + |opts| opts.extension.front_matter_delimiter = Some("q".to_owned()), + ); +} + +#[test] +fn ast_blank_line() { + let input = r#"--- +a: b +--- + +hello world +"#; + + assert_ast_match_i( + input, + ast!((document (1:1-5:11) [ + (frontmatter (1:1-3:3) []) + (paragraph (5:1-5:11) [ + (text (5:1-5:11) []) + ]) + ])), + |opts| opts.extension.front_matter_delimiter = Some("---".to_owned()), + ); +} + +#[test] +fn ast_carriage_return() { + let input = "q\r\nlayout: post\r\nq\r\nText\r\n"; + + assert_ast_match_i( + input, + ast!((document (1:1-4:4) [ + (frontmatter (1:1-3:1) []) + (paragraph (4:1-4:4) [ + (text (4:1-4:4) []) + ]) + ])), + |opts| opts.extension.front_matter_delimiter = Some("q".to_owned()), + ); +} + +#[test] +fn trailing_space_open() { + let input = "--- \nlayout: post\n---\nText\n"; + + let mut options = Options::default(); + options.extension.front_matter_delimiter = Some("---".to_owned()); + let arena = Arena::new(); + let root = parse_document(&arena, input, &options); + + let found = root + .descendants() + .filter(|n| matches!(n.data.borrow().value, NodeValue::FrontMatter(..))) + .next(); + + assert!(found.is_none(), "no FrontMatter expected"); +} + +#[test] +fn leading_space_open() { + let input = " ---\nlayout: post\n---\nText\n"; + + let mut options = Options::default(); + options.extension.front_matter_delimiter = Some("---".to_owned()); + let arena = Arena::new(); + let root = parse_document(&arena, input, &options); + + let found = root + .descendants() + .filter(|n| matches!(n.data.borrow().value, NodeValue::FrontMatter(..))) + .next(); + + assert!(found.is_none(), "no FrontMatter expected"); +} + +#[test] +fn leading_space_close() { + let input = "---\nlayout: post\n ---\nText\n"; + + let mut options = Options::default(); + options.extension.front_matter_delimiter = Some("---".to_owned()); + let arena = Arena::new(); + let root = parse_document(&arena, input, &options); + + let found = root + .descendants() + .filter(|n| matches!(n.data.borrow().value, NodeValue::FrontMatter(..))) + .next(); + + assert!(found.is_none(), "no FrontMatter expected"); +} + +#[test] +fn trailing_space_close() { + let input = "---\nlayout: post\n--- \nText\n"; + + let mut options = Options::default(); + options.extension.front_matter_delimiter = Some("---".to_owned()); + let arena = Arena::new(); + let root = parse_document(&arena, input, &options); + + let found = root + .descendants() + .filter(|n| matches!(n.data.borrow().value, NodeValue::FrontMatter(..))) + .next(); + + assert!(found.is_none(), "no FrontMatter expected"); +} + +#[test] +fn second_line() { + let input = "\n---\nlayout: post\n ---\nText\n"; + + let mut options = Options::default(); + options.extension.front_matter_delimiter = Some("---".to_owned()); + let arena = Arena::new(); + let root = parse_document(&arena, input, &options); + + let found = root + .descendants() + .filter(|n| matches!(n.data.borrow().value, NodeValue::FrontMatter(..))) + .next(); + + assert!(found.is_none(), "no FrontMatter expected"); +}