Skip to main content

dmc_transform/builtin/
component_source.rs

1use crate::pipeline::Transformer;
2use crate::visit::{NodeAction, Visitor, walk_root};
3use dmc_diagnostic::Code;
4use dmc_diagnostic::metadata::{Origin, SourceMeta};
5use dmc_parser::ast::*;
6use duck_diagnostic::{Diagnostic, Label};
7use std::path::PathBuf;
8use std::sync::Arc;
9
10/// Replace `<ComponentSource path="..." />` with a `CodeBlock` carrying the
11/// file contents (resolved against `base_dir`). `lang` comes from the file
12/// extension. Path resolution mirrors [`CodeImport`].
13#[derive(Default)]
14pub struct ComponentSource {
15  pub base_dir: Option<PathBuf>,
16}
17
18impl ComponentSource {
19  pub fn with_base_dir(p: impl Into<PathBuf>) -> Self {
20    Self { base_dir: Some(p.into()) }
21  }
22
23  fn attr_value(attrs: &[JsxAttr], name: &str) -> Option<String> {
24    attrs.iter().find(|a| a.name == name).and_then(|a| match &a.value {
25      JsxAttrValue::String(s) => Some(s.clone()),
26      _ => None,
27    })
28  }
29}
30
31impl Transformer for ComponentSource {
32  fn name(&self) -> &str {
33    "component-source"
34  }
35  fn transform(
36    &self,
37    doc: &mut Document,
38    meta: &SourceMeta,
39    diag_engine: &mut duck_diagnostic::DiagnosticEngine<Code>,
40  ) {
41    let base_dir = self.base_dir.clone().or_else(|| match &meta.origin {
42      Origin::File(p) => p.parent().map(|p| p.to_path_buf()),
43      _ => None,
44    });
45
46    if base_dir.is_none() && self.base_dir.is_none() {
47      diag_engine.emit(Diagnostic::new(
48        Code::BaseDirNotFound,
49        format!(
50          "component-source: source has no on-disk parent (origin = {:?}); relative `path=` cannot be resolved",
51          meta.origin
52        ),
53      ));
54    }
55
56    let mut v = Apply { base_dir, meta_path: meta.path.clone(), pending: Vec::new() };
57    walk_root(&mut doc.children, &mut v);
58    for d in v.pending.drain(..) {
59      diag_engine.emit(d);
60    }
61  }
62}
63
64struct Apply {
65  base_dir: Option<PathBuf>,
66  meta_path: Arc<str>,
67  pending: Vec<Diagnostic<Code>>,
68}
69
70impl Visitor for Apply {
71  fn visit_node(&mut self, node: &mut Node) -> NodeAction {
72    let (path, span) = match node {
73      Node::JsxSelfClosing(j) if j.name == "ComponentSource" => {
74        (ComponentSource::attr_value(&j.attrs, "path"), j.span.clone())
75      },
76      Node::JsxElement(j) if j.name == "ComponentSource" => {
77        (ComponentSource::attr_value(&j.attrs, "path"), j.span.clone())
78      },
79      _ => return NodeAction::Keep,
80    };
81    let Some(rel) = path else {
82      self.pending.push(
83        Diagnostic::new(Code::MissingComponentAttr, "component-source: missing required `path` attribute".to_string())
84          .with_label(Label::primary(span, Some("on this <ComponentSource>".into()))),
85      );
86      return NodeAction::Keep;
87    };
88    let abs = match &self.base_dir {
89      Some(b) => b.join(&rel),
90      None => PathBuf::from(&rel),
91    };
92    match std::fs::read_to_string(&abs) {
93      Ok(content) => {
94        let lang = abs.extension().and_then(|s| s.to_str()).map(String::from);
95        *node = Node::CodeBlock(CodeBlock { lang, meta: Some(format!("title=\"{}\"", rel)), value: content, span });
96        NodeAction::KeepSkipChildren
97      },
98      Err(e) => {
99        self.pending.push(
100          Diagnostic::new(
101            Code::ComponentSourceUnreadable,
102            format!("component-source: cannot read {} ({})", abs.display(), e),
103          )
104          .with_label(Label::primary(span, Some(format!("from {}", self.meta_path)))),
105        );
106        NodeAction::Keep
107      },
108    }
109  }
110}