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::Join;
21use horrorshow::html;
22use horrorshow::prelude::*;
23use pulldown_cmark::HeadingLevel;
24use pulldown_cmark::{CowStr, Event, InlineStr, Options, Parser};
25
26#[cfg(feature = "syntax-highlighting")]
27use crate::highlight::SyntaxHighlight;
28
29/// Markdown renderer
30#[derive(Debug, Copy, Clone, PartialEq, Eq)]
31pub struct Markdown<'a> {
32    data: &'a str,
33    base: &'a str,
34    highlight: bool,
35}
36
37impl<'a> Markdown<'a> {
38    /// Create a new markdown renderer.
39    ///
40    /// `data` should contain the markdown to be rendered and `base` should specify a relative url
41    /// prefix (for relative links and images).
42    ///
43    /// Note: `base` will only affect markdown links and images, not inline html ones.
44    pub fn new(data: &'a str, base: &'a str, highlight: bool) -> Markdown<'a> {
45        Markdown {
46            data,
47            base,
48            highlight,
49        }
50    }
51}
52
53impl<'a> RenderOnce for Markdown<'a> {
54    #[inline]
55    fn render_once(self, tmpl: &mut TemplateBuffer) {
56        self.render(tmpl)
57    }
58}
59
60impl<'a> RenderMut for Markdown<'a> {
61    #[inline]
62    fn render_mut(&mut self, tmpl: &mut TemplateBuffer) {
63        self.render(tmpl)
64    }
65}
66
67impl<'a> Render for Markdown<'a> {
68    #[inline]
69    fn render(&self, tmpl: &mut TemplateBuffer) {
70        tmpl << RenderMarkdown {
71            footnotes: HashMap::new(),
72            iter: Parser::new_ext(
73                self.data,
74                Options::ENABLE_TABLES
75                    | Options::ENABLE_FOOTNOTES
76                    | Options::ENABLE_STRIKETHROUGH
77                    | Options::ENABLE_SMART_PUNCTUATION
78                    | Options::ENABLE_DEFINITION_LIST
79                    | Options::ENABLE_TASKLISTS
80                    | Options::ENABLE_GFM,
81            ),
82            base: self.base,
83            syntax_highlight: self.highlight,
84        }
85    }
86}
87
88struct RenderMarkdown<'a, I> {
89    iter: I,
90    footnotes: HashMap<CowStr<'a>, u32>,
91    base: &'a str,
92    #[cfg_attr(not(feature = "syntax-highlighting"), allow(dead_code))]
93    syntax_highlight: bool,
94}
95
96impl<'a, I> RenderMarkdown<'a, I> {
97    fn footnote(&mut self, name: CowStr<'a>) -> u32 {
98        let next_idx = (self.footnotes.len() as u32) + 1;
99        *self.footnotes.entry(name).or_insert(next_idx)
100    }
101
102    fn make_relative<'b>(&self, dest: CowStr<'b>) -> CowStr<'b> {
103        #[allow(clippy::manual_strip)]
104        if dest.starts_with("./") {
105            if self.base.is_empty() {
106                match dest {
107                    CowStr::Borrowed(v) => CowStr::Borrowed(&v[2..]),
108                    CowStr::Boxed(v) => InlineStr::try_from(&v[2..])
109                        .map(CowStr::Inlined)
110                        .unwrap_or_else(|_| {
111                            let mut s: String = v.into();
112                            s.replace_range(0..2, "");
113                            CowStr::Boxed(s.into_boxed_str())
114                        }),
115                    CowStr::Inlined(inlined) => {
116                        CowStr::Inlined(InlineStr::try_from(&inlined[2..]).unwrap())
117                    }
118                }
119            } else {
120                CowStr::Boxed(format!("{}/{}", self.base, &dest[2..]).into())
121            }
122        } else {
123            dest
124        }
125    }
126}
127
128impl<'a, I: Iterator<Item = Event<'a>>> RenderOnce for RenderMarkdown<'a, I> {
129    fn render_once(mut self, tmpl: &mut TemplateBuffer) {
130        self.render_mut(tmpl)
131    }
132}
133
134fn class_list<'a>(classes: &'a [CowStr<'a>]) -> Option<impl RenderOnce + 'a> {
135    if classes.is_empty() {
136        None
137    } else {
138        Some(Join(" ", classes.iter().map(AsRef::as_ref)))
139    }
140}
141
142#[inline(always)]
143fn inner_text<'a>(iter: &mut impl Iterator<Item = Event<'a>>, escape: bool) -> impl RenderOnce {
144    use pulldown_cmark::Event::*;
145    FnRenderer::new(move |tmpl| {
146        let mut nest = 0;
147        for event in iter {
148            match event {
149                Start(_) => nest += 1,
150                End(_) if nest == 0 => break,
151                End(_) => nest -= 1,
152                Text(txt) | Code(txt) => {
153                    if escape {
154                        tmpl.write_str(&txt)
155                    } else {
156                        tmpl.write_raw(&txt)
157                    }
158                }
159                SoftBreak | HardBreak => tmpl.write_raw(" "),
160                Rule => tmpl.write_raw("\n"),
161                // Ignored
162                TaskListMarker(_) | FootnoteReference(_) | Html(_) | InlineHtml(_)
163                | InlineMath(_) | DisplayMath(_) => (),
164            }
165        }
166    })
167}
168
169impl<'a, I: Iterator<Item = Event<'a>>> RenderMut for RenderMarkdown<'a, I> {
170    fn render_mut(&mut self, tmpl: &mut TemplateBuffer) {
171        use pulldown_cmark::BlockQuoteKind::*;
172        use pulldown_cmark::Event::*;
173        use pulldown_cmark::{CodeBlockKind, Tag};
174
175        #[cfg(feature = "syntax-highlighting")]
176        let syntax_highlight = self.syntax_highlight;
177
178        while let Some(event) = self.iter.next() {
179            // manually reborrow
180            let tmpl = &mut *tmpl;
181            match event {
182                Start(tag) => {
183                    // Because rust doesn't reborrow? (WTF?)
184                    let s: &mut Self = &mut *self;
185                    match tag {
186                        Tag::FootnoteDefinition(name) => {
187                            tmpl << html! {
188                                div(class="footnote", id=format_args!("footnote-{}", name)) {
189                                    sup(class="footnote-label") : s.footnote(name);
190                                    : s;
191                                }
192                            }
193                        }
194                        Tag::Paragraph => tmpl << html! { p : s },
195                        Tag::BlockQuote(kind) => {
196                            tmpl << html! {
197                                blockquote(class ?= kind.map(|k| match k {
198                                    Note => "note",
199                                    Tip => "tip",
200                                    Important => "important",
201                                    Warning => "warning",
202                                    Caution => "caution",
203                                })) : s;
204                            }
205                        }
206                        Tag::Table(_) => tmpl << html! { table : s },
207                        Tag::TableHead => tmpl << html! { thead { tr : s } },
208                        Tag::TableRow => tmpl << html! { tr : s },
209                        Tag::TableCell => tmpl << html! { td : s },
210                        Tag::List(Some(0)) => tmpl << html! { ol : s },
211                        Tag::List(Some(start)) => tmpl << html! { ol(start = start) : s },
212                        Tag::List(None) => tmpl << html! { ul : s },
213                        Tag::Item => tmpl << html! { li : s },
214                        Tag::Emphasis => tmpl << html! { em: s },
215                        Tag::Strikethrough => tmpl << html! { s: s },
216                        Tag::Strong => tmpl << html! { strong: s },
217                        Tag::Heading {
218                            level,
219                            id,
220                            classes,
221                            attrs: _, // TODO
222                        } => match level {
223                            HeadingLevel::H1 => {
224                                tmpl << html! { h1 (id? = id.as_deref(), class ?= class_list(&classes)): s }
225                            }
226                            HeadingLevel::H2 => {
227                                tmpl << html! { h2 (id? = id.as_deref(), class ?= class_list(&classes)): s }
228                            }
229                            HeadingLevel::H3 => {
230                                tmpl << html! { h3 (id? = id.as_deref(), class ?= class_list(&classes)): s }
231                            }
232                            HeadingLevel::H4 => {
233                                tmpl << html! { h4 (id? = id.as_deref(), class ?= class_list(&classes)): s }
234                            }
235                            HeadingLevel::H5 => {
236                                tmpl << html! { h5 (id? = id.as_deref(), class ?= class_list(&classes)): s }
237                            }
238                            HeadingLevel::H6 => {
239                                tmpl << html! { h6 (id? = id.as_deref(), class ?= class_list(&classes)): s }
240                            }
241                        },
242                        Tag::Link {
243                            link_type: _,
244                            dest_url,
245                            title,
246                            id,
247                            ..
248                        } => {
249                            tmpl << html! {
250                                // TODO: Escape href?
251                                a(href = &*s.make_relative(dest_url),
252                                  title? = if !title.is_empty() { Some(&*title) } else { None },
253                                  id ?= if !id.is_empty() { Some(&*id) } else { None }) : s
254                            }
255                        }
256                        Tag::Image {
257                            link_type: _,
258                            dest_url,
259                            title,
260                            id,
261                        } => {
262                            tmpl << html! {
263                                img(src = &*s.make_relative(dest_url),
264                                    title? = if !title.is_empty() { Some(&*title) } else { None },
265                                    id ?= if !id.is_empty() { Some(&*id) } else { None },
266                                    alt = inner_text(&mut s.iter, true))
267                            }
268                        }
269                        Tag::CodeBlock(ref kind) => {
270                            let lang = match kind {
271                                CodeBlockKind::Fenced(info) => {
272                                    let lang = info.split(' ').next().unwrap();
273                                    (!lang.is_empty()).then_some(lang)
274                                }
275                                CodeBlockKind::Indented => None,
276                            };
277
278                            match lang {
279                                #[cfg(feature = "syntax-highlighting")]
280                                Some(lang) if syntax_highlight => {
281                                    tmpl << html! {
282                                        pre {
283                                            code(class = format_args!("lang-{lang}")) : SyntaxHighlight {
284                                                code: &inner_text(&mut s.iter, false).into_string().unwrap(),
285                                                lang,
286                                            }
287                                        }
288                                    }
289                                }
290                                Some(lang) => {
291                                    tmpl << html! { pre { code(class = format_args!("lang-{lang}")) : s } }
292                                }
293                                None => tmpl << html! { pre { code : s } },
294                            }
295                        }
296
297                        Tag::DefinitionList => tmpl << html! { dl : s },
298                        Tag::DefinitionListTitle => tmpl << html! { dt : s },
299                        Tag::DefinitionListDefinition => tmpl << html! { dd : s },
300
301                        Tag::HtmlBlock => tmpl << html! { : s },
302                        Tag::Superscript => tmpl << html! { sup : s },
303                        Tag::Subscript => tmpl << html! { sub : s },
304                        Tag::MetadataBlock(_) => {
305                            panic!("metadata blocks should not have been enabled")
306                        }
307                    }
308                }
309                End(_) => break,
310                Code(s) => tmpl << html! { code: s.as_ref() },
311                Rule => tmpl << html! { hr; },
312                TaskListMarker(checked) => {
313                    tmpl << html! {
314                        input(type="checkbox", checked?=checked, disabled?=true);
315                    }
316                }
317                FootnoteReference(name) => {
318                    tmpl << html! {
319                        sup(class="footnote-reference") {
320                            a(href=format_args!("{}/#footnote-{}", self.base, name)) : self.footnote(name);
321                        }
322                    }
323                }
324                Text(text) => tmpl << &*text,
325                InlineHtml(html) | Html(html) => tmpl << Raw(html),
326                SoftBreak => tmpl << "\n",
327                HardBreak => tmpl << html! { br },
328                InlineMath(_) | DisplayMath(_) => {
329                    panic!("math blocks should not have been enabled")
330                }
331            };
332        }
333    }
334}