gazetta_render_ext/
markdown.rs

1//  Copyright (C) 2015 Steven Allen
2//
3//  This file is part of gazetta.
4//
5//  This program is free software: you can redistribute it and/or modify it under the terms of the
6//  GNU General Public License as published by the Free Software Foundation version 3 of the
7//  License.
8//
9//  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
10//  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
11//  the GNU General Public License for more details.
12//
13//  You should have received a copy of the GNU General Public License along with this program.  If
14//  not, see <http://www.gnu.org/licenses/>.
15//
16
17use 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/// Markdown renderer
28#[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    /// Create a new markdown renderer.
36    ///
37    /// `data` should contain the markdown to be rendered and `base` should specify a relative url
38    /// prefix (for relative links and images).
39    ///
40    /// Note: `base` will only affect markdown links and images, not inline html ones.
41    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            // manually reborrow
137            let tmpl = &mut *tmpl;
138            match event {
139                Start(tag) => {
140                    // Because rust doesn't reborrow? (WTF?)
141                    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: _, // TODO
169                        } => 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                                // TODO: Escape href?
198                                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 ?= if !id.is_empty() { Some(&*id) } else { None },
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                                                // Ignored
226                                                | Code(_)
227                                                | TaskListMarker(_)
228                                                | FootnoteReference(_)
229                                                | Html(_)
230                                                | InlineHtml(_)
231                                                | InlineMath(_) | DisplayMath(_) => (),
232                                            }
233                                        }
234                                    }))
235                            }
236                        }
237                        Tag::CodeBlock(ref kind) => {
238                            // TODO Highlight code without js.
239
240                            let tmp; // lifetimes.
241                            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}