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")))]
pub trait MarkdownParseExt {
type Error: Error;
fn write_markdown(&mut self, md: &str) -> Result<(), Self::Error>;
}
#[cfg_attr(docsrs, doc(cfg(feature = "parse-markdown")))]
#[derive(Debug)]
pub enum MarkdownParseError<E> {
DocWriter(E),
UnhandledEvent(&'static str),
NestedMarkup {
outer: &'static str,
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 => { }
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())
);
}
}