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"<")?;
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""",
277 b'&' => b"&",
278 b'<' => b"<",
279 b'>' => b">",
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"&")?;
325 }
326 '\'' => {
327 self.output.write_all(b"'")?;
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"<")?;
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