doc_writer/
markdown_ext.rs

1use crate::DocumentationWriter;
2use pulldown_cmark::{CowStr, Event, Parser, Tag};
3use std::error::Error;
4use std::fmt;
5use std::fmt::{Display, Formatter};
6use MarkdownParseError::UnhandledEvent;
7
8#[cfg_attr(docsrs, doc(cfg(feature = "parse-markdown")))]
9/// Contributes the [`write_markdown`](Self::write_markdown) function to [`DocumentationWriter`]s.
10pub trait MarkdownParseExt {
11    /// This error wraps errors during markdown processing and io errors occurring during writing.
12    ///
13    /// For all the standard [`DocumentationWriter`]s it is [`MarkdownParseError<io::Error>`].
14    type Error: Error;
15
16    /// Emit events corresponding to the given markdown.
17    ///
18    /// # Examples
19    /// ```
20    /// # use doc_writer::render::MarkdownWriter;
21    /// # use doc_writer::{DocumentationWriter, MarkdownParseExt};
22    /// # let mut w = MarkdownWriter::new(Vec::<u8>::new());
23    /// w.start_description()?;
24    /// w.write_markdown("*See also* <man:pcresyntax(3)>")?;
25    /// // equivalent to calling
26    /// w.emphasis("See also")?;
27    /// w.plain(" ")?;
28    /// w.link("", "man:pcresyntax(3)")?;
29    /// // this works on all writers.
30    /// # Ok::<(), doc_writer::MarkdownParseError<std::io::Error>>(())
31    /// ```
32    fn write_markdown(&mut self, md: &str) -> Result<(), Self::Error>;
33}
34
35#[cfg_attr(docsrs, doc(cfg(feature = "parse-markdown")))]
36/// Error wraps errors during markdown processing by [`MarkdownParseExt::write_markdown`] and io errors occurring during writing.
37#[derive(Debug)]
38pub enum MarkdownParseError<E> {
39    /// Error that occurred during writing to the [`DocumentationWriter`].
40    DocWriter(E),
41
42    /// A piece of markdown was encountered that can not be translated to the API of a [`DocumentationWriter`].
43    ///
44    /// # Examples
45    /// ```
46    /// # use doc_writer::render::MarkdownWriter;
47    /// # use doc_writer::{DocumentationWriter, MarkdownParseExt};
48    /// # let mut doc = MarkdownWriter::new(Vec::<u8>::new());
49    /// doc.start_description()?;
50    /// assert_eq!("markdown block quotes can not be written to a doc writer", &format!("{}", doc.write_markdown("> a").unwrap_err()));
51    /// # Ok::<(), std::io::Error>(())
52    /// ```
53    UnhandledEvent(&'static str),
54
55    /// A piece of marked-up text was found in a place where plain text was expected.
56    ///
57    /// # Examples
58    /// ```
59    /// # use doc_writer::render::MarkdownWriter;
60    /// # use doc_writer::{DocumentationWriter, MarkdownParseExt};
61    /// # let mut doc = MarkdownWriter::new(Vec::<u8>::new());
62    /// doc.start_description()?;
63    /// assert_eq!("markdown emphasized spans can not be nested in links", &format!("{}", doc.write_markdown("[*a*](b)").unwrap_err()));
64    /// # Ok::<(), std::io::Error>(())
65    /// ```
66    NestedMarkup {
67        /// The outer element (`"links"`) in the example above.
68        outer: &'static str,
69
70        /// The inner element (`"emphasized spans"`) in the example above.
71        inner: &'static str,
72    },
73}
74
75impl<E: Error> From<E> for MarkdownParseError<E> {
76    fn from(e: E) -> Self {
77        Self::DocWriter(e)
78    }
79}
80
81impl<E: Error> Display for MarkdownParseError<E> {
82    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
83        match self {
84            MarkdownParseError::DocWriter(e) => write!(f, "an error occurred while writing: {}", e),
85            UnhandledEvent(typ) => {
86                write!(f, "markdown {} can not be written to a doc writer", typ)
87            }
88            MarkdownParseError::NestedMarkup {
89                outer: parent,
90                inner: child,
91            } => {
92                write!(f, "markdown {} can not be nested in {}", child, parent)
93            }
94        }
95    }
96}
97
98impl<E: Error> Error for MarkdownParseError<E> {}
99
100impl<D: DocumentationWriter> MarkdownParseExt for D {
101    type Error = MarkdownParseError<D::Error>;
102
103    fn write_markdown(&mut self, md: &str) -> Result<(), Self::Error> {
104        let mut parser = Parser::new(md);
105        while let Some(ev) = parser.next() {
106            match ev {
107                Event::Text(t) => self.plain(t.as_ref())?,
108                Event::SoftBreak => self.plain(" ")?,
109                Event::HardBreak => self.plain("\n")?,
110                Event::Start(typ) => {
111                    match typ {
112                        Tag::Paragraph => { /* SKIP */ }
113                        Tag::Emphasis => {
114                            self.emphasis(next_text(&mut parser, "emphasized spans")?.as_ref())?
115                        }
116                        Tag::Strong => {
117                            self.strong(next_text(&mut parser, "strong spans")?.as_ref())?
118                        }
119                        Tag::Link(_typ, dest, _title) => {
120                            let text = next_text(&mut parser, "links")?;
121                            self.link(text.as_ref(), dest.as_ref())?;
122                        }
123                        _ => return Err(MarkdownParseError::UnhandledEvent(display_tag(typ))),
124                    }
125                }
126                Event::End(typ) => {
127                    if matches!(typ, Tag::Paragraph) {
128                        self.paragraph_break()?;
129                    } else {
130                        unreachable!("unbalanced Event::End in parser stream");
131                    }
132                }
133                _ => return Err(MarkdownParseError::UnhandledEvent(display_event(ev))),
134            }
135        }
136        Ok(())
137    }
138}
139
140fn next_text<'a, E: Error>(
141    p: &mut Parser<'a>,
142    typ: &'static str,
143) -> Result<CowStr<'a>, MarkdownParseError<E>> {
144    if let Some(ev) = p.next() {
145        return match ev {
146            Event::End(_) => Ok(CowStr::from("")),
147            Event::Text(t) => {
148                if !matches!(p.next(), Some(Event::End(_))) {
149                    let mut s = String::from(t.as_ref());
150                    s.push_str(next_text(p, typ)?.as_ref());
151                    Ok(CowStr::from(s))
152                } else {
153                    Ok(t)
154                }
155            }
156            Event::SoftBreak | Event::HardBreak => Ok(CowStr::from(" ")),
157            Event::Start(t) => Err(MarkdownParseError::NestedMarkup {
158                inner: display_tag(t),
159                outer: typ,
160            }),
161            _ => return Err(MarkdownParseError::UnhandledEvent(display_event(ev))),
162        };
163    } else {
164        unreachable!("unbalanced Event::End in parser stream")
165    }
166}
167
168fn display_tag<'a>(t: Tag<'a>) -> &'static str {
169    match t {
170        Tag::Heading(_) => "headings",
171        Tag::BlockQuote => "block quotes",
172        Tag::CodeBlock(_) => "code blocks",
173        Tag::List(_) | Tag::Item => "lists",
174        Tag::FootnoteDefinition(_) => "footnotes",
175        Tag::Table(_) | Tag::TableHead | Tag::TableRow | Tag::TableCell => "tables",
176        Tag::Strikethrough => "strikethrough",
177        Tag::Image(_, _, _) => "images",
178        Tag::Paragraph => "paragraphs",
179        Tag::Emphasis => "emphasized spans",
180        Tag::Strong => "strong spans",
181        Tag::Link(_, _, _) => "links",
182    }
183}
184
185fn display_event<'a>(ev: Event<'a>) -> &'static str {
186    match ev {
187        Event::Code(_) => "code spans",
188        Event::Html(_) => "inline html",
189        Event::FootnoteReference(_) => "footnotes",
190        Event::Rule => "horizontal rules",
191        Event::TaskListMarker(_) => "task lists",
192        Event::Start(_) => "nested blocks",
193        Event::End(_) => "nested blocks",
194        Event::Text(_) => "text spans",
195        Event::HardBreak | Event::SoftBreak => "line breaks",
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use crate::markdown_ext::{MarkdownParseError, MarkdownParseExt};
202    use crate::render::MarkdownWriter;
203    use crate::DocumentationWriter;
204    use std::io;
205
206    fn md2md(md: &str) -> Result<String, MarkdownParseError<io::Error>> {
207        let mut out = Vec::<u8>::new();
208        let mut doc = MarkdownWriter::new(&mut out);
209        doc.start_description()?;
210        doc.write_markdown(md)?;
211        Ok(String::from_utf8(out).unwrap().trim().to_owned())
212    }
213
214    #[test]
215    fn test_write_markdown() {
216        assert_eq!("a\n*b*\nc", &md2md("a *b* c").unwrap());
217        assert_eq!("a\n**b**\nc", &md2md("a **b** c").unwrap());
218        assert_eq!("a\n\n\nb", &md2md("a\n\nb").unwrap());
219        assert_eq!("**grep**(3)", &md2md("<man:grep(3)>").unwrap());
220        assert_eq!(
221            "[example](https://example.org)",
222            &md2md("[example](https://example.org)").unwrap()
223        );
224        assert_eq!(
225            "[example](https://example.org)",
226            &md2md("[example][ex]\n\n[ex]: https://example.org").unwrap()
227        );
228        assert_eq!("a\n**b c**\nd", &md2md("a**b c**d").unwrap());
229
230        assert_eq!(
231            "markdown links can not be nested in emphasized spans",
232            format!("{}", md2md("*[a](b)*").unwrap_err())
233        );
234        assert_eq!(
235            "markdown block quotes can not be written to a doc writer",
236            format!("{}", md2md("> a").unwrap_err())
237        );
238    }
239}