dmc_transform/builtin/
component_preview.rs1use crate::pipeline::Transformer;
5use crate::visit::{NodeAction, Visitor, walk_root};
6use dmc_diagnostic::Code;
7use dmc_diagnostic::metadata::SourceMeta;
8use dmc_parser::ast::*;
9use duck_diagnostic::{Diagnostic, Label, diag};
10use std::path::PathBuf;
11
12#[derive(Default)]
16pub struct ComponentPreview {
17 pub registry_index: Option<PathBuf>,
18 pub registry_root: Option<PathBuf>,
19}
20
21impl ComponentPreview {
22 pub fn new(registry_index: PathBuf, registry_root: PathBuf) -> Self {
24 Self { registry_index: Some(registry_index), registry_root: Some(registry_root) }
25 }
26
27 fn lookup_entry<'a>(index: &'a serde_json::Value, name: &str) -> Option<&'a serde_json::Value> {
30 if let Some(arr) = index.as_array() {
31 arr.iter().find(|e| e.get("name").and_then(|v| v.as_str()) == Some(name))
32 } else if let Some(obj) = index.as_object() {
33 obj.get(name)
34 } else {
35 None
36 }
37 }
38
39 fn attr_value(attrs: &[JsxAttr], name: &str) -> Option<String> {
40 attrs.iter().find(|a| a.name == name).and_then(|a| match &a.value {
41 JsxAttrValue::String(s) => Some(s.clone()),
42 _ => None,
43 })
44 }
45}
46
47impl Transformer for ComponentPreview {
48 fn name(&self) -> &str {
49 "component-preview"
50 }
51 fn transform(
52 &self,
53 doc: &mut Document,
54 _meta: &SourceMeta,
55 diag_engine: &mut duck_diagnostic::DiagnosticEngine<Code>,
56 ) {
57 let Some(idx) = &self.registry_index else { return };
59 let Some(root) = &self.registry_root else { return };
60 let raw = match std::fs::read_to_string(idx) {
61 Ok(r) => r,
62 Err(e) => {
63 diag_engine.emit(diag!(
64 Code::RegistryIndexUnreadable,
65 format!("component-preview: cannot read registry index {} ({})", idx.display(), e)
66 ));
67 return;
68 },
69 };
70 let index: serde_json::Value = match serde_json::from_str(&raw) {
71 Ok(v) => v,
72 Err(e) => {
73 diag_engine.emit(diag!(
74 Code::RegistryIndexMalformed,
75 format!("component-preview: registry index {} is not valid JSON ({})", idx.display(), e)
76 ));
77 return;
78 },
79 };
80 let mut v = Apply { index, root: root.clone(), pending: Vec::new() };
81 walk_root(&mut doc.children, &mut v);
82 for d in v.pending.drain(..) {
83 diag_engine.emit(d);
84 }
85 }
86}
87
88struct Apply {
89 index: serde_json::Value,
90 root: PathBuf,
91 pending: Vec<Diagnostic<Code>>,
92}
93
94impl Visitor for Apply {
95 fn visit_node(&mut self, node: &mut Node) -> NodeAction {
96 let (name_opt, span) = match node {
97 Node::JsxSelfClosing(j) if j.name == "ComponentPreview" => {
98 (ComponentPreview::attr_value(&j.attrs, "name"), j.span.clone())
99 },
100 Node::JsxElement(j) if j.name == "ComponentPreview" => {
101 (ComponentPreview::attr_value(&j.attrs, "name"), j.span.clone())
102 },
103 _ => return NodeAction::Keep,
104 };
105 let Some(name) = name_opt else {
106 self.pending.push(
107 diag!(Code::MissingComponentAttr, "component-preview: missing required `name` attribute".to_string())
108 .with_label(Label::primary(span, Some("on this <ComponentPreview>".into()))),
109 );
110 return NodeAction::Keep;
111 };
112 let Some(entry) = ComponentPreview::lookup_entry(&self.index, &name) else {
113 self.pending.push(
114 diag!(Code::RegistryEntryNotFound, format!("component-preview: registry has no entry for `{}`", name))
115 .with_label(Label::primary(span, Some("not found".into()))),
116 );
117 return NodeAction::Keep;
118 };
119 let files = entry.get("files").and_then(|v| v.as_array());
120 let Some(files) = files else {
121 self
122 .pending
123 .push(diag!(Code::RegistryEntryNotFound, format!("component-preview: entry `{}` has no `files` array", name)));
124 return NodeAction::Keep;
125 };
126 let Some(first) = files.first() else {
127 self.pending.push(diag!(
128 Code::RegistryEntryNotFound,
129 format!("component-preview: entry `{}` has empty `files` array", name)
130 ));
131 return NodeAction::Keep;
132 };
133 let path = first.get("path").and_then(|v| v.as_str()).unwrap_or("");
134 let abs = self.root.join(path);
135 match std::fs::read_to_string(&abs) {
136 Ok(content) => {
137 let lang = abs.extension().and_then(|s| s.to_str()).map(String::from);
138 *node = Node::CodeBlock(CodeBlock { lang, meta: Some(format!("title=\"{name}\"")), value: content, span });
139 NodeAction::KeepSkipChildren
140 },
141 Err(e) => {
142 self.pending.push(
143 diag!(Code::RegistrySourceUnreadable, format!("component-preview: cannot read {} ({})", abs.display(), e))
144 .with_label(Label::primary(span, Some(format!("for `{}`", name)))),
145 );
146 NodeAction::Keep
147 },
148 }
149 }
150}