pulldown_cmark_ast/
lib.rs

1//! An abstract syntax tree for [`pulldown_cmark`].
2
3use pulldown_cmark::{BrokenLinkCallback, CowStr, Event, Options, Parser, Tag, TagEnd};
4use std::{iter, ops::Range};
5pub mod visit_mut;
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8pub struct Spanned<T> {
9    pub item: T,
10    pub span: Span,
11}
12impl<T> Spanned<T> {
13    pub fn into_inner(self) -> T {
14        self.item
15    }
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19pub struct Span(pub Range<usize>);
20
21#[derive(Default, Debug, Clone, PartialEq)]
22pub struct Ast<'a>(pub Vec<Tree<'a>>);
23
24impl<'a> IntoIterator for Ast<'a> {
25    type Item = Spanned<Event<'a>>;
26
27    type IntoIter = Box<dyn Iterator<Item = Spanned<Event<'a>>> + 'a>;
28
29    fn into_iter(self) -> Self::IntoIter {
30        let Self { 0: trees } = self;
31        Box::new(trees.into_iter().flatten())
32    }
33}
34
35#[derive(Debug, Clone, PartialEq)]
36pub enum Tree<'a> {
37    Group(Group<'a>),
38    Text(Spanned<CowStr<'a>>),
39    /// An inline code node.
40    Code(Spanned<CowStr<'a>>),
41    /// An HTML node.
42    Html(Spanned<CowStr<'a>>),
43    /// An inline HTML node.
44    InlineHtml(Spanned<CowStr<'a>>),
45    /// A reference to a footnote with given label, which may or may not be defined
46    /// by an event with a `Tag::FootnoteDefinition` tag. Definitions and references to them may
47    /// occur in any order.
48    FootnoteReference(Spanned<CowStr<'a>>),
49    /// A soft line break.
50    SoftBreak(Span),
51    /// A hard line break.
52    HardBreak(Span),
53    /// A horizontal ruler.
54    Rule(Span),
55    /// A task list marker, rendered as a checkbox in HTML. Contains a true when it is checked.
56    TaskListMarker(Spanned<bool>),
57    /// An inline math environment node.
58    InlineMath(Spanned<CowStr<'a>>),
59    /// A display math environment node.
60    DisplayMath(Spanned<CowStr<'a>>),
61}
62
63impl<'a> IntoIterator for Tree<'a> {
64    type Item = Spanned<Event<'a>>;
65
66    type IntoIter = Box<dyn Iterator<Item = Spanned<Event<'a>>> + 'a>;
67
68    fn into_iter(self) -> Self::IntoIter {
69        /// Need to return a [`Box<dyn Iterator>`] to break the mutually recursive cycle in our return types
70        fn once<'a, T: 'a>(item: T, span: Span) -> Box<dyn Iterator<Item = Spanned<T>> + 'a> {
71            Box::new(iter::once(Spanned { item, span }))
72        }
73        match self {
74            Tree::Group(it) => it.into_iter(),
75            Tree::Text(Spanned { item, span }) => once(Event::Text(item), span),
76            Tree::Code(Spanned { item, span }) => once(Event::Code(item), span),
77            Tree::Html(Spanned { item, span }) => once(Event::Html(item), span),
78            Tree::InlineHtml(Spanned { item, span }) => once(Event::InlineHtml(item), span),
79            Tree::FootnoteReference(Spanned { item, span }) => {
80                once(Event::FootnoteReference(item), span)
81            }
82            Tree::SoftBreak(span) => once(Event::SoftBreak, span),
83            Tree::HardBreak(span) => once(Event::HardBreak, span),
84            Tree::Rule(span) => once(Event::Rule, span),
85            Tree::TaskListMarker(Spanned { item, span }) => once(Event::TaskListMarker(item), span),
86            Tree::InlineMath(Spanned { item, span }) => once(Event::InlineMath(item), span),
87            Tree::DisplayMath(Spanned { item, span }) => once(Event::DisplayMath(item), span),
88        }
89    }
90}
91
92#[derive(Debug, Clone, PartialEq)]
93pub struct Group<'a> {
94    pub tag: Spanned<Tag<'a>>,
95    pub stream: Ast<'a>,
96    pub end_span: Span,
97}
98
99impl<'a> IntoIterator for Group<'a> {
100    type Item = Spanned<Event<'a>>;
101
102    type IntoIter = Box<dyn Iterator<Item = Spanned<Event<'a>>> + 'a>;
103
104    fn into_iter(self) -> Self::IntoIter {
105        let Group {
106            tag: Spanned { item: tag, span },
107            stream,
108            end_span,
109        } = self;
110        let end = Spanned {
111            item: Event::End(tag.to_end()),
112            span: end_span,
113        };
114        Box::new(
115            iter::once(Spanned {
116                item: Event::Start(tag),
117                span,
118            })
119            .chain(stream)
120            .chain(iter::once(end)),
121        )
122    }
123}
124
125impl<'a> Ast<'a> {
126    pub fn new(text: &'a str) -> Self {
127        Self::new_ext(text, Options::empty())
128    }
129    pub fn new_ext(text: &'a str, options: Options) -> Self {
130        Self::new_with_broken_link_callback(text, options, Some(|_| None))
131    }
132    pub fn new_with_broken_link_callback<C: BrokenLinkCallback<'a>>(
133        text: &'a str,
134        options: Options,
135        broken_link_callback: Option<C>,
136    ) -> Self {
137        match Self::from_events(
138            &mut Parser::new_with_broken_link_callback(text, options, broken_link_callback)
139                .into_offset_iter()
140                .map(|(item, range)| Spanned {
141                    item,
142                    span: Span(range),
143                }),
144        ) {
145            Ok((this, None)) => this,
146            Ok((_, Some(_))) | Err(_) => {
147                unreachable!("pulldown_cmark guarantees delimters are matched")
148            }
149        }
150    }
151    fn from_events(
152        evts: &mut dyn Iterator<Item = Spanned<Event<'a>>>,
153    ) -> Result<(Self, Option<Spanned<TagEnd>>), Mismatched> {
154        let mut this = Self::default();
155        while let Some(Spanned { item, span }) = evts.next() {
156            match item {
157                Event::Start(tag) => match Self::from_events(evts)? {
158                    (
159                        stream,
160                        Some(Spanned {
161                            item,
162                            span: end_span,
163                        }),
164                    ) if tag.to_end() == item => this.0.push(Tree::Group(Group {
165                        tag: Spanned { item: tag, span },
166                        stream,
167                        end_span,
168                    })),
169                    _ => return Err(Mismatched),
170                },
171                Event::End(item) => return Ok((this, Some(Spanned { item, span }))),
172                Event::Text(item) => this.0.push(Tree::Text(Spanned { item, span })),
173                Event::Code(item) => this.0.push(Tree::Code(Spanned { item, span })),
174                Event::Html(item) => this.0.push(Tree::Html(Spanned { item, span })),
175                Event::InlineHtml(item) => this.0.push(Tree::InlineHtml(Spanned { item, span })),
176                Event::FootnoteReference(item) => {
177                    this.0.push(Tree::FootnoteReference(Spanned { item, span }))
178                }
179                Event::SoftBreak => this.0.push(Tree::SoftBreak(span)),
180                Event::HardBreak => this.0.push(Tree::HardBreak(span)),
181                Event::Rule => this.0.push(Tree::Rule(span)),
182                Event::TaskListMarker(item) => {
183                    this.0.push(Tree::TaskListMarker(Spanned { item, span }))
184                }
185                Event::InlineMath(item) => this.0.push(Tree::InlineMath(Spanned { item, span })),
186                Event::DisplayMath(item) => this.0.push(Tree::DisplayMath(Spanned { item, span })),
187            }
188        }
189        Ok((this, None))
190    }
191}
192
193struct Mismatched;