1use std::collections::HashMap;
18use std::convert::TryFrom;
19
20use horrorshow::Join;
21use horrorshow::html;
22use horrorshow::prelude::*;
23use pulldown_cmark::HeadingLevel;
24use pulldown_cmark::{CowStr, Event, InlineStr, Options, Parser};
25
26#[cfg(feature = "syntax-highlighting")]
27use crate::highlight::SyntaxHighlight;
28
29#[derive(Debug, Copy, Clone, PartialEq, Eq)]
31pub struct Markdown<'a> {
32 data: &'a str,
33 base: &'a str,
34 highlight: bool,
35}
36
37impl<'a> Markdown<'a> {
38 pub fn new(data: &'a str, base: &'a str, highlight: bool) -> Markdown<'a> {
45 Markdown {
46 data,
47 base,
48 highlight,
49 }
50 }
51}
52
53impl<'a> RenderOnce for Markdown<'a> {
54 #[inline]
55 fn render_once(self, tmpl: &mut TemplateBuffer) {
56 self.render(tmpl)
57 }
58}
59
60impl<'a> RenderMut for Markdown<'a> {
61 #[inline]
62 fn render_mut(&mut self, tmpl: &mut TemplateBuffer) {
63 self.render(tmpl)
64 }
65}
66
67impl<'a> Render for Markdown<'a> {
68 #[inline]
69 fn render(&self, tmpl: &mut TemplateBuffer) {
70 tmpl << RenderMarkdown {
71 footnotes: HashMap::new(),
72 iter: Parser::new_ext(
73 self.data,
74 Options::ENABLE_TABLES
75 | Options::ENABLE_FOOTNOTES
76 | Options::ENABLE_STRIKETHROUGH
77 | Options::ENABLE_SMART_PUNCTUATION
78 | Options::ENABLE_DEFINITION_LIST
79 | Options::ENABLE_TASKLISTS
80 | Options::ENABLE_GFM,
81 ),
82 base: self.base,
83 syntax_highlight: self.highlight,
84 }
85 }
86}
87
88struct RenderMarkdown<'a, I> {
89 iter: I,
90 footnotes: HashMap<CowStr<'a>, u32>,
91 base: &'a str,
92 #[cfg_attr(not(feature = "syntax-highlighting"), allow(dead_code))]
93 syntax_highlight: bool,
94}
95
96impl<'a, I> RenderMarkdown<'a, I> {
97 fn footnote(&mut self, name: CowStr<'a>) -> u32 {
98 let next_idx = (self.footnotes.len() as u32) + 1;
99 *self.footnotes.entry(name).or_insert(next_idx)
100 }
101
102 fn make_relative<'b>(&self, dest: CowStr<'b>) -> CowStr<'b> {
103 #[allow(clippy::manual_strip)]
104 if dest.starts_with("./") {
105 if self.base.is_empty() {
106 match dest {
107 CowStr::Borrowed(v) => CowStr::Borrowed(&v[2..]),
108 CowStr::Boxed(v) => InlineStr::try_from(&v[2..])
109 .map(CowStr::Inlined)
110 .unwrap_or_else(|_| {
111 let mut s: String = v.into();
112 s.replace_range(0..2, "");
113 CowStr::Boxed(s.into_boxed_str())
114 }),
115 CowStr::Inlined(inlined) => {
116 CowStr::Inlined(InlineStr::try_from(&inlined[2..]).unwrap())
117 }
118 }
119 } else {
120 CowStr::Boxed(format!("{}/{}", self.base, &dest[2..]).into())
121 }
122 } else {
123 dest
124 }
125 }
126}
127
128impl<'a, I: Iterator<Item = Event<'a>>> RenderOnce for RenderMarkdown<'a, I> {
129 fn render_once(mut self, tmpl: &mut TemplateBuffer) {
130 self.render_mut(tmpl)
131 }
132}
133
134fn class_list<'a>(classes: &'a [CowStr<'a>]) -> Option<impl RenderOnce + 'a> {
135 if classes.is_empty() {
136 None
137 } else {
138 Some(Join(" ", classes.iter().map(AsRef::as_ref)))
139 }
140}
141
142#[inline(always)]
143fn inner_text<'a>(iter: &mut impl Iterator<Item = Event<'a>>, escape: bool) -> impl RenderOnce {
144 use pulldown_cmark::Event::*;
145 FnRenderer::new(move |tmpl| {
146 let mut nest = 0;
147 for event in iter {
148 match event {
149 Start(_) => nest += 1,
150 End(_) if nest == 0 => break,
151 End(_) => nest -= 1,
152 Text(txt) | Code(txt) => {
153 if escape {
154 tmpl.write_str(&txt)
155 } else {
156 tmpl.write_raw(&txt)
157 }
158 }
159 SoftBreak | HardBreak => tmpl.write_raw(" "),
160 Rule => tmpl.write_raw("\n"),
161 TaskListMarker(_) | FootnoteReference(_) | Html(_) | InlineHtml(_)
163 | InlineMath(_) | DisplayMath(_) => (),
164 }
165 }
166 })
167}
168
169impl<'a, I: Iterator<Item = Event<'a>>> RenderMut for RenderMarkdown<'a, I> {
170 fn render_mut(&mut self, tmpl: &mut TemplateBuffer) {
171 use pulldown_cmark::BlockQuoteKind::*;
172 use pulldown_cmark::Event::*;
173 use pulldown_cmark::{CodeBlockKind, Tag};
174
175 #[cfg(feature = "syntax-highlighting")]
176 let syntax_highlight = self.syntax_highlight;
177
178 while let Some(event) = self.iter.next() {
179 let tmpl = &mut *tmpl;
181 match event {
182 Start(tag) => {
183 let s: &mut Self = &mut *self;
185 match tag {
186 Tag::FootnoteDefinition(name) => {
187 tmpl << html! {
188 div(class="footnote", id=format_args!("footnote-{}", name)) {
189 sup(class="footnote-label") : s.footnote(name);
190 : s;
191 }
192 }
193 }
194 Tag::Paragraph => tmpl << html! { p : s },
195 Tag::BlockQuote(kind) => {
196 tmpl << html! {
197 blockquote(class ?= kind.map(|k| match k {
198 Note => "note",
199 Tip => "tip",
200 Important => "important",
201 Warning => "warning",
202 Caution => "caution",
203 })) : s;
204 }
205 }
206 Tag::Table(_) => tmpl << html! { table : s },
207 Tag::TableHead => tmpl << html! { thead { tr : s } },
208 Tag::TableRow => tmpl << html! { tr : s },
209 Tag::TableCell => tmpl << html! { td : s },
210 Tag::List(Some(0)) => tmpl << html! { ol : s },
211 Tag::List(Some(start)) => tmpl << html! { ol(start = start) : s },
212 Tag::List(None) => tmpl << html! { ul : s },
213 Tag::Item => tmpl << html! { li : s },
214 Tag::Emphasis => tmpl << html! { em: s },
215 Tag::Strikethrough => tmpl << html! { s: s },
216 Tag::Strong => tmpl << html! { strong: s },
217 Tag::Heading {
218 level,
219 id,
220 classes,
221 attrs: _, } => match level {
223 HeadingLevel::H1 => {
224 tmpl << html! { h1 (id? = id.as_deref(), class ?= class_list(&classes)): s }
225 }
226 HeadingLevel::H2 => {
227 tmpl << html! { h2 (id? = id.as_deref(), class ?= class_list(&classes)): s }
228 }
229 HeadingLevel::H3 => {
230 tmpl << html! { h3 (id? = id.as_deref(), class ?= class_list(&classes)): s }
231 }
232 HeadingLevel::H4 => {
233 tmpl << html! { h4 (id? = id.as_deref(), class ?= class_list(&classes)): s }
234 }
235 HeadingLevel::H5 => {
236 tmpl << html! { h5 (id? = id.as_deref(), class ?= class_list(&classes)): s }
237 }
238 HeadingLevel::H6 => {
239 tmpl << html! { h6 (id? = id.as_deref(), class ?= class_list(&classes)): s }
240 }
241 },
242 Tag::Link {
243 link_type: _,
244 dest_url,
245 title,
246 id,
247 ..
248 } => {
249 tmpl << html! {
250 a(href = &*s.make_relative(dest_url),
252 title? = if !title.is_empty() { Some(&*title) } else { None },
253 id ?= if !id.is_empty() { Some(&*id) } else { None }) : s
254 }
255 }
256 Tag::Image {
257 link_type: _,
258 dest_url,
259 title,
260 id,
261 } => {
262 tmpl << html! {
263 img(src = &*s.make_relative(dest_url),
264 title? = if !title.is_empty() { Some(&*title) } else { None },
265 id ?= if !id.is_empty() { Some(&*id) } else { None },
266 alt = inner_text(&mut s.iter, true))
267 }
268 }
269 Tag::CodeBlock(ref kind) => {
270 let lang = match kind {
271 CodeBlockKind::Fenced(info) => {
272 let lang = info.split(' ').next().unwrap();
273 (!lang.is_empty()).then_some(lang)
274 }
275 CodeBlockKind::Indented => None,
276 };
277
278 match lang {
279 #[cfg(feature = "syntax-highlighting")]
280 Some(lang) if syntax_highlight => {
281 tmpl << html! {
282 pre {
283 code(class = format_args!("lang-{lang}")) : SyntaxHighlight {
284 code: &inner_text(&mut s.iter, false).into_string().unwrap(),
285 lang,
286 }
287 }
288 }
289 }
290 Some(lang) => {
291 tmpl << html! { pre { code(class = format_args!("lang-{lang}")) : s } }
292 }
293 None => tmpl << html! { pre { code : s } },
294 }
295 }
296
297 Tag::DefinitionList => tmpl << html! { dl : s },
298 Tag::DefinitionListTitle => tmpl << html! { dt : s },
299 Tag::DefinitionListDefinition => tmpl << html! { dd : s },
300
301 Tag::HtmlBlock => tmpl << html! { : s },
302 Tag::Superscript => tmpl << html! { sup : s },
303 Tag::Subscript => tmpl << html! { sub : s },
304 Tag::MetadataBlock(_) => {
305 panic!("metadata blocks should not have been enabled")
306 }
307 }
308 }
309 End(_) => break,
310 Code(s) => tmpl << html! { code: s.as_ref() },
311 Rule => tmpl << html! { hr; },
312 TaskListMarker(checked) => {
313 tmpl << html! {
314 input(type="checkbox", checked?=checked, disabled?=true);
315 }
316 }
317 FootnoteReference(name) => {
318 tmpl << html! {
319 sup(class="footnote-reference") {
320 a(href=format_args!("{}/#footnote-{}", self.base, name)) : self.footnote(name);
321 }
322 }
323 }
324 Text(text) => tmpl << &*text,
325 InlineHtml(html) | Html(html) => tmpl << Raw(html),
326 SoftBreak => tmpl << "\n",
327 HardBreak => tmpl << html! { br },
328 InlineMath(_) | DisplayMath(_) => {
329 panic!("math blocks should not have been enabled")
330 }
331 };
332 }
333 }
334}