dmc_transform/builtin/
component_source.rs1use 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#[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}