Skip to main content

specfold_core/
markdown.rs

1//! Reading and writing the Markdown files documents are made of.
2
3pub mod body;
4pub mod link_dest;
5
6/// C135: where fenced code starts and ends, the CommonMark way: a fence opens with three or
7/// more backticks or tildes and closes only with the same character, at least as many, and
8/// nothing else on the line.
9#[derive(Debug, Clone, Default)]
10pub struct Fence(Option<(char, usize)>);
11
12impl Fence {
13    /// Feeds one line; true when it opens or closes a fence.
14    pub fn step(&mut self, line: &str) -> bool {
15        let t = line.trim_start();
16        let run = |ch: char| t.chars().take_while(|c| *c == ch).count();
17        match self.0 {
18            Some((ch, n)) => {
19                let m = run(ch);
20                if m >= n && t[m * ch.len_utf8()..].trim().is_empty() {
21                    self.0 = None;
22                    return true;
23                }
24                false
25            }
26            None => {
27                for ch in ['`', '~'] {
28                    let n = run(ch);
29                    // A backtick fence's info string cannot hold a backtick.
30                    if n >= 3 && !(ch == '`' && t[n..].contains('`')) {
31                        self.0 = Some((ch, n));
32                        return true;
33                    }
34                }
35                false
36            }
37        }
38    }
39
40    /// Whether the lines fed so far leave a fence open.
41    pub fn inside(&self) -> bool {
42        self.0.is_some()
43    }
44}
45
46/// C238: an ATX heading the CommonMark way: at most three spaces of indent (four make code),
47/// one to six `#`, then a space, a tab or the end of the line. Gives the level and the text,
48/// without a closing run of `#`.
49pub fn atx(line: &str) -> Option<(usize, &str)> {
50    let indent = line.len() - line.trim_start_matches(' ').len();
51    if indent > 3 {
52        return None;
53    }
54    let t = &line[indent..];
55    let n = t.chars().take_while(|c| *c == '#').count();
56    let rest = &t[n..];
57    if !(1..=6).contains(&n) || !(rest.is_empty() || rest.starts_with([' ', '\t'])) {
58        return None;
59    }
60    // C240: an optional closing run of `#` (after a space, or the whole text) is not text.
61    let text = rest.trim();
62    let open = text.trim_end_matches('#');
63    let text = if open.is_empty() || open.ends_with([' ', '\t']) { open.trim_end() } else { text };
64    Some((n, text))
65}
66
67pub mod counts;
68pub mod frontmatter;
69pub mod sections;
70pub mod tables;
71pub mod tasks;