Skip to main content

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