comrak/
html.rs

1//! The HTML renderer for the CommonMark AST, as well as helper functions.
2use crate::character_set::character_set;
3use crate::ctype::isspace;
4use crate::nodes::{
5    AstNode, ListType, NodeCode, NodeFootnoteDefinition, NodeMath, NodeTable, NodeValue,
6    TableAlignment,
7};
8use crate::parser::{Options, Plugins};
9use crate::scanners;
10use std::borrow::Cow;
11use std::cell::Cell;
12use std::collections::{HashMap, HashSet};
13use std::io::{self, Write};
14use std::str;
15use unicode_categories::UnicodeCategories;
16
17use crate::adapters::HeadingMeta;
18
19/// Formats an AST as HTML, modified by the given options.
20pub fn format_document<'a>(
21    root: &'a AstNode<'a>,
22    options: &Options,
23    output: &mut dyn Write,
24) -> io::Result<()> {
25    format_document_with_plugins(root, options, output, &Plugins::default())
26}
27
28/// Formats an AST as HTML, modified by the given options. Accepts custom plugins.
29pub fn format_document_with_plugins<'a>(
30    root: &'a AstNode<'a>,
31    options: &Options,
32    output: &mut dyn Write,
33    plugins: &Plugins,
34) -> io::Result<()> {
35    let mut writer = WriteWithLast {
36        output,
37        last_was_lf: Cell::new(true),
38    };
39    let mut f = HtmlFormatter::new(options, &mut writer, plugins);
40    f.format(root, false)?;
41    if f.footnote_ix > 0 {
42        f.output.write_all(b"</ol>\n</section>\n")?;
43    }
44    Ok(())
45}
46
47struct WriteWithLast<'w> {
48    output: &'w mut dyn Write,
49    last_was_lf: Cell<bool>,
50}
51
52impl<'w> Write for WriteWithLast<'w> {
53    fn flush(&mut self) -> io::Result<()> {
54        self.output.flush()
55    }
56
57    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
58        let l = buf.len();
59        if l > 0 {
60            self.last_was_lf.set(buf[l - 1] == 10);
61        }
62        self.output.write(buf)
63    }
64}
65
66/// Converts header strings to canonical, unique, but still human-readable,
67/// anchors.
68///
69/// To guarantee uniqueness, an anchorizer keeps track of the anchors it has
70/// returned; use one per output file.
71///
72/// ## Example
73///
74/// ```
75/// # use comrak::Anchorizer;
76/// let mut anchorizer = Anchorizer::new();
77/// // First "stuff" is unsuffixed.
78/// assert_eq!("stuff".to_string(), anchorizer.anchorize("Stuff".to_string()));
79/// // Second "stuff" has "-1" appended to make it unique.
80/// assert_eq!("stuff-1".to_string(), anchorizer.anchorize("Stuff".to_string()));
81/// ```
82#[derive(Debug, Default)]
83#[doc(hidden)]
84pub struct Anchorizer(HashSet<String>);
85
86impl Anchorizer {
87    /// Construct a new anchorizer.
88    pub fn new() -> Self {
89        Anchorizer(HashSet::new())
90    }
91
92    /// Returns a String that has been converted into an anchor using the
93    /// GFM algorithm, which involves changing spaces to dashes, removing
94    /// problem characters and, if needed, adding a suffix to make the
95    /// resultant anchor unique.
96    ///
97    /// ```
98    /// # use comrak::Anchorizer;
99    /// let mut anchorizer = Anchorizer::new();
100    /// let source = "Ticks aren't in";
101    /// assert_eq!("ticks-arent-in".to_string(), anchorizer.anchorize(source.to_string()));
102    /// ```
103    pub fn anchorize(&mut self, header: String) -> String {
104        fn is_permitted_char(&c: &char) -> bool {
105            c == ' '
106                || c == '-'
107                || c.is_letter()
108                || c.is_mark()
109                || c.is_number()
110                || c.is_punctuation_connector()
111        }
112
113        let mut id = header.to_lowercase();
114        id = id
115            .chars()
116            .filter(is_permitted_char)
117            .map(|c| if c == ' ' { '-' } else { c })
118            .collect();
119
120        let mut uniq = 0;
121        id = loop {
122            let anchor = if uniq == 0 {
123                Cow::from(&id)
124            } else {
125                Cow::from(format!("{}-{}", id, uniq))
126            };
127
128            if !self.0.contains(&*anchor) {
129                break anchor.into_owned();
130            }
131
132            uniq += 1;
133        };
134        self.0.insert(id.clone());
135        id
136    }
137}
138
139struct HtmlFormatter<'o, 'c> {
140    output: &'o mut WriteWithLast<'o>,
141    options: &'o Options<'c>,
142    anchorizer: Anchorizer,
143    footnote_ix: u32,
144    written_footnote_ix: u32,
145    plugins: &'o Plugins<'o>,
146}
147
148fn tagfilter(literal: &[u8]) -> bool {
149    static TAGFILTER_BLACKLIST: [&str; 9] = [
150        "title",
151        "textarea",
152        "style",
153        "xmp",
154        "iframe",
155        "noembed",
156        "noframes",
157        "script",
158        "plaintext",
159    ];
160
161    if literal.len() < 3 || literal[0] != b'<' {
162        return false;
163    }
164
165    let mut i = 1;
166    if literal[i] == b'/' {
167        i += 1;
168    }
169
170    let lc = unsafe { String::from_utf8_unchecked(literal[i..].to_vec()) }.to_lowercase();
171    for t in TAGFILTER_BLACKLIST.iter() {
172        if lc.starts_with(t) {
173            let j = i + t.len();
174            return isspace(literal[j])
175                || literal[j] == b'>'
176                || (literal[j] == b'/' && literal.len() >= j + 2 && literal[j + 1] == b'>');
177        }
178    }
179
180    false
181}
182
183fn tagfilter_block(input: &[u8], o: &mut dyn Write) -> io::Result<()> {
184    let size = input.len();
185    let mut i = 0;
186
187    while i < size {
188        let org = i;
189        while i < size && input[i] != b'<' {
190            i += 1;
191        }
192
193        if i > org {
194            o.write_all(&input[org..i])?;
195        }
196
197        if i >= size {
198            break;
199        }
200
201        if tagfilter(&input[i..]) {
202            o.write_all(b"&lt;")?;
203        } else {
204            o.write_all(b"<")?;
205        }
206
207        i += 1;
208    }
209
210    Ok(())
211}
212
213fn dangerous_url(input: &[u8]) -> bool {
214    scanners::dangerous_url(input).is_some()
215}
216
217/// Writes buffer to output, escaping anything that could be interpreted as an
218/// HTML tag.
219///
220/// Namely:
221///
222/// * U+0022 QUOTATION MARK " is rendered as &quot;
223/// * U+0026 AMPERSAND & is rendered as &amp;
224/// * U+003C LESS-THAN SIGN < is rendered as &lt;
225/// * U+003E GREATER-THAN SIGN > is rendered as &gt;
226/// * Everything else is passed through unchanged.
227///
228/// Note that this is appropriate and sufficient for free text, but not for
229/// URLs in attributes.  See escape_href.
230pub fn escape(output: &mut dyn Write, buffer: &[u8]) -> io::Result<()> {
231    const HTML_UNSAFE: [bool; 256] = character_set!(b"&<>\"");
232
233    let mut offset = 0;
234    for (i, &byte) in buffer.iter().enumerate() {
235        if HTML_UNSAFE[byte as usize] {
236            let esc: &[u8] = match byte {
237                b'"' => b"&quot;",
238                b'&' => b"&amp;",
239                b'<' => b"&lt;",
240                b'>' => b"&gt;",
241                _ => unreachable!(),
242            };
243            output.write_all(&buffer[offset..i])?;
244            output.write_all(esc)?;
245            offset = i + 1;
246        }
247    }
248    output.write_all(&buffer[offset..])?;
249    Ok(())
250}
251
252/// Writes buffer to output, escaping in a manner appropriate for URLs in HTML
253/// attributes.
254///
255/// Namely:
256///
257/// * U+0026 AMPERSAND & is rendered as &amp;
258/// * U+0027 APOSTROPHE ' is rendered as &#x27;
259/// * Alphanumeric and a range of non-URL safe characters.
260///
261/// The inclusion of characters like "%" in those which are not escaped is
262/// explained somewhat here:
263///
264/// <https://github.com/github/cmark-gfm/blob/c32ef78bae851cb83b7ad52d0fbff880acdcd44a/src/houdini_href_e.c#L7-L31>
265///
266/// In other words, if a CommonMark user enters:
267///
268/// ```markdown
269/// [hi](https://ddg.gg/?q=a%20b)
270/// ```
271///
272/// We assume they actually want the query string "?q=a%20b", a search for
273/// the string "a b", rather than "?q=a%2520b", a search for the literal
274/// string "a%20b".
275pub fn escape_href(output: &mut dyn Write, buffer: &[u8]) -> io::Result<()> {
276    const HREF_SAFE: [bool; 256] = character_set!(
277        b"-_.+!*(),%#@?=;:/,+$~",
278        b"abcdefghijklmnopqrstuvwxyz",
279        b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
280    );
281
282    let size = buffer.len();
283    let mut i = 0;
284
285    while i < size {
286        let org = i;
287        while i < size && HREF_SAFE[buffer[i] as usize] {
288            i += 1;
289        }
290
291        if i > org {
292            output.write_all(&buffer[org..i])?;
293        }
294
295        if i >= size {
296            break;
297        }
298
299        match buffer[i] as char {
300            '&' => {
301                output.write_all(b"&amp;")?;
302            }
303            '\'' => {
304                output.write_all(b"&#x27;")?;
305            }
306            _ => write!(output, "%{:02X}", buffer[i])?,
307        }
308
309        i += 1;
310    }
311
312    Ok(())
313}
314
315/// Writes an opening HTML tag, using an iterator to enumerate the attributes.
316/// Note that attribute values are automatically escaped.
317pub fn write_opening_tag<Str>(
318    output: &mut dyn Write,
319    tag: &str,
320    attributes: impl IntoIterator<Item = (Str, Str)>,
321) -> io::Result<()>
322where
323    Str: AsRef<str>,
324{
325    write!(output, "<{}", tag)?;
326    for (attr, val) in attributes {
327        write!(output, " {}=\"", attr.as_ref())?;
328        escape(output, val.as_ref().as_bytes())?;
329        output.write_all(b"\"")?;
330    }
331    output.write_all(b">")?;
332    Ok(())
333}
334
335impl<'o, 'c> HtmlFormatter<'o, 'c>
336where
337    'c: 'o,
338{
339    fn new(
340        options: &'o Options<'c>,
341        output: &'o mut WriteWithLast<'o>,
342        plugins: &'o Plugins,
343    ) -> Self {
344        HtmlFormatter {
345            options,
346            output,
347            anchorizer: Anchorizer::new(),
348            footnote_ix: 0,
349            written_footnote_ix: 0,
350            plugins,
351        }
352    }
353
354    fn cr(&mut self) -> io::Result<()> {
355        if !self.output.last_was_lf.get() {
356            self.output.write_all(b"\n")?;
357        }
358        Ok(())
359    }
360
361    fn escape(&mut self, buffer: &[u8]) -> io::Result<()> {
362        escape(&mut self.output, buffer)
363    }
364
365    fn escape_href(&mut self, buffer: &[u8]) -> io::Result<()> {
366        escape_href(&mut self.output, buffer)
367    }
368
369    fn format<'a>(&mut self, node: &'a AstNode<'a>, plain: bool) -> io::Result<()> {
370        // Traverse the AST iteratively using a work stack, with pre- and
371        // post-child-traversal phases. During pre-order traversal render the
372        // opening tags, then push the node back onto the stack for the
373        // post-order traversal phase, then push the children in reverse order
374        // onto the stack and begin rendering first child.
375
376        enum Phase {
377            Pre,
378            Post,
379        }
380        let mut stack = vec![(node, plain, Phase::Pre)];
381
382        while let Some((node, plain, phase)) = stack.pop() {
383            match phase {
384                Phase::Pre => {
385                    let new_plain = if plain {
386                        match node.data.borrow().value {
387                            NodeValue::Text(ref literal)
388                            | NodeValue::Code(NodeCode { ref literal, .. })
389                            | NodeValue::HtmlInline(ref literal) => {
390                                self.escape(literal.as_bytes())?;
391                            }
392                            NodeValue::LineBreak | NodeValue::SoftBreak => {
393                                self.output.write_all(b" ")?;
394                            }
395                            NodeValue::Math(NodeMath { ref literal, .. }) => {
396                                self.escape(literal.as_bytes())?;
397                            }
398                            _ => (),
399                        }
400                        plain
401                    } else {
402                        stack.push((node, false, Phase::Post));
403                        self.format_node(node, true)?
404                    };
405
406                    for ch in node.reverse_children() {
407                        stack.push((ch, new_plain, Phase::Pre));
408                    }
409                }
410                Phase::Post => {
411                    debug_assert!(!plain);
412                    self.format_node(node, false)?;
413                }
414            }
415        }
416
417        Ok(())
418    }
419
420    fn collect_text<'a>(node: &'a AstNode<'a>, output: &mut Vec<u8>) {
421        match node.data.borrow().value {
422            NodeValue::Text(ref literal) | NodeValue::Code(NodeCode { ref literal, .. }) => {
423                output.extend_from_slice(literal.as_bytes())
424            }
425            NodeValue::LineBreak | NodeValue::SoftBreak => output.push(b' '),
426            NodeValue::Math(NodeMath { ref literal, .. }) => {
427                output.extend_from_slice(literal.as_bytes())
428            }
429            _ => {
430                for n in node.children() {
431                    Self::collect_text(n, output);
432                }
433            }
434        }
435    }
436
437    fn format_node<'a>(&mut self, node: &'a AstNode<'a>, entering: bool) -> io::Result<bool> {
438        match node.data.borrow().value {
439            NodeValue::Document => (),
440            NodeValue::FrontMatter(_) => (),
441            NodeValue::BlockQuote => {
442                if entering {
443                    self.cr()?;
444                    self.output.write_all(b"<blockquote")?;
445                    self.render_sourcepos(node)?;
446                    self.output.write_all(b">\n")?;
447                } else {
448                    self.cr()?;
449                    self.output.write_all(b"</blockquote>\n")?;
450                }
451            }
452            NodeValue::List(ref nl) => {
453                if entering {
454                    self.cr()?;
455                    match nl.list_type {
456                        ListType::Bullet => {
457                            self.output.write_all(b"<ul")?;
458                            if nl.is_task_list && self.options.render.tasklist_classes {
459                                self.output.write_all(b" class=\"contains-task-list\"")?;
460                            }
461                            self.render_sourcepos(node)?;
462                            self.output.write_all(b">\n")?;
463                        }
464                        ListType::Ordered => {
465                            self.output.write_all(b"<ol")?;
466                            if nl.is_task_list && self.options.render.tasklist_classes {
467                                self.output.write_all(b" class=\"contains-task-list\"")?;
468                            }
469                            self.render_sourcepos(node)?;
470                            if nl.start == 1 {
471                                self.output.write_all(b">\n")?;
472                            } else {
473                                writeln!(self.output, " start=\"{}\">", nl.start)?;
474                            }
475                        }
476                    }
477                } else if nl.list_type == ListType::Bullet {
478                    self.output.write_all(b"</ul>\n")?;
479                } else {
480                    self.output.write_all(b"</ol>\n")?;
481                }
482            }
483            NodeValue::Item(..) => {
484                if entering {
485                    self.cr()?;
486                    self.output.write_all(b"<li")?;
487                    self.render_sourcepos(node)?;
488                    self.output.write_all(b">")?;
489                } else {
490                    self.output.write_all(b"</li>\n")?;
491                }
492            }
493            NodeValue::DescriptionList => {
494                if entering {
495                    self.cr()?;
496                    self.output.write_all(b"<dl")?;
497                    self.render_sourcepos(node)?;
498                    self.output.write_all(b">\n")?;
499                } else {
500                    self.output.write_all(b"</dl>\n")?;
501                }
502            }
503            NodeValue::DescriptionItem(..) => (),
504            NodeValue::DescriptionTerm => {
505                if entering {
506                    self.output.write_all(b"<dt")?;
507                    self.render_sourcepos(node)?;
508                    self.output.write_all(b">")?;
509                } else {
510                    self.output.write_all(b"</dt>\n")?;
511                }
512            }
513            NodeValue::DescriptionDetails => {
514                if entering {
515                    self.output.write_all(b"<dd")?;
516                    self.render_sourcepos(node)?;
517                    self.output.write_all(b">")?;
518                } else {
519                    self.output.write_all(b"</dd>\n")?;
520                }
521            }
522            NodeValue::Heading(ref nch) => match self.plugins.render.heading_adapter {
523                None => {
524                    if entering {
525                        self.cr()?;
526                        write!(self.output, "<h{}", nch.level)?;
527                        self.render_sourcepos(node)?;
528                        self.output.write_all(b">")?;
529
530                        if let Some(ref prefix) = self.options.extension.header_ids {
531                            let mut text_content = Vec::with_capacity(20);
532                            Self::collect_text(node, &mut text_content);
533
534                            let mut id = String::from_utf8(text_content).unwrap();
535                            id = self.anchorizer.anchorize(id);
536                            write!(
537                                        self.output,
538                                        "<a href=\"#{}\" aria-hidden=\"true\" class=\"anchor\" id=\"{}{}\"></a>",
539                                        id,
540                                        prefix,
541                                        id
542                                    )?;
543                        }
544                    } else {
545                        writeln!(self.output, "</h{}>", nch.level)?;
546                    }
547                }
548                Some(adapter) => {
549                    let mut text_content = Vec::with_capacity(20);
550                    Self::collect_text(node, &mut text_content);
551                    let content = String::from_utf8(text_content).unwrap();
552                    let heading = HeadingMeta {
553                        level: nch.level,
554                        content,
555                    };
556
557                    if entering {
558                        self.cr()?;
559                        adapter.enter(
560                            self.output,
561                            &heading,
562                            if self.options.render.sourcepos {
563                                Some(node.data.borrow().sourcepos)
564                            } else {
565                                None
566                            },
567                        )?;
568                    } else {
569                        adapter.exit(self.output, &heading)?;
570                    }
571                }
572            },
573            NodeValue::CodeBlock(ref ncb) => {
574                if entering {
575                    if ncb.info.eq("math") {
576                        self.render_math_code_block(node, &ncb.literal)?;
577                    } else {
578                        self.cr()?;
579
580                        let mut first_tag = 0;
581                        let mut pre_attributes: HashMap<String, String> = HashMap::new();
582                        let mut code_attributes: HashMap<String, String> = HashMap::new();
583                        let code_attr: String;
584
585                        let literal = &ncb.literal.as_bytes();
586                        let info = &ncb.info.as_bytes();
587
588                        if !info.is_empty() {
589                            while first_tag < info.len() && !isspace(info[first_tag]) {
590                                first_tag += 1;
591                            }
592
593                            let lang_str = str::from_utf8(&info[..first_tag]).unwrap();
594                            let info_str = str::from_utf8(&info[first_tag..]).unwrap().trim();
595
596                            if self.options.render.github_pre_lang {
597                                pre_attributes.insert(String::from("lang"), lang_str.to_string());
598
599                                if self.options.render.full_info_string && !info_str.is_empty() {
600                                    pre_attributes.insert(
601                                        String::from("data-meta"),
602                                        info_str.trim().to_string(),
603                                    );
604                                }
605                            } else {
606                                code_attr = format!("language-{}", lang_str);
607                                code_attributes.insert(String::from("class"), code_attr);
608
609                                if self.options.render.full_info_string && !info_str.is_empty() {
610                                    code_attributes
611                                        .insert(String::from("data-meta"), info_str.to_string());
612                                }
613                            }
614                        }
615
616                        if self.options.render.sourcepos {
617                            let ast = node.data.borrow();
618                            pre_attributes
619                                .insert("data-sourcepos".to_string(), ast.sourcepos.to_string());
620                        }
621
622                        match self.plugins.render.codefence_syntax_highlighter {
623                            None => {
624                                write_opening_tag(self.output, "pre", pre_attributes)?;
625                                write_opening_tag(self.output, "code", code_attributes)?;
626
627                                self.escape(literal)?;
628
629                                self.output.write_all(b"</code></pre>\n")?
630                            }
631                            Some(highlighter) => {
632                                highlighter.write_pre_tag(self.output, pre_attributes)?;
633                                highlighter.write_code_tag(self.output, code_attributes)?;
634
635                                highlighter.write_highlighted(
636                                    self.output,
637                                    match str::from_utf8(&info[..first_tag]) {
638                                        Ok(lang) => Some(lang),
639                                        Err(_) => None,
640                                    },
641                                    &ncb.literal,
642                                )?;
643
644                                self.output.write_all(b"</code></pre>\n")?
645                            }
646                        }
647                    }
648                }
649            }
650            NodeValue::HtmlBlock(ref nhb) => {
651                // No sourcepos.
652                if entering {
653                    self.cr()?;
654                    let literal = nhb.literal.as_bytes();
655                    if self.options.render.escape {
656                        self.escape(literal)?;
657                    } else if !self.options.render.unsafe_ {
658                        self.output.write_all(b"<!-- raw HTML omitted -->")?;
659                    } else if self.options.extension.tagfilter {
660                        tagfilter_block(literal, &mut self.output)?;
661                    } else {
662                        self.output.write_all(literal)?;
663                    }
664                    self.cr()?;
665                }
666            }
667            NodeValue::ThematicBreak => {
668                if entering {
669                    self.cr()?;
670                    self.output.write_all(b"<hr")?;
671                    self.render_sourcepos(node)?;
672                    self.output.write_all(b" />\n")?;
673                }
674            }
675            NodeValue::Paragraph => {
676                let tight = match node
677                    .parent()
678                    .and_then(|n| n.parent())
679                    .map(|n| n.data.borrow().value.clone())
680                {
681                    Some(NodeValue::List(nl)) => nl.tight,
682                    Some(NodeValue::DescriptionItem(nd)) => nd.tight,
683                    _ => false,
684                };
685
686                let tight = tight
687                    || matches!(
688                        node.parent().map(|n| n.data.borrow().value.clone()),
689                        Some(NodeValue::DescriptionTerm)
690                    );
691
692                if !tight {
693                    if entering {
694                        self.cr()?;
695                        self.output.write_all(b"<p")?;
696                        self.render_sourcepos(node)?;
697                        self.output.write_all(b">")?;
698                    } else {
699                        if let NodeValue::FootnoteDefinition(nfd) =
700                            &node.parent().unwrap().data.borrow().value
701                        {
702                            if node.next_sibling().is_none() {
703                                self.output.write_all(b" ")?;
704                                self.put_footnote_backref(nfd)?;
705                            }
706                        }
707                        self.output.write_all(b"</p>\n")?;
708                    }
709                }
710            }
711            NodeValue::Text(ref literal) => {
712                // Nowhere to put sourcepos.
713                if entering {
714                    self.escape(literal.as_bytes())?;
715                }
716            }
717            NodeValue::LineBreak => {
718                // Unreliable sourcepos.
719                if entering {
720                    self.output.write_all(b"<br")?;
721                    if self.options.render.experimental_inline_sourcepos {
722                        self.render_sourcepos(node)?;
723                    }
724                    self.output.write_all(b" />\n")?;
725                }
726            }
727            NodeValue::SoftBreak => {
728                // Unreliable sourcepos.
729                if entering {
730                    if self.options.render.hardbreaks {
731                        self.output.write_all(b"<br")?;
732                        if self.options.render.experimental_inline_sourcepos {
733                            self.render_sourcepos(node)?;
734                        }
735                        self.output.write_all(b" />\n")?;
736                    } else {
737                        self.output.write_all(b"\n")?;
738                    }
739                }
740            }
741            NodeValue::Code(NodeCode { ref literal, .. }) => {
742                // Unreliable sourcepos.
743                if entering {
744                    self.output.write_all(b"<code")?;
745                    if self.options.render.experimental_inline_sourcepos {
746                        self.render_sourcepos(node)?;
747                    }
748                    self.output.write_all(b">")?;
749                    self.escape(literal.as_bytes())?;
750                    self.output.write_all(b"</code>")?;
751                }
752            }
753            NodeValue::HtmlInline(ref literal) => {
754                // No sourcepos.
755                if entering {
756                    let literal = literal.as_bytes();
757                    if self.options.render.escape {
758                        self.escape(literal)?;
759                    } else if !self.options.render.unsafe_ {
760                        self.output.write_all(b"<!-- raw HTML omitted -->")?;
761                    } else if self.options.extension.tagfilter && tagfilter(literal) {
762                        self.output.write_all(b"&lt;")?;
763                        self.output.write_all(&literal[1..])?;
764                    } else {
765                        self.output.write_all(literal)?;
766                    }
767                }
768            }
769            NodeValue::Raw(ref literal) => {
770                // No sourcepos.
771                if entering {
772                    self.output.write_all(literal.as_bytes())?;
773                }
774            }
775            NodeValue::Strong => {
776                // Unreliable sourcepos.
777                let parent_node = node.parent();
778                if !self.options.render.gfm_quirks
779                    || (parent_node.is_none()
780                        || !matches!(parent_node.unwrap().data.borrow().value, NodeValue::Strong))
781                {
782                    if entering {
783                        self.output.write_all(b"<strong")?;
784                        if self.options.render.experimental_inline_sourcepos {
785                            self.render_sourcepos(node)?;
786                        }
787                        self.output.write_all(b">")?;
788                    } else {
789                        self.output.write_all(b"</strong>")?;
790                    }
791                }
792            }
793            NodeValue::Emph => {
794                // Unreliable sourcepos.
795                if entering {
796                    self.output.write_all(b"<em")?;
797                    if self.options.render.experimental_inline_sourcepos {
798                        self.render_sourcepos(node)?;
799                    }
800                    self.output.write_all(b">")?;
801                } else {
802                    self.output.write_all(b"</em>")?;
803                }
804            }
805            NodeValue::Strikethrough => {
806                // Unreliable sourcepos.
807                if entering {
808                    self.output.write_all(b"<del")?;
809                    if self.options.render.experimental_inline_sourcepos {
810                        self.render_sourcepos(node)?;
811                    }
812                    self.output.write_all(b">")?;
813                } else {
814                    self.output.write_all(b"</del>")?;
815                }
816            }
817            NodeValue::Superscript => {
818                // Unreliable sourcepos.
819                if entering {
820                    self.output.write_all(b"<sup")?;
821                    if self.options.render.experimental_inline_sourcepos {
822                        self.render_sourcepos(node)?;
823                    }
824                    self.output.write_all(b">")?;
825                } else {
826                    self.output.write_all(b"</sup>")?;
827                }
828            }
829            NodeValue::Link(ref nl) => {
830                // Unreliable sourcepos.
831                let parent_node = node.parent();
832
833                if !self.options.parse.relaxed_autolinks
834                    || (parent_node.is_none()
835                        || !matches!(
836                            parent_node.unwrap().data.borrow().value,
837                            NodeValue::Link(..)
838                        ))
839                {
840                    if entering {
841                        self.output.write_all(b"<a")?;
842                        if self.options.render.experimental_inline_sourcepos {
843                            self.render_sourcepos(node)?;
844                        }
845                        self.output.write_all(b" href=\"")?;
846                        let url = nl.url.as_bytes();
847                        if self.options.render.unsafe_ || !dangerous_url(url) {
848                            if let Some(rewriter) = &self.options.extension.link_url_rewriter {
849                                self.escape_href(rewriter.to_html(&nl.url).as_bytes())?;
850                            } else {
851                                self.escape_href(url)?;
852                            }
853                        }
854                        if !nl.title.is_empty() {
855                            self.output.write_all(b"\" title=\"")?;
856                            self.escape(nl.title.as_bytes())?;
857                        }
858                        self.output.write_all(b"\">")?;
859                    } else {
860                        self.output.write_all(b"</a>")?;
861                    }
862                }
863            }
864            NodeValue::Image(ref nl) => {
865                // Unreliable sourcepos.
866                if entering {
867                    if self.options.render.figure_with_caption {
868                        self.output.write_all(b"<figure>")?;
869                    }
870                    self.output.write_all(b"<img")?;
871                    if self.options.render.experimental_inline_sourcepos {
872                        self.render_sourcepos(node)?;
873                    }
874                    self.output.write_all(b" src=\"")?;
875                    let url = nl.url.as_bytes();
876                    if self.options.render.unsafe_ || !dangerous_url(url) {
877                        if let Some(rewriter) = &self.options.extension.image_url_rewriter {
878                            self.escape_href(rewriter.to_html(&nl.url).as_bytes())?;
879                        } else {
880                            self.escape_href(url)?;
881                        }
882                    }
883                    self.output.write_all(b"\" alt=\"")?;
884                    return Ok(true);
885                } else {
886                    if !nl.title.is_empty() {
887                        self.output.write_all(b"\" title=\"")?;
888                        self.escape(nl.title.as_bytes())?;
889                    }
890                    self.output.write_all(b"\" />")?;
891                    if self.options.render.figure_with_caption {
892                        if !nl.title.is_empty() {
893                            self.output.write_all(b"<figcaption>")?;
894                            self.escape(nl.title.as_bytes())?;
895                            self.output.write_all(b"</figcaption>")?;
896                        }
897                        self.output.write_all(b"</figure>")?;
898                    }
899                }
900            }
901            #[cfg(feature = "shortcodes")]
902            NodeValue::ShortCode(ref nsc) => {
903                // Nowhere to put sourcepos.
904                if entering {
905                    self.output.write_all(nsc.emoji.as_bytes())?;
906                }
907            }
908            NodeValue::Table(..) => {
909                if entering {
910                    self.cr()?;
911                    self.output.write_all(b"<table")?;
912                    self.render_sourcepos(node)?;
913                    self.output.write_all(b">\n")?;
914                } else {
915                    if !node
916                        .last_child()
917                        .unwrap()
918                        .same_node(node.first_child().unwrap())
919                    {
920                        self.cr()?;
921                        self.output.write_all(b"</tbody>\n")?;
922                    }
923                    self.cr()?;
924                    self.output.write_all(b"</table>\n")?;
925                }
926            }
927            NodeValue::TableRow(header) => {
928                if entering {
929                    self.cr()?;
930                    if header {
931                        self.output.write_all(b"<thead>\n")?;
932                    } else if let Some(n) = node.previous_sibling() {
933                        if let NodeValue::TableRow(true) = n.data.borrow().value {
934                            self.output.write_all(b"<tbody>\n")?;
935                        }
936                    }
937                    self.output.write_all(b"<tr")?;
938                    self.render_sourcepos(node)?;
939                    self.output.write_all(b">")?;
940                } else {
941                    self.cr()?;
942                    self.output.write_all(b"</tr>")?;
943                    if header {
944                        self.cr()?;
945                        self.output.write_all(b"</thead>")?;
946                    }
947                }
948            }
949            NodeValue::TableCell => {
950                let row = &node.parent().unwrap().data.borrow().value;
951                let in_header = match *row {
952                    NodeValue::TableRow(header) => header,
953                    _ => panic!(),
954                };
955
956                let table = &node.parent().unwrap().parent().unwrap().data.borrow().value;
957                let alignments = match *table {
958                    NodeValue::Table(NodeTable { ref alignments, .. }) => alignments,
959                    _ => panic!(),
960                };
961
962                if entering {
963                    self.cr()?;
964                    if in_header {
965                        self.output.write_all(b"<th")?;
966                        self.render_sourcepos(node)?;
967                    } else {
968                        self.output.write_all(b"<td")?;
969                        self.render_sourcepos(node)?;
970                    }
971
972                    let mut start = node.parent().unwrap().first_child().unwrap();
973                    let mut i = 0;
974                    while !start.same_node(node) {
975                        i += 1;
976                        start = start.next_sibling().unwrap();
977                    }
978
979                    match alignments[i] {
980                        TableAlignment::Left => {
981                            self.output.write_all(b" align=\"left\"")?;
982                        }
983                        TableAlignment::Right => {
984                            self.output.write_all(b" align=\"right\"")?;
985                        }
986                        TableAlignment::Center => {
987                            self.output.write_all(b" align=\"center\"")?;
988                        }
989                        TableAlignment::None => (),
990                    }
991
992                    self.output.write_all(b">")?;
993                } else if in_header {
994                    self.output.write_all(b"</th>")?;
995                } else {
996                    self.output.write_all(b"</td>")?;
997                }
998            }
999            NodeValue::FootnoteDefinition(ref nfd) => {
1000                if entering {
1001                    if self.footnote_ix == 0 {
1002                        self.output.write_all(b"<section")?;
1003                        self.render_sourcepos(node)?;
1004                        self.output
1005                            .write_all(b" class=\"footnotes\" data-footnotes>\n<ol>\n")?;
1006                    }
1007                    self.footnote_ix += 1;
1008                    self.output.write_all(b"<li")?;
1009                    self.render_sourcepos(node)?;
1010                    self.output.write_all(b" id=\"fn-")?;
1011                    self.escape_href(nfd.name.as_bytes())?;
1012                    self.output.write_all(b"\">")?;
1013                } else {
1014                    if self.put_footnote_backref(nfd)? {
1015                        self.output.write_all(b"\n")?;
1016                    }
1017                    self.output.write_all(b"</li>\n")?;
1018                }
1019            }
1020            NodeValue::FootnoteReference(ref nfr) => {
1021                // Unreliable sourcepos.
1022                if entering {
1023                    let mut ref_id = format!("fnref-{}", nfr.name);
1024                    if nfr.ref_num > 1 {
1025                        ref_id = format!("{}-{}", ref_id, nfr.ref_num);
1026                    }
1027
1028                    self.output.write_all(b"<sup")?;
1029                    if self.options.render.experimental_inline_sourcepos {
1030                        self.render_sourcepos(node)?;
1031                    }
1032                    self.output
1033                        .write_all(b" class=\"footnote-ref\"><a href=\"#fn-")?;
1034                    self.escape_href(nfr.name.as_bytes())?;
1035                    self.output.write_all(b"\" id=\"")?;
1036                    self.escape_href(ref_id.as_bytes())?;
1037                    write!(self.output, "\" data-footnote-ref>{}</a></sup>", nfr.ix)?;
1038                }
1039            }
1040            NodeValue::TaskItem(symbol) => {
1041                if entering {
1042                    self.cr()?;
1043                    self.output.write_all(b"<li")?;
1044                    if self.options.render.tasklist_classes {
1045                        self.output.write_all(b" class=\"task-list-item\"")?;
1046                    }
1047                    self.render_sourcepos(node)?;
1048                    self.output.write_all(b">")?;
1049                    self.output.write_all(b"<input type=\"checkbox\"")?;
1050                    if self.options.render.tasklist_classes {
1051                        self.output
1052                            .write_all(b" class=\"task-list-item-checkbox\"")?;
1053                    }
1054                    if symbol.is_some() {
1055                        self.output.write_all(b" checked=\"\"")?;
1056                    }
1057                    self.output.write_all(b" disabled=\"\" /> ")?;
1058                } else {
1059                    self.output.write_all(b"</li>\n")?;
1060                }
1061            }
1062            NodeValue::MultilineBlockQuote(_) => {
1063                if entering {
1064                    self.cr()?;
1065                    self.output.write_all(b"<blockquote")?;
1066                    self.render_sourcepos(node)?;
1067                    self.output.write_all(b">\n")?;
1068                } else {
1069                    self.cr()?;
1070                    self.output.write_all(b"</blockquote>\n")?;
1071                }
1072            }
1073            NodeValue::Escaped => {
1074                // Unreliable sourcepos.
1075                if self.options.render.escaped_char_spans {
1076                    if entering {
1077                        self.output.write_all(b"<span data-escaped-char")?;
1078                        if self.options.render.experimental_inline_sourcepos {
1079                            self.render_sourcepos(node)?;
1080                        }
1081                        self.output.write_all(b">")?;
1082                    } else {
1083                        self.output.write_all(b"</span>")?;
1084                    }
1085                }
1086            }
1087            NodeValue::Math(NodeMath {
1088                ref literal,
1089                display_math,
1090                dollar_math,
1091                ..
1092            }) => {
1093                if entering {
1094                    self.render_math_inline(node, literal, display_math, dollar_math)?;
1095                }
1096            }
1097            NodeValue::WikiLink(ref nl) => {
1098                // Unreliable sourcepos.
1099                if entering {
1100                    self.output.write_all(b"<a")?;
1101                    if self.options.render.experimental_inline_sourcepos {
1102                        self.render_sourcepos(node)?;
1103                    }
1104                    self.output.write_all(b" href=\"")?;
1105                    let url = nl.url.as_bytes();
1106                    if self.options.render.unsafe_ || !dangerous_url(url) {
1107                        self.escape_href(url)?;
1108                    }
1109                    self.output.write_all(b"\" data-wikilink=\"true")?;
1110                    self.output.write_all(b"\">")?;
1111                } else {
1112                    self.output.write_all(b"</a>")?;
1113                }
1114            }
1115            NodeValue::Underline => {
1116                // Unreliable sourcepos.
1117                if entering {
1118                    self.output.write_all(b"<u")?;
1119                    if self.options.render.experimental_inline_sourcepos {
1120                        self.render_sourcepos(node)?;
1121                    }
1122                    self.output.write_all(b">")?;
1123                } else {
1124                    self.output.write_all(b"</u>")?;
1125                }
1126            }
1127            NodeValue::Subscript => {
1128                // Unreliable sourcepos.
1129                if entering {
1130                    self.output.write_all(b"<sub")?;
1131                    if self.options.render.experimental_inline_sourcepos {
1132                        self.render_sourcepos(node)?;
1133                    }
1134                    self.output.write_all(b">")?;
1135                } else {
1136                    self.output.write_all(b"</sub>")?;
1137                }
1138            }
1139            NodeValue::SpoileredText => {
1140                // Unreliable sourcepos.
1141                if entering {
1142                    self.output.write_all(b"<span")?;
1143                    if self.options.render.experimental_inline_sourcepos {
1144                        self.render_sourcepos(node)?;
1145                    }
1146                    self.output.write_all(b" class=\"spoiler\">")?;
1147                } else {
1148                    self.output.write_all(b"</span>")?;
1149                }
1150            }
1151            NodeValue::EscapedTag(ref net) => {
1152                // Nowhere to put sourcepos.
1153                self.output.write_all(net.as_bytes())?;
1154            }
1155            NodeValue::Alert(ref alert) => {
1156                if entering {
1157                    self.cr()?;
1158                    self.output.write_all(b"<div class=\"markdown-alert ")?;
1159                    self.output
1160                        .write_all(alert.alert_type.css_class().as_bytes())?;
1161                    self.output.write_all(b"\"")?;
1162                    self.render_sourcepos(node)?;
1163                    self.output.write_all(b">\n")?;
1164                    self.output
1165                        .write_all(b"<p class=\"markdown-alert-title\">")?;
1166                    match alert.title {
1167                        Some(ref title) => self.escape(title.as_bytes())?,
1168                        None => {
1169                            self.output
1170                                .write_all(alert.alert_type.default_title().as_bytes())?;
1171                        }
1172                    }
1173                    self.output.write_all(b"</p>\n")?;
1174                } else {
1175                    self.cr()?;
1176                    self.output.write_all(b"</div>\n")?;
1177                }
1178            }
1179        }
1180        Ok(false)
1181    }
1182
1183    fn render_sourcepos<'a>(&mut self, node: &'a AstNode<'a>) -> io::Result<()> {
1184        if self.options.render.sourcepos {
1185            let ast = node.data.borrow();
1186            if ast.sourcepos.start.line > 0 {
1187                write!(self.output, " data-sourcepos=\"{}\"", ast.sourcepos)?;
1188            }
1189        }
1190        Ok(())
1191    }
1192
1193    fn put_footnote_backref(&mut self, nfd: &NodeFootnoteDefinition) -> io::Result<bool> {
1194        if self.written_footnote_ix >= self.footnote_ix {
1195            return Ok(false);
1196        }
1197
1198        self.written_footnote_ix = self.footnote_ix;
1199
1200        let mut ref_suffix = String::new();
1201        let mut superscript = String::new();
1202
1203        for ref_num in 1..=nfd.total_references {
1204            if ref_num > 1 {
1205                ref_suffix = format!("-{}", ref_num);
1206                superscript = format!("<sup class=\"footnote-ref\">{}</sup>", ref_num);
1207                write!(self.output, " ")?;
1208            }
1209
1210            self.output.write_all(b"<a href=\"#fnref-")?;
1211            self.escape_href(nfd.name.as_bytes())?;
1212            write!(
1213                self.output,
1214                "{}\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"{}{}\" aria-label=\"Back to reference {}{}\">↩{}</a>",
1215                ref_suffix, self.footnote_ix, ref_suffix, self.footnote_ix, ref_suffix, superscript
1216            )?;
1217        }
1218        Ok(true)
1219    }
1220
1221    // Renders a math dollar inline, `$...$` and `$$...$$` using `<span>` to be similar
1222    // to other renderers.
1223    fn render_math_inline<'a>(
1224        &mut self,
1225        node: &'a AstNode<'a>,
1226        literal: &String,
1227        display_math: bool,
1228        dollar_math: bool,
1229    ) -> io::Result<()> {
1230        let mut tag_attributes: Vec<(String, String)> = Vec::new();
1231        let style_attr = if display_math { "display" } else { "inline" };
1232        let tag: &str = if dollar_math { "span" } else { "code" };
1233
1234        tag_attributes.push((String::from("data-math-style"), String::from(style_attr)));
1235
1236        // Unreliable sourcepos.
1237        if self.options.render.experimental_inline_sourcepos && self.options.render.sourcepos {
1238            let ast = node.data.borrow();
1239            tag_attributes.push(("data-sourcepos".to_string(), ast.sourcepos.to_string()));
1240        }
1241
1242        write_opening_tag(self.output, tag, tag_attributes)?;
1243        self.escape(literal.as_bytes())?;
1244        write!(self.output, "</{}>", tag)?;
1245
1246        Ok(())
1247    }
1248
1249    // Renders a math code block, ```` ```math ```` using `<pre><code>`
1250    fn render_math_code_block<'a>(
1251        &mut self,
1252        node: &'a AstNode<'a>,
1253        literal: &String,
1254    ) -> io::Result<()> {
1255        self.cr()?;
1256
1257        // use vectors to ensure attributes always written in the same order,
1258        // for testing stability
1259        let mut pre_attributes: Vec<(String, String)> = Vec::new();
1260        let mut code_attributes: Vec<(String, String)> = Vec::new();
1261        let lang_str = "math";
1262
1263        if self.options.render.github_pre_lang {
1264            pre_attributes.push((String::from("lang"), lang_str.to_string()));
1265            pre_attributes.push((String::from("data-math-style"), String::from("display")));
1266        } else {
1267            let code_attr = format!("language-{}", lang_str);
1268            code_attributes.push((String::from("class"), code_attr));
1269            code_attributes.push((String::from("data-math-style"), String::from("display")));
1270        }
1271
1272        if self.options.render.sourcepos {
1273            let ast = node.data.borrow();
1274            pre_attributes.push(("data-sourcepos".to_string(), ast.sourcepos.to_string()));
1275        }
1276
1277        write_opening_tag(self.output, "pre", pre_attributes)?;
1278        write_opening_tag(self.output, "code", code_attributes)?;
1279
1280        self.escape(literal.as_bytes())?;
1281        self.output.write_all(b"</code></pre>\n")?;
1282
1283        Ok(())
1284    }
1285}