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