wayver's git archive


an obsidian renderer
git clone https://git.wayver.dev/sable

sable-frontmatter/src/json.rs@2b84405277e54ab809e328cf0237374d4b4dbd0c

raw
Date Commit Message Author Files + -
2026-02-23 01:55 initial mvp wayverd 139 17808 0
...

1//! Parse JSON frontmatter
2
3use miette::{SourceOffset, SourceSpan};
4
5use crate::Metadata;
6
7/// JSON error.
8#[derive(Debug, miette::Diagnostic, thiserror::Error)]
9pub enum Error {
10    /// The JSON wasn't 'valid'.
11    ///
12    /// IE: the 'parser' couldn't count the braces properly.
13    #[error("frontmatter is not valid json")]
14    Invalid,
15    /// Too many braces were counted.
16    ///
17    /// This was 'thrown' to prevent an 'endless' loop of parser.
18    #[error("json frontmatter exceeded parse depth of 128 braces (come on :/)")]
19    DepthExceeded,
20    /// JSON parse error
21    #[error(transparent)]
22    Parse(ParseError),
23}
24
25/// JSON parse error
26#[derive(Debug, miette::Diagnostic, thiserror::Error)]
27#[error("failed to deserialize toml frontmatter")]
28pub struct ParseError {
29    /// The 'source' of the frontmatter.
30    #[source_code]
31    src: String,
32    /// The location where [`serde_json`] failed to parse.
33    #[label("{err}")]
34    location: SourceSpan,
35
36    /// The error emitted.
37    #[source]
38    err: serde_json::Error,
39}
40
41/// Parse JSON frontmatter by counting the braces.
42///
43/// # Errors
44///
45/// This is will if the basic parser cannot count braces properly,
46/// the brace count is too high (to prevent endless parsing),
47/// or the contents failed to parse.
48pub fn parse(data: &str) -> Result<(Option<Metadata>, &str), Error> {
49    const MAX_DEPTH: usize = 128;
50
51    let data = data.trim_start();
52
53    if !data.starts_with('{') {
54        return Err(Error::Invalid);
55    }
56
57    let mut depth = 0;
58
59    let mut braces = 0;
60
61    let mut is_in_string = false;
62    let mut escape_next = false;
63
64    let mut split_point = 0;
65
66    for (i, ch) in data.char_indices() {
67        if escape_next {
68            escape_next = false;
69            continue;
70        }
71
72        match ch {
73            '"' if !is_in_string => {
74                is_in_string = true;
75            }
76            '"' if is_in_string => {
77                is_in_string = false;
78            }
79            '\\' if is_in_string => {
80                escape_next = true;
81            }
82            '{' if !is_in_string => {
83                depth += 1;
84
85                braces += 1;
86
87                if depth > MAX_DEPTH {
88                    return Err(Error::DepthExceeded);
89                }
90            }
91            '}' if !is_in_string => {
92                braces -= 1;
93
94                if depth > 0 {
95                    depth = depth.saturating_sub(1);
96                }
97
98                if braces == 0 {
99                    split_point = i;
100
101                    break;
102                }
103            }
104            _ => {}
105        }
106    }
107
108    if braces != 0 {
109        return Err(Error::Invalid);
110    }
111
112    let (frontmatter, body) = data.split_at(split_point);
113
114    let parsed = serde_json::from_str(frontmatter).map_err(|err| {
115        Error::Parse(ParseError {
116            src: frontmatter.to_string(),
117            location: SourceSpan::new(
118                SourceOffset::from_location(frontmatter, err.line(), err.column()),
119                1,
120            ),
121            err,
122        })
123    })?;
124
125    Ok((Some(parsed), body))
126}
127