cmark_writer/writer/runtime/visitor/
mod.rs

1//! Shared AST visitor utilities used by writer backends.
2//!
3//! The goal of this module is to provide a single dispatch table for the
4//! `Node` enum so that different backends (CommonMark, HTML, etc.) can
5//! implement `NodeHandler` and reuse the traversal logic without
6//! copy-pasting large `match` expressions.
7
8use crate::ast::{CodeBlockType, CustomNode, HeadingType, HtmlElement, ListItem, Node};
9use ecow::EcoString;
10
11#[cfg(feature = "gfm")]
12use crate::ast::TableAlignment;
13
14/// Trait implemented by writer backends that want to consume the AST.
15#[allow(missing_docs)]
16pub trait NodeHandler {
17    /// Error type produced during traversal.
18    type Error;
19
20    /// Dispatch a single node. Most implementers will not override this and
21    /// will instead implement the per-variant methods below.
22    fn visit_node(&mut self, node: &Node) -> Result<(), Self::Error> {
23        walk_node(self, node)
24    }
25
26    /// Visit a sequence of nodes.
27    fn visit_nodes(&mut self, nodes: &[Node]) -> Result<(), Self::Error> {
28        for node in nodes {
29            self.visit_node(node)?;
30        }
31        Ok(())
32    }
33
34    fn document(&mut self, children: &[Node]) -> Result<(), Self::Error> {
35        self.visit_nodes(children)
36    }
37
38    fn paragraph(&mut self, content: &[Node]) -> Result<(), Self::Error> {
39        self.visit_nodes(content)
40    }
41
42    fn text(&mut self, _text: &EcoString) -> Result<(), Self::Error> {
43        Ok(())
44    }
45
46    fn emphasis(&mut self, content: &[Node]) -> Result<(), Self::Error> {
47        self.visit_nodes(content)
48    }
49
50    fn strong(&mut self, content: &[Node]) -> Result<(), Self::Error> {
51        self.visit_nodes(content)
52    }
53
54    fn thematic_break(&mut self) -> Result<(), Self::Error> {
55        Ok(())
56    }
57
58    fn heading(
59        &mut self,
60        _level: u8,
61        content: &[Node],
62        _heading_type: &HeadingType,
63    ) -> Result<(), Self::Error> {
64        self.visit_nodes(content)
65    }
66
67    fn inline_code(&mut self, _code: &EcoString) -> Result<(), Self::Error> {
68        Ok(())
69    }
70
71    fn code_block(
72        &mut self,
73        _language: &Option<EcoString>,
74        _content: &EcoString,
75        _kind: &CodeBlockType,
76    ) -> Result<(), Self::Error> {
77        Ok(())
78    }
79
80    fn html_block(&mut self, _content: &EcoString) -> Result<(), Self::Error> {
81        Ok(())
82    }
83
84    fn html_element(&mut self, _element: &HtmlElement) -> Result<(), Self::Error> {
85        Ok(())
86    }
87
88    fn block_quote(&mut self, content: &[Node]) -> Result<(), Self::Error> {
89        self.visit_nodes(content)
90    }
91
92    fn unordered_list(&mut self, _items: &[ListItem]) -> Result<(), Self::Error> {
93        Ok(())
94    }
95
96    fn ordered_list(&mut self, _start: u32, _items: &[ListItem]) -> Result<(), Self::Error> {
97        Ok(())
98    }
99
100    #[cfg(feature = "gfm")]
101    fn table(
102        &mut self,
103        _headers: &[Node],
104        _alignments: &[TableAlignment],
105        _rows: &[Vec<Node>],
106    ) -> Result<(), Self::Error> {
107        Ok(())
108    }
109
110    #[cfg(not(feature = "gfm"))]
111    fn table(&mut self, _headers: &[Node], _rows: &[Vec<Node>]) -> Result<(), Self::Error> {
112        Ok(())
113    }
114
115    fn link(
116        &mut self,
117        _url: &EcoString,
118        _title: &Option<EcoString>,
119        content: &[Node],
120    ) -> Result<(), Self::Error> {
121        self.visit_nodes(content)
122    }
123
124    fn image(
125        &mut self,
126        _url: &EcoString,
127        _title: &Option<EcoString>,
128        _alt: &[Node],
129    ) -> Result<(), Self::Error> {
130        Ok(())
131    }
132
133    fn soft_break(&mut self) -> Result<(), Self::Error> {
134        Ok(())
135    }
136
137    fn hard_break(&mut self) -> Result<(), Self::Error> {
138        Ok(())
139    }
140
141    fn autolink(&mut self, _url: &EcoString, _is_email: bool) -> Result<(), Self::Error> {
142        Ok(())
143    }
144
145    #[cfg(feature = "gfm")]
146    fn extended_autolink(&mut self, _url: &EcoString) -> Result<(), Self::Error> {
147        Ok(())
148    }
149
150    fn link_reference_definition(
151        &mut self,
152        _label: &EcoString,
153        _destination: &EcoString,
154        _title: &Option<EcoString>,
155    ) -> Result<(), Self::Error> {
156        Ok(())
157    }
158
159    fn reference_link(&mut self, _label: &EcoString, content: &[Node]) -> Result<(), Self::Error> {
160        self.visit_nodes(content)
161    }
162
163    #[cfg(feature = "gfm")]
164    fn strikethrough(&mut self, content: &[Node]) -> Result<(), Self::Error> {
165        self.visit_nodes(content)
166    }
167
168    fn custom(&mut self, _node: &dyn CustomNode) -> Result<(), Self::Error> {
169        Ok(())
170    }
171
172    fn unsupported(&mut self, _node: &Node) -> Result<(), Self::Error> {
173        Ok(())
174    }
175}
176
177/// Dispatch a single node to the provided handler.
178pub fn walk_node<H: NodeHandler + ?Sized>(handler: &mut H, node: &Node) -> Result<(), H::Error> {
179    match node {
180        Node::Document(children) => handler.document(children),
181        Node::Paragraph(content) => handler.paragraph(content),
182        Node::Text(text) => handler.text(text),
183        Node::Emphasis(content) => handler.emphasis(content),
184        Node::Strong(content) => handler.strong(content),
185        Node::ThematicBreak => handler.thematic_break(),
186        Node::Heading {
187            level,
188            content,
189            heading_type,
190        } => handler.heading(*level, content, heading_type),
191        Node::InlineCode(code) => handler.inline_code(code),
192        Node::CodeBlock {
193            language,
194            content,
195            block_type,
196        } => handler.code_block(language, content, block_type),
197        Node::HtmlBlock(content) => handler.html_block(content),
198        Node::HtmlElement(element) => handler.html_element(element),
199        Node::BlockQuote(content) => handler.block_quote(content),
200        Node::UnorderedList(items) => handler.unordered_list(items),
201        Node::OrderedList { start, items } => handler.ordered_list(*start, items),
202        Node::Link {
203            url,
204            title,
205            content,
206        } => handler.link(url, title, content),
207        Node::Image { url, title, alt } => handler.image(url, title, alt),
208        Node::Autolink { url, is_email } => handler.autolink(url, *is_email),
209        Node::SoftBreak => handler.soft_break(),
210        Node::HardBreak => handler.hard_break(),
211        Node::LinkReferenceDefinition {
212            label,
213            destination,
214            title,
215        } => handler.link_reference_definition(label, destination, title),
216        Node::ReferenceLink { label, content } => handler.reference_link(label, content),
217        Node::Custom(custom_node) => handler.custom(custom_node.as_ref()),
218        #[cfg(feature = "gfm")]
219        Node::Table {
220            headers,
221            alignments,
222            rows,
223        } => handler.table(headers, alignments, rows),
224        #[cfg(not(feature = "gfm"))]
225        Node::Table { headers, rows, .. } => handler.table(headers, rows),
226        #[cfg(feature = "gfm")]
227        Node::Strikethrough(content) => handler.strikethrough(content),
228        #[cfg(feature = "gfm")]
229        Node::ExtendedAutolink(url) => handler.extended_autolink(url),
230        #[allow(unreachable_patterns)]
231        _ => handler.unsupported(node),
232    }
233}