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                    | 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            // manually reborrow
139            let tmpl = &mut *tmpl;
140            match event {
141                Start(tag) => {
142                    // Because rust doesn't reborrow? (WTF?)
143                    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: _, // TODO
181                        } => 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                                // TODO: Escape href?
210                                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                                                // Ignored
238                                                | Code(_)
239                                                | TaskListMarker(_)
240                                                | FootnoteReference(_)
241                                                | Html(_)
242                                                | InlineHtml(_)
243                                                | InlineMath(_) | DisplayMath(_) => (),
244                                            }
245                                        }
246                                    }))
247                            }
248                        }
249                        Tag::CodeBlock(ref kind) => {
250                            // TODO Highlight code without js.
251
252                            let tmp; // lifetimes.
253                            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}