1use 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
20pub 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
29pub 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#[derive(Debug, Default)]
84#[doc(hidden)]
85pub struct Anchorizer(HashSet<String>);
86
87impl Anchorizer {
88 pub fn new() -> Self {
90 Anchorizer(HashSet::new())
91 }
92
93 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"<")?;
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
208pub 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""",
229 b'&' => b"&",
230 b'<' => b"<",
231 b'>' => b">",
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
243pub 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"&")?;
293 }
294 '\'' => {
295 output.write_all(b"'")?;
296 }
297 _ => write!(output, "%{:02X}", buffer[i])?,
298 }
299
300 i += 1;
301 }
302
303 Ok(())
304}
305
306pub 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 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 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 if entering {
705 self.escape(literal.as_bytes())?;
706 }
707 }
708 NodeValue::LineBreak => {
709 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 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 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 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"<")?;
754 self.output.write_all(&literal[1..])?;
755 } else {
756 self.output.write_all(literal)?;
757 }
758 }
759 }
760 NodeValue::Raw(ref literal) => {
761 if entering {
763 self.output.write_all(literal.as_bytes())?;
764 }
765 }
766 NodeValue::Strong => {
767 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}