/rust/registry/src/index.crates.io-6f17d22bba15001f/pulldown-cmark-0.8.0/src/html.rs
Line | Count | Source (jump to first uncovered line) |
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::parse::Event::*; |
28 | | use crate::parse::{Alignment, CodeBlockKind, Event, LinkType, Tag}; |
29 | | use crate::strings::CowStr; |
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 | 0 | fn new(iter: I, writer: W) -> Self { |
58 | 0 | Self { |
59 | 0 | iter, |
60 | 0 | writer, |
61 | 0 | end_newline: true, |
62 | 0 | table_state: TableState::Head, |
63 | 0 | table_alignments: vec![], |
64 | 0 | table_cell_index: 0, |
65 | 0 | numbers: HashMap::new(), |
66 | 0 | } |
67 | 0 | } |
68 | | |
69 | | /// Writes a new line. |
70 | 0 | fn write_newline(&mut self) -> io::Result<()> { |
71 | 0 | self.end_newline = true; |
72 | 0 | self.writer.write_str("\n") |
73 | 0 | } |
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 | 0 | self.writer.write_str(s)?; |
79 | | |
80 | 0 | if !s.is_empty() { |
81 | 0 | self.end_newline = s.ends_with('\n'); |
82 | 0 | } |
83 | 0 | Ok(()) |
84 | 0 | } |
85 | | |
86 | 0 | pub fn run(mut self) -> io::Result<()> { |
87 | 0 | while let Some(event) = self.iter.next() { |
88 | 0 | match event { |
89 | 0 | Start(tag) => { |
90 | 0 | self.start_tag(tag)?; |
91 | | } |
92 | 0 | End(tag) => { |
93 | 0 | self.end_tag(tag)?; |
94 | | } |
95 | 0 | Text(text) => { |
96 | 0 | escape_html(&mut self.writer, &text)?; |
97 | 0 | self.end_newline = text.ends_with('\n'); |
98 | | } |
99 | 0 | Code(text) => { |
100 | 0 | self.write("<code>")?; |
101 | 0 | escape_html(&mut self.writer, &text)?; |
102 | 0 | self.write("</code>")?; |
103 | | } |
104 | 0 | Html(html) => { |
105 | 0 | self.write(&html)?; |
106 | | } |
107 | | SoftBreak => { |
108 | 0 | self.write_newline()?; |
109 | | } |
110 | | HardBreak => { |
111 | 0 | self.write("<br />\n")?; |
112 | | } |
113 | | Rule => { |
114 | 0 | if self.end_newline { |
115 | 0 | self.write("<hr />\n")?; |
116 | | } else { |
117 | 0 | self.write("\n<hr />\n")?; |
118 | | } |
119 | | } |
120 | 0 | FootnoteReference(name) => { |
121 | 0 | let len = self.numbers.len() + 1; |
122 | 0 | self.write("<sup class=\"footnote-reference\"><a href=\"#")?; |
123 | 0 | escape_html(&mut self.writer, &name)?; |
124 | 0 | self.write("\">")?; |
125 | 0 | let number = *self.numbers.entry(name).or_insert(len); |
126 | 0 | write!(&mut self.writer, "{}", number)?; |
127 | 0 | self.write("</a></sup>")?; |
128 | | } |
129 | | TaskListMarker(true) => { |
130 | 0 | self.write("<input disabled=\"\" type=\"checkbox\" checked=\"\"/>\n")?; |
131 | | } |
132 | | TaskListMarker(false) => { |
133 | 0 | self.write("<input disabled=\"\" type=\"checkbox\"/>\n")?; |
134 | | } |
135 | | } |
136 | | } |
137 | 0 | Ok(()) |
138 | 0 | } |
139 | | |
140 | | /// Writes the start of an HTML tag. |
141 | | fn start_tag(&mut self, tag: Tag<'a>) -> io::Result<()> { |
142 | 0 | match tag { |
143 | | Tag::Paragraph => { |
144 | 0 | if self.end_newline { |
145 | 0 | self.write("<p>") |
146 | | } else { |
147 | 0 | self.write("\n<p>") |
148 | | } |
149 | | } |
150 | 0 | Tag::Heading(level) => { |
151 | 0 | if self.end_newline { |
152 | 0 | self.end_newline = false; |
153 | 0 | write!(&mut self.writer, "<h{}>", level) |
154 | | } else { |
155 | 0 | write!(&mut self.writer, "\n<h{}>", level) |
156 | | } |
157 | | } |
158 | 0 | Tag::Table(alignments) => { |
159 | 0 | self.table_alignments = alignments; |
160 | 0 | self.write("<table>") |
161 | | } |
162 | | Tag::TableHead => { |
163 | 0 | self.table_state = TableState::Head; |
164 | 0 | self.table_cell_index = 0; |
165 | 0 | self.write("<thead><tr>") |
166 | | } |
167 | | Tag::TableRow => { |
168 | 0 | self.table_cell_index = 0; |
169 | 0 | self.write("<tr>") |
170 | | } |
171 | | Tag::TableCell => { |
172 | 0 | match self.table_state { |
173 | | TableState::Head => { |
174 | 0 | self.write("<th")?; |
175 | | } |
176 | | TableState::Body => { |
177 | 0 | self.write("<td")?; |
178 | | } |
179 | | } |
180 | 0 | match self.table_alignments.get(self.table_cell_index) { |
181 | 0 | Some(&Alignment::Left) => self.write(" align=\"left\">"), |
182 | 0 | Some(&Alignment::Center) => self.write(" align=\"center\">"), |
183 | 0 | Some(&Alignment::Right) => self.write(" align=\"right\">"), |
184 | 0 | _ => self.write(">"), |
185 | | } |
186 | | } |
187 | | Tag::BlockQuote => { |
188 | 0 | if self.end_newline { |
189 | 0 | self.write("<blockquote>\n") |
190 | | } else { |
191 | 0 | self.write("\n<blockquote>\n") |
192 | | } |
193 | | } |
194 | 0 | Tag::CodeBlock(info) => { |
195 | 0 | if !self.end_newline { |
196 | 0 | self.write_newline()?; |
197 | 0 | } |
198 | 0 | match info { |
199 | 0 | CodeBlockKind::Fenced(info) => { |
200 | 0 | let lang = info.split(' ').next().unwrap(); |
201 | 0 | if lang.is_empty() { |
202 | 0 | self.write("<pre><code>") |
203 | | } else { |
204 | 0 | self.write("<pre><code class=\"language-")?; |
205 | 0 | escape_html(&mut self.writer, lang)?; |
206 | 0 | self.write("\">") |
207 | | } |
208 | | } |
209 | 0 | CodeBlockKind::Indented => self.write("<pre><code>"), |
210 | | } |
211 | | } |
212 | | Tag::List(Some(1)) => { |
213 | 0 | if self.end_newline { |
214 | 0 | self.write("<ol>\n") |
215 | | } else { |
216 | 0 | self.write("\n<ol>\n") |
217 | | } |
218 | | } |
219 | 0 | Tag::List(Some(start)) => { |
220 | 0 | if self.end_newline { |
221 | 0 | self.write("<ol start=\"")?; |
222 | | } else { |
223 | 0 | self.write("\n<ol start=\"")?; |
224 | | } |
225 | 0 | write!(&mut self.writer, "{}", start)?; |
226 | 0 | self.write("\">\n") |
227 | | } |
228 | | Tag::List(None) => { |
229 | 0 | if self.end_newline { |
230 | 0 | self.write("<ul>\n") |
231 | | } else { |
232 | 0 | self.write("\n<ul>\n") |
233 | | } |
234 | | } |
235 | | Tag::Item => { |
236 | 0 | if self.end_newline { |
237 | 0 | self.write("<li>") |
238 | | } else { |
239 | 0 | self.write("\n<li>") |
240 | | } |
241 | | } |
242 | 0 | Tag::Emphasis => self.write("<em>"), |
243 | 0 | Tag::Strong => self.write("<strong>"), |
244 | 0 | Tag::Strikethrough => self.write("<del>"), |
245 | 0 | Tag::Link(LinkType::Email, dest, title) => { |
246 | 0 | self.write("<a href=\"mailto:")?; |
247 | 0 | escape_href(&mut self.writer, &dest)?; |
248 | 0 | if !title.is_empty() { |
249 | 0 | self.write("\" title=\"")?; |
250 | 0 | escape_html(&mut self.writer, &title)?; |
251 | 0 | } |
252 | 0 | self.write("\">") |
253 | | } |
254 | 0 | Tag::Link(_link_type, dest, title) => { |
255 | 0 | self.write("<a href=\"")?; |
256 | 0 | escape_href(&mut self.writer, &dest)?; |
257 | 0 | if !title.is_empty() { |
258 | 0 | self.write("\" title=\"")?; |
259 | 0 | escape_html(&mut self.writer, &title)?; |
260 | 0 | } |
261 | 0 | self.write("\">") |
262 | | } |
263 | 0 | Tag::Image(_link_type, dest, title) => { |
264 | 0 | self.write("<img src=\"")?; |
265 | 0 | escape_href(&mut self.writer, &dest)?; |
266 | 0 | self.write("\" alt=\"")?; |
267 | 0 | self.raw_text()?; |
268 | 0 | if !title.is_empty() { |
269 | 0 | self.write("\" title=\"")?; |
270 | 0 | escape_html(&mut self.writer, &title)?; |
271 | 0 | } |
272 | 0 | self.write("\" />") |
273 | | } |
274 | 0 | Tag::FootnoteDefinition(name) => { |
275 | 0 | if self.end_newline { |
276 | 0 | self.write("<div class=\"footnote-definition\" id=\"")?; |
277 | | } else { |
278 | 0 | self.write("\n<div class=\"footnote-definition\" id=\"")?; |
279 | | } |
280 | 0 | escape_html(&mut self.writer, &*name)?; |
281 | 0 | self.write("\"><sup class=\"footnote-definition-label\">")?; |
282 | 0 | let len = self.numbers.len() + 1; |
283 | 0 | let number = *self.numbers.entry(name).or_insert(len); |
284 | 0 | write!(&mut self.writer, "{}", number)?; |
285 | 0 | self.write("</sup>") |
286 | | } |
287 | | } |
288 | 0 | } |
289 | | |
290 | | fn end_tag(&mut self, tag: Tag) -> io::Result<()> { |
291 | 0 | match tag { |
292 | | Tag::Paragraph => { |
293 | 0 | self.write("</p>\n")?; |
294 | | } |
295 | 0 | Tag::Heading(level) => { |
296 | 0 | self.write("</h")?; |
297 | 0 | write!(&mut self.writer, "{}", level)?; |
298 | 0 | self.write(">\n")?; |
299 | | } |
300 | | Tag::Table(_) => { |
301 | 0 | self.write("</tbody></table>\n")?; |
302 | | } |
303 | | Tag::TableHead => { |
304 | 0 | self.write("</tr></thead><tbody>\n")?; |
305 | 0 | self.table_state = TableState::Body; |
306 | | } |
307 | | Tag::TableRow => { |
308 | 0 | self.write("</tr>\n")?; |
309 | | } |
310 | | Tag::TableCell => { |
311 | 0 | match self.table_state { |
312 | | TableState::Head => { |
313 | 0 | self.write("</th>")?; |
314 | | } |
315 | | TableState::Body => { |
316 | 0 | self.write("</td>")?; |
317 | | } |
318 | | } |
319 | 0 | self.table_cell_index += 1; |
320 | | } |
321 | | Tag::BlockQuote => { |
322 | 0 | self.write("</blockquote>\n")?; |
323 | | } |
324 | | Tag::CodeBlock(_) => { |
325 | 0 | self.write("</code></pre>\n")?; |
326 | | } |
327 | | Tag::List(Some(_)) => { |
328 | 0 | self.write("</ol>\n")?; |
329 | | } |
330 | | Tag::List(None) => { |
331 | 0 | self.write("</ul>\n")?; |
332 | | } |
333 | | Tag::Item => { |
334 | 0 | self.write("</li>\n")?; |
335 | | } |
336 | | Tag::Emphasis => { |
337 | 0 | self.write("</em>")?; |
338 | | } |
339 | | Tag::Strong => { |
340 | 0 | self.write("</strong>")?; |
341 | | } |
342 | | Tag::Strikethrough => { |
343 | 0 | self.write("</del>")?; |
344 | | } |
345 | | Tag::Link(_, _, _) => { |
346 | 0 | self.write("</a>")?; |
347 | | } |
348 | 0 | Tag::Image(_, _, _) => (), // shouldn't happen, handled in start |
349 | | Tag::FootnoteDefinition(_) => { |
350 | 0 | self.write("</div>\n")?; |
351 | | } |
352 | | } |
353 | 0 | Ok(()) |
354 | 0 | } |
355 | | |
356 | | // run raw text, consuming end tag |
357 | 0 | fn raw_text(&mut self) -> io::Result<()> { |
358 | 0 | let mut nest = 0; |
359 | 0 | while let Some(event) = self.iter.next() { |
360 | 0 | match event { |
361 | 0 | Start(_) => nest += 1, |
362 | | End(_) => { |
363 | 0 | if nest == 0 { |
364 | 0 | break; |
365 | 0 | } |
366 | 0 | nest -= 1; |
367 | | } |
368 | 0 | Html(text) | Code(text) | Text(text) => { |
369 | 0 | escape_html(&mut self.writer, &text)?; |
370 | 0 | self.end_newline = text.ends_with('\n'); |
371 | | } |
372 | | SoftBreak | HardBreak | Rule => { |
373 | 0 | self.write(" ")?; |
374 | | } |
375 | 0 | FootnoteReference(name) => { |
376 | 0 | let len = self.numbers.len() + 1; |
377 | 0 | let number = *self.numbers.entry(name).or_insert(len); |
378 | 0 | write!(&mut self.writer, "[{}]", number)?; |
379 | | } |
380 | 0 | TaskListMarker(true) => self.write("[x]")?, |
381 | 0 | TaskListMarker(false) => self.write("[ ]")?, |
382 | | } |
383 | | } |
384 | 0 | Ok(()) |
385 | 0 | } |
386 | | } |
387 | | |
388 | | /// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and |
389 | | /// push it to a `String`. |
390 | | /// |
391 | | /// # Examples |
392 | | /// |
393 | | /// ``` |
394 | | /// use pulldown_cmark::{html, Parser}; |
395 | | /// |
396 | | /// let markdown_str = r#" |
397 | | /// hello |
398 | | /// ===== |
399 | | /// |
400 | | /// * alpha |
401 | | /// * beta |
402 | | /// "#; |
403 | | /// let parser = Parser::new(markdown_str); |
404 | | /// |
405 | | /// let mut html_buf = String::new(); |
406 | | /// html::push_html(&mut html_buf, parser); |
407 | | /// |
408 | | /// assert_eq!(html_buf, r#"<h1>hello</h1> |
409 | | /// <ul> |
410 | | /// <li>alpha</li> |
411 | | /// <li>beta</li> |
412 | | /// </ul> |
413 | | /// "#); |
414 | | /// ``` |
415 | 0 | pub fn push_html<'a, I>(s: &mut String, iter: I) |
416 | 0 | where |
417 | 0 | I: Iterator<Item = Event<'a>>, |
418 | 0 | { |
419 | 0 | HtmlWriter::new(iter, s).run().unwrap(); |
420 | 0 | } |
421 | | |
422 | | /// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and |
423 | | /// write it out to a writable stream. |
424 | | /// |
425 | | /// **Note**: using this function with an unbuffered writer like a file or socket |
426 | | /// will result in poor performance. Wrap these in a |
427 | | /// [`BufWriter`](https://doc.rust-lang.org/std/io/struct.BufWriter.html) to |
428 | | /// prevent unnecessary slowdowns. |
429 | | /// |
430 | | /// # Examples |
431 | | /// |
432 | | /// ``` |
433 | | /// use pulldown_cmark::{html, Parser}; |
434 | | /// use std::io::Cursor; |
435 | | /// |
436 | | /// let markdown_str = r#" |
437 | | /// hello |
438 | | /// ===== |
439 | | /// |
440 | | /// * alpha |
441 | | /// * beta |
442 | | /// "#; |
443 | | /// let mut bytes = Vec::new(); |
444 | | /// let parser = Parser::new(markdown_str); |
445 | | /// |
446 | | /// html::write_html(Cursor::new(&mut bytes), parser); |
447 | | /// |
448 | | /// assert_eq!(&String::from_utf8_lossy(&bytes)[..], r#"<h1>hello</h1> |
449 | | /// <ul> |
450 | | /// <li>alpha</li> |
451 | | /// <li>beta</li> |
452 | | /// </ul> |
453 | | /// "#); |
454 | | /// ``` |
455 | 0 | pub fn write_html<'a, I, W>(writer: W, iter: I) -> io::Result<()> |
456 | 0 | where |
457 | 0 | I: Iterator<Item = Event<'a>>, |
458 | 0 | W: Write, |
459 | 0 | { |
460 | 0 | HtmlWriter::new(iter, WriteWrapper(writer)).run() |
461 | 0 | } |