rate_app/
markdown.rs

1/// Original author of this code is [Nathan Ringo](https://github.com/remexre)
2/// Source: https://github.com/acmumn/mentoring/blob/master/web-client/src/view/markdown.rs
3use pulldown_cmark::{Alignment, CodeBlockKind, Event, Options, Parser, Tag};
4use yew::virtual_dom::{VNode, VTag, VText};
5use yew::{html, Classes, Html};
6
7/// Adds a class to the VTag.
8/// You can also provide multiple classes separated by ascii whitespaces.
9///
10/// Note that this has a complexity of O(n),
11/// where n is the number of classes already in VTag plus
12/// the number of classes to be added.
13fn add_class(vtag: &mut VTag, class: impl Into<Classes>) {
14    let mut classes: Classes = vtag
15        .attributes
16        .iter()
17        .find(|(k, _)| *k == "class")
18        .map(|(_, v)| Classes::from(v.to_owned()))
19        .unwrap_or_default();
20    classes.push(class);
21    vtag.add_attribute("class", classes.to_string());
22}
23
24/// Renders a string of Markdown to HTML with the default options (footnotes
25/// disabled, tables enabled).
26pub fn render(src: &str) -> Html {
27    let mut elems = vec![];
28    let mut spine = vec![];
29
30    macro_rules! add_child {
31        ($child:expr) => {{
32            let l = spine.len();
33            assert_ne!(l, 0);
34            spine[l - 1].add_child($child);
35        }};
36    }
37
38    let mut options = Options::empty();
39    options.insert(Options::ENABLE_TABLES);
40
41    for ev in Parser::new_ext(src, options) {
42        match ev {
43            Event::Start(tag) => {
44                spine.push(make_tag(tag));
45            }
46            Event::End(tag) => {
47                // TODO Verify stack end.
48                let l = spine.len();
49                assert!(l >= 1);
50                let mut top = spine.pop().unwrap();
51                if let Tag::CodeBlock(_) = tag {
52                    let mut pre = VTag::new("pre");
53                    add_class(&mut pre, "bg-secondary text-white p-2");
54                    pre.add_child(top.into());
55                    top = pre;
56                } else if let Tag::Table(aligns) = tag {
57                    for r in top.children.iter_mut() {
58                        if let VNode::VTag(ref mut vtag) = r {
59                            for (i, c) in vtag.children.iter_mut().enumerate() {
60                                if let VNode::VTag(ref mut vtag) = c {
61                                    match aligns[i] {
62                                        Alignment::None => {}
63                                        Alignment::Left => add_class(vtag, "text-left"),
64                                        Alignment::Center => add_class(vtag, "text-center"),
65                                        Alignment::Right => add_class(vtag, "text-right"),
66                                    }
67                                }
68                            }
69                        }
70                    }
71                } else if let Tag::TableHead = tag {
72                    for c in top.children.iter_mut() {
73                        if let VNode::VTag(ref mut vtag) = c {
74                            // TODO
75                            //                            vtag.tag = "th".into();
76                            vtag.add_attribute("scope", "col");
77                        }
78                    }
79                }
80                if l == 1 {
81                    elems.push(top);
82                } else {
83                    spine[l - 2].add_child(top.into());
84                }
85            }
86            Event::Text(text) => add_child!(VText::new(text.to_string()).into()),
87            Event::Code(code) => {
88                let mut tag = VTag::new("code");
89                tag.add_child(VText::new(code.to_string()).into());
90                add_child!(tag.into());
91            }
92            Event::Rule => add_child!(VTag::new("hr").into()),
93            Event::SoftBreak => add_child!(VText::new("\n").into()),
94            Event::HardBreak => add_child!(VTag::new("br").into()),
95            _ => println!("Unknown event: {:#?}", ev),
96        }
97    }
98
99    if elems.len() == 1 {
100        VNode::VTag(Box::new(elems.pop().unwrap()))
101    } else {
102        html! {
103            <div>{ for elems.into_iter() }</div>
104        }
105    }
106}
107
108fn make_tag(t: Tag) -> VTag {
109    match t {
110        Tag::Paragraph => VTag::new("p"),
111        Tag::Heading(n) => {
112            assert!(n > 0);
113            assert!(n < 7);
114            VTag::new(format!("h{}", n))
115        }
116        Tag::BlockQuote => {
117            let mut el = VTag::new("blockquote");
118            el.add_attribute("class", "blockquote");
119            el
120        }
121        Tag::CodeBlock(code_block_kind) => {
122            let mut el = VTag::new("code");
123
124            if let CodeBlockKind::Fenced(lang) = code_block_kind {
125                // Different color schemes may be used for different code blocks,
126                // but a different library (likely js based at the moment) would be necessary to actually provide the
127                // highlighting support by locating the language classes and applying dom transforms
128                // on their contents.
129                match lang.as_ref() {
130                    "html" => el.add_attribute("class", "html-language"),
131                    "rust" => el.add_attribute("class", "rust-language"),
132                    "java" => el.add_attribute("class", "java-language"),
133                    "c" => el.add_attribute("class", "c-language"),
134                    _ => {} // Add your own language highlighting support
135                };
136            }
137
138            el
139        }
140        Tag::List(None) => VTag::new("ul"),
141        Tag::List(Some(1)) => VTag::new("ol"),
142        Tag::List(Some(ref start)) => {
143            let mut el = VTag::new("ol");
144            el.add_attribute("start", start.to_string());
145            el
146        }
147        Tag::Item => VTag::new("li"),
148        Tag::Table(_) => {
149            let mut el = VTag::new("table");
150            el.add_attribute("class", "table");
151            el
152        }
153        Tag::TableHead => VTag::new("th"),
154        Tag::TableRow => VTag::new("tr"),
155        Tag::TableCell => VTag::new("td"),
156        Tag::Emphasis => {
157            let mut el = VTag::new("span");
158            el.add_attribute("class", "fst-italic");
159            el
160        }
161        Tag::Strong => {
162            let mut el = VTag::new("span");
163            el.add_attribute("class", "fw-bold");
164            el
165        }
166        Tag::Link(_link_type, ref href, ref title) => {
167            let mut el = VTag::new("a");
168            el.add_attribute("href", href.to_string());
169            let title = title.clone().into_string();
170            if !title.is_empty() {
171                el.add_attribute("title", title);
172            }
173            el
174        }
175        Tag::Image(_link_type, ref src, ref title) => {
176            let mut el = VTag::new("img");
177            el.add_attribute("src", src.to_string());
178            let title = title.clone().into_string();
179            if !title.is_empty() {
180                el.add_attribute("title", title);
181            }
182            el
183        }
184        Tag::FootnoteDefinition(ref _footnote_id) => VTag::new("span"), // Footnotes are not rendered as anything special
185        Tag::Strikethrough => {
186            let mut el = VTag::new("span");
187            el.add_attribute("class", "text-decoration-strikethrough");
188            el
189        }
190    }
191}