vizia_core/views/
markdown.rs

1#![cfg(feature = "markdown")]
2
3use std::cell::RefCell;
4
5use comrak::nodes::{Ast, NodeValue};
6use comrak::{parse_document, Arena, Options};
7
8use crate::prelude::*;
9
10/// A view which parses and displays markdown as rich text.
11pub struct Markdown {}
12
13impl Markdown {
14    /// Create a new [Markdown] view.
15    pub fn new<'a>(cx: &'a mut Context, document: &str) -> Handle<'a, Self> {
16        Self {}
17            .build(cx, |cx| {
18                // The returned nodes are created in the supplied Arena, and are bound by its lifetime.
19                let arena = Arena::new();
20
21                let mut options = Options::default();
22                options.extension.strikethrough = true;
23                options.extension.table = true;
24
25                // Parse the document into a root `AstNode`
26                let root = parse_document(&arena, document, &options);
27
28                for node in root.children() {
29                    parse_node(cx, node, 0);
30                }
31            })
32            .height(Auto)
33    }
34}
35
36impl View for Markdown {
37    fn element(&self) -> Option<&'static str> {
38        Some("markdown")
39    }
40}
41
42fn parse_node<'a>(
43    cx: &mut Context,
44    node: &'a comrak::arena_tree::Node<'a, RefCell<Ast>>,
45    list_level: usize,
46) {
47    match &node.data.borrow().value {
48        NodeValue::Paragraph => {
49            Label::rich(cx, "", |cx| {
50                for child in node.children() {
51                    parse_node(cx, child, list_level);
52                }
53            })
54            .class("p");
55        }
56
57        NodeValue::Heading(heading) => {
58            Label::rich(cx, "", |cx| {
59                for child in node.children() {
60                    parse_node(cx, child, list_level);
61                }
62            })
63            .class(match heading.level {
64                1 => "h1",
65                2 => "h2",
66                3 => "h3",
67                4 => "h4",
68                5 => "h5",
69                6 => "h6",
70                _ => "h6",
71            });
72        }
73
74        NodeValue::Text(text) => {
75            TextSpan::new(cx, text, |_| {}).class("span");
76        }
77
78        NodeValue::Emph => {
79            TextSpan::new(cx, "", |cx| {
80                for child in node.children() {
81                    parse_node(cx, child, list_level);
82                }
83            })
84            .class("emph");
85        }
86
87        NodeValue::Strong => {
88            TextSpan::new(cx, "", |cx| {
89                for child in node.children() {
90                    parse_node(cx, child, list_level);
91                }
92            })
93            .class("strong");
94        }
95
96        NodeValue::Strikethrough => {
97            TextSpan::new(cx, "", |cx| {
98                for child in node.children() {
99                    parse_node(cx, child, list_level);
100                }
101            })
102            .class("strikethrough");
103        }
104
105        NodeValue::List(_list) => {
106            VStack::new(cx, |cx| {
107                for child in node.children() {
108                    parse_node(cx, child, list_level);
109                }
110            })
111            .height(Auto)
112            .left(Pixels(20.0));
113        }
114
115        NodeValue::Item(_list) => {
116            HStack::new(cx, |cx| {
117                Label::new(cx, "\u{2022} ").width(Auto);
118                VStack::new(cx, |cx| {
119                    for child in node.children() {
120                        parse_node(cx, child, list_level + 1);
121                    }
122                })
123                .height(Auto);
124            })
125            .class("li")
126            .height(Auto);
127        }
128
129        NodeValue::Code(code) => {
130            TextSpan::new(cx, &code.literal.to_owned(), |_| {}).class("code");
131        }
132
133        NodeValue::CodeBlock(code_block) => {
134            let mut code = code_block.literal.to_owned();
135            code.pop().unwrap();
136            ScrollView::new(cx, |cx| {
137                Label::new(cx, code).class("code");
138            })
139            .show_vertical_scrollbar(false)
140            .height(Auto)
141            .width(Stretch(1.0));
142        }
143
144        NodeValue::Link(link) => {
145            let url = link.url.clone();
146            TextSpan::new(cx, "", |cx| {
147                for child in node.children() {
148                    parse_node(cx, child, list_level);
149                }
150            })
151            .cursor(CursorIcon::Hand)
152            .pointer_events(PointerEvents::Auto)
153            .on_press(move |_| {
154                open::that(url.as_str()).unwrap();
155            })
156            .class("link");
157        }
158
159        NodeValue::SoftBreak => {
160            TextSpan::new(cx, "\n", |cx| {
161                for child in node.children() {
162                    parse_node(cx, child, list_level);
163                }
164            });
165        }
166
167        NodeValue::Table(_table) => {
168            VStack::new(cx, |cx| {
169                for child in node.children() {
170                    parse_node(cx, child, list_level);
171                }
172            })
173            .class("table")
174            .width(Stretch(1.0))
175            .height(Auto);
176        }
177
178        NodeValue::TableRow(headers) => {
179            HStack::new(cx, |cx| {
180                for child in node.children() {
181                    parse_node(cx, child, list_level);
182                }
183            })
184            .class("table-row")
185            .toggle_class("table-headers", *headers)
186            .width(Stretch(1.0))
187            .height(Auto);
188            Divider::horizontal(cx);
189        }
190
191        NodeValue::TableCell => {
192            Label::rich(cx, "", |cx| {
193                for child in node.children() {
194                    parse_node(cx, child, list_level);
195                }
196            })
197            .class("table-cell")
198            .width(Stretch(1.0));
199        }
200
201        _ => {}
202    }
203}