markdown_walker/
lib.rs

1#![deny(missing_docs)]
2//! # Markdown Walker
3//!
4//! A markdown walker trait for traversing markdown AST made by the Comrak crate.
5//!
6//! ## Example
7//!
8//! ```rust
9//! use std::{cell::RefCell, io};
10//! use comrak::{arena_tree::Node, nodes::{Ast, NodeLink}};
11//! use markdown_walker::MarkdownWalker;
12//!
13//! #[derive(Debug, Default, PartialEq)]
14//! struct ImageCount(usize);
15//!
16//! impl MarkdownWalker for ImageCount {
17//!     fn visit_image<'arena>(
18//!         &mut self,
19//!         _node: &'arena Node<'arena, RefCell<Ast>>,
20//!         _link: &NodeLink,
21//!     ) -> io::Result<()> {
22//!         self.0 += 1;
23//!         Ok(())
24//!     }
25//! }
26//!
27//! #[test]
28//! fn test_image_count() {
29//!     let markdown = r#"
30//! ![Image 1](image1.png)
31//! ![Image 2](image2.png)
32//! ![Image 3](image3.png)
33//! "#;
34//!
35//!     let image_count = ImageCount::from_markdown(markdown).unwrap();
36//!     assert_eq!(image_count, ImageCount(3));
37//! }
38//!
39//! ```
40
41use std::{cell::RefCell, io};
42
43use comrak::{
44    arena_tree::Node,
45    nodes::{
46        Ast, NodeCode, NodeCodeBlock, NodeDescriptionItem, NodeFootnoteDefinition,
47        NodeFootnoteReference, NodeHeading, NodeHtmlBlock, NodeLink, NodeList, NodeMath,
48        NodeMultilineBlockQuote, NodeShortCode, NodeTable, NodeValue, NodeWikiLink,
49    },
50};
51
52#[allow(unused_variables)]
53/// The main trait we export from this crate. This gives you the ability to build your own
54/// types from a given markdown AST made by the Comrak crate. The trait has a default
55/// implementation for every one of its methods, so you can choose to override only the
56/// methods you need for your type.
57///
58/// ## From Markdown
59///
60/// Any type which implements [`Default`] can be created from a markdown string. The default
61/// trait lets the walker initialize the type with default values and then parse given markdown
62/// string and return the type.
63///
64/// See the [`from_markdown`] method for more information.
65///
66/// ## Visit
67///
68/// The [`visit`] method is the main method you need to implement to build your type from the
69/// markdown AST directly. This method leaves a bit more work on the table for you, but you
70/// can leverage it for a more performant traversal, since you'll only be marshaling the AST
71/// once.
72///
73/// [`Default`]: https://doc.rust-lang.org/std/default/trait.Default.html
74/// [`from_markdown`]: #method.from_markdown
75/// [`visit`]: #method.visit
76///
77pub trait MarkdownWalker {
78    /// Create a new instance of the type from a markdown string. This method is only available
79    /// for types that implement the [`Default`] trait. See the [`visit`] method for the lower
80    /// level API counterpart.
81    ///
82    /// [`visit`]: #method.visit
83    fn from_markdown(markdown: impl AsRef<str>) -> io::Result<Self>
84    where
85        Self: Default,
86    {
87        use comrak::{parse_document, Arena, ExtensionOptions, Options};
88
89        let arena = Arena::new();
90        let extension = ExtensionOptions::builder()
91            .autolink(true)
92            .description_lists(true)
93            .footnotes(true)
94            .math_code(true)
95            .math_dollars(true)
96            .multiline_block_quotes(true)
97            .shortcodes(true)
98            .spoiler(true)
99            .strikethrough(true)
100            .subscript(true)
101            .superscript(true)
102            .table(true)
103            .tagfilter(true)
104            .tasklist(true)
105            .underline(true)
106            .wikilinks_title_after_pipe(true)
107            .wikilinks_title_before_pipe(true)
108            .build();
109
110        let options = Options {
111            extension,
112            ..Options::default()
113        };
114
115        let nodes = parse_document(&arena, markdown.as_ref(), &options);
116
117        let mut this = Self::default();
118        this.visit(nodes)?;
119
120        Ok(this)
121    }
122
123    /// Visit a node in the markdown AST. This method recursively traverses the AST and calls
124    /// the appropriate method for each node type. This method powers [`from_markdown`], but
125    /// works on the direct AST instead of a markdown string.
126    ///
127    /// [`from_markdown`]: #method.from_markdown
128    fn visit<'arena>(&mut self, node: &'arena Node<'arena, RefCell<Ast>>) -> io::Result<()> {
129        use NodeValue::*;
130
131        match &node.data.borrow().value {
132            Document => self.visit_document(node)?,
133
134            Code(code) => self.visit_code(node, code)?,
135            CodeBlock(code_block) => self.visit_code_block(node, code_block)?,
136            EscapedTag(tag) => self.visit_escaped_tag(node, tag)?,
137            FrontMatter(frontmatter) => self.visit_front_matter(node, frontmatter)?,
138            FootnoteDefinition(footnote_definition) => {
139                self.visit_footnote_definition(node, footnote_definition)?
140            }
141            FootnoteReference(node_footnote_reference) => {
142                self.visit_footnote_reference(node, node_footnote_reference)?
143            }
144            Heading(heading) => self.visit_heading(node, heading)?,
145            HtmlBlock(html_block) => self.visit_html_block(node, html_block)?,
146            HtmlInline(inline) => self.visit_html_inline(node, inline)?,
147            Image(link) => self.visit_image(node, link)?,
148            Item(list) => self.visit_item(node, list)?,
149            Link(link) => self.visit_link(node, link)?,
150            List(list) => self.visit_list(node, list)?,
151            Math(math) => self.visit_math(node, math)?,
152            MultilineBlockQuote(multiline_block_quote) => {
153                self.visit_multiline_block_quote(node, multiline_block_quote)?
154            }
155            Raw(raw) => self.visit_raw(node, raw)?,
156            TaskItem(task_item) => self.visit_task_item(node, task_item)?,
157            Text(text) => self.visit_text(node, text)?,
158            ShortCode(short_code) => self.visit_short_code(node, short_code)?,
159            WikiLink(wiki_link) => self.visit_wiki_link(node, wiki_link)?,
160
161            DescriptionItem(description_item) => {
162                self.visit_description_item(node, description_item)?
163            }
164            DescriptionList => self.visit_description_list(node)?,
165            DescriptionTerm => self.visit_description_term(node)?,
166            DescriptionDetails => self.visit_description_details(node)?,
167
168            Table(table) => self.visit_table(node, table)?,
169            TableRow(table_row) => self.visit_table_row(node, table_row)?,
170            TableCell => self.visit_table_cell(node)?,
171
172            BlockQuote => self.visit_block_quote(node)?,
173            Emph => self.visit_emph(node)?,
174            Escaped => self.visit_escaped(node)?,
175            LineBreak => self.visit_line_break(node)?,
176            Paragraph => self.visit_paragraph(node)?,
177            SoftBreak => self.visit_soft_break(node)?,
178            SpoileredText => self.visit_spoilered_text(node)?,
179            Strikethrough => self.visit_strikethrough(node)?,
180            Strong => self.visit_strong(node)?,
181            Subscript => self.visit_subscript(node)?,
182            Superscript => self.visit_superscript(node)?,
183            ThematicBreak => self.visit_thematic_break(node)?,
184            Underline => self.visit_underline(node)?,
185        }
186
187        for child in node.children() {
188            self.visit(child)?;
189        }
190
191        Ok(())
192    }
193
194    /// Visit a block quote node.
195    fn visit_block_quote<'arena>(
196        &mut self,
197        node: &'arena Node<'arena, RefCell<Ast>>,
198    ) -> io::Result<()> {
199        Ok(())
200    }
201
202    /// Visit a code node, which is a code span that contains a [`NodeCode`] ref.
203    fn visit_code<'arena>(
204        &mut self,
205        node: &'arena Node<'arena, RefCell<Ast>>,
206        code: &NodeCode,
207    ) -> io::Result<()> {
208        Ok(())
209    }
210
211    /// Visit a code block node, which contains a [`NodeCodeBlock`] ref.
212    fn visit_code_block<'arena>(
213        &mut self,
214        node: &'arena Node<'arena, RefCell<Ast>>,
215        code_block: &NodeCodeBlock,
216    ) -> io::Result<()> {
217        Ok(())
218    }
219
220    /// Visit a description item node, which contains a [`NodeDescriptionItem`] ref.
221    fn visit_description_item<'arena>(
222        &mut self,
223        node: &'arena Node<'arena, RefCell<Ast>>,
224        description_item: &NodeDescriptionItem,
225    ) -> io::Result<()> {
226        Ok(())
227    }
228
229    /// Visit a description list node.
230    fn visit_description_list<'arena>(
231        &mut self,
232        node: &'arena Node<'arena, RefCell<Ast>>,
233    ) -> io::Result<()> {
234        Ok(())
235    }
236
237    /// Visit a description term node.
238    fn visit_description_term<'arena>(
239        &mut self,
240        node: &'arena Node<'arena, RefCell<Ast>>,
241    ) -> io::Result<()> {
242        Ok(())
243    }
244
245    /// Visit a description details node.
246    fn visit_description_details<'arena>(
247        &mut self,
248        node: &'arena Node<'arena, RefCell<Ast>>,
249    ) -> io::Result<()> {
250        Ok(())
251    }
252
253    /// Visit a document node.
254    fn visit_document<'arena>(
255        &mut self,
256        node: &'arena Node<'arena, RefCell<Ast>>,
257    ) -> io::Result<()> {
258        Ok(())
259    }
260
261    /// Visit an escaped content node.
262    fn visit_escaped<'arena>(
263        &mut self,
264        node: &'arena Node<'arena, RefCell<Ast>>,
265    ) -> io::Result<()> {
266        Ok(())
267    }
268
269    /// Visit an escaped tag node. The tag is a [`String`] that contains the escaped tag content.
270    fn visit_escaped_tag<'arena>(
271        &mut self,
272        node: &'arena Node<'arena, RefCell<Ast>>,
273        tag: &String,
274    ) -> io::Result<()> {
275        Ok(())
276    }
277
278    /// Visit an emphasis node.
279    fn visit_emph<'arena>(&mut self, node: &'arena Node<'arena, RefCell<Ast>>) -> io::Result<()> {
280        Ok(())
281    }
282
283    /// Visit a footnote definition node, which contains a [`NodeFootnoteDefinition`] ref.
284    fn visit_footnote_definition<'arena>(
285        &mut self,
286        node: &'arena Node<'arena, RefCell<Ast>>,
287        footnote_definition: &NodeFootnoteDefinition,
288    ) -> io::Result<()> {
289        Ok(())
290    }
291
292    /// Visit a footnote reference node, which contains a [`NodeFootnoteReference`] ref.
293    fn visit_footnote_reference<'arena>(
294        &mut self,
295        node: &'arena Node<'arena, RefCell<Ast>>,
296        footnote_reference: &NodeFootnoteReference,
297    ) -> io::Result<()> {
298        Ok(())
299    }
300
301    /// Visit a front matter node. The front matter is a [`String`] that contains the front matter content.
302    fn visit_front_matter<'arena>(
303        &mut self,
304        node: &'arena Node<'arena, RefCell<Ast>>,
305        frontmatter: &String,
306    ) -> io::Result<()> {
307        Ok(())
308    }
309
310    /// Visit a heading node, which contains a [`NodeHeading`] ref.
311    fn visit_heading<'arena>(
312        &mut self,
313        node: &'arena Node<'arena, RefCell<Ast>>,
314        heading: &NodeHeading,
315    ) -> io::Result<()> {
316        Ok(())
317    }
318
319    /// Visit an HTML block node, which is a [`NodeHtmlBlock`] ref.
320    fn visit_html_block<'arena>(
321        &mut self,
322        node: &'arena Node<'arena, RefCell<Ast>>,
323        html_block: &NodeHtmlBlock,
324    ) -> io::Result<()> {
325        Ok(())
326    }
327
328    /// Visit an HTML inline node. The inline is a [`String`] that contains the HTML inline content.
329    fn visit_html_inline<'arena>(
330        &mut self,
331        node: &'arena Node<'arena, RefCell<Ast>>,
332        inline: &String,
333    ) -> io::Result<()> {
334        Ok(())
335    }
336
337    /// Visit an image node, which contains a [`NodeLink`] ref for the image link.
338    fn visit_image<'arena>(
339        &mut self,
340        node: &'arena Node<'arena, RefCell<Ast>>,
341        link: &NodeLink,
342    ) -> io::Result<()> {
343        Ok(())
344    }
345
346    /// Visit an item node, which contains a [`NodeList`] ref describing the container list.
347    fn visit_item<'arena>(
348        &mut self,
349        node: &'arena Node<'arena, RefCell<Ast>>,
350        list: &NodeList,
351    ) -> io::Result<()> {
352        Ok(())
353    }
354
355    /// Visit a link node, which contains a [`NodeLink`] ref for the link.
356    fn visit_link<'arena>(
357        &mut self,
358        node: &'arena Node<'arena, RefCell<Ast>>,
359        link: &NodeLink,
360    ) -> io::Result<()> {
361        Ok(())
362    }
363
364    /// Visit a line break node.
365    fn visit_line_break<'arena>(
366        &mut self,
367        node: &'arena Node<'arena, RefCell<Ast>>,
368    ) -> io::Result<()> {
369        Ok(())
370    }
371
372    /// Visit a list node, which contains a [`NodeList`] ref describing the list.
373    fn visit_list<'arena>(
374        &mut self,
375        node: &'arena Node<'arena, RefCell<Ast>>,
376        list: &NodeList,
377    ) -> io::Result<()> {
378        Ok(())
379    }
380
381    /// Visit a math node, which contains a [`NodeMath`] ref.
382    fn visit_math<'arena>(
383        &mut self,
384        node: &'arena Node<'arena, RefCell<Ast>>,
385        math: &NodeMath,
386    ) -> io::Result<()> {
387        Ok(())
388    }
389
390    /// Visit a multiline block quote node, which contains a [`NodeMultilineBlockQuote`] ref.
391    fn visit_multiline_block_quote<'arena>(
392        &mut self,
393        node: &'arena Node<'arena, RefCell<Ast>>,
394        multiline_block_quote: &NodeMultilineBlockQuote,
395    ) -> io::Result<()> {
396        Ok(())
397    }
398
399    /// Visit a paragraph node.
400    fn visit_paragraph<'arena>(
401        &mut self,
402        node: &'arena Node<'arena, RefCell<Ast>>,
403    ) -> io::Result<()> {
404        Ok(())
405    }
406
407    /// Visit a raw node. The raw is a [`String`] that contains the raw content.
408    fn visit_raw<'arena>(
409        &mut self,
410        node: &'arena Node<'arena, RefCell<Ast>>,
411        raw: &String,
412    ) -> io::Result<()> {
413        Ok(())
414    }
415
416    /// Visit a short code node, which contains a [`NodeShortCode`] ref.
417    fn visit_short_code<'arena>(
418        &mut self,
419        node: &'arena Node<'arena, RefCell<Ast>>,
420        short_code: &NodeShortCode,
421    ) -> io::Result<()> {
422        Ok(())
423    }
424
425    /// Visit a soft break node.
426    fn visit_soft_break<'arena>(
427        &mut self,
428        node: &'arena Node<'arena, RefCell<Ast>>,
429    ) -> io::Result<()> {
430        Ok(())
431    }
432
433    /// Visit a spoilered text node.
434    fn visit_spoilered_text<'arena>(
435        &mut self,
436        node: &'arena Node<'arena, RefCell<Ast>>,
437    ) -> io::Result<()> {
438        Ok(())
439    }
440
441    /// Visit a strong node.
442    fn visit_strong<'arena>(&mut self, node: &'arena Node<'arena, RefCell<Ast>>) -> io::Result<()> {
443        Ok(())
444    }
445
446    /// Visit a strikethrough node.
447    fn visit_strikethrough<'arena>(
448        &mut self,
449        node: &'arena Node<'arena, RefCell<Ast>>,
450    ) -> io::Result<()> {
451        Ok(())
452    }
453
454    /// Visit a subscript node.
455    fn visit_subscript<'arena>(
456        &mut self,
457        node: &'arena Node<'arena, RefCell<Ast>>,
458    ) -> io::Result<()> {
459        Ok(())
460    }
461
462    /// Visit a superscript node.
463    fn visit_superscript<'arena>(
464        &mut self,
465        node: &'arena Node<'arena, RefCell<Ast>>,
466    ) -> io::Result<()> {
467        Ok(())
468    }
469
470    /// Visit a table node, which contains a [`NodeTable`] ref.
471    fn visit_table<'arena>(
472        &mut self,
473        node: &'arena Node<'arena, RefCell<Ast>>,
474        table: &NodeTable,
475    ) -> io::Result<()> {
476        Ok(())
477    }
478
479    /// Visit a table cell node.
480    fn visit_table_cell<'arena>(
481        &mut self,
482        node: &'arena Node<'arena, RefCell<Ast>>,
483    ) -> io::Result<()> {
484        Ok(())
485    }
486
487    /// Visit a table row node.
488    fn visit_table_row<'arena>(
489        &mut self,
490        node: &'arena Node<'arena, RefCell<Ast>>,
491        table_row: &bool,
492    ) -> io::Result<()> {
493        Ok(())
494    }
495
496    /// Visit a task item node.
497    fn visit_task_item<'arena>(
498        &mut self,
499        node: &'arena Node<'arena, RefCell<Ast>>,
500        task_item: &Option<char>,
501    ) -> io::Result<()> {
502        Ok(())
503    }
504
505    /// Visit a text node. The text is a [`String`] that contains the text content.
506    fn visit_text<'arena>(
507        &mut self,
508        node: &'arena Node<'arena, RefCell<Ast>>,
509        text: &String,
510    ) -> io::Result<()> {
511        Ok(())
512    }
513
514    /// Visit a thematic break node.
515    fn visit_thematic_break<'arena>(
516        &mut self,
517        node: &'arena Node<'arena, RefCell<Ast>>,
518    ) -> io::Result<()> {
519        Ok(())
520    }
521
522    /// Visit an underline node.
523    fn visit_underline<'arena>(
524        &mut self,
525        node: &'arena Node<'arena, RefCell<Ast>>,
526    ) -> io::Result<()> {
527        Ok(())
528    }
529
530    /// Visit a wiki link node, which contains a [`NodeWikiLink`] ref.
531    fn visit_wiki_link<'arena>(
532        &mut self,
533        node: &'arena Node<'arena, RefCell<Ast>>,
534        wiki_link: &NodeWikiLink,
535    ) -> io::Result<()> {
536        Ok(())
537    }
538}