Skip to main content

use_markdown/
frontmatter.rs

1/// Extracts top-of-document frontmatter contents without the boundary lines.
2pub fn extract_frontmatter(markdown: &str) -> Option<&str> {
3    let bounds = detect_frontmatter_bounds(markdown)?;
4    Some(&markdown[bounds.content_start..bounds.content_end])
5}
6
7/// Returns `true` when the document starts with YAML-like or TOML-like frontmatter.
8pub fn has_frontmatter(markdown: &str) -> bool {
9    detect_frontmatter_bounds(markdown).is_some()
10}
11
12/// Returns the document without a leading frontmatter block.
13pub fn strip_frontmatter(markdown: &str) -> &str {
14    detect_frontmatter_bounds(markdown)
15        .map(|bounds| &markdown[bounds.full_end..])
16        .unwrap_or(markdown)
17}
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub(crate) struct FrontmatterBounds {
21    pub content_start: usize,
22    pub content_end: usize,
23    pub full_end: usize,
24    pub line_count: usize,
25}
26
27pub(crate) fn frontmatter_line_count(markdown: &str) -> usize {
28    detect_frontmatter_bounds(markdown)
29        .map(|bounds| bounds.line_count)
30        .unwrap_or(0)
31}
32
33pub(crate) fn detect_frontmatter_bounds(markdown: &str) -> Option<FrontmatterBounds> {
34    let bom_length = if markdown.starts_with('\u{feff}') {
35        '\u{feff}'.len_utf8()
36    } else {
37        0
38    };
39
40    let (_, first_line_end, first_line) = next_line(markdown, bom_length)?;
41    let fence = match first_line.trim() {
42        "---" => "---",
43        "+++" => "+++",
44        _ => return None,
45    };
46
47    let content_start = first_line_end;
48    let mut line_count = 1usize;
49    let mut offset = first_line_end;
50
51    while let Some((line_start, line_end, line)) = next_line(markdown, offset) {
52        line_count += 1;
53        if line.trim() == fence {
54            let content_end = trim_trailing_line_breaks(&markdown[content_start..line_start]).len()
55                + content_start;
56            return Some(FrontmatterBounds {
57                content_start,
58                content_end,
59                full_end: line_end,
60                line_count,
61            });
62        }
63
64        offset = line_end;
65    }
66
67    None
68}
69
70fn trim_trailing_line_breaks(input: &str) -> &str {
71    input.trim_end_matches(['\r', '\n'])
72}
73
74fn next_line(markdown: &str, start: usize) -> Option<(usize, usize, &str)> {
75    if start >= markdown.len() {
76        return None;
77    }
78
79    let remainder = &markdown[start..];
80    if let Some(relative_end) = remainder.find('\n') {
81        let line_end = start + relative_end + 1;
82        let mut content_end = start + relative_end;
83        if relative_end > 0 && remainder.as_bytes()[relative_end - 1] == b'\r' {
84            content_end -= 1;
85        }
86
87        return Some((start, line_end, &markdown[start..content_end]));
88    }
89
90    Some((start, markdown.len(), &markdown[start..]))
91}