1use crate::error::DocsError;
7use crate::island::Island;
8use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
9use std::fmt::Write as _;
10
11pub fn render(markdown: &str, base: &str) -> Result<String, DocsError> {
18 let mut options = Options::empty();
19 options.insert(Options::ENABLE_TABLES);
20 options.insert(Options::ENABLE_STRIKETHROUGH);
21 options.insert(Options::ENABLE_TASKLISTS);
22 options.insert(Options::ENABLE_FOOTNOTES);
23
24 let mut out = String::new();
25 let mut headings: Vec<u8> = Vec::new();
26 let mut list_open = false;
27 let mut in_code_block = false;
28 let mut parser = Parser::new_ext(markdown, options);
29
30 while let Some(event) = parser.next() {
31 match event {
32 Event::Start(Tag::Heading { level, .. }) => {
33 headings.push(level as u8);
35 let _ = write!(out, "<{level}>");
36 }
37 Event::End(TagEnd::Heading(_)) => {
38 let level = headings.pop().unwrap_or(1);
39 let _ = write!(out, "</h{level}>");
40 }
41 Event::Start(Tag::Paragraph) => {
42 if !list_open {
43 out.push_str("<p>");
44 }
45 }
46 Event::End(TagEnd::Paragraph) => {
47 if !list_open {
48 out.push_str("</p>");
49 }
50 }
51 Event::Start(Tag::Emphasis) => out.push_str("<em>"),
52 Event::End(TagEnd::Emphasis) => out.push_str("</em>"),
53 Event::Start(Tag::Strong) => out.push_str("<strong>"),
54 Event::End(TagEnd::Strong) => out.push_str("</strong>"),
55 Event::Start(Tag::Strikethrough) => out.push_str("<del>"),
56 Event::End(TagEnd::Strikethrough) => out.push_str("</del>"),
57 Event::Code(code) => {
58 let _ = write!(out, "<code>{}</code>", escape_html(&code));
59 }
60 Event::Start(Tag::Link { dest_url, .. }) => {
61 let _ = write!(out, "<a href=\"{}\">", escape_attr(&dest_url));
62 }
63 Event::End(TagEnd::Link) => out.push_str("</a>"),
64 Event::Start(Tag::Image { dest_url, title, .. }) => {
65 let _ = write!(
66 out,
67 "<img src=\"{}\" alt=\"\"{}>",
68 escape_attr(&dest_url),
69 if title.is_empty() {
70 String::new()
71 } else {
72 format!(" title=\"{}\"", escape_attr(&title))
73 }
74 );
75 }
76 Event::End(TagEnd::Image) => {}
77 Event::Start(Tag::BlockQuote(_)) => out.push_str("<blockquote>"),
78 Event::End(TagEnd::BlockQuote(_)) => out.push_str("</blockquote>"),
79 Event::Start(Tag::CodeBlock(kind)) => match kind {
80 CodeBlockKind::Fenced(info) if info.starts_with("island") => {
81 let mut body = String::new();
85 for inner in parser.by_ref() {
86 match inner {
87 Event::Text(t) => {
88 body.push_str(&t);
89 break;
90 }
91 Event::End(TagEnd::CodeBlock) => break,
92 _ => {}
93 }
94 }
95 let island = Island::parse(&info, &body)?;
96 out.push_str(&island.to_html(base));
97 let _ = parser.next(); }
99 _ => {
100 in_code_block = true;
101 out.push_str("<pre><code>");
102 }
103 },
104 Event::End(TagEnd::CodeBlock) => {
105 if in_code_block {
106 in_code_block = false;
107 out.push_str("</code></pre>");
108 }
109 }
110 Event::Start(Tag::List(start)) => {
111 let ordered = start.is_some();
112 let _ = write!(out, "<{}>", if ordered { "ol" } else { "ul" });
113 list_open = true;
114 }
115 Event::End(TagEnd::List(_)) => {
116 list_open = false;
117 if out.contains("<ol><li") || out.contains("<ol>\n<li") {
120 out = out.replacen("<ol>", "</li></ol>", 1);
121 } else if out.contains("<ul><li") {
122 out = out.replacen("<ul>", "</li></ul>", 1);
123 }
124 }
125 Event::Start(Tag::Item) => out.push_str("<li>"),
126 Event::End(TagEnd::Item) => out.push_str("</li>"),
127 Event::TaskListMarker(checked) => {
128 let _ = write!(
129 out,
130 "<input type=\"checkbox\" disabled{}>",
131 if checked { " checked" } else { "" }
132 );
133 }
134 Event::Rule => out.push_str("<hr>"),
135 Event::Text(text) => out.push_str(&escape_html(&text)),
142 Event::Html(text) | Event::InlineHtml(text) => out.push_str(&text),
143 Event::SoftBreak => out.push('\n'),
144 Event::HardBreak => out.push_str("<br>"),
145 _ => {}
146 }
147 }
148 Ok(out)
149}
150
151fn escape_html(src: &str) -> String {
152 src.replace('&', "&")
153 .replace('<', "<")
154 .replace('>', ">")
155}
156
157fn escape_attr(src: &str) -> String {
158 escape_html(src).replace('"', """)
159}