Repository: kivikakk/comrak (Rust)

Issue:
The parsing of front matter may not be correct
It seems that markdown with only front matter without a newline at the end may not be parsed correctly.

## Incorrect case

source:

```rust
let text = "---
foo: bar
---";
let arena = Arena::new();
let mut options = Options::default();
options.extension.front_matter_delimiter = Some("---".to_owned());
let ast = parse_document(&arena, text, &options);
println!("{:?}", ast);
```

output:

```
Node {
  data: RefCell { value: Ast { value: Document, sourcepos: Sourcepos { start: LineColumn { line: 1, column: 1 }, end: LineColumn { line: 3, column: 3 } }, internal_offset: 0, content: "", open: false, last_line_blank: false, table_visited: false, line_offsets: [] } },
  children: [
    Node { data: RefCell { value: Ast { value: ThematicBreak, sourcepos: Sourcepos { start: LineColumn { line: 1, column: 1 }, end: LineColumn { line: 1, column: 3 } }, internal_offset: 0, content: "", open: false, last_line_blank: false, table_visited: false, line_offsets: [] } }, children: [] },
    Node { data: RefCell { value: Ast { value: Heading(NodeHeading { level: 2, setext: true }), sourcepos: Sourcepos { start: LineColumn { line: 2, column: 1 }, end: LineColumn { line: 3, column: 3 } }, internal_offset: 0, content: "foo: bar\n", open: false, last_line_blank: false, table_visited: false, line_offsets: [0] } }, children: [Node { data: RefCell { value: Ast { value: Text("foo: bar"), sourcepos: Sourcepos { start: LineColumn { line: 2, column: 1 }, end: LineColumn { line: 2, column: 8 } }, internal_offset: 0, content: "", open: false, last_line_blank: false, table_visited: false, line_offsets: [] } }, children: [] }] }
  ]
}
```

## Correct case

source:

```rust
let text = "---
foo: bar
---
";
let arena = Arena::new();
let mut options = Options::default();
options.extension.front_matter_delimiter = Some("---".to_owned());
let ast = parse_document(&arena, text, &options);
println!("{:?}", ast);
```

output:

```
Node {
  data: RefCell { value: Ast { value: Document, sourcepos: Sourcepos { start: LineColumn { line: 1, column: 1 }, end: LineColumn { line: 0, column: 0 } }, internal_offset: 0, content: "", open: false, last_line_blank: false, table_visited: false, line_offsets: [] } },
  children: [
    Node { data: RefCell { value: Ast { value: FrontMatter("---\nfoo: bar\n---\n"), sourcepos: Sourcepos { start: LineColumn { line: 0, column: 1 }, end: LineColumn { line: 0, column: 0 } }, internal_offset: 0, content: "", open: false, last_line_blank: false, table_visited: false, line_offsets: [] } }, children: [] }
  ]
}
```

## Environment

* rustc 1.83.0 (90b35a623 2024-11-26)
* comrak 0.31.0

Thanks for a great library ❤️ 



Discussion:
This is an interesting corner case — I don't see why we shouldn't handle this correctly. Thanks for the report!



Patch:
diff --git a/src/strings.rs b/src/strings.rs
index 42206bc3..0a13db95 100644
--- a/src/strings.rs
+++ b/src/strings.rs
@@ -299,11 +299,16 @@ pub fn split_off_front_matter<'s>(mut s: &'s str, delimiter: &str) -> Option<(&'
     start += match s[start..]
         .find(&("\n".to_string() + delimiter + "\r\n"))
         .or_else(|| s[start..].find(&("\n".to_string() + delimiter + "\n")))
+        .or_else(|| s[start..].find(&("\n".to_string() + delimiter))) // delimiter followed by EOF
     {
         Some(n) => n + 1 + delimiter.len(),
         None => return None,
     };
 
+    if start == s.len() {
+        return Some((s, ""));
+    }
+
     start += if s[start..].starts_with('\n') {
         1
     } else if s[start..].starts_with("\r\n") {


Test patch:
diff --git a/src/tests/front_matter.rs b/src/tests/front_matter.rs
index 0e6ba755..d3146693 100644
--- a/src/tests/front_matter.rs
+++ b/src/tests/front_matter.rs
@@ -179,3 +179,37 @@ fn second_line() {
 
     assert!(found.is_none(), "no FrontMatter expected");
 }
+
+#[test]
+fn fm_only_with_trailing_newline() {
+    let input = "---\nfoo: bar\n---\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_some(), "front matter expected");
+}
+
+#[test]
+fn fm_only_without_trailing_newline() {
+    let input = "---\nfoo: bar\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_some(), "front matter expected");
+}