Skip to main content

dmc/
loaders.rs

1//! Per-extension loaders: bytes -> `Loaded { data, content }` for schema
2//! validation. `MatterLoader` runs the full mdx compile; `YamlLoader` /
3//! `JsonLoader` parse data files directly.
4
5use dmc_diagnostic::Code;
6use duck_diagnostic::DiagnosticEngine;
7use serde_json::Value;
8use std::path::Path;
9
10use crate::engine::compile::Compiler;
11
12/// `data` is frontmatter for mdx, the whole doc for yaml/json.
13pub struct Loaded {
14  pub data: Value,
15  pub content: String,
16}
17
18pub trait Loader: Send + Sync {
19  fn test(&self, path: &Path) -> bool;
20  fn load(&self, path: &Path, source: &str, diag_engine: &mut DiagnosticEngine<Code>) -> Result<Loaded, String>;
21}
22
23/// `.md` / `.mdx` / `.markdown`. Stashes `CompileOutput` under
24/// `data.__compiled` so the schema can refine it (e.g.
25/// `transform: ctx => ctx.html`).
26pub struct MatterLoader;
27
28impl Loader for MatterLoader {
29  fn test(&self, path: &Path) -> bool {
30    matches!(path.extension().and_then(|s| s.to_str()), Some("md") | Some("mdx") | Some("markdown"))
31  }
32
33  fn load(&self, _path: &Path, source: &str, diag_engine: &mut DiagnosticEngine<Code>) -> Result<Loaded, String> {
34    let out = Compiler::compile(source, diag_engine);
35    let mut data = if let Value::Object(_) = out.frontmatter {
36      out.frontmatter.clone()
37    } else {
38      Value::Object(serde_json::Map::new())
39    };
40    if let Value::Object(map) = &mut data {
41      map.insert("__compiled".into(), serde_json::to_value(&out).unwrap_or(Value::Null));
42    }
43    Ok(Loaded { data, content: source.to_string() })
44  }
45}
46
47pub struct YamlLoader;
48
49impl Loader for YamlLoader {
50  fn test(&self, path: &Path) -> bool {
51    matches!(path.extension().and_then(|s| s.to_str()), Some("yaml") | Some("yml"))
52  }
53
54  fn load(&self, _path: &Path, source: &str, _diag_engine: &mut DiagnosticEngine<Code>) -> Result<Loaded, String> {
55    let v: serde_yaml::Value = serde_yaml::from_str(source).map_err(|e| format!("yaml parse: {e}"))?;
56    let json = serde_json::to_value(v).map_err(|e| format!("yaml->json: {e}"))?;
57    Ok(Loaded { data: json, content: source.to_string() })
58  }
59}
60
61pub struct JsonLoader;
62
63impl Loader for JsonLoader {
64  fn test(&self, path: &Path) -> bool {
65    matches!(path.extension().and_then(|s| s.to_str()), Some("json"))
66  }
67
68  fn load(&self, _path: &Path, source: &str, _diag_engine: &mut DiagnosticEngine<Code>) -> Result<Loaded, String> {
69    let v: Value = serde_json::from_str(source).map_err(|e| format!("json parse: {e}"))?;
70    Ok(Loaded { data: v, content: source.to_string() })
71  }
72}
73
74/// Ordered loader chain; first match wins.
75pub struct LoaderRegistry {
76  loaders: Vec<Box<dyn Loader>>,
77}
78
79impl LoaderRegistry {
80  pub fn with_defaults() -> Self {
81    Self { loaders: vec![Box::new(MatterLoader), Box::new(YamlLoader), Box::new(JsonLoader)] }
82  }
83
84  pub fn pick(&self, path: &Path) -> Option<&dyn Loader> {
85    self.loaders.iter().find(|l| l.test(path)).map(|l| l.as_ref())
86  }
87}