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