dmc_transform/builtin/
component_source.rs1use crate::pipeline::Transformer;
5use crate::visit::{NodeAction, Visitor, walk_root};
6use dmc_diagnostic::metadata::{Origin, SourceMeta};
7use dmc_diagnostic::{Code, DiagResult};
8use dmc_parser::ast::*;
9use duck_diagnostic::{Diagnostic, Label, diag};
10use std::path::PathBuf;
11use std::sync::Arc;
12
13#[derive(Default)]
19pub struct ComponentSource {
20 pub base_dir: Option<PathBuf>,
21}
22
23impl ComponentSource {
24 pub fn with_base_dir(p: impl Into<PathBuf>) -> Self {
25 Self { base_dir: Some(p.into()) }
26 }
27
28 fn attr_value(attrs: &[JsxAttr], name: &str) -> Option<String> {
29 attrs.iter().find(|a| a.name == name).and_then(|a| match &a.value {
30 JsxAttrValue::String(s) => Some(s.clone()),
31 _ => None,
32 })
33 }
34}
35
36impl Transformer for ComponentSource {
37 fn name(&self) -> &str {
38 "component-source"
39 }
40 fn transform(
41 &self,
42 doc: &mut Document,
43 meta: &SourceMeta,
44 diag_engine: &mut duck_diagnostic::DiagnosticEngine<Code>,
45 ) {
46 let base_dir = self.base_dir.clone().or_else(|| match &meta.origin {
47 Origin::File(p) => p.parent().map(|p| p.to_path_buf()),
48 _ => None,
49 });
50
51 if base_dir.is_none() && self.base_dir.is_none() {
52 diag_engine.emit(diag!(
53 Code::BaseDirNotFound,
54 format!(
55 "component-source: source has no on-disk parent (origin = {:?}); relative `path=` cannot be resolved",
56 meta.origin
57 )
58 ));
59 }
60
61 let mut v = Apply { base_dir, meta_path: meta.path.clone(), pending: Vec::new() };
62 walk_root(&mut doc.children, &mut v);
63 for d in v.pending.drain(..) {
64 diag_engine.emit(d);
65 }
66 }
67}
68
69struct Apply {
70 base_dir: Option<PathBuf>,
71 meta_path: Arc<str>,
72 pending: Vec<Diagnostic<Code>>,
73}
74
75#[allow(clippy::result_large_err)]
79fn make_code_block(abs: &PathBuf, rel_label: &str, span: &duck_diagnostic::Span) -> DiagResult<Node> {
80 let content =
81 std::fs::read_to_string(abs).map_err(|e| diag!(Code::IoRead, format!("read {}: {}", abs.display(), e)))?;
82 let lang = abs.extension().and_then(|s| s.to_str()).map(String::from);
83 Ok(Node::CodeBlock(CodeBlock {
84 lang,
85 meta: Some(format!("title=\"{}\"", rel_label)),
86 value: content,
87 span: span.clone(),
88 }))
89}
90
91#[allow(clippy::result_large_err)]
94fn collect_blocks(abs: &PathBuf, rel: &str, span: &duck_diagnostic::Span) -> DiagResult<Vec<Node>> {
95 let stat = std::fs::metadata(abs).map_err(|e| diag!(Code::IoRead, format!("stat {}: {}", abs.display(), e)))?;
96 if stat.is_dir() {
97 let mut entries: Vec<_> = std::fs::read_dir(abs)
98 .map_err(|e| diag!(Code::IoRead, format!("read_dir {}: {}", abs.display(), e)))?
99 .filter_map(|e| e.ok())
100 .filter(|e| e.file_type().map(|t| t.is_file()).unwrap_or(false))
101 .collect();
102 entries.sort_by_key(|e| e.file_name());
103 let mut blocks = Vec::with_capacity(entries.len());
104 for e in entries {
105 let path = e.path();
106 let label = e.file_name().to_string_lossy().into_owned();
107 blocks.push(make_code_block(&path, &label, span)?);
108 }
109 Ok(blocks)
110 } else {
111 let label = abs.file_name().map(|s| s.to_string_lossy().into_owned()).unwrap_or_else(|| rel.to_string());
112 Ok(vec![make_code_block(abs, &label, span)?])
113 }
114}
115
116impl Visitor for Apply {
117 fn visit_node(&mut self, node: &mut Node) -> NodeAction {
118 let (path_attr, attrs, span, was_self_closing) = match node {
119 Node::JsxSelfClosing(j) if j.name == "ComponentSource" => {
120 (ComponentSource::attr_value(&j.attrs, "path"), std::mem::take(&mut j.attrs), j.span.clone(), true)
121 },
122 Node::JsxElement(j) if j.name == "ComponentSource" => {
123 (ComponentSource::attr_value(&j.attrs, "path"), std::mem::take(&mut j.attrs), j.span.clone(), false)
124 },
125 _ => return NodeAction::Keep,
126 };
127
128 let Some(rel) = path_attr else {
129 self.pending.push(
130 diag!(Code::MissingComponentAttr, "component-source: missing required `path` attribute".to_string())
131 .with_label(Label::primary(span, Some("on this <ComponentSource>".into()))),
132 );
133 if let Node::JsxSelfClosing(j) = node {
135 j.attrs = attrs;
136 } else if let Node::JsxElement(j) = node {
137 j.attrs = attrs;
138 }
139 return NodeAction::Keep;
140 };
141
142 let abs = match &self.base_dir {
143 Some(b) => b.join(&rel),
144 None => PathBuf::from(&rel),
145 };
146 let children = match collect_blocks(&abs, &rel, &span) {
147 Ok(bs) => bs,
148 Err(e) => {
149 self.pending.push(
150 diag!(
151 Code::ComponentSourceUnreadable,
152 format!("component-source: cannot read {} ({})", abs.display(), e.message)
153 )
154 .with_label(Label::primary(span.clone(), Some(format!("from {}", self.meta_path)))),
155 );
156 if was_self_closing {
157 if let Node::JsxSelfClosing(j) = node {
158 j.attrs = attrs;
159 }
160 } else if let Node::JsxElement(j) = node {
161 j.attrs = attrs;
162 }
163 return NodeAction::Keep;
164 },
165 };
166
167 *node = Node::JsxElement(JsxElement { name: "ComponentSource".into(), attrs, children, span });
171 NodeAction::KeepSkipChildren
172 }
173}