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")))]
9pub trait MarkdownParseExt {
11 type Error: Error;
15
16 fn write_markdown(&mut self, md: &str) -> Result<(), Self::Error>;
33}
34
35#[cfg_attr(docsrs, doc(cfg(feature = "parse-markdown")))]
36#[derive(Debug)]
38pub enum MarkdownParseError<E> {
39 DocWriter(E),
41
42 UnhandledEvent(&'static str),
54
55 NestedMarkup {
67 outer: &'static str,
69
70 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 => { }
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}