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