aboutsummaryrefslogtreecommitdiff
path: root/src/post/frontmatter.rs
blob: 67b96ab9e42fd2d743c85c522025de9f63fd651d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/// This code was borrowed from @fasterthanlime.
use color_eyre::eyre::Result;
pub use xesite_types::Frontmatter as Data;

enum State {
    SearchForStart,
    ReadingMarker { count: usize, end: bool },
    ReadingFrontMatter { buf: String, line_start: bool },
    SkipNewline { end: bool },
}

#[derive(Debug, thiserror::Error)]
enum Error {
    #[error("EOF while parsing frontmatter")]
    EOF,
    #[error("Error parsing yaml: {0:?}")]
    Yaml(#[from] serde_yaml::Error),
}

pub fn parse(input: &str) -> Result<(Data, usize)> {
    let mut state = State::SearchForStart;

    let mut payload = None;
    let offset;

    let mut chars = input.char_indices();
    'parse: loop {
        let (idx, ch) = match chars.next() {
            Some(x) => x,
            None => return Err(Error::EOF)?,
        };
        match &mut state {
            State::SearchForStart => match ch {
                '-' => {
                    state = State::ReadingMarker {
                        count: 1,
                        end: false,
                    };
                }
                '\n' | '\t' | ' ' => {
                    // ignore whitespace
                }
                _ => {
                    panic!("Start of frontmatter not found");
                }
            },
            State::ReadingMarker { count, end } => match ch {
                '-' => {
                    *count += 1;
                    if *count == 3 {
                        state = State::SkipNewline { end: *end };
                    }
                }
                _ => {
                    panic!("Malformed frontmatter marker");
                }
            },
            State::SkipNewline { end } => match ch {
                '\n' => {
                    if *end {
                        offset = idx + 1;
                        break 'parse;
                    } else {
                        state = State::ReadingFrontMatter {
                            buf: String::new(),
                            line_start: true,
                        };
                    }
                }
                _ => panic!("Expected newline, got {:?}", ch),
            },
            State::ReadingFrontMatter { buf, line_start } => match ch {
                '-' if *line_start => {
                    let mut state_temp = State::ReadingMarker {
                        count: 1,
                        end: true,
                    };
                    std::mem::swap(&mut state, &mut state_temp);
                    if let State::ReadingFrontMatter { buf, .. } = state_temp {
                        payload = Some(buf);
                    } else {
                        unreachable!();
                    }
                }
                ch => {
                    buf.push(ch);
                    *line_start = ch == '\n';
                }
            },
        }
    }

    // unwrap justification: option set in state machine, Rust can't statically analyze it
    let payload = payload.unwrap();

    let fm: Data = serde_yaml::from_str(&payload)?;

    Ok((fm, offset))
}