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