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/// One loaded source file: schema-validated `data` (frontmatter for mdx,
13/// the whole doc for yaml/json) plus the original `content` string.
14pub struct Loaded {
15  pub data: Value,
16  pub content: String,
17}
18
19/// Pluggable per-extension loader: `test` claims a path, `load` parses it.
20pub trait Loader: Send + Sync {
21  fn test(&self, path: &Path) -> bool;
22  fn load(&self, path: &Path, source: &str, diag_engine: &mut DiagnosticEngine<Code>) -> Result<Loaded, String>;
23}
24
25/// `.md` / `.mdx` / `.markdown` loader. Runs the full compile and stashes
26/// the `CompileOutput` under `data.__compiled` so the schema can refine it
27/// (e.g. `transform: ctx => ctx.html`).
28pub struct MatterLoader;
29
30impl Loader for MatterLoader {
31  fn test(&self, path: &Path) -> bool {
32    matches!(path.extension().and_then(|s| s.to_str()), Some("md") | Some("mdx") | Some("markdown"))
33  }
34
35  fn load(&self, _path: &Path, source: &str, diag_engine: &mut DiagnosticEngine<Code>) -> Result<Loaded, String> {
36    let out = Compiler::compile(source, diag_engine);
37    let mut data = if let Value::Object(_) = out.frontmatter {
38      out.frontmatter.clone()
39    } else {
40      Value::Object(serde_json::Map::new())
41    };
42    if let Value::Object(map) = &mut data {
43      map.insert("__compiled".into(), serde_json::to_value(&out).unwrap_or(Value::Null));
44    }
45    Ok(Loaded { data, content: source.to_string() })
46  }
47}
48
49/// `.yaml` / `.yml` loader. Parses to `serde_yaml::Value`, then converts
50/// to `serde_json::Value` for schema interop.
51pub struct YamlLoader;
52
53impl Loader for YamlLoader {
54  fn test(&self, path: &Path) -> bool {
55    matches!(path.extension().and_then(|s| s.to_str()), Some("yaml") | Some("yml"))
56  }
57
58  fn load(&self, _path: &Path, source: &str, _diag_engine: &mut DiagnosticEngine<Code>) -> Result<Loaded, String> {
59    let v: serde_yaml::Value = serde_yaml::from_str(source).map_err(|e| format!("yaml parse: {e}"))?;
60    let json = serde_json::to_value(v).map_err(|e| format!("yaml->json: {e}"))?;
61    Ok(Loaded { data: json, content: source.to_string() })
62  }
63}
64
65/// `.json` loader. Straight `serde_json::from_str`.
66pub struct JsonLoader;
67
68impl Loader for JsonLoader {
69  fn test(&self, path: &Path) -> bool {
70    matches!(path.extension().and_then(|s| s.to_str()), Some("json"))
71  }
72
73  fn load(&self, _path: &Path, source: &str, _diag_engine: &mut DiagnosticEngine<Code>) -> Result<Loaded, String> {
74    let v: Value = serde_json::from_str(source).map_err(|e| format!("json parse: {e}"))?;
75    Ok(Loaded { data: v, content: source.to_string() })
76  }
77}
78
79/// Ordered loader chain; first match wins. Defaults: Matter, Yaml, Json.
80pub struct LoaderRegistry {
81  loaders: Vec<Box<dyn Loader>>,
82}
83
84impl LoaderRegistry {
85  /// Registry pre-loaded with the three built-in loaders.
86  pub fn with_defaults() -> Self {
87    Self { loaders: vec![Box::new(MatterLoader), Box::new(YamlLoader), Box::new(JsonLoader)] }
88  }
89
90  /// First loader whose `test()` accepts `path`, or `None`.
91  pub fn pick(&self, path: &Path) -> Option<&dyn Loader> {
92    self.loaders.iter().find(|l| l.test(path)).map(|l| l.as_ref())
93  }
94}