frontmatter_gen/
extractor.rs1use crate::error::Error;
6use crate::types::Format;
7
8pub fn extract_raw_frontmatter(
38 content: &str,
39) -> Result<(&str, &str), Error> {
40 if let Some(yaml) =
42 extract_delimited_frontmatter(content, "---\n", "\n---")
43 .or_else(|| {
44 extract_delimited_frontmatter(
45 content, "---\r\n", "\r\n---",
46 )
47 })
48 {
49 let remaining = &content[content
50 .find("\n---\n")
51 .or_else(|| content.find("\r\n---\r\n"))
52 .map_or(content.len(), |i| i + 5)..];
53 return Ok((yaml, remaining.trim_start()));
54 }
55
56 if let Some(toml) =
58 extract_delimited_frontmatter(content, "+++\n", "\n+++")
59 .or_else(|| {
60 extract_delimited_frontmatter(
61 content, "+++\r\n", "\r\n+++",
62 )
63 })
64 {
65 let remaining = &content[content
66 .find("\n+++\n")
67 .or_else(|| content.find("\r\n+++\r\n"))
68 .map_or(content.len(), |i| i + 5)..];
69 return Ok((toml, remaining.trim_start()));
70 }
71
72 if let Ok(json) = extract_json_frontmatter(content) {
74 let remaining = &content[json.len()..];
75 return Ok((json, remaining.trim_start()));
76 }
77
78 if content.starts_with("---\n---")
80 || content.starts_with("+++\n+++")
81 {
82 return Err(Error::InvalidFormat);
83 }
84
85 Err(Error::InvalidFormat)
86}
87
88pub fn extract_json_frontmatter(content: &str) -> Result<&str, Error> {
118 const MAX_DEPTH: usize = 100; let trimmed = content.trim_start();
120
121 if !trimmed.starts_with('{') {
123 return Err(Error::InvalidJson);
124 }
125
126 let mut brace_count = 0;
127 let mut depth = 0;
128 let mut in_string = false;
129 let mut escape_next = false;
130
131 for (idx, ch) in trimmed.char_indices() {
133 if escape_next {
134 escape_next = false;
135 continue;
136 }
137
138 match ch {
139 '"' if !in_string => in_string = true,
140 '"' if in_string => in_string = false,
141 '\\' if in_string => escape_next = true,
142 '{' if !in_string => {
143 brace_count += 1;
144 depth += 1;
145 if depth > MAX_DEPTH {
147 return Err(Error::JsonDepthLimitExceeded);
148 }
149 }
150 '}' if !in_string => {
151 brace_count -= 1;
152 depth = depth.saturating_sub(1);
154 if brace_count == 0 {
156 return Ok(&trimmed[..=idx]);
157 }
158 }
159 _ => {}
160 }
161 }
162
163 Err(Error::InvalidJson)
165}
166
167pub fn detect_format(raw_frontmatter: &str) -> Result<Format, Error> {
194 let trimmed = raw_frontmatter.trim_start();
195
196 if trimmed.starts_with("---") {
198 return Ok(Format::Yaml);
199 }
200
201 if trimmed.starts_with('{') {
203 return Ok(Format::Json);
204 }
205
206 if trimmed.contains(':') && !trimmed.contains('{') {
208 return Ok(Format::Yaml);
209 }
210
211 if trimmed.contains('=') {
213 return Ok(Format::Toml);
214 }
215
216 Err(Error::InvalidFormat)
218}
219
220#[must_use]
245pub fn extract_delimited_frontmatter<'a>(
246 content: &'a str,
247 start_delim: &str,
248 end_delim: &str,
249) -> Option<&'a str> {
250 let start_index = content.find(start_delim)? + start_delim.len();
251 let end_index = content.find(end_delim)?;
252
253 if start_index <= end_index {
254 Some(content[start_index..end_index].trim())
255 } else {
256 None
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 mod extract_raw_frontmatter {
266 use super::*;
267
268 #[test]
269 fn test_extract_yaml() {
270 let content = r"---
271title: Example
272---
273Content here";
274 let result = extract_raw_frontmatter(content).unwrap();
275 assert_eq!(result.0, "title: Example");
276 assert_eq!(result.1, "Content here");
277 }
278
279 #[test]
280 fn test_extract_toml() {
281 let content = r#"+++
282title = "Example"
283+++
284Content here"#;
285 let result = extract_raw_frontmatter(content).unwrap();
286 assert_eq!(result.0, r#"title = "Example""#);
287 assert_eq!(result.1, "Content here");
288 }
289
290 #[test]
291 fn test_extract_json() {
292 let content = r#"{ "title": "Example" }
293Content here"#;
294 let result = extract_raw_frontmatter(content).unwrap();
295 assert_eq!(result.0, r#"{ "title": "Example" }"#);
296 assert_eq!(result.1, "Content here");
297 }
298
299 #[test]
300 fn test_invalid_format() {
301 let content = "Invalid frontmatter";
302 let result = detect_format(content);
303 assert!(matches!(result, Err(Error::InvalidFormat)));
304 }
305
306 #[test]
307 fn test_extract_crlf_yaml_and_toml() {
308 let yaml_crlf =
309 "---\r\ntitle: Example\r\n---\r\nContent here";
310 let (raw_yaml, body_yaml) =
311 extract_raw_frontmatter(yaml_crlf).unwrap();
312 assert_eq!(raw_yaml, "title: Example");
313 assert_eq!(body_yaml, "Content here");
314
315 let toml_crlf =
316 "+++\r\ntitle = \"Example\"\r\n+++\r\nContent here";
317 let (raw_toml, body_toml) =
318 extract_raw_frontmatter(toml_crlf).unwrap();
319 assert_eq!(raw_toml, "title = \"Example\"");
320 assert_eq!(body_toml, "Content here");
321
322 assert_eq!(
323 detect_format("---\ntitle: Example\n---").unwrap(),
324 Format::Yaml
325 );
326 assert!(matches!(
327 extract_json_frontmatter("{ \"unclosed\": 1"),
328 Err(Error::InvalidJson)
329 ));
330 }
331 }
332
333 mod extract_json_frontmatter {
335 use super::*;
336
337 #[test]
338 fn test_valid_json() {
339 let content = r#"{ "title": "Example" }
340Content here"#;
341 let result = extract_json_frontmatter(content).unwrap();
342 assert_eq!(result, r#"{ "title": "Example" }"#);
343 }
344
345 #[test]
346 fn test_nested_json() {
347 let content = r#"{ "a": { "b": { "c": { "d": { "e": {} }}}}}
348Content here"#;
349 let result = extract_json_frontmatter(content);
350 assert!(result.is_ok());
351 assert_eq!(
352 result.unwrap(),
353 r#"{ "a": { "b": { "c": { "d": { "e": {} }}}}}"#
354 );
355 }
356
357 #[test]
358 fn test_too_deep_json() {
359 let mut content = String::from("{ ");
360 for _ in 0..101 {
361 content.push_str(r#""a": { "#);
362 }
363 content.push_str(&"}".repeat(101));
364 content.push_str("\nContent here");
365
366 let result = extract_json_frontmatter(&content);
367 assert!(matches!(
368 result,
369 Err(Error::JsonDepthLimitExceeded)
370 ));
371 }
372
373 #[test]
374 fn test_escaped_characters() {
375 let content = r#"{ "title": "Example with \"quotes\" and {braces}", "content": "Some text with \\ backslash" }
376Actual content starts here"#;
377 let result = extract_json_frontmatter(content).unwrap();
378 assert_eq!(
379 result,
380 r#"{ "title": "Example with \"quotes\" and {braces}", "content": "Some text with \\ backslash" }"#
381 );
382 }
383
384 #[test]
385 fn test_invalid_json() {
386 let content = "Not a JSON frontmatter";
387 let result = extract_json_frontmatter(content);
388 assert!(matches!(result, Err(Error::InvalidJson)));
389 }
390 }
391
392 mod detect_format {
394 use super::*;
395
396 #[test]
397 fn test_yaml_format() {
398 let content = "title: Example";
399 let result = detect_format(content).unwrap();
400 assert_eq!(result, Format::Yaml);
401 }
402
403 #[test]
404 fn test_toml_format() {
405 let content = "title = \"Example\"";
406 let result = detect_format(content).unwrap();
407 assert_eq!(result, Format::Toml);
408 }
409
410 #[test]
411 fn test_json_format() {
412 let content = r#"{ "title": "Example" }"#;
413 let result = detect_format(content).unwrap();
414 assert_eq!(result, Format::Json);
415 }
416
417 #[test]
418 fn test_invalid_format() {
419 let content = "Invalid content";
420 let result = detect_format(content);
421 assert!(matches!(result, Err(Error::InvalidFormat)));
422 }
423 }
424
425 mod extract_delimited_frontmatter {
427 use super::*;
428
429 #[test]
430 fn test_valid_yaml() {
431 let content = "---\ntitle: Example\n---\nContent here";
432 let result = extract_delimited_frontmatter(
433 content, "---\n", "\n---\n",
434 )
435 .unwrap();
436 assert_eq!(result, "title: Example");
437 }
438
439 #[test]
440 fn test_valid_toml() {
441 let content = "+++\ntitle = \"Example\"\n+++\nContent here";
442 let result = extract_delimited_frontmatter(
443 content, "+++\n", "\n+++\n",
444 )
445 .unwrap();
446 assert_eq!(result, r#"title = "Example""#);
447 }
448
449 #[test]
450 fn test_valid_windows_format() {
451 let content =
452 "---\r\ntitle: Example\r\n---\r\nContent here";
453 let result = extract_delimited_frontmatter(
454 content,
455 "---\r\n",
456 "\r\n---\r\n",
457 )
458 .unwrap();
459 assert_eq!(result, "title: Example");
460 }
461
462 #[test]
463 fn test_missing_start_delimiter() {
464 let content = "title: Example\n---\nContent here";
465 let result = extract_delimited_frontmatter(
466 content, "---\n", "\n---\n",
467 );
468 assert!(result.is_none());
469 }
470
471 #[test]
472 fn test_missing_end_delimiter() {
473 let content = "---\ntitle: Example\nContent here";
474 let result = extract_delimited_frontmatter(
475 content, "---\n", "\n---\n",
476 );
477 assert!(result.is_none());
478 }
479 }
480}