Coverage Report

Created: 2025-06-02 07:01

/rust/registry/src/index.crates.io-6f17d22bba15001f/pulldown-cmark-0.13.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
25
use crate::strings::CowStr;
26
use crate::Event::*;
27
use crate::{Alignment, BlockQuoteKind, CodeBlockKind, Event, LinkType, Tag, TagEnd};
28
use pulldown_cmark_escape::{
29
    escape_href, escape_html, escape_html_body_text, FmtWriter, IoWriter, StrWrite,
30
};
31
32
enum TableState {
33
    Head,
34
    Body,
35
}
36
37
struct HtmlWriter<'a, I, W> {
38
    /// Iterator supplying events.
39
    iter: I,
40
41
    /// Writer to write to.
42
    writer: W,
43
44
    /// Whether or not the last write wrote a newline.
45
    end_newline: bool,
46
47
    /// Whether if inside a metadata block (text should not be written)
48
    in_non_writing_block: bool,
49
50
    table_state: TableState,
51
    table_alignments: Vec<Alignment>,
52
    table_cell_index: usize,
53
    numbers: HashMap<CowStr<'a>, usize>,
54
}
55
56
impl<'a, I, W> HtmlWriter<'a, I, W>
57
where
58
    I: Iterator<Item = Event<'a>>,
59
    W: StrWrite,
60
{
61
0
    fn new(iter: I, writer: W) -> Self {
62
0
        Self {
63
0
            iter,
64
0
            writer,
65
0
            end_newline: true,
66
0
            in_non_writing_block: false,
67
0
            table_state: TableState::Head,
68
0
            table_alignments: vec![],
69
0
            table_cell_index: 0,
70
0
            numbers: HashMap::new(),
71
0
        }
72
0
    }
73
74
    /// Writes a new line.
75
    #[inline]
76
0
    fn write_newline(&mut self) -> Result<(), W::Error> {
77
0
        self.end_newline = true;
78
0
        self.writer.write_str("\n")
79
0
    }
80
81
    /// Writes a buffer, and tracks whether or not a newline was written.
82
    #[inline]
83
0
    fn write(&mut self, s: &str) -> Result<(), W::Error> {
84
0
        self.writer.write_str(s)?;
85
86
0
        if !s.is_empty() {
87
0
            self.end_newline = s.ends_with('\n');
88
0
        }
89
0
        Ok(())
90
0
    }
91
92
0
    fn run(mut self) -> Result<(), W::Error> {
93
0
        while let Some(event) = self.iter.next() {
94
0
            match event {
95
0
                Start(tag) => {
96
0
                    self.start_tag(tag)?;
97
                }
98
0
                End(tag) => {
99
0
                    self.end_tag(tag)?;
100
                }
101
0
                Text(text) => {
102
0
                    if !self.in_non_writing_block {
103
0
                        escape_html_body_text(&mut self.writer, &text)?;
104
0
                        self.end_newline = text.ends_with('\n');
105
0
                    }
106
                }
107
0
                Code(text) => {
108
0
                    self.write("<code>")?;
109
0
                    escape_html_body_text(&mut self.writer, &text)?;
110
0
                    self.write("</code>")?;
111
                }
112
0
                InlineMath(text) => {
113
0
                    self.write(r#"<span class="math math-inline">"#)?;
114
0
                    escape_html(&mut self.writer, &text)?;
115
0
                    self.write("</span>")?;
116
                }
117
0
                DisplayMath(text) => {
118
0
                    self.write(r#"<span class="math math-display">"#)?;
119
0
                    escape_html(&mut self.writer, &text)?;
120
0
                    self.write("</span>")?;
121
                }
122
0
                Html(html) | InlineHtml(html) => {
123
0
                    self.write(&html)?;
124
                }
125
                SoftBreak => {
126
0
                    self.write_newline()?;
127
                }
128
                HardBreak => {
129
0
                    self.write("<br />\n")?;
130
                }
131
                Rule => {
132
0
                    if self.end_newline {
133
0
                        self.write("<hr />\n")?;
134
                    } else {
135
0
                        self.write("\n<hr />\n")?;
136
                    }
137
                }
138
0
                FootnoteReference(name) => {
139
0
                    let len = self.numbers.len() + 1;
140
0
                    self.write("<sup class=\"footnote-reference\"><a href=\"#")?;
141
0
                    escape_html(&mut self.writer, &name)?;
142
0
                    self.write("\">")?;
143
0
                    let number = *self.numbers.entry(name).or_insert(len);
144
0
                    write!(&mut self.writer, "{}", number)?;
145
0
                    self.write("</a></sup>")?;
146
                }
147
                TaskListMarker(true) => {
148
0
                    self.write("<input disabled=\"\" type=\"checkbox\" checked=\"\"/>\n")?;
149
                }
150
                TaskListMarker(false) => {
151
0
                    self.write("<input disabled=\"\" type=\"checkbox\"/>\n")?;
152
                }
153
            }
154
        }
155
0
        Ok(())
156
0
    }
157
158
    /// Writes the start of an HTML tag.
159
0
    fn start_tag(&mut self, tag: Tag<'a>) -> Result<(), W::Error> {
160
0
        match tag {
161
0
            Tag::HtmlBlock => Ok(()),
162
            Tag::Paragraph => {
163
0
                if self.end_newline {
164
0
                    self.write("<p>")
165
                } else {
166
0
                    self.write("\n<p>")
167
                }
168
            }
169
            Tag::Heading {
170
0
                level,
171
0
                id,
172
0
                classes,
173
0
                attrs,
174
0
            } => {
175
0
                if self.end_newline {
176
0
                    self.write("<")?;
177
                } else {
178
0
                    self.write("\n<")?;
179
                }
180
0
                write!(&mut self.writer, "{}", level)?;
181
0
                if let Some(id) = id {
182
0
                    self.write(" id=\"")?;
183
0
                    escape_html(&mut self.writer, &id)?;
184
0
                    self.write("\"")?;
185
0
                }
186
0
                let mut classes = classes.iter();
187
0
                if let Some(class) = classes.next() {
188
0
                    self.write(" class=\"")?;
189
0
                    escape_html(&mut self.writer, class)?;
190
0
                    for class in classes {
191
0
                        self.write(" ")?;
192
0
                        escape_html(&mut self.writer, class)?;
193
                    }
194
0
                    self.write("\"")?;
195
0
                }
196
0
                for (attr, value) in attrs {
197
0
                    self.write(" ")?;
198
0
                    escape_html(&mut self.writer, &attr)?;
199
0
                    if let Some(val) = value {
200
0
                        self.write("=\"")?;
201
0
                        escape_html(&mut self.writer, &val)?;
202
0
                        self.write("\"")?;
203
                    } else {
204
0
                        self.write("=\"\"")?;
205
                    }
206
                }
207
0
                self.write(">")
208
            }
209
0
            Tag::Table(alignments) => {
210
0
                self.table_alignments = alignments;
211
0
                self.write("<table>")
212
            }
213
            Tag::TableHead => {
214
0
                self.table_state = TableState::Head;
215
0
                self.table_cell_index = 0;
216
0
                self.write("<thead><tr>")
217
            }
218
            Tag::TableRow => {
219
0
                self.table_cell_index = 0;
220
0
                self.write("<tr>")
221
            }
222
            Tag::TableCell => {
223
0
                match self.table_state {
224
                    TableState::Head => {
225
0
                        self.write("<th")?;
226
                    }
227
                    TableState::Body => {
228
0
                        self.write("<td")?;
229
                    }
230
                }
231
0
                match self.table_alignments.get(self.table_cell_index) {
232
0
                    Some(&Alignment::Left) => self.write(" style=\"text-align: left\">"),
233
0
                    Some(&Alignment::Center) => self.write(" style=\"text-align: center\">"),
234
0
                    Some(&Alignment::Right) => self.write(" style=\"text-align: right\">"),
235
0
                    _ => self.write(">"),
236
                }
237
            }
238
0
            Tag::BlockQuote(kind) => {
239
0
                let class_str = match kind {
240
0
                    None => "",
241
0
                    Some(kind) => match kind {
242
0
                        BlockQuoteKind::Note => " class=\"markdown-alert-note\"",
243
0
                        BlockQuoteKind::Tip => " class=\"markdown-alert-tip\"",
244
0
                        BlockQuoteKind::Important => " class=\"markdown-alert-important\"",
245
0
                        BlockQuoteKind::Warning => " class=\"markdown-alert-warning\"",
246
0
                        BlockQuoteKind::Caution => " class=\"markdown-alert-caution\"",
247
                    },
248
                };
249
0
                if self.end_newline {
250
0
                    self.write(&format!("<blockquote{}>\n", class_str))
251
                } else {
252
0
                    self.write(&format!("\n<blockquote{}>\n", class_str))
253
                }
254
            }
255
0
            Tag::CodeBlock(info) => {
256
0
                if !self.end_newline {
257
0
                    self.write_newline()?;
258
0
                }
259
0
                match info {
260
0
                    CodeBlockKind::Fenced(info) => {
261
0
                        let lang = info.split(' ').next().unwrap();
262
0
                        if lang.is_empty() {
263
0
                            self.write("<pre><code>")
264
                        } else {
265
0
                            self.write("<pre><code class=\"language-")?;
266
0
                            escape_html(&mut self.writer, lang)?;
267
0
                            self.write("\">")
268
                        }
269
                    }
270
0
                    CodeBlockKind::Indented => self.write("<pre><code>"),
271
                }
272
            }
273
            Tag::List(Some(1)) => {
274
0
                if self.end_newline {
275
0
                    self.write("<ol>\n")
276
                } else {
277
0
                    self.write("\n<ol>\n")
278
                }
279
            }
280
0
            Tag::List(Some(start)) => {
281
0
                if self.end_newline {
282
0
                    self.write("<ol start=\"")?;
283
                } else {
284
0
                    self.write("\n<ol start=\"")?;
285
                }
286
0
                write!(&mut self.writer, "{}", start)?;
287
0
                self.write("\">\n")
288
            }
289
            Tag::List(None) => {
290
0
                if self.end_newline {
291
0
                    self.write("<ul>\n")
292
                } else {
293
0
                    self.write("\n<ul>\n")
294
                }
295
            }
296
            Tag::Item => {
297
0
                if self.end_newline {
298
0
                    self.write("<li>")
299
                } else {
300
0
                    self.write("\n<li>")
301
                }
302
            }
303
            Tag::DefinitionList => {
304
0
                if self.end_newline {
305
0
                    self.write("<dl>\n")
306
                } else {
307
0
                    self.write("\n<dl>\n")
308
                }
309
            }
310
            Tag::DefinitionListTitle => {
311
0
                if self.end_newline {
312
0
                    self.write("<dt>")
313
                } else {
314
0
                    self.write("\n<dt>")
315
                }
316
            }
317
            Tag::DefinitionListDefinition => {
318
0
                if self.end_newline {
319
0
                    self.write("<dd>")
320
                } else {
321
0
                    self.write("\n<dd>")
322
                }
323
            }
324
0
            Tag::Subscript => self.write("<sub>"),
325
0
            Tag::Superscript => self.write("<sup>"),
326
0
            Tag::Emphasis => self.write("<em>"),
327
0
            Tag::Strong => self.write("<strong>"),
328
0
            Tag::Strikethrough => self.write("<del>"),
329
            Tag::Link {
330
                link_type: LinkType::Email,
331
0
                dest_url,
332
0
                title,
333
0
                id: _,
334
0
            } => {
335
0
                self.write("<a href=\"mailto:")?;
336
0
                escape_href(&mut self.writer, &dest_url)?;
337
0
                if !title.is_empty() {
338
0
                    self.write("\" title=\"")?;
339
0
                    escape_html(&mut self.writer, &title)?;
340
0
                }
341
0
                self.write("\">")
342
            }
343
            Tag::Link {
344
                link_type: _,
345
0
                dest_url,
346
0
                title,
347
0
                id: _,
348
0
            } => {
349
0
                self.write("<a href=\"")?;
350
0
                escape_href(&mut self.writer, &dest_url)?;
351
0
                if !title.is_empty() {
352
0
                    self.write("\" title=\"")?;
353
0
                    escape_html(&mut self.writer, &title)?;
354
0
                }
355
0
                self.write("\">")
356
            }
357
            Tag::Image {
358
                link_type: _,
359
0
                dest_url,
360
0
                title,
361
0
                id: _,
362
0
            } => {
363
0
                self.write("<img src=\"")?;
364
0
                escape_href(&mut self.writer, &dest_url)?;
365
0
                self.write("\" alt=\"")?;
366
0
                self.raw_text()?;
367
0
                if !title.is_empty() {
368
0
                    self.write("\" title=\"")?;
369
0
                    escape_html(&mut self.writer, &title)?;
370
0
                }
371
0
                self.write("\" />")
372
            }
373
0
            Tag::FootnoteDefinition(name) => {
374
0
                if self.end_newline {
375
0
                    self.write("<div class=\"footnote-definition\" id=\"")?;
376
                } else {
377
0
                    self.write("\n<div class=\"footnote-definition\" id=\"")?;
378
                }
379
0
                escape_html(&mut self.writer, &name)?;
380
0
                self.write("\"><sup class=\"footnote-definition-label\">")?;
381
0
                let len = self.numbers.len() + 1;
382
0
                let number = *self.numbers.entry(name).or_insert(len);
383
0
                write!(&mut self.writer, "{}", number)?;
384
0
                self.write("</sup>")
385
            }
386
            Tag::MetadataBlock(_) => {
387
0
                self.in_non_writing_block = true;
388
0
                Ok(())
389
            }
390
        }
391
0
    }
392
393
0
    fn end_tag(&mut self, tag: TagEnd) -> Result<(), W::Error> {
394
0
        match tag {
395
0
            TagEnd::HtmlBlock => {}
396
            TagEnd::Paragraph => {
397
0
                self.write("</p>\n")?;
398
            }
399
0
            TagEnd::Heading(level) => {
400
0
                self.write("</")?;
401
0
                write!(&mut self.writer, "{}", level)?;
402
0
                self.write(">\n")?;
403
            }
404
            TagEnd::Table => {
405
0
                self.write("</tbody></table>\n")?;
406
            }
407
            TagEnd::TableHead => {
408
0
                self.write("</tr></thead><tbody>\n")?;
409
0
                self.table_state = TableState::Body;
410
            }
411
            TagEnd::TableRow => {
412
0
                self.write("</tr>\n")?;
413
            }
414
            TagEnd::TableCell => {
415
0
                match self.table_state {
416
                    TableState::Head => {
417
0
                        self.write("</th>")?;
418
                    }
419
                    TableState::Body => {
420
0
                        self.write("</td>")?;
421
                    }
422
                }
423
0
                self.table_cell_index += 1;
424
            }
425
            TagEnd::BlockQuote(_) => {
426
0
                self.write("</blockquote>\n")?;
427
            }
428
            TagEnd::CodeBlock => {
429
0
                self.write("</code></pre>\n")?;
430
            }
431
            TagEnd::List(true) => {
432
0
                self.write("</ol>\n")?;
433
            }
434
            TagEnd::List(false) => {
435
0
                self.write("</ul>\n")?;
436
            }
437
            TagEnd::Item => {
438
0
                self.write("</li>\n")?;
439
            }
440
            TagEnd::DefinitionList => {
441
0
                self.write("</dl>\n")?;
442
            }
443
            TagEnd::DefinitionListTitle => {
444
0
                self.write("</dt>\n")?;
445
            }
446
            TagEnd::DefinitionListDefinition => {
447
0
                self.write("</dd>\n")?;
448
            }
449
            TagEnd::Emphasis => {
450
0
                self.write("</em>")?;
451
            }
452
            TagEnd::Superscript => {
453
0
                self.write("</sup>")?;
454
            }
455
            TagEnd::Subscript => {
456
0
                self.write("</sub>")?;
457
            }
458
            TagEnd::Strong => {
459
0
                self.write("</strong>")?;
460
            }
461
            TagEnd::Strikethrough => {
462
0
                self.write("</del>")?;
463
            }
464
            TagEnd::Link => {
465
0
                self.write("</a>")?;
466
            }
467
0
            TagEnd::Image => (), // shouldn't happen, handled in start
468
            TagEnd::FootnoteDefinition => {
469
0
                self.write("</div>\n")?;
470
            }
471
0
            TagEnd::MetadataBlock(_) => {
472
0
                self.in_non_writing_block = false;
473
0
            }
474
        }
475
0
        Ok(())
476
0
    }
477
478
    // run raw text, consuming end tag
479
0
    fn raw_text(&mut self) -> Result<(), W::Error> {
480
0
        let mut nest = 0;
481
0
        while let Some(event) = self.iter.next() {
482
0
            match event {
483
0
                Start(_) => nest += 1,
484
                End(_) => {
485
0
                    if nest == 0 {
486
0
                        break;
487
0
                    }
488
0
                    nest -= 1;
489
                }
490
0
                Html(_) => {}
491
0
                InlineHtml(text) | Code(text) | Text(text) => {
492
                    // Don't use escape_html_body_text here.
493
                    // The output of this function is used in the `alt` attribute.
494
0
                    escape_html(&mut self.writer, &text)?;
495
0
                    self.end_newline = text.ends_with('\n');
496
                }
497
0
                InlineMath(text) => {
498
0
                    self.write("$")?;
499
0
                    escape_html(&mut self.writer, &text)?;
500
0
                    self.write("$")?;
501
                }
502
0
                DisplayMath(text) => {
503
0
                    self.write("$$")?;
504
0
                    escape_html(&mut self.writer, &text)?;
505
0
                    self.write("$$")?;
506
                }
507
                SoftBreak | HardBreak | Rule => {
508
0
                    self.write(" ")?;
509
                }
510
0
                FootnoteReference(name) => {
511
0
                    let len = self.numbers.len() + 1;
512
0
                    let number = *self.numbers.entry(name).or_insert(len);
513
0
                    write!(&mut self.writer, "[{}]", number)?;
514
                }
515
0
                TaskListMarker(true) => self.write("[x]")?,
516
0
                TaskListMarker(false) => self.write("[ ]")?,
517
            }
518
        }
519
0
        Ok(())
520
0
    }
521
}
522
523
/// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and
524
/// push it to a `String`.
525
///
526
/// # Examples
527
///
528
/// ```
529
/// use pulldown_cmark::{html, Parser};
530
///
531
/// let markdown_str = r#"
532
/// hello
533
/// =====
534
///
535
/// * alpha
536
/// * beta
537
/// "#;
538
/// let parser = Parser::new(markdown_str);
539
///
540
/// let mut html_buf = String::new();
541
/// html::push_html(&mut html_buf, parser);
542
///
543
/// assert_eq!(html_buf, r#"<h1>hello</h1>
544
/// <ul>
545
/// <li>alpha</li>
546
/// <li>beta</li>
547
/// </ul>
548
/// "#);
549
/// ```
550
0
pub fn push_html<'a, I>(s: &mut String, iter: I)
551
0
where
552
0
    I: Iterator<Item = Event<'a>>,
553
0
{
554
0
    write_html_fmt(s, iter).unwrap()
555
0
}
556
557
/// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and
558
/// write it out to an I/O stream.
559
///
560
/// **Note**: using this function with an unbuffered writer like a file or socket
561
/// will result in poor performance. Wrap these in a
562
/// [`BufWriter`](https://doc.rust-lang.org/std/io/struct.BufWriter.html) to
563
/// prevent unnecessary slowdowns.
564
///
565
/// # Examples
566
///
567
/// ```
568
/// use pulldown_cmark::{html, Parser};
569
/// use std::io::Cursor;
570
///
571
/// let markdown_str = r#"
572
/// hello
573
/// =====
574
///
575
/// * alpha
576
/// * beta
577
/// "#;
578
/// let mut bytes = Vec::new();
579
/// let parser = Parser::new(markdown_str);
580
///
581
/// html::write_html_io(Cursor::new(&mut bytes), parser);
582
///
583
/// assert_eq!(&String::from_utf8_lossy(&bytes)[..], r#"<h1>hello</h1>
584
/// <ul>
585
/// <li>alpha</li>
586
/// <li>beta</li>
587
/// </ul>
588
/// "#);
589
/// ```
590
0
pub fn write_html_io<'a, I, W>(writer: W, iter: I) -> std::io::Result<()>
591
0
where
592
0
    I: Iterator<Item = Event<'a>>,
593
0
    W: std::io::Write,
594
0
{
595
0
    HtmlWriter::new(iter, IoWriter(writer)).run()
596
0
}
597
598
/// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and
599
/// write it into Unicode-accepting buffer or stream.
600
///
601
/// # Examples
602
///
603
/// ```
604
/// use pulldown_cmark::{html, Parser};
605
///
606
/// let markdown_str = r#"
607
/// hello
608
/// =====
609
///
610
/// * alpha
611
/// * beta
612
/// "#;
613
/// let mut buf = String::new();
614
/// let parser = Parser::new(markdown_str);
615
///
616
/// html::write_html_fmt(&mut buf, parser);
617
///
618
/// assert_eq!(buf, r#"<h1>hello</h1>
619
/// <ul>
620
/// <li>alpha</li>
621
/// <li>beta</li>
622
/// </ul>
623
/// "#);
624
/// ```
625
0
pub fn write_html_fmt<'a, I, W>(writer: W, iter: I) -> std::fmt::Result
626
0
where
627
0
    I: Iterator<Item = Event<'a>>,
628
0
    W: std::fmt::Write,
629
0
{
630
0
    HtmlWriter::new(iter, FmtWriter(writer)).run()
631
0
}