supersigil_parser/
frontmatter.rs1use std::collections::HashMap;
4use std::path::Path;
5
6use supersigil_core::{Frontmatter, ParseError};
7
8#[derive(Debug, Clone, PartialEq)]
10pub enum FrontMatterResult {
11 Supersigil {
13 frontmatter: Frontmatter,
15 extra: HashMap<String, yaml_serde::Value>,
17 },
18 NotSupersigil,
20}
21
22fn is_delimiter(line: &str) -> bool {
25 let trimmed = line.trim_end();
26 trimmed == "---"
27}
28
29pub fn extract_front_matter<'a>(
39 content: &'a str,
40 path: &Path,
41) -> Result<Option<(&'a str, &'a str)>, ParseError> {
42 let first_newline = content.find('\n');
44 let first_line = match first_newline {
45 Some(pos) => &content[..pos],
46 None => content,
47 };
48
49 if !is_delimiter(first_line) {
50 return Ok(None);
51 }
52
53 let yaml_start = match first_newline {
55 Some(pos) => pos + 1,
56 None => {
58 return Err(ParseError::UnclosedFrontMatter {
59 path: path.to_path_buf(),
60 });
61 }
62 };
63
64 let rest = &content[yaml_start..];
66 let mut offset = 0;
67 for line in rest.split('\n') {
68 if is_delimiter(line) {
69 let yaml = &content[yaml_start..yaml_start + offset];
70 let body_start = yaml_start + offset + line.len();
71 let body_start = if content.as_bytes().get(body_start) == Some(&b'\n') {
73 body_start + 1
74 } else {
75 body_start
76 };
77 let body = &content[body_start..];
78 return Ok(Some((yaml, body)));
79 }
80 offset += line.len() + 1;
82 }
83
84 Err(ParseError::UnclosedFrontMatter {
85 path: path.to_path_buf(),
86 })
87}
88
89pub fn deserialize_front_matter(yaml: &str, path: &Path) -> Result<FrontMatterResult, ParseError> {
101 if yaml.trim().is_empty() {
103 return Ok(FrontMatterResult::NotSupersigil);
104 }
105
106 let mut mapping: HashMap<String, yaml_serde::Value> =
108 yaml_serde::from_str(yaml).map_err(|e| ParseError::InvalidYaml {
109 path: path.to_path_buf(),
110 message: e.to_string(),
111 })?;
112
113 let Some(supersigil_value) = mapping.remove("supersigil") else {
115 return Ok(FrontMatterResult::NotSupersigil);
116 };
117
118 let yaml_serde::Value::Mapping(supersigil_map) = supersigil_value else {
121 return Err(ParseError::InvalidYaml {
122 path: path.to_path_buf(),
123 message: "supersigil value must be a mapping".to_owned(),
124 });
125 };
126
127 let get_optional_string = |map: &yaml_serde::Mapping, key: &str| -> Option<String> {
128 map.get(key)
129 .and_then(yaml_serde::Value::as_str)
130 .map(str::to_owned)
131 };
132
133 let id = get_optional_string(&supersigil_map, "id").ok_or_else(|| ParseError::MissingId {
134 path: path.to_path_buf(),
135 })?;
136
137 let frontmatter = Frontmatter {
138 id,
139 doc_type: get_optional_string(&supersigil_map, "type"),
140 status: get_optional_string(&supersigil_map, "status"),
141 };
142
143 Ok(FrontMatterResult::Supersigil {
145 frontmatter,
146 extra: mapping,
147 })
148}