doc-writer 0.2.0

Generate documentation in multiple formats.
Documentation
use crate::DocumentationWriter;
use pulldown_cmark::{CowStr, Event, Parser, Tag};
use std::error::Error;
use std::fmt;
use std::fmt::{Display, Formatter};
use MarkdownParseError::UnhandledEvent;

#[cfg_attr(docsrs, doc(cfg(feature = "parse-markdown")))]
/// Contributes the [`write_markdown`](Self::write_markdown) function to [`DocumentationWriter`]s.
pub trait MarkdownParseExt {
    /// This error wraps errors during markdown processing and io errors occurring during writing.
    ///
    /// For all the standard [`DocumentationWriter`]s it is [`MarkdownParseError<io::Error>`].
    type Error: Error;

    /// Emit events corresponding to the given markdown.
    ///
    /// # Examples
    /// ```
    /// # use doc_writer::render::MarkdownWriter;
    /// # use doc_writer::{DocumentationWriter, MarkdownParseExt};
    /// # let mut w = MarkdownWriter::new(Vec::<u8>::new());
    /// w.start_description()?;
    /// w.write_markdown("*See also* <man:pcresyntax(3)>")?;
    /// // equivalent to calling
    /// w.emphasis("See also")?;
    /// w.plain(" ")?;
    /// w.link("", "man:pcresyntax(3)")?;
    /// // this works on all writers.
    /// # Ok::<(), doc_writer::MarkdownParseError<std::io::Error>>(())
    /// ```
    fn write_markdown(&mut self, md: &str) -> Result<(), Self::Error>;
}

#[cfg_attr(docsrs, doc(cfg(feature = "parse-markdown")))]
/// Error wraps errors during markdown processing by [`MarkdownParseExt::write_markdown`] and io errors occurring during writing.
#[derive(Debug)]
pub enum MarkdownParseError<E> {
    /// Error that occurred during writing to the [`DocumentationWriter`].
    DocWriter(E),

    /// A piece of markdown was encountered that can not be translated to the API of a [`DocumentationWriter`].
    ///
    /// # Examples
    /// ```
    /// # use doc_writer::render::MarkdownWriter;
    /// # use doc_writer::{DocumentationWriter, MarkdownParseExt};
    /// # let mut doc = MarkdownWriter::new(Vec::<u8>::new());
    /// doc.start_description()?;
    /// assert_eq!("markdown block quotes can not be written to a doc writer", &format!("{}", doc.write_markdown("> a").unwrap_err()));
    /// # Ok::<(), std::io::Error>(())
    /// ```
    UnhandledEvent(&'static str),

    /// A piece of marked-up text was found in a place where plain text was expected.
    ///
    /// # Examples
    /// ```
    /// # use doc_writer::render::MarkdownWriter;
    /// # use doc_writer::{DocumentationWriter, MarkdownParseExt};
    /// # let mut doc = MarkdownWriter::new(Vec::<u8>::new());
    /// doc.start_description()?;
    /// assert_eq!("markdown emphasized spans can not be nested in links", &format!("{}", doc.write_markdown("[*a*](b)").unwrap_err()));
    /// # Ok::<(), std::io::Error>(())
    /// ```
    NestedMarkup {
        /// The outer element (`"links"`) in the example above.
        outer: &'static str,

        /// The inner element (`"emphasized spans"`) in the example above.
        inner: &'static str,
    },
}

impl<E: Error> From<E> for MarkdownParseError<E> {
    fn from(e: E) -> Self {
        Self::DocWriter(e)
    }
}

impl<E: Error> Display for MarkdownParseError<E> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            MarkdownParseError::DocWriter(e) => write!(f, "an error occurred while writing: {}", e),
            UnhandledEvent(typ) => {
                write!(f, "markdown {} can not be written to a doc writer", typ)
            }
            MarkdownParseError::NestedMarkup {
                outer: parent,
                inner: child,
            } => {
                write!(f, "markdown {} can not be nested in {}", child, parent)
            }
        }
    }
}

impl<E: Error> Error for MarkdownParseError<E> {}

impl<D: DocumentationWriter> MarkdownParseExt for D {
    type Error = MarkdownParseError<D::Error>;

    fn write_markdown(&mut self, md: &str) -> Result<(), Self::Error> {
        let mut parser = Parser::new(md);
        while let Some(ev) = parser.next() {
            match ev {
                Event::Text(t) => self.plain(t.as_ref())?,
                Event::SoftBreak => self.plain(" ")?,
                Event::HardBreak => self.plain("\n")?,
                Event::Start(typ) => {
                    match typ {
                        Tag::Paragraph => { /* SKIP */ }
                        Tag::Emphasis => {
                            self.emphasis(next_text(&mut parser, "emphasized spans")?.as_ref())?
                        }
                        Tag::Strong => {
                            self.strong(next_text(&mut parser, "strong spans")?.as_ref())?
                        }
                        Tag::Link(_typ, dest, _title) => {
                            let text = next_text(&mut parser, "links")?;
                            self.link(text.as_ref(), dest.as_ref())?;
                        }
                        _ => return Err(MarkdownParseError::UnhandledEvent(display_tag(typ))),
                    }
                }
                Event::End(typ) => {
                    if matches!(typ, Tag::Paragraph) {
                        self.paragraph_break()?;
                    } else {
                        unreachable!("unbalanced Event::End in parser stream");
                    }
                }
                _ => return Err(MarkdownParseError::UnhandledEvent(display_event(ev))),
            }
        }
        Ok(())
    }
}

fn next_text<'a, E: Error>(
    p: &mut Parser<'a>,
    typ: &'static str,
) -> Result<CowStr<'a>, MarkdownParseError<E>> {
    if let Some(ev) = p.next() {
        return match ev {
            Event::End(_) => Ok(CowStr::from("")),
            Event::Text(t) => {
                if !matches!(p.next(), Some(Event::End(_))) {
                    let mut s = String::from(t.as_ref());
                    s.push_str(next_text(p, typ)?.as_ref());
                    Ok(CowStr::from(s))
                } else {
                    Ok(t)
                }
            }
            Event::SoftBreak | Event::HardBreak => Ok(CowStr::from(" ")),
            Event::Start(t) => Err(MarkdownParseError::NestedMarkup {
                inner: display_tag(t),
                outer: typ,
            }),
            _ => return Err(MarkdownParseError::UnhandledEvent(display_event(ev))),
        };
    } else {
        unreachable!("unbalanced Event::End in parser stream")
    }
}

fn display_tag<'a>(t: Tag<'a>) -> &'static str {
    match t {
        Tag::Heading(_) => "headings",
        Tag::BlockQuote => "block quotes",
        Tag::CodeBlock(_) => "code blocks",
        Tag::List(_) | Tag::Item => "lists",
        Tag::FootnoteDefinition(_) => "footnotes",
        Tag::Table(_) | Tag::TableHead | Tag::TableRow | Tag::TableCell => "tables",
        Tag::Strikethrough => "strikethrough",
        Tag::Image(_, _, _) => "images",
        Tag::Paragraph => "paragraphs",
        Tag::Emphasis => "emphasized spans",
        Tag::Strong => "strong spans",
        Tag::Link(_, _, _) => "links",
    }
}

fn display_event<'a>(ev: Event<'a>) -> &'static str {
    match ev {
        Event::Code(_) => "code spans",
        Event::Html(_) => "inline html",
        Event::FootnoteReference(_) => "footnotes",
        Event::Rule => "horizontal rules",
        Event::TaskListMarker(_) => "task lists",
        Event::Start(_) => "nested blocks",
        Event::End(_) => "nested blocks",
        Event::Text(_) => "text spans",
        Event::HardBreak | Event::SoftBreak => "line breaks",
    }
}

#[cfg(test)]
mod tests {
    use crate::markdown_ext::{MarkdownParseError, MarkdownParseExt};
    use crate::render::MarkdownWriter;
    use crate::DocumentationWriter;
    use std::io;

    fn md2md(md: &str) -> Result<String, MarkdownParseError<io::Error>> {
        let mut out = Vec::<u8>::new();
        let mut doc = MarkdownWriter::new(&mut out);
        doc.start_description()?;
        doc.write_markdown(md)?;
        Ok(String::from_utf8(out).unwrap().trim().to_owned())
    }

    #[test]
    fn test_write_markdown() {
        assert_eq!("a\n*b*\nc", &md2md("a *b* c").unwrap());
        assert_eq!("a\n**b**\nc", &md2md("a **b** c").unwrap());
        assert_eq!("a\n\n\nb", &md2md("a\n\nb").unwrap());
        assert_eq!("**grep**(3)", &md2md("<man:grep(3)>").unwrap());
        assert_eq!(
            "[example](https://example.org)",
            &md2md("[example](https://example.org)").unwrap()
        );
        assert_eq!(
            "[example](https://example.org)",
            &md2md("[example][ex]\n\n[ex]: https://example.org").unwrap()
        );
        assert_eq!("a\n**b c**\nd", &md2md("a**b c**d").unwrap());

        assert_eq!(
            "markdown links can not be nested in emphasized spans",
            format!("{}", md2md("*[a](b)*").unwrap_err())
        );
        assert_eq!(
            "markdown block quotes can not be written to a doc writer",
            format!("{}", md2md("> a").unwrap_err())
        );
    }
}