1use std::collections::HashMap;
18use std::convert::TryFrom;
19
20use horrorshow::html;
21use horrorshow::prelude::*;
22use horrorshow::Concat;
23use horrorshow::Join;
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 if dest.starts_with("./") {
94 if self.base.is_empty() {
95 match dest {
96 CowStr::Borrowed(v) => CowStr::Borrowed(&v[2..]),
97 CowStr::Boxed(v) => InlineStr::try_from(&v[2..])
98 .map(CowStr::Inlined)
99 .unwrap_or_else(|_| {
100 let mut s: String = v.into();
101 s.replace_range(0..2, "");
102 CowStr::Boxed(s.into_boxed_str())
103 }),
104 CowStr::Inlined(inlined) => {
105 CowStr::Inlined(InlineStr::try_from(&inlined[2..]).unwrap())
106 }
107 }
108 } else {
109 CowStr::Boxed(format!("{}/{}", self.base, &dest[2..]).into())
110 }
111 } else {
112 dest
113 }
114 }
115}
116
117impl<'a, I: Iterator<Item = Event<'a>>> RenderOnce for RenderMarkdown<'a, I> {
118 fn render_once(mut self, tmpl: &mut TemplateBuffer) {
119 self.render_mut(tmpl)
120 }
121}
122
123fn class_list<'a>(classes: &'a [CowStr<'a>]) -> Option<impl RenderOnce + 'a> {
124 if classes.is_empty() {
125 None
126 } else {
127 Some(Join(" ", classes.iter().map(AsRef::as_ref)))
128 }
129}
130
131impl<'a, I: Iterator<Item = Event<'a>>> RenderMut for RenderMarkdown<'a, I> {
132 fn render_mut(&mut self, tmpl: &mut TemplateBuffer) {
133 use pulldown_cmark::BlockQuoteKind::*;
134 use pulldown_cmark::Event::*;
135 use pulldown_cmark::{CodeBlockKind, Tag};
136
137 while let Some(event) = self.iter.next() {
138 let tmpl = &mut *tmpl;
140 match event {
141 Start(tag) => {
142 let s: &mut Self = &mut *self;
144 match tag {
145 Tag::FootnoteDefinition(name) => {
146 tmpl << html! {
147 div(class="footnote", id=format_args!("footnote-{}", name)) {
148 sup(class="footnote-label") : s.footnote(name);
149 : s;
150 }
151 }
152 }
153 Tag::Paragraph => tmpl << html! { p : s },
154 Tag::BlockQuote(kind) => {
155 tmpl << html! {
156 blockquote(class ?= kind.map(|k| match k {
157 Note => "note",
158 Tip => "tip",
159 Important => "important",
160 Warning => "warning",
161 Caution => "caution",
162 })) : s;
163 }
164 }
165 Tag::Table(_) => tmpl << html! { table : s },
166 Tag::TableHead => tmpl << html! { thead { tr : s } },
167 Tag::TableRow => tmpl << html! { tr : s },
168 Tag::TableCell => tmpl << html! { td : s },
169 Tag::List(Some(0)) => tmpl << html! { ol : s },
170 Tag::List(Some(start)) => tmpl << html! { ol(start = start) : s },
171 Tag::List(None) => tmpl << html! { ul : s },
172 Tag::Item => tmpl << html! { li : s },
173 Tag::Emphasis => tmpl << html! { em: s },
174 Tag::Strikethrough => tmpl << html! { s: s },
175 Tag::Strong => tmpl << html! { strong: s },
176 Tag::Heading {
177 level,
178 id,
179 classes,
180 attrs: _, } => match level {
182 HeadingLevel::H1 => {
183 tmpl << html! { h1 (id? = id.as_deref(), class ?= class_list(&classes)): s }
184 }
185 HeadingLevel::H2 => {
186 tmpl << html! { h2 (id? = id.as_deref(), class ?= class_list(&classes)): s }
187 }
188 HeadingLevel::H3 => {
189 tmpl << html! { h3 (id? = id.as_deref(), class ?= class_list(&classes)): s }
190 }
191 HeadingLevel::H4 => {
192 tmpl << html! { h4 (id? = id.as_deref(), class ?= class_list(&classes)): s }
193 }
194 HeadingLevel::H5 => {
195 tmpl << html! { h5 (id? = id.as_deref(), class ?= class_list(&classes)): s }
196 }
197 HeadingLevel::H6 => {
198 tmpl << html! { h6 (id? = id.as_deref(), class ?= class_list(&classes)): s }
199 }
200 },
201 Tag::Link {
202 link_type: _,
203 dest_url,
204 title,
205 id,
206 ..
207 } => {
208 tmpl << html! {
209 a(href = &*s.make_relative(dest_url),
211 title? = if !title.is_empty() { Some(&*title) } else { None },
212 id ?= if !id.is_empty() { Some(&*id) } else { None }) : s
213 }
214 }
215 Tag::Image {
216 link_type: _,
217 dest_url,
218 title,
219 id,
220 } => {
221 tmpl << html! {
222 img(src = &*s.make_relative(dest_url),
223 title? = if !title.is_empty() { Some(&*title) } else { None },
224 id ?= if !id.is_empty() { Some(&*id) } else { None },
225 alt = FnRenderer::new(|tmpl| {
226 let mut nest = 0;
227 for event in s.iter.by_ref() {
228 let tmpl = &mut *tmpl;
229 match event {
230 | Start(_) => nest += 1,
231 | End(_) if nest == 0 => break,
232 | End(_) => nest -= 1,
233 | Text(txt) => tmpl << &*txt,
234 | SoftBreak
235 | HardBreak => tmpl << " ",
236 | Rule => (),
237 | Code(_)
239 | TaskListMarker(_)
240 | FootnoteReference(_)
241 | Html(_)
242 | InlineHtml(_)
243 | InlineMath(_) | DisplayMath(_) => (),
244 }
245 }
246 }))
247 }
248 }
249 Tag::CodeBlock(ref kind) => {
250 let tmp; let class = match kind {
254 CodeBlockKind::Fenced(info) => {
255 tmp = ["lang-", info.split(' ').next().unwrap()];
256 Some(Concat(&tmp))
257 }
258 CodeBlockKind::Indented => None,
259 };
260
261 tmpl << html! {
262 pre {
263 code(class? = class) : s
264 }
265 };
266 }
267
268 Tag::DefinitionList => tmpl << html! { dl : s },
269 Tag::DefinitionListTitle => tmpl << html! { dt : s },
270 Tag::DefinitionListDefinition => tmpl << html! { dd : s },
271
272 Tag::HtmlBlock => tmpl << html! { : s },
273 Tag::Superscript => tmpl << html! { sup : s },
274 Tag::Subscript => tmpl << html! { sub : s },
275 Tag::MetadataBlock(_) => {
276 panic!("metadata blocks should not have been enabled")
277 }
278 }
279 }
280 End(_) => break,
281 Code(s) => tmpl << html! { code: s.as_ref() },
282 Rule => tmpl << html! { hr; },
283 TaskListMarker(checked) => {
284 tmpl << html! {
285 input(type="checkbox", checked?=checked, disabled?=true);
286 }
287 }
288 FootnoteReference(name) => {
289 tmpl << html! {
290 sup(class="footnote-reference") {
291 a(href=format_args!("{}/#footnote-{}", self.base, name)) : self.footnote(name);
292 }
293 }
294 }
295 Text(text) => tmpl << &*text,
296 InlineHtml(html) | Html(html) => tmpl << Raw(html),
297 SoftBreak => tmpl << "\n",
298 HardBreak => tmpl << html! { br },
299 InlineMath(_) | DisplayMath(_) => {
300 panic!("math blocks should not have been enabled")
301 }
302 };
303 }
304 }
305}