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