1 | // Copyright 2015 Google Inc. All rights reserved. |
2 | // |
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy |
4 | // of this software and associated documentation files (the "Software"), to deal |
5 | // in the Software without restriction, including without limitation the rights |
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
7 | // copies of the Software, and to permit persons to whom the Software is |
8 | // furnished to do so, subject to the following conditions: |
9 | // |
10 | // The above copyright notice and this permission notice shall be included in |
11 | // all copies or substantial portions of the Software. |
12 | // |
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
19 | // THE SOFTWARE. |
20 | |
21 | //! HTML renderer that takes an iterator of events as input. |
22 | |
23 | use std::collections::HashMap; |
24 | use std::io::{self, Write}; |
25 | |
26 | use crate::escape::{escape_href, escape_html, StrWrite, WriteWrapper}; |
27 | use crate::strings::CowStr; |
28 | use crate::Event::*; |
29 | use crate::{Alignment, CodeBlockKind, Event, LinkType, Tag}; |
30 | |
31 | enum TableState { |
32 | Head, |
33 | Body, |
34 | } |
35 | |
36 | struct HtmlWriter<'a, I, W> { |
37 | /// Iterator supplying events. |
38 | iter: I, |
39 | |
40 | /// Writer to write to. |
41 | writer: W, |
42 | |
43 | /// Whether or not the last write wrote a newline. |
44 | end_newline: bool, |
45 | |
46 | table_state: TableState, |
47 | table_alignments: Vec<Alignment>, |
48 | table_cell_index: usize, |
49 | numbers: HashMap<CowStr<'a>, usize>, |
50 | } |
51 | |
52 | impl<'a, I, W> HtmlWriter<'a, I, W> |
53 | where |
54 | I: Iterator<Item = Event<'a>>, |
55 | W: StrWrite, |
56 | { |
57 | fn new(iter: I, writer: W) -> Self { |
58 | Self { |
59 | iter, |
60 | writer, |
61 | end_newline: true, |
62 | table_state: TableState::Head, |
63 | table_alignments: vec![], |
64 | table_cell_index: 0, |
65 | numbers: HashMap::new(), |
66 | } |
67 | } |
68 | |
69 | /// Writes a new line. |
70 | fn write_newline(&mut self) -> io::Result<()> { |
71 | self.end_newline = true; |
72 | self.writer.write_str(" \n" ) |
73 | } |
74 | |
75 | /// Writes a buffer, and tracks whether or not a newline was written. |
76 | #[inline ] |
77 | fn write(&mut self, s: &str) -> io::Result<()> { |
78 | self.writer.write_str(s)?; |
79 | |
80 | if !s.is_empty() { |
81 | self.end_newline = s.ends_with(' \n' ); |
82 | } |
83 | Ok(()) |
84 | } |
85 | |
86 | fn run(mut self) -> io::Result<()> { |
87 | while let Some(event) = self.iter.next() { |
88 | match event { |
89 | Start(tag) => { |
90 | self.start_tag(tag)?; |
91 | } |
92 | End(tag) => { |
93 | self.end_tag(tag)?; |
94 | } |
95 | Text(text) => { |
96 | escape_html(&mut self.writer, &text)?; |
97 | self.end_newline = text.ends_with(' \n' ); |
98 | } |
99 | Code(text) => { |
100 | self.write("<code>" )?; |
101 | escape_html(&mut self.writer, &text)?; |
102 | self.write("</code>" )?; |
103 | } |
104 | Html(html) => { |
105 | self.write(&html)?; |
106 | } |
107 | SoftBreak => { |
108 | self.write_newline()?; |
109 | } |
110 | HardBreak => { |
111 | self.write("<br /> \n" )?; |
112 | } |
113 | Rule => { |
114 | if self.end_newline { |
115 | self.write("<hr /> \n" )?; |
116 | } else { |
117 | self.write(" \n<hr /> \n" )?; |
118 | } |
119 | } |
120 | FootnoteReference(name) => { |
121 | let len = self.numbers.len() + 1; |
122 | self.write("<sup class= \"footnote-reference \"><a href= \"#" )?; |
123 | escape_html(&mut self.writer, &name)?; |
124 | self.write(" \">" )?; |
125 | let number = *self.numbers.entry(name).or_insert(len); |
126 | write!(&mut self.writer, " {}" , number)?; |
127 | self.write("</a></sup>" )?; |
128 | } |
129 | TaskListMarker(true) => { |
130 | self.write("<input disabled= \"\" type= \"checkbox \" checked= \"\"/> \n" )?; |
131 | } |
132 | TaskListMarker(false) => { |
133 | self.write("<input disabled= \"\" type= \"checkbox \"/> \n" )?; |
134 | } |
135 | } |
136 | } |
137 | Ok(()) |
138 | } |
139 | |
140 | /// Writes the start of an HTML tag. |
141 | fn start_tag(&mut self, tag: Tag<'a>) -> io::Result<()> { |
142 | match tag { |
143 | Tag::Paragraph => { |
144 | if self.end_newline { |
145 | self.write("<p>" ) |
146 | } else { |
147 | self.write(" \n<p>" ) |
148 | } |
149 | } |
150 | Tag::Heading(level, id, classes) => { |
151 | if self.end_newline { |
152 | self.end_newline = false; |
153 | self.write("<" )?; |
154 | } else { |
155 | self.write(" \n<" )?; |
156 | } |
157 | write!(&mut self.writer, " {}" , level)?; |
158 | if let Some(id) = id { |
159 | self.write(" id= \"" )?; |
160 | escape_html(&mut self.writer, id)?; |
161 | self.write(" \"" )?; |
162 | } |
163 | let mut classes = classes.iter(); |
164 | if let Some(class) = classes.next() { |
165 | self.write(" class= \"" )?; |
166 | escape_html(&mut self.writer, class)?; |
167 | for class in classes { |
168 | self.write(" " )?; |
169 | escape_html(&mut self.writer, class)?; |
170 | } |
171 | self.write(" \"" )?; |
172 | } |
173 | self.write(">" ) |
174 | } |
175 | Tag::Table(alignments) => { |
176 | self.table_alignments = alignments; |
177 | self.write("<table>" ) |
178 | } |
179 | Tag::TableHead => { |
180 | self.table_state = TableState::Head; |
181 | self.table_cell_index = 0; |
182 | self.write("<thead><tr>" ) |
183 | } |
184 | Tag::TableRow => { |
185 | self.table_cell_index = 0; |
186 | self.write("<tr>" ) |
187 | } |
188 | Tag::TableCell => { |
189 | match self.table_state { |
190 | TableState::Head => { |
191 | self.write("<th" )?; |
192 | } |
193 | TableState::Body => { |
194 | self.write("<td" )?; |
195 | } |
196 | } |
197 | match self.table_alignments.get(self.table_cell_index) { |
198 | Some(&Alignment::Left) => self.write(" style= \"text-align: left \">" ), |
199 | Some(&Alignment::Center) => self.write(" style= \"text-align: center \">" ), |
200 | Some(&Alignment::Right) => self.write(" style= \"text-align: right \">" ), |
201 | _ => self.write(">" ), |
202 | } |
203 | } |
204 | Tag::BlockQuote => { |
205 | if self.end_newline { |
206 | self.write("<blockquote> \n" ) |
207 | } else { |
208 | self.write(" \n<blockquote> \n" ) |
209 | } |
210 | } |
211 | Tag::CodeBlock(info) => { |
212 | if !self.end_newline { |
213 | self.write_newline()?; |
214 | } |
215 | match info { |
216 | CodeBlockKind::Fenced(info) => { |
217 | let lang = info.split(' ' ).next().unwrap(); |
218 | if lang.is_empty() { |
219 | self.write("<pre><code>" ) |
220 | } else { |
221 | self.write("<pre><code class= \"language-" )?; |
222 | escape_html(&mut self.writer, lang)?; |
223 | self.write(" \">" ) |
224 | } |
225 | } |
226 | CodeBlockKind::Indented => self.write("<pre><code>" ), |
227 | } |
228 | } |
229 | Tag::List(Some(1)) => { |
230 | if self.end_newline { |
231 | self.write("<ol> \n" ) |
232 | } else { |
233 | self.write(" \n<ol> \n" ) |
234 | } |
235 | } |
236 | Tag::List(Some(start)) => { |
237 | if self.end_newline { |
238 | self.write("<ol start= \"" )?; |
239 | } else { |
240 | self.write(" \n<ol start= \"" )?; |
241 | } |
242 | write!(&mut self.writer, " {}" , start)?; |
243 | self.write(" \"> \n" ) |
244 | } |
245 | Tag::List(None) => { |
246 | if self.end_newline { |
247 | self.write("<ul> \n" ) |
248 | } else { |
249 | self.write(" \n<ul> \n" ) |
250 | } |
251 | } |
252 | Tag::Item => { |
253 | if self.end_newline { |
254 | self.write("<li>" ) |
255 | } else { |
256 | self.write(" \n<li>" ) |
257 | } |
258 | } |
259 | Tag::Emphasis => self.write("<em>" ), |
260 | Tag::Strong => self.write("<strong>" ), |
261 | Tag::Strikethrough => self.write("<del>" ), |
262 | Tag::Link(LinkType::Email, dest, title) => { |
263 | self.write("<a href= \"mailto:" )?; |
264 | escape_href(&mut self.writer, &dest)?; |
265 | if !title.is_empty() { |
266 | self.write(" \" title= \"" )?; |
267 | escape_html(&mut self.writer, &title)?; |
268 | } |
269 | self.write(" \">" ) |
270 | } |
271 | Tag::Link(_link_type, dest, title) => { |
272 | self.write("<a href= \"" )?; |
273 | escape_href(&mut self.writer, &dest)?; |
274 | if !title.is_empty() { |
275 | self.write(" \" title= \"" )?; |
276 | escape_html(&mut self.writer, &title)?; |
277 | } |
278 | self.write(" \">" ) |
279 | } |
280 | Tag::Image(_link_type, dest, title) => { |
281 | self.write("<img src= \"" )?; |
282 | escape_href(&mut self.writer, &dest)?; |
283 | self.write(" \" alt= \"" )?; |
284 | self.raw_text()?; |
285 | if !title.is_empty() { |
286 | self.write(" \" title= \"" )?; |
287 | escape_html(&mut self.writer, &title)?; |
288 | } |
289 | self.write(" \" />" ) |
290 | } |
291 | Tag::FootnoteDefinition(name) => { |
292 | if self.end_newline { |
293 | self.write("<div class= \"footnote-definition \" id= \"" )?; |
294 | } else { |
295 | self.write(" \n<div class= \"footnote-definition \" id= \"" )?; |
296 | } |
297 | escape_html(&mut self.writer, &*name)?; |
298 | self.write(" \"><sup class= \"footnote-definition-label \">" )?; |
299 | let len = self.numbers.len() + 1; |
300 | let number = *self.numbers.entry(name).or_insert(len); |
301 | write!(&mut self.writer, " {}" , number)?; |
302 | self.write("</sup>" ) |
303 | } |
304 | } |
305 | } |
306 | |
307 | fn end_tag(&mut self, tag: Tag) -> io::Result<()> { |
308 | match tag { |
309 | Tag::Paragraph => { |
310 | self.write("</p> \n" )?; |
311 | } |
312 | Tag::Heading(level, _id, _classes) => { |
313 | self.write("</" )?; |
314 | write!(&mut self.writer, " {}" , level)?; |
315 | self.write("> \n" )?; |
316 | } |
317 | Tag::Table(_) => { |
318 | self.write("</tbody></table> \n" )?; |
319 | } |
320 | Tag::TableHead => { |
321 | self.write("</tr></thead><tbody> \n" )?; |
322 | self.table_state = TableState::Body; |
323 | } |
324 | Tag::TableRow => { |
325 | self.write("</tr> \n" )?; |
326 | } |
327 | Tag::TableCell => { |
328 | match self.table_state { |
329 | TableState::Head => { |
330 | self.write("</th>" )?; |
331 | } |
332 | TableState::Body => { |
333 | self.write("</td>" )?; |
334 | } |
335 | } |
336 | self.table_cell_index += 1; |
337 | } |
338 | Tag::BlockQuote => { |
339 | self.write("</blockquote> \n" )?; |
340 | } |
341 | Tag::CodeBlock(_) => { |
342 | self.write("</code></pre> \n" )?; |
343 | } |
344 | Tag::List(Some(_)) => { |
345 | self.write("</ol> \n" )?; |
346 | } |
347 | Tag::List(None) => { |
348 | self.write("</ul> \n" )?; |
349 | } |
350 | Tag::Item => { |
351 | self.write("</li> \n" )?; |
352 | } |
353 | Tag::Emphasis => { |
354 | self.write("</em>" )?; |
355 | } |
356 | Tag::Strong => { |
357 | self.write("</strong>" )?; |
358 | } |
359 | Tag::Strikethrough => { |
360 | self.write("</del>" )?; |
361 | } |
362 | Tag::Link(_, _, _) => { |
363 | self.write("</a>" )?; |
364 | } |
365 | Tag::Image(_, _, _) => (), // shouldn't happen, handled in start |
366 | Tag::FootnoteDefinition(_) => { |
367 | self.write("</div> \n" )?; |
368 | } |
369 | } |
370 | Ok(()) |
371 | } |
372 | |
373 | // run raw text, consuming end tag |
374 | fn raw_text(&mut self) -> io::Result<()> { |
375 | let mut nest = 0; |
376 | while let Some(event) = self.iter.next() { |
377 | match event { |
378 | Start(_) => nest += 1, |
379 | End(_) => { |
380 | if nest == 0 { |
381 | break; |
382 | } |
383 | nest -= 1; |
384 | } |
385 | Html(text) | Code(text) | Text(text) => { |
386 | escape_html(&mut self.writer, &text)?; |
387 | self.end_newline = text.ends_with(' \n' ); |
388 | } |
389 | SoftBreak | HardBreak | Rule => { |
390 | self.write(" " )?; |
391 | } |
392 | FootnoteReference(name) => { |
393 | let len = self.numbers.len() + 1; |
394 | let number = *self.numbers.entry(name).or_insert(len); |
395 | write!(&mut self.writer, "[ {}]" , number)?; |
396 | } |
397 | TaskListMarker(true) => self.write("[x]" )?, |
398 | TaskListMarker(false) => self.write("[ ]" )?, |
399 | } |
400 | } |
401 | Ok(()) |
402 | } |
403 | } |
404 | |
405 | /// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and |
406 | /// push it to a `String`. |
407 | /// |
408 | /// # Examples |
409 | /// |
410 | /// ``` |
411 | /// use pulldown_cmark::{html, Parser}; |
412 | /// |
413 | /// let markdown_str = r#" |
414 | /// hello |
415 | /// ===== |
416 | /// |
417 | /// * alpha |
418 | /// * beta |
419 | /// "# ; |
420 | /// let parser = Parser::new(markdown_str); |
421 | /// |
422 | /// let mut html_buf = String::new(); |
423 | /// html::push_html(&mut html_buf, parser); |
424 | /// |
425 | /// assert_eq!(html_buf, r#"<h1>hello</h1> |
426 | /// <ul> |
427 | /// <li>alpha</li> |
428 | /// <li>beta</li> |
429 | /// </ul> |
430 | /// "# ); |
431 | /// ``` |
432 | pub fn push_html<'a, I>(s: &mut String, iter: I) |
433 | where |
434 | I: Iterator<Item = Event<'a>>, |
435 | { |
436 | HtmlWriter::new(iter, writer:s).run().unwrap(); |
437 | } |
438 | |
439 | /// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and |
440 | /// write it out to a writable stream. |
441 | /// |
442 | /// **Note**: using this function with an unbuffered writer like a file or socket |
443 | /// will result in poor performance. Wrap these in a |
444 | /// [`BufWriter`](https://doc.rust-lang.org/std/io/struct.BufWriter.html) to |
445 | /// prevent unnecessary slowdowns. |
446 | /// |
447 | /// # Examples |
448 | /// |
449 | /// ``` |
450 | /// use pulldown_cmark::{html, Parser}; |
451 | /// use std::io::Cursor; |
452 | /// |
453 | /// let markdown_str = r#" |
454 | /// hello |
455 | /// ===== |
456 | /// |
457 | /// * alpha |
458 | /// * beta |
459 | /// "# ; |
460 | /// let mut bytes = Vec::new(); |
461 | /// let parser = Parser::new(markdown_str); |
462 | /// |
463 | /// html::write_html(Cursor::new(&mut bytes), parser); |
464 | /// |
465 | /// assert_eq!(&String::from_utf8_lossy(&bytes)[..], r#"<h1>hello</h1> |
466 | /// <ul> |
467 | /// <li>alpha</li> |
468 | /// <li>beta</li> |
469 | /// </ul> |
470 | /// "# ); |
471 | /// ``` |
472 | pub fn write_html<'a, I, W>(writer: W, iter: I) -> io::Result<()> |
473 | where |
474 | I: Iterator<Item = Event<'a>>, |
475 | W: Write, |
476 | { |
477 | HtmlWriter::new(iter, writer:WriteWrapper(writer)).run() |
478 | } |
479 | |