1use super::error::SchemaError;
5use super::schema::*;
6
7pub fn parse_schema(src: &str) -> Result<Schema, SchemaError> {
9 let lines: Vec<&str> = src.lines().collect();
10 let mut opts = SchemaOpts::default();
11 let mut i = 0;
12
13 while i < lines.len() {
15 let (content, _) = split_desc(lines[i]);
16 let t = content.trim();
17 if t.is_empty() {
18 i += 1;
19 } else if let Some(rest) = t.strip_prefix('%') {
20 apply_directive(rest, i + 1, &mut opts)?;
21 i += 1;
22 } else {
23 break;
24 }
25 }
26
27 let mut frontmatter = Vec::new();
29 if i < lines.len() && split_desc(lines[i]).0.trim() == "---" {
30 i += 1;
31 loop {
32 let line = lines.get(i).ok_or_else(|| {
33 SchemaError::new(i, 1, "unterminated frontmatter (missing `---`)")
34 })?;
35 let (content, desc) = split_desc(line);
36 if content.trim() == "---" {
37 i += 1;
38 break;
39 }
40 if !content.trim().is_empty() {
41 let mut f = parse_field(content, i + 1)?;
42 f.desc = desc;
43 frontmatter.push(f);
44 }
45 i += 1;
46 }
47 }
48
49 let mut flat: Vec<(usize, Node)> = Vec::new();
51 while i < lines.len() {
52 let line = lines[i];
53 let (content, desc) = split_desc(line);
54 if content.trim().is_empty() {
55 i += 1;
56 continue;
57 }
58 let indent = content.len() - content.trim_start().len();
59 let node = parse_node(content.trim_start(), desc, i + 1)?;
60 flat.push((indent, node));
61 i += 1;
62 }
63 let mut pos = 0;
64 let body = build_tree(&flat, &mut pos, -1);
65
66 Ok(Schema {
67 opts,
68 frontmatter,
69 body,
70 })
71}
72
73fn apply_directive(rest: &str, line: usize, opts: &mut SchemaOpts) -> Result<(), SchemaError> {
76 let (k, v) = rest
77 .split_once('=')
78 .ok_or_else(|| SchemaError::new(line, 1, "directive needs `key = value`"))?;
79 let (k, v) = (k.trim(), v.trim());
80 match (k, v) {
81 ("ordered", "true") => opts.ordered = true,
82 ("ordered", "false") => opts.ordered = false,
83 ("strict", "true") => opts.strict = true,
84 ("strict", "false") => opts.strict = false,
85 ("frontmatter", "open") => opts.frontmatter_open = true,
86 ("frontmatter", "closed") => opts.frontmatter_open = false,
87 _ => {
88 return Err(SchemaError::new(
89 line,
90 1,
91 format!("unknown directive `%{rest}`"),
92 ))
93 }
94 }
95 Ok(())
96}
97
98fn parse_field(content: &str, line: usize) -> Result<FieldSchema, SchemaError> {
101 let (left, ty_src) = content
102 .split_once(':')
103 .ok_or_else(|| SchemaError::new(line, 1, "frontmatter field needs `key: type`"))?;
104
105 let left = left.trim();
107 let key_end = left
108 .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
109 .unwrap_or(left.len());
110 if key_end == 0 {
111 return Err(SchemaError::new(line, 1, "frontmatter field needs a key"));
112 }
113 let key = left[..key_end].to_string();
114 let mut rest = left[key_end..].trim_start();
115 let optional = if let Some(r) = rest.strip_prefix('?') {
116 rest = r.trim_start();
117 true
118 } else {
119 false
120 };
121 let alias = if let Some(r) = rest.strip_prefix('@') {
122 r.trim().to_string()
123 } else {
124 key.clone()
125 };
126
127 let ty = parse_field_type(ty_src.trim(), line)?;
128 Ok(FieldSchema {
129 key,
130 alias,
131 optional,
132 ty,
133 desc: None,
134 })
135}
136
137fn parse_field_type(s: &str, line: usize) -> Result<FieldType, SchemaError> {
138 match s {
139 "string" => return Ok(FieldType::Str),
140 "int" => return Ok(FieldType::Int),
141 "bool" => return Ok(FieldType::Bool),
142 "date" => return Ok(FieldType::Date),
143 _ => {}
144 }
145 if let Some(inner) = s.strip_prefix("enum(").and_then(|s| s.strip_suffix(')')) {
146 let vals = inner
147 .split(',')
148 .map(|v| v.trim().to_string())
149 .filter(|v| !v.is_empty())
150 .collect();
151 return Ok(FieldType::Enum(vals));
152 }
153 if let Some(inner) = s.strip_prefix('[') {
154 let close = inner
156 .find(']')
157 .ok_or_else(|| SchemaError::new(line, 1, "list type missing `]`"))?;
158 let elem = parse_field_type(inner[..close].trim(), line)?;
159 return Ok(FieldType::List(Box::new(elem)));
160 }
161 if s.starts_with('/') {
162 return Ok(FieldType::Regex(parse_regex(s).0));
163 }
164 Err(SchemaError::new(line, 1, format!("unknown type `{s}`")))
165}
166
167fn parse_node(content: &str, desc: Option<String>, line: usize) -> Result<Node, SchemaError> {
170 let (kind, head_src) = detect_marker(content)
171 .ok_or_else(|| SchemaError::new(line, 1, format!("unrecognized marker: `{content}`")))?;
172
173 let (card, rest) = parse_card(head_src);
174 let (name, rest) = parse_name(rest);
175 let label = parse_label(rest.trim());
176 let head = Head { name, card, desc };
177
178 Ok(match kind {
179 MarkerKind::Heading(level) => Node::Heading {
180 level,
181 title: label.unwrap_or(Match::Regex(".*".into())),
182 head,
183 children: Vec::new(),
184 },
185 MarkerKind::Prose => Node::Prose { text: label, head },
186 MarkerKind::Bullet => list(ListStyle::Bullet, label, head),
187 MarkerKind::Ordered => list(ListStyle::Ordered, label, head),
188 MarkerKind::Checklist => list(ListStyle::Checklist, label, head),
189 })
190}
191
192fn list(style: ListStyle, item: Option<Match>, head: Head) -> Node {
193 Node::List {
194 style,
195 item,
196 head,
197 children: Vec::new(),
198 }
199}
200
201enum MarkerKind {
202 Heading(u8),
203 Bullet,
204 Ordered,
205 Checklist,
206 Prose,
207}
208
209fn detect_marker(s: &str) -> Option<(MarkerKind, &str)> {
212 if let Some(rest) = s.strip_prefix("- [ ]") {
213 return Some((MarkerKind::Checklist, rest.trim_start()));
214 }
215 if s.starts_with('#') {
216 let level = s.chars().take_while(|&c| c == '#').count();
217 if (1..=6).contains(&level) {
218 let after = &s[level..];
219 if after.is_empty() || after.starts_with(' ') {
221 return Some((MarkerKind::Heading(level as u8), after.trim_start()));
222 }
223 }
224 return None;
225 }
226 if let Some(rest) = s.strip_prefix("- ") {
227 return Some((MarkerKind::Bullet, rest.trim_start()));
228 }
229 if s == "-" {
230 return Some((MarkerKind::Bullet, ""));
231 }
232 let digits = s.chars().take_while(|c| c.is_ascii_digit()).count();
234 if digits > 0 && s[digits..].starts_with('.') {
235 return Some((MarkerKind::Ordered, s[digits + 1..].trim_start()));
236 }
237 if let Some(rest) = s.strip_prefix('>') {
238 return Some((MarkerKind::Prose, rest.trim_start()));
239 }
240 None
241}
242
243fn parse_card(s: &str) -> (Card, &str) {
244 match s.as_bytes().first() {
245 Some(b'+') => (Card::Plus, &s[1..]),
246 Some(b'*') => (Card::Star, &s[1..]),
247 Some(b'?') => (Card::Optional, &s[1..]),
248 Some(b'{') => parse_range(s),
249 _ => (Card::Required, s),
250 }
251}
252
253fn parse_range(s: &str) -> (Card, &str) {
254 let Some(close) = s.find('}') else {
255 return (Card::Required, s);
256 };
257 let body = &s[1..close];
258 let (min_s, max_s) = match body.split_once(',') {
259 Some((a, b)) => (a.trim(), Some(b.trim())),
260 None => (body.trim(), None),
261 };
262 let Ok(min) = min_s.parse::<u32>() else {
263 return (Card::Required, s);
264 };
265 let max = match max_s {
266 None => Some(min),
267 Some("") => None,
268 Some(m) => match m.parse::<u32>() {
269 Ok(v) => Some(v),
270 Err(_) => return (Card::Required, s),
271 },
272 };
273 (Card::Range(min, max), &s[close + 1..])
274}
275
276fn parse_name(s: &str) -> (Option<String>, &str) {
277 let Some(rest) = s.strip_prefix('@') else {
278 return (None, s);
279 };
280 let end = rest
281 .find(|c: char| !(c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'))
282 .unwrap_or(rest.len());
283 if end == 0 {
284 return (None, s);
285 }
286 (Some(rest[..end].to_string()), &rest[end..])
287}
288
289fn parse_label(s: &str) -> Option<Match> {
290 let s = s.trim();
291 if s.is_empty() {
292 return None;
293 }
294 if s.starts_with('/') {
295 return Some(Match::Regex(parse_regex(s).0));
296 }
297 if let Some(rest) = s.strip_prefix('"') {
298 let lit = match rest.rsplit_once('"') {
299 Some((inner, _)) => inner,
300 None => rest,
301 };
302 return Some(Match::Literal(unescape(lit)));
303 }
304 Some(Match::Literal(unescape(s)))
305}
306
307fn parse_regex(s: &str) -> (String, &str) {
310 debug_assert!(s.starts_with('/'));
311 let body = &s[1..];
312 let mut end = None;
314 let bytes = body.as_bytes();
315 let mut k = 0;
316 while k < bytes.len() {
317 if bytes[k] == b'\\' {
318 k += 2;
319 continue;
320 }
321 if bytes[k] == b'/' {
322 end = Some(k);
323 break;
324 }
325 k += 1;
326 }
327 let Some(end) = end else {
328 return (body.to_string(), "");
329 };
330 let pat = &body[..end];
331 let after = &body[end + 1..];
332 let flag_len = after
333 .chars()
334 .take_while(|c| c.is_ascii_alphabetic())
335 .count();
336 let flags = &after[..flag_len];
337 let pattern = if flags.is_empty() {
338 pat.to_string()
339 } else {
340 format!("(?{flags}){pat}")
341 };
342 (pattern, &after[flag_len..])
343}
344
345fn unescape(s: &str) -> String {
347 let mut out = String::with_capacity(s.len());
348 let mut chars = s.chars();
349 while let Some(c) = chars.next() {
350 if c == '\\' {
351 if let Some(n) = chars.next() {
352 out.push(n);
353 }
354 } else {
355 out.push(c);
356 }
357 }
358 out
359}
360
361fn split_desc(line: &str) -> (&str, Option<String>) {
366 let bytes = line.as_bytes();
367 let mut k = 0;
368 while k + 1 < bytes.len() {
369 if bytes[k] == b'\\' {
370 k += 2;
371 continue;
372 }
373 if bytes[k] == b'<' && bytes[k + 1] == b'?' {
374 let before = &line[..k];
375 let rest = &line[k + 2..];
376 let inner = rest.strip_suffix("?>").unwrap_or(rest);
377 return (before, Some(inner.trim().to_string()));
378 }
379 k += 1;
380 }
381 (line, None)
382}
383
384fn build_tree(flat: &[(usize, Node)], pos: &mut usize, parent_indent: isize) -> Vec<Node> {
389 let mut out = Vec::new();
390 while *pos < flat.len() {
391 let indent = flat[*pos].0 as isize;
392 if indent <= parent_indent {
393 break;
394 }
395 let mut node = flat[*pos].1.clone();
396 *pos += 1;
397 let children = build_tree(flat, pos, indent);
398 node.set_children(children);
399 out.push(node);
400 }
401 out
402}