yew_markdown/
lib.rs

1use web_framework_markdown::{
2    render_markdown, Context, CowStr, ElementAttributes, HtmlElement, MarkdownProps,
3};
4
5use core::ops::Range;
6
7use std::collections::BTreeMap;
8
9pub use web_framework_markdown::{ComponentCreationError, LinkDescription, Options};
10
11use yew::prelude::{
12    function_component, html, AttrValue, Callback, Html, Properties, UseStateHandle,
13};
14
15pub type MdComponentProps = web_framework_markdown::MdComponentProps<Html>;
16
17use web_sys::{window, MouseEvent};
18
19#[derive(Clone, Debug)]
20pub struct MarkdownMouseEvent {
21    /// the original mouse event triggered when a text element was clicked on
22    pub mouse_event: MouseEvent,
23
24    /// the corresponding range in the markdown source, as a slice of [`u8`][u8]
25    pub position: Range<usize>,
26    // TODO: add a clonable tag for the type of the element
27    // pub tag: pulldown_cmark::Tag<'a>,
28}
29
30/// component store.
31/// It is called when therer is a `<CustomComponent>` inside the markdown source.
32/// It is basically a hashmap but more efficient for a small number of items
33#[derive(PartialEq, Clone)]
34pub struct CustomComponents(
35    BTreeMap<&'static str, Callback<MdComponentProps, Result<Html, ComponentCreationError>>>,
36);
37
38impl Default for CustomComponents {
39    fn default() -> Self {
40        Self(Default::default())
41    }
42}
43
44impl CustomComponents {
45    pub fn new() -> Self {
46        Self(Default::default())
47    }
48
49    /// register a new component.
50    /// The function `component` takes a context and props of type `MdComponentProps`
51    /// and returns html
52    pub fn register<F>(&mut self, name: &'static str, component: F)
53    where
54        F: Fn(MdComponentProps) -> Result<Html, ComponentCreationError> + 'static,
55    {
56        self.0.insert(name, Callback::from(component));
57    }
58}
59
60impl<'a> Context<'a, 'static> for &'a Props {
61    type View = Html;
62
63    type Handler<T: 'static> = Callback<T>;
64
65    type MouseEvent = MouseEvent;
66
67    fn props(self) -> MarkdownProps {
68        let Props {
69            theme,
70            wikilinks,
71            hard_line_breaks,
72            parse_options,
73            ..
74        } = self;
75
76        MarkdownProps {
77            theme: *theme,
78            wikilinks: *wikilinks,
79            hard_line_breaks: *hard_line_breaks,
80            parse_options: *parse_options,
81        }
82    }
83
84    #[cfg(feature = "debug")]
85    fn send_debug_info(self, info: Vec<String>) {
86        if let Some(sender) = &self.send_debug_info {
87            sender.emit(info)
88        }
89    }
90
91    fn el_with_attributes(
92        self,
93        e: HtmlElement,
94        inside: Self::View,
95        attributes: ElementAttributes<Callback<MouseEvent>>,
96    ) -> Self::View {
97        let style = attributes.style.map(|x| x.to_string());
98        let classes: Vec<_> = attributes.classes.iter().map(|x| x.to_string()).collect();
99        let on_click = attributes.on_click;
100
101        match e {
102            HtmlElement::Div => {
103                html! {<div style={style} onclick={on_click} class={classes}>{inside}</div>}
104            }
105            HtmlElement::Span => {
106                html! {<span style={style} onclick={on_click} class={classes}>{inside}</span>}
107            }
108            HtmlElement::Paragraph => {
109                html! {<p  style={style} onclick={on_click} class={classes}>{inside}</p>}
110            }
111            HtmlElement::Ul => {
112                html! {<ul  style={style} onclick={on_click} class={classes}>{inside}</ul>}
113            }
114            HtmlElement::Ol(start) => {
115                html! {<ol start={start.to_string()}  style={style} onclick={on_click} class={classes}>{inside}</ol>}
116            }
117            HtmlElement::Li => {
118                html! {<li  style={style} onclick={on_click} class={classes}>{inside}</li>}
119            }
120            HtmlElement::BlockQuote => {
121                html! {<blockquote  style={style} onclick={on_click} class={classes}>{inside}</blockquote>}
122            }
123            HtmlElement::Heading(1) => {
124                html! {<h1  style={style} onclick={on_click} class={classes}>{inside}</h1>}
125            }
126            HtmlElement::Heading(2) => {
127                html! {<h2  style={style} onclick={on_click} class={classes}>{inside}</h2>}
128            }
129            HtmlElement::Heading(3) => {
130                html! {<h3  style={style} onclick={on_click} class={classes}>{inside}</h3>}
131            }
132            HtmlElement::Heading(4) => {
133                html! {<h4  style={style} onclick={on_click} class={classes}>{inside}</h4>}
134            }
135            HtmlElement::Heading(5) => {
136                html! {<h5  style={style} onclick={on_click} class={classes}>{inside}</h5>}
137            }
138            HtmlElement::Heading(6) => {
139                html! {<h6  style={style} onclick={on_click} class={classes}>{inside}</h6>}
140            }
141            HtmlElement::Heading(_) => panic!(),
142            HtmlElement::Table => {
143                html! {<table  style={style} onclick={on_click} class={classes}>{inside}</table>}
144            }
145            HtmlElement::Thead => {
146                html! {<thead  style={style} onclick={on_click} class={classes}>{inside}</thead>}
147            }
148            HtmlElement::Trow => {
149                html! {<tr  style={style} onclick={on_click} class={classes}>{inside}</tr>}
150            }
151            HtmlElement::Tcell => {
152                html! {<td  style={style} onclick={on_click} class={classes}>{inside}</td>}
153            }
154            HtmlElement::Italics => {
155                html! {<i  style={style} onclick={on_click} class={classes}>{inside}</i>}
156            }
157            HtmlElement::Bold => {
158                html! {<b  style={style} onclick={on_click} class={classes}>{inside}</b>}
159            }
160            HtmlElement::StrikeThrough => {
161                html! {<s  style={style} onclick={on_click} class={classes}>{inside}</s>}
162            }
163            HtmlElement::Pre => {
164                html! {<pre  style={style} onclick={on_click} class={classes}>{inside}</pre>}
165            }
166            HtmlElement::Code => {
167                html! {<code  style={style} onclick={on_click} class={classes}>{inside}</code>}
168            }
169        }
170    }
171
172    fn el_span_with_inner_html(
173        self,
174        inner_html: String,
175        attributes: ElementAttributes<Callback<MouseEvent>>,
176    ) -> Self::View {
177        let style = attributes.style.map(|x| x.to_string());
178        let classes: Vec<_> = attributes.classes.iter().map(|x| x.to_string()).collect();
179        let onclick = attributes.on_click;
180
181        html! {
182            <span style={style} onclick={onclick} class={classes}>
183                {Html::from_html_unchecked(inner_html.into())}
184            </span>
185        }
186    }
187
188    fn el_hr(self, attributes: ElementAttributes<Callback<MouseEvent>>) -> Self::View {
189        let style = attributes.style.map(|x| x.to_string());
190        let classes: Vec<_> = attributes.classes.iter().map(|x| x.to_string()).collect();
191        let on_click = attributes.on_click;
192        html! {<hr  style={style} onclick={on_click} class={classes}/>}
193    }
194
195    fn el_br(self) -> Self::View {
196        html! {<br/>}
197    }
198
199    fn el_fragment(self, children: Vec<Self::View>) -> Self::View {
200        children.into_iter().collect()
201    }
202
203    fn el_a(self, children: Self::View, href: String) -> Self::View {
204        html! {<a href={href.to_string()}>{children}</a>}
205    }
206
207    fn el_img(self, src: String, alt: String) -> Self::View {
208        html! {<img src={src} alt={alt}/>}
209    }
210
211    fn el_text(self, text: CowStr<'a>) -> Self::View {
212        html! {text}
213    }
214
215    fn mount_dynamic_link(self, rel: &str, href: &str, integrity: &str, crossorigin: &str) {
216        let document = window().unwrap().document().unwrap();
217
218        let link = document.create_element("link").unwrap();
219
220        link.set_attribute("rel", rel).unwrap();
221        link.set_attribute("href", href).unwrap();
222        link.set_attribute("integrity", integrity).unwrap();
223        link.set_attribute("crossorigin", crossorigin).unwrap();
224
225        document.head().unwrap().append_child(&link).unwrap();
226    }
227
228    fn el_input_checkbox(
229        self,
230        checked: bool,
231        attributes: ElementAttributes<Callback<MouseEvent>>,
232    ) -> Self::View {
233        let style = attributes.style.map(|x| x.to_string());
234        let classes: Vec<_> = attributes.classes.iter().map(|x| x.to_string()).collect();
235        let on_click = attributes.on_click;
236        html! {
237            <input type="checkbox" checked={checked}
238                onclick={on_click}
239                class={classes}
240                style={style}
241            />
242        }
243    }
244
245    fn call_handler<T: 'static>(callback: &Self::Handler<T>, input: T) {
246        callback.emit(input)
247    }
248
249    fn make_md_handler(
250        self,
251        position: Range<usize>,
252        stop_propagation: bool,
253    ) -> Self::Handler<MouseEvent> {
254        match &self.onclick {
255            Some(f) => {
256                let f = f.clone();
257                Callback::from(move |e: MouseEvent| {
258                    if stop_propagation {
259                        e.stop_propagation()
260                    }
261                    let report = MarkdownMouseEvent {
262                        mouse_event: e,
263                        position: position.clone(),
264                    };
265                    f.emit(report)
266                })
267            }
268            None => Callback::noop(),
269        }
270    }
271
272    fn has_custom_links(self) -> bool {
273        self.render_links.is_some()
274    }
275
276    fn render_links(self, link: LinkDescription<Html>) -> Result<Html, String> {
277        let f = self.render_links.clone().unwrap();
278        Ok(f.emit(link))
279    }
280
281    fn set_frontmatter(&mut self, frontmatter: String) {
282        if let Some(setter) = &self.frontmatter {
283            setter.set(frontmatter)
284        }
285    }
286
287    fn has_custom_component(self, name: &str) -> bool {
288        self.components.0.get(name).is_some()
289    }
290
291    fn render_custom_component(
292        self,
293        name: &str,
294        input: MdComponentProps,
295    ) -> Result<Self::View, ComponentCreationError> {
296        let f = self.components.0.get(name).unwrap();
297        f.emit(input)
298    }
299}
300
301#[derive(PartialEq, Properties, Clone)]
302pub struct Props {
303    pub src: AttrValue,
304
305    #[prop_or_default]
306    pub onclick: Option<Callback<MarkdownMouseEvent, ()>>,
307
308    #[prop_or_default]
309    pub render_links: Option<Callback<LinkDescription<Html>, Html>>,
310
311    #[prop_or_default]
312    pub theme: Option<&'static str>,
313
314    #[prop_or(false)]
315    pub wikilinks: bool,
316
317    #[prop_or(false)]
318    pub hard_line_breaks: bool,
319
320    #[prop_or_default]
321    pub parse_options: Option<Options>,
322
323    #[prop_or_default]
324    pub components: CustomComponents,
325
326    #[prop_or_default]
327    pub frontmatter: Option<UseStateHandle<String>>,
328
329    #[prop_or_default]
330    pub send_debug_info: Option<Callback<Vec<String>>>,
331}
332
333#[function_component]
334pub fn Markdown(props: &Props) -> Html {
335    render_markdown(props, &props.src)
336}