1use dmc_diagnostic::Code;
2use dmc_parser::ast::*;
3use duck_diagnostic::{Diagnostic, DiagnosticEngine};
4
5use crate::{NodeSink, WalkCtx, Walker};
6
7#[derive(Debug)]
25pub struct MdxBodyEmitter {
26 stack: Vec<Frame>,
27 imports: Vec<String>,
28 exports: Vec<String>,
29 diag_engine: DiagnosticEngine<Code>,
30 in_table_depth: usize,
31}
32
33#[derive(Default, Debug)]
34struct Frame {
35 parts: Vec<String>,
36}
37
38impl NodeSink for MdxBodyEmitter {
39 fn enter(&mut self, node: &Node, _ctx: &WalkCtx) {
40 if self.in_table_depth > 0 {
41 return;
42 }
43 match node {
44 Node::Text(t) => self.push_part(Self::js_string(&t.value)),
45 Node::InlineCode(c) => {
46 self.push_part(format!("jsx(\"code\", {{ children: {} }})", Self::js_string(&c.value)));
47 },
48 Node::CodeBlock(cb) => self.push_part(self.code_block_expr(cb)),
49 Node::Image(i) => self.push_part(self.image_expr(i)),
50 Node::HorizontalRule(_) => self.push_part("jsx(\"hr\", {})".to_string()),
51 Node::HardBreak(_) => self.push_part("jsx(\"br\", {})".to_string()),
52 Node::SoftBreak(_) => self.push_part(Self::js_string("\n")),
53 Node::JsxSelfClosing(s) => {
54 let expr = self.jsx_self_closing_expr(s);
55 self.push_part(expr);
56 },
57 Node::JsxExpression(j) => self.push_part(j.value.trim().to_string()),
58
59 Node::Table(t) => {
60 let expr = self.table_expr(t);
61 self.push_part(expr);
62 self.in_table_depth += 1;
63 },
64
65 Node::Frontmatter(_) => {},
66 Node::Import(i) => self.imports.push(i.raw.trim_end().to_string()),
67 Node::Export(x) => self.exports.push(x.raw.trim_end().to_string()),
68
69 _ => self.open_frame(node),
70 }
71 }
72
73 fn leave(&mut self, node: &Node, _ctx: &WalkCtx) {
74 if let Node::Table(_) = node {
75 self.in_table_depth = self.in_table_depth.saturating_sub(1);
76 return;
77 }
78 if self.in_table_depth > 0 {
79 return;
80 }
81 self.close_frame(node);
82 }
83}
84
85impl Default for MdxBodyEmitter {
86 fn default() -> Self {
87 Self::new()
88 }
89}
90
91impl MdxBodyEmitter {
92 pub fn new() -> Self {
93 Self {
94 stack: vec![Frame::default()],
95 imports: Vec::new(),
96 exports: Vec::new(),
97 diag_engine: DiagnosticEngine::new(),
98 in_table_depth: 0,
99 }
100 }
101
102 pub fn render(doc: &Document) -> (String, DiagnosticEngine<Code>) {
105 let mut emitter = Self::new();
106 Walker::new(doc).walk(&mut [&mut emitter]);
107 emitter.into_parts()
108 }
109
110 pub fn into_parts(self) -> (String, DiagnosticEngine<Code>) {
113 let diag = self.diag_engine;
114 let body_str = Self::assemble(self.stack, self.imports, self.exports);
115 (body_str, diag)
116 }
117
118 fn assemble(stack: Vec<Frame>, imports: Vec<String>, exports: Vec<String>) -> String {
119 let root_parts = stack.into_iter().next().map(|f| f.parts).unwrap_or_default();
120 let body = format!("jsxs(Fragment, {{ children: [{}] }})", root_parts.join(", "));
121 let mut prelude = String::new();
122 for i in &imports {
123 prelude.push_str(i);
124 prelude.push('\n');
125 }
126 for e in &exports {
127 prelude.push_str(e);
128 prelude.push('\n');
129 }
130 format!(
131 "{prelude}function _createMdxContent(props) {{\n const _components = (props && props.components) || {{}};\n const {{ Fragment, jsx, jsxs }} = arguments[0];\n return {body};\n}}\nreturn _createMdxContent(arguments[0]);\n",
132 )
133 }
134
135 pub fn into_string(self) -> String {
138 Self::assemble(self.stack, self.imports, self.exports)
139 }
140
141 fn diag(&mut self, code: Code, message: impl Into<String>) {
142 self.diag_engine.emit(Diagnostic::new(code, message.into()));
143 }
144
145 fn open_frame(&mut self, _node: &Node) {
149 self.stack.push(Frame::default());
150 }
151
152 fn close_frame(&mut self, node: &Node) {
155 if !Self::is_container(node) {
156 return;
157 }
158 let kids = self.pop_kids_array();
159 let expr = match node {
160 Node::Heading(h) => {
161 format!("jsxs(\"h{}\", {{ id: {}, children: {} }})", h.level, Self::js_string(&h.slug()), kids,)
162 },
163 Node::Paragraph(_) => format!("jsxs(\"p\", {{ children: {} }})", kids),
164 Node::Bold(_) => format!("jsxs(\"strong\", {{ children: {} }})", kids),
165 Node::Italic(_) => format!("jsxs(\"em\", {{ children: {} }})", kids),
166 Node::Strikethrough(_) => format!("jsxs(\"del\", {{ children: {} }})", kids),
167 Node::Blockquote(_) => format!("jsxs(\"blockquote\", {{ children: {} }})", kids),
168 Node::List(l) => {
169 let tag = if l.ordered { "ol" } else { "ul" };
170 format!("jsxs(\"{}\", {{ children: {} }})", tag, kids)
171 },
172 Node::ListItem(_) | Node::TaskListItem(_) => format!("jsxs(\"li\", {{ children: {} }})", kids),
173 Node::Link(l) => {
174 let mut props = format!("href: {}", Self::js_string(&l.href));
175 if let Some(title) = &l.title {
176 props.push_str(&format!(", \"aria-label\": {}", Self::js_string(title)));
177 }
178 format!("jsxs(\"a\", {{ {}, children: {} }})", props, kids)
179 },
180 Node::JsxElement(e) => self.jsx_element_expr(e, kids),
181 Node::JsxFragment(_) => format!("jsxs(Fragment, {{ children: {} }})", kids),
182 _ => unreachable!("is_container guards every other variant"),
183 };
184 self.push_part(expr);
185 }
186
187 fn is_container(n: &Node) -> bool {
189 matches!(
190 n,
191 Node::Heading(_)
192 | Node::Paragraph(_)
193 | Node::Bold(_)
194 | Node::Italic(_)
195 | Node::Strikethrough(_)
196 | Node::Blockquote(_)
197 | Node::List(_)
198 | Node::ListItem(_)
199 | Node::TaskListItem(_)
200 | Node::Link(_)
201 | Node::JsxElement(_)
202 | Node::JsxFragment(_)
203 )
204 }
205
206 fn pop_kids_array(&mut self) -> String {
208 let parts = self.stack.pop().map(|f| f.parts).unwrap_or_default();
209 format!("[{}]", parts.join(", "))
210 }
211
212 fn push_part(&mut self, expr: String) {
214 if let Some(frame) = self.stack.last_mut() {
215 frame.parts.push(expr);
216 }
217 }
218
219 fn code_block_expr(&self, cb: &CodeBlock) -> String {
222 match &cb.lang {
223 Some(lang) => format!(
224 "jsx(\"pre\", {{ children: jsx(\"code\", {{ className: {}, children: {} }}) }})",
225 Self::js_string(&format!("gentledmc-language-{}", lang)),
226 Self::js_string(&cb.value),
227 ),
228 None => format!("jsx(\"pre\", {{ children: jsx(\"code\", {{ children: {} }}) }})", Self::js_string(&cb.value),),
229 }
230 }
231
232 fn image_expr(&self, i: &Image) -> String {
233 format!("jsx(\"img\", {{ src: {}, alt: {} }})", Self::js_string(&i.src), Self::js_string(&i.alt))
234 }
235
236 fn jsx_element_expr(&mut self, e: &JsxElement, kids: String) -> String {
237 if e.name.is_empty() {
238 self.diag(Code::MalformedJsxTagName, "mdx-body: JSX element has empty name; rendered as Fragment".to_string());
239 return format!("jsxs(Fragment, {{ children: {} }})", kids);
240 }
241 let mut props = self.jsx_props(&e.attrs);
242 if !props.is_empty() {
243 props.push_str(", ");
244 }
245 format!("jsxs({}, {{ {}children: {} }})", e.name, props, kids)
246 }
247
248 fn jsx_self_closing_expr(&mut self, s: &JsxSelfClosing) -> String {
249 if s.name.is_empty() {
250 self.diag(Code::MalformedJsxTagName, "mdx-body: self-closing JSX has empty name; emitted as null".to_string());
251 return "null".to_string();
252 }
253 let props = self.jsx_props(&s.attrs);
254 format!("jsx({}, {{ {} }})", s.name, props)
255 }
256
257 fn jsx_props(&self, attrs: &[JsxAttr]) -> String {
258 let mut parts = Vec::new();
259 for a in attrs {
260 let key = format!("\"{}\"", a.name);
261 let v = match &a.value {
262 JsxAttrValue::String(s) => Self::js_string(s),
263 JsxAttrValue::Expression(e) => e.trim().to_string(),
264 JsxAttrValue::Boolean => "true".to_string(),
265 };
266 parts.push(format!("{}: {}", key, v));
267 }
268 parts.join(", ")
269 }
270
271 fn table_expr(&mut self, t: &Table) -> String {
275 let mut sections: Vec<String> = Vec::new();
276
277 if let Some(header) = t.children.first() {
278 let mut head_cells: Vec<String> = Vec::with_capacity(header.cells.len());
279 for (i, cell) in header.cells.iter().enumerate() {
280 let align = t.align.get(i).copied().unwrap_or(TableAlign::None);
281 head_cells.push(self.table_cell_expr("th", cell, align));
282 }
283 let head_row = format!("jsxs(\"tr\", {{ children: [{}] }})", head_cells.join(", "));
284 sections.push(format!("jsxs(\"thead\", {{ children: [{}] }})", head_row));
285 }
286
287 if t.children.len() > 1 {
288 let mut body_rows: Vec<String> = Vec::with_capacity(t.children.len() - 1);
289 for row in &t.children[1..] {
290 let mut row_cells: Vec<String> = Vec::with_capacity(row.cells.len());
291 for (i, cell) in row.cells.iter().enumerate() {
292 let align = t.align.get(i).copied().unwrap_or(TableAlign::None);
293 row_cells.push(self.table_cell_expr("td", cell, align));
294 }
295 body_rows.push(format!("jsxs(\"tr\", {{ children: [{}] }})", row_cells.join(", ")));
296 }
297 sections.push(format!("jsxs(\"tbody\", {{ children: [{}] }})", body_rows.join(", ")));
298 }
299
300 format!("jsxs(\"table\", {{ children: [{}] }})", sections.join(", "))
301 }
302
303 fn table_cell_expr(&mut self, tag: &str, cell: &TableCell, align: TableAlign) -> String {
304 let kids: Vec<String> = cell.children.iter().map(|n| self.inline_expr(n)).collect();
305 let kids_arr = format!("[{}]", kids.join(", "));
306 let align_str = match align {
307 TableAlign::Left => Some("left"),
308 TableAlign::Right => Some("right"),
309 TableAlign::Center => Some("center"),
310 TableAlign::None => None,
311 };
312 match align_str {
313 Some(a) => format!("jsxs(\"{}\", {{ align: {}, children: {} }})", tag, Self::js_string(a), kids_arr,),
314 None => format!("jsxs(\"{}\", {{ children: {} }})", tag, kids_arr),
315 }
316 }
317
318 fn inline_expr(&mut self, node: &Node) -> String {
321 match node {
322 Node::Text(t) => Self::js_string(&t.value),
323 Node::InlineCode(c) => format!("jsx(\"code\", {{ children: {} }})", Self::js_string(&c.value)),
324 Node::CodeBlock(cb) => self.code_block_expr(cb),
325 Node::Image(i) => self.image_expr(i),
326 Node::HorizontalRule(_) => "jsx(\"hr\", {})".to_string(),
327 Node::HardBreak(_) => "jsx(\"br\", {})".to_string(),
328 Node::SoftBreak(_) => Self::js_string("\n"),
329 Node::JsxSelfClosing(s) => self.jsx_self_closing_expr(s),
330 Node::JsxExpression(j) => j.value.trim().to_string(),
331 Node::Bold(i) => self.wrap_jsxs("strong", &i.children),
332 Node::Italic(i) => self.wrap_jsxs("em", &i.children),
333 Node::Strikethrough(i) => self.wrap_jsxs("del", &i.children),
334 Node::Paragraph(p) => self.wrap_jsxs("p", &p.children),
335 Node::Blockquote(b) => self.wrap_jsxs("blockquote", &b.children),
336 Node::List(l) => {
337 let tag = if l.ordered { "ol" } else { "ul" };
338 self.wrap_jsxs(tag, &l.children)
339 },
340 Node::ListItem(li) => self.wrap_jsxs("li", &li.children),
341 Node::TaskListItem(t) => self.wrap_jsxs("li", &t.children),
342 Node::Heading(h) => {
343 let kids: Vec<String> = h.children.iter().map(|n| self.inline_expr(n)).collect();
344 format!("jsxs(\"h{}\", {{ id: {}, children: [{}] }})", h.level, Self::js_string(&h.slug()), kids.join(", "),)
345 },
346 Node::Link(l) => {
347 let kids: Vec<String> = l.children.iter().map(|n| self.inline_expr(n)).collect();
348 let mut props = format!("href: {}", Self::js_string(&l.href));
349 if let Some(title) = &l.title {
350 props.push_str(&format!(", \"aria-label\": {}", Self::js_string(title)));
351 }
352 format!("jsxs(\"a\", {{ {}, children: [{}] }})", props, kids.join(", "))
353 },
354 Node::JsxElement(e) => {
355 let kids: Vec<String> = e.children.iter().map(|n| self.inline_expr(n)).collect();
356 let kids_arr = format!("[{}]", kids.join(", "));
357 self.jsx_element_expr(e, kids_arr)
358 },
359 Node::JsxFragment(f) => {
360 let kids: Vec<String> = f.children.iter().map(|n| self.inline_expr(n)).collect();
361 format!("jsxs(Fragment, {{ children: [{}] }})", kids.join(", "))
362 },
363 Node::Table(t) => self.table_expr(t),
364 Node::Frontmatter(_)
365 | Node::Import(_)
366 | Node::Export(_)
367 | Node::Document(_)
368 | Node::TableRow(_)
369 | Node::TableCell(_) => "null".to_string(),
370 }
371 }
372
373 fn wrap_jsxs(&mut self, tag: &str, children: &[Node]) -> String {
374 let kids: Vec<String> = children.iter().map(|n| self.inline_expr(n)).collect();
375 format!("jsxs(\"{}\", {{ children: [{}] }})", tag, kids.join(", "))
376 }
377
378 fn js_string(s: &str) -> String {
383 let mut out = String::with_capacity(s.len() + 2);
384 out.push('"');
385 for ch in s.chars() {
386 match ch {
387 '\\' => out.push_str("\\\\"),
388 '"' => out.push_str("\\\""),
389 '\n' => out.push_str("\\n"),
390 '\r' => out.push_str("\\r"),
391 '\t' => out.push_str("\\t"),
392 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
393 c => out.push(c),
394 }
395 }
396 out.push('"');
397 out
398 }
399}
400
401pub fn render_mdx_body(doc: &Document) -> String {
404 MdxBodyEmitter::render(doc).0
405}