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_stack: Vec<bool> = Vec::new();
30 let mut in_code_block = false;
31 let mut parser = Parser::new_ext(markdown, options);
32
33 while let Some(event) = parser.next() {
34 match event {
35 Event::Start(Tag::Heading { level, .. }) => {
36 headings.push(level as u8);
38 let _ = write!(out, "<{level}>");
39 }
40 Event::End(TagEnd::Heading(_)) => {
41 let level = headings.pop().unwrap_or(1);
42 let _ = write!(out, "</h{level}>");
43 }
44 Event::Start(Tag::Paragraph) => {
45 if list_stack.is_empty() {
46 out.push_str("<p>");
47 }
48 }
49 Event::End(TagEnd::Paragraph) => {
50 if list_stack.is_empty() {
51 out.push_str("</p>");
52 }
53 }
54 Event::Start(Tag::Emphasis) => out.push_str("<em>"),
55 Event::End(TagEnd::Emphasis) => out.push_str("</em>"),
56 Event::Start(Tag::Strong) => out.push_str("<strong>"),
57 Event::End(TagEnd::Strong) => out.push_str("</strong>"),
58 Event::Start(Tag::Strikethrough) => out.push_str("<del>"),
59 Event::End(TagEnd::Strikethrough) => out.push_str("</del>"),
60 Event::Code(code) => {
61 let _ = write!(out, "<code>{}</code>", escape_html(&code));
62 }
63 Event::Start(Tag::Link { dest_url, .. }) => {
64 let _ = write!(out, "<a href=\"{}\">", escape_attr(&dest_url));
65 }
66 Event::End(TagEnd::Link) => out.push_str("</a>"),
67 Event::Start(Tag::Image {
68 dest_url, title, ..
69 }) => {
70 let mut alt = String::new();
74 for inner in parser.by_ref() {
75 match inner {
76 Event::Text(t) => alt.push_str(&t),
77 Event::End(TagEnd::Image) => break,
78 _ => {}
79 }
80 }
81 let _ = write!(
82 out,
83 "<img src=\"{}\" alt=\"{}\"{}>",
84 escape_attr(&dest_url),
85 escape_attr(&alt),
86 if title.is_empty() {
87 String::new()
88 } else {
89 format!(" title=\"{}\"", escape_attr(&title))
90 }
91 );
92 }
93 Event::End(TagEnd::Image) => {}
94 Event::Start(Tag::BlockQuote(_)) => out.push_str("<blockquote>"),
95 Event::End(TagEnd::BlockQuote(_)) => out.push_str("</blockquote>"),
96 Event::Start(Tag::CodeBlock(kind)) => match kind {
97 CodeBlockKind::Fenced(info) if info.starts_with("island") => {
98 let mut body = String::new();
102 for inner in parser.by_ref() {
103 match inner {
104 Event::Text(t) => {
105 body.push_str(&t);
106 break;
107 }
108 Event::End(TagEnd::CodeBlock) => break,
109 _ => {}
110 }
111 }
112 let island = Island::parse(&info, &body)?;
113 out.push_str(&island.to_html(base));
114 let _ = parser.next(); }
116 _ => {
117 in_code_block = true;
118 out.push_str("<pre><code>");
119 }
120 },
121 Event::End(TagEnd::CodeBlock) => {
122 if in_code_block {
123 in_code_block = false;
124 out.push_str("</code></pre>");
125 }
126 }
127 Event::Start(Tag::List(start)) => {
128 let ordered = start.is_some();
129 let _ = write!(out, "<{}>", if ordered { "ol" } else { "ul" });
130 list_stack.push(ordered);
131 }
132 Event::End(TagEnd::List(_)) => {
133 match list_stack.pop() {
137 Some(true) => out.push_str("</ol>"),
138 Some(false) => out.push_str("</ul>"),
139 None => {}
140 }
141 }
142 Event::Start(Tag::Item) => out.push_str("<li>"),
143 Event::End(TagEnd::Item) => out.push_str("</li>"),
144 Event::TaskListMarker(checked) => {
145 let _ = write!(
146 out,
147 "<input type=\"checkbox\" disabled{}>",
148 if checked { " checked" } else { "" }
149 );
150 }
151 Event::Rule => out.push_str("<hr>"),
152 Event::Text(text) => out.push_str(&escape_html(&text)),
159 Event::Html(text) | Event::InlineHtml(text) => out.push_str(&text),
160 Event::SoftBreak => out.push('\n'),
161 Event::HardBreak => out.push_str("<br>"),
162 _ => {}
163 }
164 }
165 Ok(out)
166}
167
168fn escape_html(src: &str) -> String {
169 src.replace('&', "&")
170 .replace('<', "<")
171 .replace('>', ">")
172}
173
174fn escape_attr(src: &str) -> String {
175 escape_html(src).replace('"', """)
176}