Skip to main content

header_parsing/
lib.rs

1#![deny(missing_docs)]
2
3//! This crate provides functionality for parsing markdown style headers.
4
5/// Represents the changes to be made to a path.
6///
7/// This is generated when a header is found.
8/// It's still possible to access the previous path of headers before applying the changes.
9#[must_use]
10pub struct PathChanges<'a> {
11    drop: usize,
12    /// The header name that will be added.
13    pub header: Box<str>,
14    /// The path to which changes will be applied.
15    pub path: &'a mut Vec<Box<str>>,
16}
17
18impl PathChanges<'_> {
19    /// The nesting level of the found header, starting at `0` for a top level header (`#`).
20    ///
21    /// This is available before [`apply`](Self::apply) and equals the length the path will have
22    /// after applying, minus one.
23    pub fn level(&self) -> usize {
24        self.path.len() - self.drop
25    }
26
27    /// Applies the changes to the path and returns the [`level`](Self::level) of the header.
28    pub fn apply(self) -> usize {
29        let Self { drop, header, path } = self;
30
31        for _ in 0..drop {
32            path.pop();
33        }
34
35        path.push(header);
36
37        path.len() - 1
38    }
39}
40
41/// Indicates that a subheader was found without a corresponding header.
42pub struct SubheaderWithoutHeader;
43
44/// Parses a header from a line of text.
45///
46/// # Arguments
47///
48/// * `path`: A list of headers including the current header.
49/// * `line`: The line of text to parse.
50///
51/// # Returns
52///
53/// If the line is not a header, returns `None`.
54/// If the line is a valid header, returns the `PathChanges` which have to be used to update the path.
55/// If the header is a subheader without a corresponding header, an error is returned.
56pub fn parse_header<'a>(
57    path: &'a mut Vec<Box<str>>,
58    line: &str,
59) -> Option<Result<PathChanges<'a>, SubheaderWithoutHeader>> {
60    let mut start = 0;
61
62    let mut chars = line.chars();
63    while Some('#') == chars.next() {
64        start += 1;
65    }
66
67    if start == 0 {
68        return None;
69    }
70
71    let level = start - 1;
72
73    let len = path.len();
74
75    Some(if len < level {
76        Err(SubheaderWithoutHeader)
77    } else {
78        Ok(PathChanges {
79            drop: len - level,
80            header: line[start..].trim().into(),
81            path,
82        })
83    })
84}
85
86/// The classification of a single line by a [`Parser`].
87pub enum Line<'a> {
88    /// The line was a header at the given [`level`](PathChanges::level) (`0` for `#`).
89    ///
90    /// The current path of headers is available through [`Parser::path`].
91    Header {
92        /// The nesting level of the header, starting at `0`.
93        level: usize,
94    },
95    /// The line was not a header. It is passed through unchanged.
96    Content(&'a str),
97}
98
99/// A stateful line parser that tracks the current path of headers.
100///
101/// This wraps [`parse_header`] and applies the changes automatically, so consumers only need to
102/// match on [`Line`] instead of tracking the path themselves.
103#[derive(Default)]
104pub struct Parser {
105    path: Vec<Box<str>>,
106}
107
108impl Parser {
109    /// Creates a parser with an empty path.
110    pub fn new() -> Self {
111        Self::default()
112    }
113
114    /// The current path of headers, outermost first.
115    pub fn path(&self) -> &[Box<str>] {
116        &self.path
117    }
118
119    /// Classifies a single line.
120    ///
121    /// On a header, the path is updated before returning and [`path`](Self::path) reflects the new
122    /// state. Returns an error if the header is a subheader without a corresponding header.
123    pub fn line<'a>(&mut self, line: &'a str) -> Result<Line<'a>, SubheaderWithoutHeader> {
124        match parse_header(&mut self.path, line) {
125            None => Ok(Line::Content(line)),
126            Some(changes) => changes.map(|changes| Line::Header {
127                level: changes.apply(),
128            }),
129        }
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::{Line, Parser};
136
137    #[test]
138    fn dedent_reports_correct_level() {
139        let mut parser = Parser::new();
140        let levels: Vec<usize> = ["# A", "## B", "# C"]
141            .into_iter()
142            .map(|line| match parser.line(line) {
143                Ok(Line::Header { level }) => level,
144                _ => panic!("expected header"),
145            })
146            .collect();
147
148        assert_eq!(levels, [0, 1, 0]);
149        assert_eq!(parser.path(), &["C".into()]);
150    }
151}