Skip to main content

dmc_codegen/
lib.rs

1//! Codegen layer: turn a parsed `Document` into renderable output.
2//!
3//! Two emitters live here:
4//! - [`HtmlEmitter`] - static HTML (SSR / SSG output).
5//! - [`MdxBodyEmitter`] - JS body for MDX runtime React rendering.
6//!
7//! Both implement [`NodeSink`]. A single [`Walker`] does one pre-order
8//! DFS, fanning each node to every active sink - no N tree traversals.
9mod escape;
10pub mod html;
11pub mod mdx;
12use dmc_parser::ast::{Document, Node};
13pub use html::{HtmlEmitter, render_html};
14pub use mdx::{MdxBodyEmitter, render_mdx_body};
15
16/// Callback pair invoked by [`Walker`] at every node.
17pub trait NodeSink {
18  fn enter(&mut self, node: &Node, ctx: &WalkCtx);
19  fn leave(&mut self, _node: &Node, _ctx: &WalkCtx) {}
20}
21
22/// Position info handed to every sink callback. Read-only.
23pub struct WalkCtx<'a> {
24  /// Ancestor count above this node. Top-level children = 0.
25  pub depth: usize,
26  /// Index among parent's children. 0-based.
27  pub index: usize,
28  /// `None` when visiting a top-level child of the document.
29  pub parent: Option<&'a Node>,
30}
31
32impl<'a> WalkCtx<'a> {
33  pub fn root() -> Self {
34    Self { depth: 0, index: 0, parent: None }
35  }
36  pub fn child(&self, parent: &'a Node, index: usize) -> Self {
37    Self { depth: self.depth + 1, index, parent: Some(parent) }
38  }
39}
40
41/// Pre-order DFS over `doc.children`. At every node, every sink's
42/// `enter` fires (in slice order); the walker then recurses into the
43/// node's children; finally every sink's `leave` fires (in reverse
44/// slice order, LIFO).
45///
46/// `Document` itself is not surfaced as a `Node::Document` event; the
47/// walker iterates `doc.children` directly. Sinks needing a document
48/// boundary subscribe to `Frontmatter` or to their first node.
49pub struct Walker<'a> {
50  doc: &'a Document,
51}
52
53impl<'a> Walker<'a> {
54  pub fn new(doc: &'a Document) -> Self {
55    Self { doc }
56  }
57
58  /// Drive the walk. `enter` fires slice-order, `leave` fires LIFO.
59  pub fn walk(self, sinks: &mut [&mut dyn NodeSink]) {
60    for (i, child) in self.doc.children.iter().enumerate() {
61      Self::walk_node(child, &WalkCtx { depth: 0, index: i, parent: None }, sinks);
62    }
63  }
64
65  fn walk_node(node: &'a Node, ctx: &WalkCtx<'a>, sinks: &mut [&mut dyn NodeSink]) {
66    for sink in sinks.iter_mut() {
67      sink.enter(node, ctx);
68    }
69    match node {
70      // Table rows/cells aren't `Node`s - handled inline.
71      Node::Table(t) => {
72        for row in &t.children {
73          for cell in &row.cells {
74            for (i, kid) in cell.children.iter().enumerate() {
75              Self::walk_node(kid, &ctx.child(node, i), sinks);
76            }
77          }
78        }
79      },
80      _ => {
81        for (i, kid) in Node::children_of(node).iter().enumerate() {
82          Self::walk_node(kid, &ctx.child(node, i), sinks);
83        }
84      },
85    }
86    for sink in sinks.iter_mut().rev() {
87      sink.leave(node, ctx);
88    }
89  }
90}