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