| // Copyright 2015 Google Inc. All rights reserved. |
| // |
| // Permission is hereby granted, free of charge, to any person obtaining a copy |
| // of this software and associated documentation files (the "Software"), to deal |
| // in the Software without restriction, including without limitation the rights |
| // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| // copies of the Software, and to permit persons to whom the Software is |
| // furnished to do so, subject to the following conditions: |
| // |
| // The above copyright notice and this permission notice shall be included in |
| // all copies or substantial portions of the Software. |
| // |
| // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| // THE SOFTWARE. |
| |
| //! HTML renderer that takes an iterator of events as input. |
| |
| use std::collections::HashMap; |
| use std::fmt::{Arguments, Write as FmtWrite}; |
| use std::io::{self, ErrorKind, Write}; |
| |
| use crate::escape::{escape_href, escape_html}; |
| use crate::parse::Event::*; |
| use crate::parse::{Alignment, Event, LinkType, Tag}; |
| use crate::strings::CowStr; |
| |
| enum TableState { |
| Head, |
| Body, |
| } |
| |
| /// This wrapper exists because we can't have both a blanket implementation |
| /// for all types implementing `Write` and types of the for `&mut W` where |
| /// `W: StrWrite`. Since we need the latter a lot, we choose to wrap |
| /// `Write` types. |
| struct WriteWrapper<W>(W); |
| |
| /// Trait that allows writing string slices. This is basically an extension |
| /// of `std::io::Write` in order to include `String`. |
| pub(crate) trait StrWrite { |
| fn write_str(&mut self, s: &str) -> io::Result<()>; |
| |
| fn write_fmt(&mut self, args: Arguments) -> io::Result<()>; |
| } |
| |
| impl<W> StrWrite for WriteWrapper<W> |
| where |
| W: Write, |
| { |
| #[inline] |
| fn write_str(&mut self, s: &str) -> io::Result<()> { |
| self.0.write_all(s.as_bytes()) |
| } |
| |
| #[inline] |
| fn write_fmt(&mut self, args: Arguments) -> io::Result<()> { |
| self.0.write_fmt(args) |
| } |
| } |
| |
| impl<'w> StrWrite for String { |
| #[inline] |
| fn write_str(&mut self, s: &str) -> io::Result<()> { |
| self.push_str(s); |
| Ok(()) |
| } |
| |
| #[inline] |
| fn write_fmt(&mut self, args: Arguments) -> io::Result<()> { |
| // FIXME: translate fmt error to io error? |
| FmtWrite::write_fmt(self, args).map_err(|_| ErrorKind::Other.into()) |
| } |
| } |
| |
| impl<W> StrWrite for &'_ mut W |
| where |
| W: StrWrite, |
| { |
| #[inline] |
| fn write_str(&mut self, s: &str) -> io::Result<()> { |
| (**self).write_str(s) |
| } |
| |
| #[inline] |
| fn write_fmt(&mut self, args: Arguments) -> io::Result<()> { |
| (**self).write_fmt(args) |
| } |
| } |
| |
| struct HtmlWriter<'a, I, W> { |
| /// Iterator supplying events. |
| iter: I, |
| |
| /// Writer to write to. |
| writer: W, |
| |
| /// Whether or not the last write wrote a newline. |
| end_newline: bool, |
| |
| table_state: TableState, |
| table_alignments: Vec<Alignment>, |
| table_cell_index: usize, |
| numbers: HashMap<CowStr<'a>, usize>, |
| } |
| |
| impl<'a, I, W> HtmlWriter<'a, I, W> |
| where |
| I: Iterator<Item = Event<'a>>, |
| W: StrWrite, |
| { |
| fn new(iter: I, writer: W) -> Self { |
| Self { |
| iter, |
| writer, |
| end_newline: true, |
| table_state: TableState::Head, |
| table_alignments: vec![], |
| table_cell_index: 0, |
| numbers: HashMap::new(), |
| } |
| } |
| |
| /// Writes a new line. |
| fn write_newline(&mut self) -> io::Result<()> { |
| self.end_newline = true; |
| self.writer.write_str("\n") |
| } |
| |
| /// Writes a buffer, and tracks whether or not a newline was written. |
| #[inline] |
| fn write(&mut self, s: &str) -> io::Result<()> { |
| self.writer.write_str(s)?; |
| |
| if !s.is_empty() { |
| self.end_newline = s.ends_with('\n'); |
| } |
| Ok(()) |
| } |
| |
| pub fn run(mut self) -> io::Result<()> { |
| while let Some(event) = self.iter.next() { |
| match event { |
| Start(tag) => { |
| self.start_tag(tag)?; |
| } |
| End(tag) => { |
| self.end_tag(tag)?; |
| } |
| Text(text) => { |
| escape_html(&mut self.writer, &text)?; |
| self.end_newline = text.ends_with('\n'); |
| } |
| Code(text) => { |
| self.write("<code>")?; |
| escape_html(&mut self.writer, &text)?; |
| self.write("</code>")?; |
| } |
| Html(html) => { |
| self.write(&html)?; |
| } |
| SoftBreak => { |
| self.write_newline()?; |
| } |
| HardBreak => { |
| self.write("<br />\n")?; |
| } |
| Rule => { |
| if self.end_newline { |
| self.write("<hr />\n")?; |
| } else { |
| self.write("\n<hr />\n")?; |
| } |
| } |
| FootnoteReference(name) => { |
| let len = self.numbers.len() + 1; |
| self.write("<sup class=\"footnote-reference\"><a href=\"#")?; |
| escape_html(&mut self.writer, &name)?; |
| self.write("\">")?; |
| let number = *self.numbers.entry(name).or_insert(len); |
| write!(&mut self.writer, "{}", number)?; |
| self.write("</a></sup>")?; |
| } |
| TaskListMarker(true) => { |
| self.write("<input disabled=\"\" type=\"checkbox\" checked=\"\"/>\n")?; |
| } |
| TaskListMarker(false) => { |
| self.write("<input disabled=\"\" type=\"checkbox\"/>\n")?; |
| } |
| } |
| } |
| Ok(()) |
| } |
| |
| /// Writes the start of an HTML tag. |
| fn start_tag(&mut self, tag: Tag<'a>) -> io::Result<()> { |
| match tag { |
| Tag::Paragraph => { |
| if self.end_newline { |
| self.write("<p>") |
| } else { |
| self.write("\n<p>") |
| } |
| } |
| Tag::Heading(level) => { |
| if self.end_newline { |
| self.end_newline = false; |
| write!(&mut self.writer, "<h{}>", level) |
| } else { |
| write!(&mut self.writer, "\n<h{}>", level) |
| } |
| } |
| Tag::Table(alignments) => { |
| self.table_alignments = alignments; |
| self.write("<table>") |
| } |
| Tag::TableHead => { |
| self.table_state = TableState::Head; |
| self.table_cell_index = 0; |
| self.write("<thead><tr>") |
| } |
| Tag::TableRow => { |
| self.table_cell_index = 0; |
| self.write("<tr>") |
| } |
| Tag::TableCell => { |
| match self.table_state { |
| TableState::Head => { |
| self.write("<th")?; |
| } |
| TableState::Body => { |
| self.write("<td")?; |
| } |
| } |
| match self.table_alignments.get(self.table_cell_index) { |
| Some(&Alignment::Left) => self.write(" align=\"left\">"), |
| Some(&Alignment::Center) => self.write(" align=\"center\">"), |
| Some(&Alignment::Right) => self.write(" align=\"right\">"), |
| _ => self.write(">"), |
| } |
| } |
| Tag::BlockQuote => { |
| if self.end_newline { |
| self.write("<blockquote>\n") |
| } else { |
| self.write("\n<blockquote>\n") |
| } |
| } |
| Tag::CodeBlock(info) => { |
| if !self.end_newline { |
| self.write_newline()?; |
| } |
| let lang = info.split(' ').next().unwrap(); |
| if lang.is_empty() { |
| self.write("<pre><code>") |
| } else { |
| self.write("<pre><code class=\"language-")?; |
| escape_html(&mut self.writer, lang)?; |
| self.write("\">") |
| } |
| } |
| Tag::List(Some(1)) => { |
| if self.end_newline { |
| self.write("<ol>\n") |
| } else { |
| self.write("\n<ol>\n") |
| } |
| } |
| Tag::List(Some(start)) => { |
| if self.end_newline { |
| self.write("<ol start=\"")?; |
| } else { |
| self.write("\n<ol start=\"")?; |
| } |
| write!(&mut self.writer, "{}", start)?; |
| self.write("\">\n") |
| } |
| Tag::List(None) => { |
| if self.end_newline { |
| self.write("<ul>\n") |
| } else { |
| self.write("\n<ul>\n") |
| } |
| } |
| Tag::Item => { |
| if self.end_newline { |
| self.write("<li>") |
| } else { |
| self.write("\n<li>") |
| } |
| } |
| Tag::Emphasis => self.write("<em>"), |
| Tag::Strong => self.write("<strong>"), |
| Tag::Strikethrough => self.write("<del>"), |
| Tag::Link(LinkType::Email, dest, title) => { |
| self.write("<a href=\"mailto:")?; |
| escape_href(&mut self.writer, &dest)?; |
| if !title.is_empty() { |
| self.write("\" title=\"")?; |
| escape_html(&mut self.writer, &title)?; |
| } |
| self.write("\">") |
| } |
| Tag::Link(_link_type, dest, title) => { |
| self.write("<a href=\"")?; |
| escape_href(&mut self.writer, &dest)?; |
| if !title.is_empty() { |
| self.write("\" title=\"")?; |
| escape_html(&mut self.writer, &title)?; |
| } |
| self.write("\">") |
| } |
| Tag::Image(_link_type, dest, title) => { |
| self.write("<img src=\"")?; |
| escape_href(&mut self.writer, &dest)?; |
| self.write("\" alt=\"")?; |
| self.raw_text()?; |
| if !title.is_empty() { |
| self.write("\" title=\"")?; |
| escape_html(&mut self.writer, &title)?; |
| } |
| self.write("\" />") |
| } |
| Tag::FootnoteDefinition(name) => { |
| if self.end_newline { |
| self.write("<div class=\"footnote-definition\" id=\"")?; |
| } else { |
| self.write("\n<div class=\"footnote-definition\" id=\"")?; |
| } |
| escape_html(&mut self.writer, &*name)?; |
| self.write("\"><sup class=\"footnote-definition-label\">")?; |
| let len = self.numbers.len() + 1; |
| let number = *self.numbers.entry(name).or_insert(len); |
| write!(&mut self.writer, "{}", number)?; |
| self.write("</sup>") |
| } |
| } |
| } |
| |
| fn end_tag(&mut self, tag: Tag) -> io::Result<()> { |
| match tag { |
| Tag::Paragraph => { |
| self.write("</p>\n")?; |
| } |
| Tag::Heading(level) => { |
| self.write("</h")?; |
| write!(&mut self.writer, "{}", level)?; |
| self.write(">\n")?; |
| } |
| Tag::Table(_) => { |
| self.write("</tbody></table>\n")?; |
| } |
| Tag::TableHead => { |
| self.write("</tr></thead><tbody>\n")?; |
| self.table_state = TableState::Body; |
| } |
| Tag::TableRow => { |
| self.write("</tr>\n")?; |
| } |
| Tag::TableCell => { |
| match self.table_state { |
| TableState::Head => { |
| self.write("</th>")?; |
| } |
| TableState::Body => { |
| self.write("</td>")?; |
| } |
| } |
| self.table_cell_index += 1; |
| } |
| Tag::BlockQuote => { |
| self.write("</blockquote>\n")?; |
| } |
| Tag::CodeBlock(_) => { |
| self.write("</code></pre>\n")?; |
| } |
| Tag::List(Some(_)) => { |
| self.write("</ol>\n")?; |
| } |
| Tag::List(None) => { |
| self.write("</ul>\n")?; |
| } |
| Tag::Item => { |
| self.write("</li>\n")?; |
| } |
| Tag::Emphasis => { |
| self.write("</em>")?; |
| } |
| Tag::Strong => { |
| self.write("</strong>")?; |
| } |
| Tag::Strikethrough => { |
| self.write("</del>")?; |
| } |
| Tag::Link(_, _, _) => { |
| self.write("</a>")?; |
| } |
| Tag::Image(_, _, _) => (), // shouldn't happen, handled in start |
| Tag::FootnoteDefinition(_) => { |
| self.write("</div>\n")?; |
| } |
| } |
| Ok(()) |
| } |
| |
| // run raw text, consuming end tag |
| fn raw_text(&mut self) -> io::Result<()> { |
| let mut nest = 0; |
| while let Some(event) = self.iter.next() { |
| match event { |
| Start(_) => nest += 1, |
| End(_) => { |
| if nest == 0 { |
| break; |
| } |
| nest -= 1; |
| } |
| Html(text) | Code(text) | Text(text) => { |
| escape_html(&mut self.writer, &text)?; |
| self.end_newline = text.ends_with('\n'); |
| } |
| SoftBreak | HardBreak | Rule => { |
| self.write(" ")?; |
| } |
| FootnoteReference(name) => { |
| let len = self.numbers.len() + 1; |
| let number = *self.numbers.entry(name).or_insert(len); |
| write!(&mut self.writer, "[{}]", number)?; |
| } |
| TaskListMarker(true) => self.write("[x]")?, |
| TaskListMarker(false) => self.write("[ ]")?, |
| } |
| } |
| Ok(()) |
| } |
| } |
| |
| /// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and |
| /// push it to a `String`. |
| /// |
| /// # Examples |
| /// |
| /// ``` |
| /// use pulldown_cmark::{html, Parser}; |
| /// |
| /// let markdown_str = r#" |
| /// hello |
| /// ===== |
| /// |
| /// * alpha |
| /// * beta |
| /// "#; |
| /// let parser = Parser::new(markdown_str); |
| /// |
| /// let mut html_buf = String::new(); |
| /// html::push_html(&mut html_buf, parser); |
| /// |
| /// assert_eq!(html_buf, r#"<h1>hello</h1> |
| /// <ul> |
| /// <li>alpha</li> |
| /// <li>beta</li> |
| /// </ul> |
| /// "#); |
| /// ``` |
| pub fn push_html<'a, I>(s: &mut String, iter: I) |
| where |
| I: Iterator<Item = Event<'a>>, |
| { |
| HtmlWriter::new(iter, s).run().unwrap(); |
| } |
| |
| /// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and |
| /// write it out to a writable stream. |
| /// |
| /// **Note**: using this function with an unbuffered writer like a file or socket |
| /// will result in poor performance. Wrap these in a |
| /// [`BufWriter`](https://doc.rust-lang.org/std/io/struct.BufWriter.html) to |
| /// prevent unnecessary slowdowns. |
| /// |
| /// # Examples |
| /// |
| /// ``` |
| /// use pulldown_cmark::{html, Parser}; |
| /// use std::io::Cursor; |
| /// |
| /// let markdown_str = r#" |
| /// hello |
| /// ===== |
| /// |
| /// * alpha |
| /// * beta |
| /// "#; |
| /// let mut bytes = Vec::new(); |
| /// let parser = Parser::new(markdown_str); |
| /// |
| /// html::write_html(Cursor::new(&mut bytes), parser); |
| /// |
| /// assert_eq!(&String::from_utf8_lossy(&bytes)[..], r#"<h1>hello</h1> |
| /// <ul> |
| /// <li>alpha</li> |
| /// <li>beta</li> |
| /// </ul> |
| /// "#); |
| /// ``` |
| pub fn write_html<'a, I, W>(writer: W, iter: I) -> io::Result<()> |
| where |
| I: Iterator<Item = Event<'a>>, |
| W: Write, |
| { |
| HtmlWriter::new(iter, WriteWrapper(writer)).run() |
| } |