1 use ctype::isspace;
2 use nodes::{AstNode, ListType, NodeCode, NodeValue, TableAlignment};
3 use parser::{ComrakOptions, ComrakPlugins};
4 use regex::Regex;
5 use scanners;
6 use std::borrow::Cow;
7 use std::cell::Cell;
8 use std::collections::{HashMap, HashSet};
9 use std::io::{self, Write};
10 use std::str;
11 use strings::build_opening_tag;
12 
13 /// Formats an AST as HTML, modified by the given options.
format_document<'a>( root: &'a AstNode<'a>, options: &ComrakOptions, output: &mut dyn Write, ) -> io::Result<()>14 pub fn format_document<'a>(
15     root: &'a AstNode<'a>,
16     options: &ComrakOptions,
17     output: &mut dyn Write,
18 ) -> io::Result<()> {
19     format_document_with_plugins(root, &options, output, &ComrakPlugins::default())
20 }
21 
22 /// Formats an AST as HTML, modified by the given options. Accepts custom plugins.
format_document_with_plugins<'a>( root: &'a AstNode<'a>, options: &ComrakOptions, output: &mut dyn Write, plugins: &ComrakPlugins, ) -> io::Result<()>23 pub fn format_document_with_plugins<'a>(
24     root: &'a AstNode<'a>,
25     options: &ComrakOptions,
26     output: &mut dyn Write,
27     plugins: &ComrakPlugins,
28 ) -> io::Result<()> {
29     let mut writer = WriteWithLast {
30         output,
31         last_was_lf: Cell::new(true),
32     };
33     let mut f = HtmlFormatter::new(options, &mut writer, plugins);
34     f.format(root, false)?;
35     if f.footnote_ix > 0 {
36         f.output.write_all(b"</ol>\n</section>\n")?;
37     }
38     Ok(())
39 }
40 
41 pub struct WriteWithLast<'w> {
42     output: &'w mut dyn Write,
43     pub last_was_lf: Cell<bool>,
44 }
45 
46 impl<'w> Write for WriteWithLast<'w> {
flush(&mut self) -> io::Result<()>47     fn flush(&mut self) -> io::Result<()> {
48         self.output.flush()
49     }
50 
write(&mut self, buf: &[u8]) -> io::Result<usize>51     fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
52         let l = buf.len();
53         if l > 0 {
54             self.last_was_lf.set(buf[l - 1] == 10);
55         }
56         self.output.write(buf)
57     }
58 }
59 
60 /// Converts header Strings to canonical, unique, but still human-readable, anchors.
61 ///
62 /// To guarantee uniqueness, an anchorizer keeps track of the anchors
63 /// it has returned.  So, for example, to parse several MarkDown
64 /// files, use a new anchorizer per file.
65 ///
66 /// ## Example
67 ///
68 /// ```
69 /// use comrak::Anchorizer;
70 ///
71 /// let mut anchorizer = Anchorizer::new();
72 ///
73 /// // First "stuff" is unsuffixed.
74 /// assert_eq!("stuff".to_string(), anchorizer.anchorize("Stuff".to_string()));
75 /// // Second "stuff" has "-1" appended to make it unique.
76 /// assert_eq!("stuff-1".to_string(), anchorizer.anchorize("Stuff".to_string()));
77 /// ```
78 #[derive(Debug, Default)]
79 pub struct Anchorizer(HashSet<String>);
80 
81 impl Anchorizer {
82     /// Construct a new anchorizer.
new() -> Self83     pub fn new() -> Self {
84         Anchorizer(HashSet::new())
85     }
86 
87     /// Returns a String that has been converted into an anchor using the
88     /// GFM algorithm, which involves changing spaces to dashes, removing
89     /// problem characters and, if needed, adding a suffix to make the
90     /// resultant anchor unique.
91     ///
92     /// ```
93     /// use comrak::Anchorizer;
94     ///
95     /// let mut anchorizer = Anchorizer::new();
96     ///
97     /// let source = "Ticks aren't in";
98     ///
99     /// assert_eq!("ticks-arent-in".to_string(), anchorizer.anchorize(source.to_string()));
100     /// ```
anchorize(&mut self, header: String) -> String101     pub fn anchorize(&mut self, header: String) -> String {
102         lazy_static! {
103             static ref REJECTED_CHARS: Regex = Regex::new(r"[^\p{L}\p{M}\p{N}\p{Pc} -]").unwrap();
104         }
105 
106         let mut id = header;
107         id = id.to_lowercase();
108         id = REJECTED_CHARS.replace_all(&id, "").to_string();
109         id = 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.to_string();
121             }
122 
123             uniq += 1;
124         };
125         self.0.insert(id.clone());
126         id
127     }
128 }
129 
130 struct HtmlFormatter<'o> {
131     output: &'o mut WriteWithLast<'o>,
132     options: &'o ComrakOptions,
133     anchorizer: Anchorizer,
134     footnote_ix: u32,
135     written_footnote_ix: u32,
136     plugins: &'o ComrakPlugins<'o>,
137 }
138 
139 #[rustfmt::skip]
140 const NEEDS_ESCAPED : [bool; 256] = [
141     false, false, false, false, false, false, false, false,
142     false, false, false, false, false, false, false, false,
143     false, false, false, false, false, false, false, false,
144     false, false, false, false, false, false, false, false,
145     false, false, true,  false, false, false, true,  false,
146     false, false, false, false, false, false, false, false,
147     false, false, false, false, false, false, false, false,
148     false, false, false, false, true, false, true, false,
149     false, false, false, false, false, false, false, false,
150     false, false, false, false, false, false, false, false,
151     false, false, false, false, false, false, false, false,
152     false, false, false, false, false, false, false, false,
153     false, false, false, false, false, false, false, false,
154     false, false, false, false, false, false, false, false,
155     false, false, false, false, false, false, false, false,
156     false, false, false, false, false, false, false, false,
157     false, false, false, false, false, false, false, false,
158     false, false, false, false, false, false, false, false,
159     false, false, false, false, false, false, false, false,
160     false, false, false, false, false, false, false, false,
161     false, false, false, false, false, false, false, false,
162     false, false, false, false, false, false, false, false,
163     false, false, false, false, false, false, false, false,
164     false, false, false, false, false, false, false, false,
165     false, false, false, false, false, false, false, false,
166     false, false, false, false, false, false, false, false,
167     false, false, false, false, false, false, false, false,
168     false, false, false, false, false, false, false, false,
169     false, false, false, false, false, false, false, false,
170     false, false, false, false, false, false, false, false,
171     false, false, false, false, false, false, false, false,
172     false, false, false, false, false, false, false, false,
173 ];
174 
tagfilter(literal: &[u8]) -> bool175 fn tagfilter(literal: &[u8]) -> bool {
176     lazy_static! {
177         static ref TAGFILTER_BLACKLIST: [&'static str; 9] = [
178             "title",
179             "textarea",
180             "style",
181             "xmp",
182             "iframe",
183             "noembed",
184             "noframes",
185             "script",
186             "plaintext"
187         ];
188     }
189 
190     if literal.len() < 3 || literal[0] != b'<' {
191         return false;
192     }
193 
194     let mut i = 1;
195     if literal[i] == b'/' {
196         i += 1;
197     }
198 
199     for t in TAGFILTER_BLACKLIST.iter() {
200         if unsafe { String::from_utf8_unchecked(literal[i..].to_vec()) }
201             .to_lowercase()
202             .starts_with(t)
203         {
204             let j = i + t.len();
205             return isspace(literal[j])
206                 || literal[j] == b'>'
207                 || (literal[j] == b'/' && literal.len() >= j + 2 && literal[j + 1] == b'>');
208         }
209     }
210 
211     false
212 }
213 
tagfilter_block(input: &[u8], o: &mut dyn Write) -> io::Result<()>214 fn tagfilter_block(input: &[u8], o: &mut dyn Write) -> io::Result<()> {
215     let size = input.len();
216     let mut i = 0;
217 
218     while i < size {
219         let org = i;
220         while i < size && input[i] != b'<' {
221             i += 1;
222         }
223 
224         if i > org {
225             o.write_all(&input[org..i])?;
226         }
227 
228         if i >= size {
229             break;
230         }
231 
232         if tagfilter(&input[i..]) {
233             o.write_all(b"&lt;")?;
234         } else {
235             o.write_all(b"<")?;
236         }
237 
238         i += 1;
239     }
240 
241     Ok(())
242 }
243 
dangerous_url(input: &[u8]) -> bool244 fn dangerous_url(input: &[u8]) -> bool {
245     scanners::dangerous_url(input).is_some()
246 }
247 
248 impl<'o> HtmlFormatter<'o> {
new( options: &'o ComrakOptions, output: &'o mut WriteWithLast<'o>, plugins: &'o ComrakPlugins, ) -> Self249     fn new(
250         options: &'o ComrakOptions,
251         output: &'o mut WriteWithLast<'o>,
252         plugins: &'o ComrakPlugins,
253     ) -> Self {
254         HtmlFormatter {
255             options,
256             output,
257             anchorizer: Anchorizer::new(),
258             footnote_ix: 0,
259             written_footnote_ix: 0,
260             plugins,
261         }
262     }
263 
cr(&mut self) -> io::Result<()>264     fn cr(&mut self) -> io::Result<()> {
265         if !self.output.last_was_lf.get() {
266             self.output.write_all(b"\n")?;
267         }
268         Ok(())
269     }
270 
escape(&mut self, buffer: &[u8]) -> io::Result<()>271     fn escape(&mut self, buffer: &[u8]) -> io::Result<()> {
272         let mut offset = 0;
273         for (i, &byte) in buffer.iter().enumerate() {
274             if NEEDS_ESCAPED[byte as usize] {
275                 let esc: &[u8] = match byte {
276                     b'"' => b"&quot;",
277                     b'&' => b"&amp;",
278                     b'<' => b"&lt;",
279                     b'>' => b"&gt;",
280                     _ => unreachable!(),
281                 };
282                 self.output.write_all(&buffer[offset..i])?;
283                 self.output.write_all(esc)?;
284                 offset = i + 1;
285             }
286         }
287         self.output.write_all(&buffer[offset..])?;
288         Ok(())
289     }
290 
escape_href(&mut self, buffer: &[u8]) -> io::Result<()>291     fn escape_href(&mut self, buffer: &[u8]) -> io::Result<()> {
292         lazy_static! {
293             static ref HREF_SAFE: [bool; 256] = {
294                 let mut a = [false; 256];
295                 for &c in b"-_.+!*(),%#@?=;:/,+$~abcdefghijklmnopqrstuvwxyz".iter() {
296                     a[c as usize] = true;
297                 }
298                 for &c in b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".iter() {
299                     a[c as usize] = true;
300                 }
301                 a
302             };
303         }
304 
305         let size = buffer.len();
306         let mut i = 0;
307 
308         while i < size {
309             let org = i;
310             while i < size && HREF_SAFE[buffer[i] as usize] {
311                 i += 1;
312             }
313 
314             if i > org {
315                 self.output.write_all(&buffer[org..i])?;
316             }
317 
318             if i >= size {
319                 break;
320             }
321 
322             match buffer[i] as char {
323                 '&' => {
324                     self.output.write_all(b"&amp;")?;
325                 }
326                 '\'' => {
327                     self.output.write_all(b"&#x27;")?;
328                 }
329                 _ => write!(self.output, "%{:02X}", buffer[i])?,
330             }
331 
332             i += 1;
333         }
334 
335         Ok(())
336     }
337 
format<'a>(&mut self, node: &'a AstNode<'a>, plain: bool) -> io::Result<()>338     fn format<'a>(&mut self, node: &'a AstNode<'a>, plain: bool) -> io::Result<()> {
339         // Traverse the AST iteratively using a work stack, with pre- and
340         // post-child-traversal phases. During pre-order traversal render the
341         // opening tags, then push the node back onto the stack for the
342         // post-order traversal phase, then push the children in reverse order
343         // onto the stack and begin rendering first child.
344 
345         enum Phase {
346             Pre,
347             Post,
348         }
349         let mut stack = vec![(node, plain, Phase::Pre)];
350 
351         while let Some((node, plain, phase)) = stack.pop() {
352             match phase {
353                 Phase::Pre => {
354                     let new_plain;
355                     if plain {
356                         match node.data.borrow().value {
357                             NodeValue::Text(ref literal)
358                             | NodeValue::Code(NodeCode { ref literal, .. })
359                             | NodeValue::HtmlInline(ref literal) => {
360                                 self.escape(literal)?;
361                             }
362                             NodeValue::LineBreak | NodeValue::SoftBreak => {
363                                 self.output.write_all(b" ")?;
364                             }
365                             _ => (),
366                         }
367                         new_plain = plain;
368                     } else {
369                         stack.push((node, false, Phase::Post));
370                         new_plain = self.format_node(node, true)?;
371                     }
372 
373                     for ch in node.reverse_children() {
374                         stack.push((ch, new_plain, Phase::Pre));
375                     }
376                 }
377                 Phase::Post => {
378                     debug_assert!(!plain);
379                     self.format_node(node, false)?;
380                 }
381             }
382         }
383 
384         Ok(())
385     }
386 
collect_text<'a>(&self, node: &'a AstNode<'a>, output: &mut Vec<u8>)387     fn collect_text<'a>(&self, node: &'a AstNode<'a>, output: &mut Vec<u8>) {
388         match node.data.borrow().value {
389             NodeValue::Text(ref literal) | NodeValue::Code(NodeCode { ref literal, .. }) => {
390                 output.extend_from_slice(literal)
391             }
392             NodeValue::LineBreak | NodeValue::SoftBreak => output.push(b' '),
393             _ => {
394                 for n in node.children() {
395                     self.collect_text(n, output);
396                 }
397             }
398         }
399     }
400 
format_node<'a>(&mut self, node: &'a AstNode<'a>, entering: bool) -> io::Result<bool>401     fn format_node<'a>(&mut self, node: &'a AstNode<'a>, entering: bool) -> io::Result<bool> {
402         match node.data.borrow().value {
403             NodeValue::Document => (),
404             NodeValue::FrontMatter(_) => (),
405             NodeValue::BlockQuote => {
406                 if entering {
407                     self.cr()?;
408                     self.output.write_all(b"<blockquote>\n")?;
409                 } else {
410                     self.cr()?;
411                     self.output.write_all(b"</blockquote>\n")?;
412                 }
413             }
414             NodeValue::List(ref nl) => {
415                 if entering {
416                     self.cr()?;
417                     if nl.list_type == ListType::Bullet {
418                         self.output.write_all(b"<ul>\n")?;
419                     } else if nl.start == 1 {
420                         self.output.write_all(b"<ol>\n")?;
421                     } else {
422                         writeln!(self.output, "<ol start=\"{}\">", nl.start)?;
423                     }
424                 } else if nl.list_type == ListType::Bullet {
425                     self.output.write_all(b"</ul>\n")?;
426                 } else {
427                     self.output.write_all(b"</ol>\n")?;
428                 }
429             }
430             NodeValue::Item(..) => {
431                 if entering {
432                     self.cr()?;
433                     self.output.write_all(b"<li>")?;
434                 } else {
435                     self.output.write_all(b"</li>\n")?;
436                 }
437             }
438             NodeValue::DescriptionList => {
439                 if entering {
440                     self.cr()?;
441                     self.output.write_all(b"<dl>")?;
442                 } else {
443                     self.output.write_all(b"</dl>\n")?;
444                 }
445             }
446             NodeValue::DescriptionItem(..) => (),
447             NodeValue::DescriptionTerm => {
448                 if entering {
449                     self.output.write_all(b"<dt>")?;
450                 } else {
451                     self.output.write_all(b"</dt>\n")?;
452                 }
453             }
454             NodeValue::DescriptionDetails => {
455                 if entering {
456                     self.output.write_all(b"<dd>")?;
457                 } else {
458                     self.output.write_all(b"</dd>\n")?;
459                 }
460             }
461             NodeValue::Heading(ref nch) => {
462                 if entering {
463                     self.cr()?;
464                     write!(self.output, "<h{}>", nch.level)?;
465 
466                     if let Some(ref prefix) = self.options.extension.header_ids {
467                         let mut text_content = Vec::with_capacity(20);
468                         self.collect_text(node, &mut text_content);
469 
470                         let mut id = String::from_utf8(text_content).unwrap();
471                         id = self.anchorizer.anchorize(id);
472                         write!(
473                             self.output,
474                             "<a href=\"#{}\" aria-hidden=\"true\" class=\"anchor\" id=\"{}{}\"></a>",
475                             id,
476                             prefix,
477                             id
478                         )?;
479                     }
480                 } else {
481                     writeln!(self.output, "</h{}>", nch.level)?;
482                 }
483             }
484             NodeValue::CodeBlock(ref ncb) => {
485                 if entering {
486                     self.cr()?;
487 
488                     let mut first_tag = 0;
489                     let mut pre_attributes: HashMap<String, String> = HashMap::new();
490                     let mut code_attributes: HashMap<String, String> = HashMap::new();
491                     let code_attr: String;
492 
493                     if !ncb.info.is_empty() {
494                         while first_tag < ncb.info.len() && !isspace(ncb.info[first_tag]) {
495                             first_tag += 1;
496                         }
497 
498                         if self.options.render.github_pre_lang {
499                             pre_attributes.insert(
500                                 String::from("lang"),
501                                 String::from_utf8(Vec::from(&ncb.info[..first_tag])).unwrap(),
502                             );
503                         } else {
504                             code_attr = format!(
505                                 "language-{}",
506                                 str::from_utf8(&ncb.info[..first_tag]).unwrap()
507                             );
508                             code_attributes.insert(String::from("class"), code_attr);
509                         }
510                     }
511 
512                     match self.plugins.render.codefence_syntax_highlighter {
513                         None => {
514                             self.output
515                                 .write_all(build_opening_tag("pre", &pre_attributes).as_bytes())?;
516                             self.output.write_all(
517                                 build_opening_tag("code", &code_attributes).as_bytes(),
518                             )?;
519 
520                             self.escape(&ncb.literal)?;
521 
522                             self.output.write_all(b"</code></pre>\n")?
523                         }
524                         Some(highlighter) => {
525                             self.output
526                                 .write_all(highlighter.build_pre_tag(&pre_attributes).as_bytes())?;
527                             self.output.write_all(
528                                 highlighter.build_code_tag(&code_attributes).as_bytes(),
529                             )?;
530 
531                             self.output.write_all(
532                                 highlighter
533                                     .highlight(
534                                         match str::from_utf8(&ncb.info[..first_tag]) {
535                                             Ok(lang) => Some(lang),
536                                             Err(_) => None,
537                                         },
538                                         str::from_utf8(ncb.literal.as_slice()).unwrap(),
539                                     )
540                                     .as_bytes(),
541                             )?;
542 
543                             self.output.write_all(b"</code></pre>\n")?
544                         }
545                     }
546                 }
547             }
548             NodeValue::HtmlBlock(ref nhb) => {
549                 if entering {
550                     self.cr()?;
551                     if self.options.render.escape {
552                         self.escape(&nhb.literal)?;
553                     } else if !self.options.render.unsafe_ {
554                         self.output.write_all(b"<!-- raw HTML omitted -->")?;
555                     } else if self.options.extension.tagfilter {
556                         tagfilter_block(&nhb.literal, &mut self.output)?;
557                     } else {
558                         self.output.write_all(&nhb.literal)?;
559                     }
560                     self.cr()?;
561                 }
562             }
563             NodeValue::ThematicBreak => {
564                 if entering {
565                     self.cr()?;
566                     self.output.write_all(b"<hr />\n")?;
567                 }
568             }
569             NodeValue::Paragraph => {
570                 let tight = match node
571                     .parent()
572                     .and_then(|n| n.parent())
573                     .map(|n| n.data.borrow().value.clone())
574                 {
575                     Some(NodeValue::List(nl)) => nl.tight,
576                     _ => false,
577                 };
578 
579                 let tight = tight
580                     || matches!(
581                         node.parent().map(|n| n.data.borrow().value.clone()),
582                         Some(NodeValue::DescriptionTerm)
583                     );
584 
585                 if !tight {
586                     if entering {
587                         self.cr()?;
588                         self.output.write_all(b"<p>")?;
589                     } else {
590                         if matches!(
591                             node.parent().unwrap().data.borrow().value,
592                             NodeValue::FootnoteDefinition(..)
593                         ) && node.next_sibling().is_none()
594                         {
595                             self.output.write_all(b" ")?;
596                             self.put_footnote_backref()?;
597                         }
598                         self.output.write_all(b"</p>\n")?;
599                     }
600                 }
601             }
602             NodeValue::Text(ref literal) => {
603                 if entering {
604                     self.escape(literal)?;
605                 }
606             }
607             NodeValue::LineBreak => {
608                 if entering {
609                     self.output.write_all(b"<br />\n")?;
610                 }
611             }
612             NodeValue::SoftBreak => {
613                 if entering {
614                     if self.options.render.hardbreaks {
615                         self.output.write_all(b"<br />\n")?;
616                     } else {
617                         self.output.write_all(b"\n")?;
618                     }
619                 }
620             }
621             NodeValue::Code(NodeCode { ref literal, .. }) => {
622                 if entering {
623                     self.output.write_all(b"<code>")?;
624                     self.escape(literal)?;
625                     self.output.write_all(b"</code>")?;
626                 }
627             }
628             NodeValue::HtmlInline(ref literal) => {
629                 if entering {
630                     if self.options.render.escape {
631                         self.escape(&literal)?;
632                     } else if !self.options.render.unsafe_ {
633                         self.output.write_all(b"<!-- raw HTML omitted -->")?;
634                     } else if self.options.extension.tagfilter && tagfilter(literal) {
635                         self.output.write_all(b"&lt;")?;
636                         self.output.write_all(&literal[1..])?;
637                     } else {
638                         self.output.write_all(literal)?;
639                     }
640                 }
641             }
642             NodeValue::Strong => {
643                 if entering {
644                     self.output.write_all(b"<strong>")?;
645                 } else {
646                     self.output.write_all(b"</strong>")?;
647                 }
648             }
649             NodeValue::Emph => {
650                 if entering {
651                     self.output.write_all(b"<em>")?;
652                 } else {
653                     self.output.write_all(b"</em>")?;
654                 }
655             }
656             NodeValue::Strikethrough => {
657                 if entering {
658                     self.output.write_all(b"<del>")?;
659                 } else {
660                     self.output.write_all(b"</del>")?;
661                 }
662             }
663             NodeValue::Superscript => {
664                 if entering {
665                     self.output.write_all(b"<sup>")?;
666                 } else {
667                     self.output.write_all(b"</sup>")?;
668                 }
669             }
670             NodeValue::Link(ref nl) => {
671                 if entering {
672                     self.output.write_all(b"<a href=\"")?;
673                     if self.options.render.unsafe_ || !dangerous_url(&nl.url) {
674                         self.escape_href(&nl.url)?;
675                     }
676                     if !nl.title.is_empty() {
677                         self.output.write_all(b"\" title=\"")?;
678                         self.escape(&nl.title)?;
679                     }
680                     self.output.write_all(b"\">")?;
681                 } else {
682                     self.output.write_all(b"</a>")?;
683                 }
684             }
685             NodeValue::Image(ref nl) => {
686                 if entering {
687                     self.output.write_all(b"<img src=\"")?;
688                     if self.options.render.unsafe_ || !dangerous_url(&nl.url) {
689                         self.escape_href(&nl.url)?;
690                     }
691                     self.output.write_all(b"\" alt=\"")?;
692                     return Ok(true);
693                 } else {
694                     if !nl.title.is_empty() {
695                         self.output.write_all(b"\" title=\"")?;
696                         self.escape(&nl.title)?;
697                     }
698                     self.output.write_all(b"\" />")?;
699                 }
700             }
701             NodeValue::Table(..) => {
702                 if entering {
703                     self.cr()?;
704                     self.output.write_all(b"<table>\n")?;
705                 } else {
706                     if !node
707                         .last_child()
708                         .unwrap()
709                         .same_node(node.first_child().unwrap())
710                     {
711                         self.cr()?;
712                         self.output.write_all(b"</tbody>\n")?;
713                     }
714                     self.cr()?;
715                     self.output.write_all(b"</table>\n")?;
716                 }
717             }
718             NodeValue::TableRow(header) => {
719                 if entering {
720                     self.cr()?;
721                     if header {
722                         self.output.write_all(b"<thead>\n")?;
723                     } else if let Some(n) = node.previous_sibling() {
724                         if let NodeValue::TableRow(true) = n.data.borrow().value {
725                             self.output.write_all(b"<tbody>\n")?;
726                         }
727                     }
728                     self.output.write_all(b"<tr>")?;
729                 } else {
730                     self.cr()?;
731                     self.output.write_all(b"</tr>")?;
732                     if header {
733                         self.cr()?;
734                         self.output.write_all(b"</thead>")?;
735                     }
736                 }
737             }
738             NodeValue::TableCell => {
739                 let row = &node.parent().unwrap().data.borrow().value;
740                 let in_header = match *row {
741                     NodeValue::TableRow(header) => header,
742                     _ => panic!(),
743                 };
744 
745                 let table = &node.parent().unwrap().parent().unwrap().data.borrow().value;
746                 let alignments = match *table {
747                     NodeValue::Table(ref alignments) => alignments,
748                     _ => panic!(),
749                 };
750 
751                 if entering {
752                     self.cr()?;
753                     if in_header {
754                         self.output.write_all(b"<th")?;
755                     } else {
756                         self.output.write_all(b"<td")?;
757                     }
758 
759                     let mut start = node.parent().unwrap().first_child().unwrap();
760                     let mut i = 0;
761                     while !start.same_node(node) {
762                         i += 1;
763                         start = start.next_sibling().unwrap();
764                     }
765 
766                     match alignments[i] {
767                         TableAlignment::Left => {
768                             self.output.write_all(b" align=\"left\"")?;
769                         }
770                         TableAlignment::Right => {
771                             self.output.write_all(b" align=\"right\"")?;
772                         }
773                         TableAlignment::Center => {
774                             self.output.write_all(b" align=\"center\"")?;
775                         }
776                         TableAlignment::None => (),
777                     }
778 
779                     self.output.write_all(b">")?;
780                 } else if in_header {
781                     self.output.write_all(b"</th>")?;
782                 } else {
783                     self.output.write_all(b"</td>")?;
784                 }
785             }
786             NodeValue::FootnoteDefinition(_) => {
787                 if entering {
788                     if self.footnote_ix == 0 {
789                         self.output
790                             .write_all(b"<section class=\"footnotes\">\n<ol>\n")?;
791                     }
792                     self.footnote_ix += 1;
793                     writeln!(self.output, "<li id=\"fn{}\">", self.footnote_ix)?;
794                 } else {
795                     if self.put_footnote_backref()? {
796                         self.output.write_all(b"\n")?;
797                     }
798                     self.output.write_all(b"</li>\n")?;
799                 }
800             }
801             NodeValue::FootnoteReference(ref r) => {
802                 if entering {
803                     let r = str::from_utf8(r).unwrap();
804                     write!(
805                         self.output,
806                         "<sup class=\"footnote-ref\"><a href=\"#fn{}\" id=\"fnref{}\">{}</a></sup>",
807                         r, r, r
808                     )?;
809                 }
810             }
811             NodeValue::TaskItem(checked) => {
812                 if entering {
813                     if checked {
814                         self.output.write_all(
815                             b"<input type=\"checkbox\" disabled=\"\" checked=\"\" /> ",
816                         )?;
817                     } else {
818                         self.output
819                             .write_all(b"<input type=\"checkbox\" disabled=\"\" /> ")?;
820                     }
821                 }
822             }
823         }
824         Ok(false)
825     }
826 
put_footnote_backref(&mut self) -> io::Result<bool>827     fn put_footnote_backref(&mut self) -> io::Result<bool> {
828         if self.written_footnote_ix >= self.footnote_ix {
829             return Ok(false);
830         }
831 
832         self.written_footnote_ix = self.footnote_ix;
833         write!(
834             self.output,
835             "<a href=\"#fnref{}\" class=\"footnote-backref\">↩</a>",
836             self.footnote_ix
837         )?;
838         Ok(true)
839     }
840 }
841