dioxus_markdown/
lib.rs

1use web_framework_markdown::{render_markdown, CowStr, MarkdownProps};
2
3use std::collections::BTreeMap;
4
5pub type MdComponentProps = web_framework_markdown::MdComponentProps<Element>;
6
7use core::ops::Range;
8
9pub use web_framework_markdown::{
10    ComponentCreationError, Context, ElementAttributes, HtmlElement, LinkDescription, Options,
11};
12
13use dioxus::prelude::*;
14
15pub type HtmlCallback<T> = Callback<T, Element>;
16
17#[cfg(feature = "debug")]
18pub mod debug {
19    use dioxus::signals::{GlobalMemo, GlobalSignal, Signal};
20
21    pub(crate) static DEBUG_INFO_SOURCE: GlobalSignal<Vec<String>> = Signal::global(|| Vec::new());
22    pub static DEBUG_INFO: GlobalMemo<Vec<String>> = Signal::global_memo(|| DEBUG_INFO_SOURCE());
23}
24
25#[derive(Clone, PartialEq, Default, Props)]
26pub struct MdProps {
27    src: ReadOnlySignal<String>,
28
29    /// the callback called when a component is clicked.
30    /// if you want to controll what happens when a link is clicked,
31    /// use [`render_links`][render_links]
32    on_click: Option<EventHandler<MarkdownMouseEvent>>,
33
34    ///
35    render_links: Option<HtmlCallback<LinkDescription<Element>>>,
36
37    /// the name of the theme used for syntax highlighting.
38    /// Only the default themes of [syntect::Theme] are supported
39    theme: Option<&'static str>,
40
41    /// wether to enable wikilinks support.
42    /// Wikilinks look like [[shortcut link]] or [[url|name]]
43    #[props(default = false)]
44    wikilinks: bool,
45
46    /// wether to convert soft breaks to hard breaks.
47    #[props(default = false)]
48    hard_line_breaks: bool,
49
50    /// pulldown_cmark options.
51    /// See [`Options`][pulldown_cmark_wikilink::Options] for reference.
52    parse_options: Option<Options>,
53
54    #[props(default)]
55    components: ReadOnlySignal<CustomComponents>,
56
57    frontmatter: Option<Signal<String>>,
58}
59
60#[derive(Clone, Debug)]
61pub struct MarkdownMouseEvent {
62    /// the original mouse event triggered when a text element was clicked on
63    pub mouse_event: MouseEvent,
64
65    /// the corresponding range in the markdown source, as a slice of [`u8`][u8]
66    pub position: Range<usize>,
67    // TODO: add a clonable tag for the type of the element
68    // pub tag: pulldown_cmark::Tag<'a>,
69}
70
71#[derive(Clone, Copy)]
72pub struct MdContext(ReadOnlySignal<MdProps>);
73
74/// component store.
75/// It is called when therer is a `<CustomComponent>` inside the markdown source.
76/// It is basically a hashmap but more efficient for a small number of items
77#[derive(Default)]
78pub struct CustomComponents(
79    BTreeMap<String, Callback<MdComponentProps, Result<Element, ComponentCreationError>>>,
80);
81
82impl CustomComponents {
83    pub fn new() -> Self {
84        Self(Default::default())
85    }
86
87    /// register a new component.
88    /// The function `component` takes a context and props of type `MdComponentProps`
89    /// and returns html
90    pub fn register<F>(&mut self, name: &'static str, component: F)
91    where
92        F: Fn(MdComponentProps) -> Result<Element, ComponentCreationError> + 'static,
93    {
94        self.0.insert(name.to_string(), Callback::new(component));
95    }
96
97    pub fn get_callback(
98        &self,
99        name: &str,
100    ) -> Option<&Callback<MdComponentProps, Result<Element, ComponentCreationError>>> {
101        self.0.get(name)
102    }
103}
104
105impl<'src> Context<'src, 'static> for MdContext {
106    type View = Element;
107
108    type Handler<T: 'static> = EventHandler<T>;
109
110    type MouseEvent = MouseEvent;
111
112    #[cfg(feature = "debug")]
113    fn send_debug_info(self, info: Vec<String>) {
114        *debug::DEBUG_INFO_SOURCE.write() = info;
115    }
116
117    fn el_with_attributes(
118        self,
119        e: HtmlElement,
120        inside: Self::View,
121        attributes: ElementAttributes<EventHandler<MouseEvent>>,
122    ) -> Self::View {
123        let class = attributes.classes.join(" ");
124        let style = attributes.style.unwrap_or_default();
125        let onclick = attributes.on_click.unwrap_or_default();
126        let onclick = move |e| onclick.call(e);
127
128        match e {
129            HtmlElement::Div => {
130                rsx! {div {onclick:onclick, style: "{style}", class: "{class}", {inside}} }
131            }
132            HtmlElement::Span => {
133                rsx! {span {onclick: onclick, style: "{style}", class: "{class}", {inside} } }
134            }
135            HtmlElement::Paragraph => {
136                rsx! {p {onclick: onclick, style: "{style}", class: "{class}", {inside} } }
137            }
138            HtmlElement::BlockQuote => {
139                rsx! {blockquote {onclick: onclick, style: "{style}", class: "{class}", {inside} } }
140            }
141            HtmlElement::Ul => {
142                rsx! {ul {onclick: onclick, style: "{style}", class: "{class}", {inside} } }
143            }
144            HtmlElement::Ol(x) => {
145                rsx! {ol {onclick: onclick, style: "{style}", class: "{class}", start: x as i64, {inside} } }
146            }
147            HtmlElement::Li => {
148                rsx! {li {onclick: onclick, style: "{style}", class: "{class}", {inside} } }
149            }
150            HtmlElement::Heading(1) => {
151                rsx! {h1 {onclick: onclick, style: "{style}", class: "{class}", {inside} } }
152            }
153            HtmlElement::Heading(2) => {
154                rsx! {h2 {onclick: onclick, style: "{style}", class: "{class}", {inside} } }
155            }
156            HtmlElement::Heading(3) => {
157                rsx! {h3 {onclick: onclick, style: "{style}", class: "{class}", {inside} } }
158            }
159            HtmlElement::Heading(4) => {
160                rsx! {h4 {onclick: onclick, style: "{style}", class: "{class}", {inside} } }
161            }
162            HtmlElement::Heading(5) => {
163                rsx! {h5 {onclick: onclick, style: "{style}", class: "{class}", {inside} } }
164            }
165            HtmlElement::Heading(6) => {
166                rsx! {h6 {onclick: onclick, style: "{style}", class: "{class}", {inside} } }
167            }
168            HtmlElement::Heading(_) => panic!(),
169            HtmlElement::Table => {
170                rsx! {table {onclick: onclick, style: "{style}", class: "{class}", {inside} } }
171            }
172            HtmlElement::Thead => {
173                rsx! {thead {onclick: onclick, style: "{style}", class: "{class}", {inside} } }
174            }
175            HtmlElement::Trow => {
176                rsx! {tr {onclick: onclick, style: "{style}", class: "{class}", {inside} } }
177            }
178            HtmlElement::Tcell => {
179                rsx! {td {onclick: onclick, style: "{style}", class: "{class}", {inside} } }
180            }
181            HtmlElement::Italics => {
182                rsx! {i {onclick: onclick, style: "{style}", class: "{class}", {inside} } }
183            }
184            HtmlElement::Bold => {
185                rsx! {b {onclick: onclick, style: "{style}", class: "{class}", {inside} } }
186            }
187            HtmlElement::StrikeThrough => {
188                rsx! {s {onclick: onclick, style: "{style}", class: "{class}", {inside} } }
189            }
190            HtmlElement::Pre => {
191                rsx! {p {onclick: onclick, style: "{style}", class: "{class}", {inside} } }
192            }
193            HtmlElement::Code => {
194                rsx! {code {onclick: onclick, style: "{style}", class: "{class}", {inside} } }
195            }
196        }
197    }
198
199    fn el_span_with_inner_html(
200        self,
201        inner_html: String,
202        attributes: ElementAttributes<EventHandler<MouseEvent>>,
203    ) -> Self::View {
204        let class = attributes.classes.join(" ");
205        let style = attributes.style.unwrap_or_default();
206        let onclick = move |e| {
207            if let Some(f) = &attributes.on_click {
208                f.call(e)
209            }
210        };
211        rsx! {
212            span {
213                dangerous_inner_html: "{inner_html}",
214                style: "{style}",
215                class: "{class}",
216                onclick: onclick
217            }
218        }
219    }
220
221    fn el_hr(self, attributes: ElementAttributes<EventHandler<MouseEvent>>) -> Self::View {
222        let class = attributes.classes.join(" ");
223        let style = attributes.style.unwrap_or_default();
224        let onclick = move |e| {
225            if let Some(f) = &attributes.on_click {
226                f.call(e)
227            }
228        };
229        rsx!(hr {
230            onclick: onclick,
231            style: "{style}",
232            class: "{class}"
233        })
234    }
235
236    fn el_br(self) -> Self::View {
237        rsx!(br {})
238    }
239
240    fn el_fragment(self, children: Vec<Self::View>) -> Self::View {
241        rsx! {
242            {children.into_iter()}
243        }
244    }
245
246    fn el_a(self, children: Self::View, href: String) -> Self::View {
247        rsx! {a {
248            href: "{href}",
249            {children}
250        }}
251    }
252
253    fn el_img(self, src: String, alt: String) -> Self::View {
254        rsx!(img {
255            src: "{src}",
256            alt: "{alt}"
257        })
258    }
259
260    fn el_text<'a>(self, text: CowStr<'a>) -> Self::View {
261        rsx! {
262            {text.as_ref()}
263        }
264    }
265
266    fn mount_dynamic_link(self, _rel: &str, _href: &str, _integrity: &str, _crossorigin: &str) {
267        // let create_eval = use_eval(self.0);
268
269        // let eval = create_eval(
270        //     r#"
271        //     // https://stackoverflow.com/a/18510577
272        //     let rel = await dioxus.recv();
273        //     let href = await dioxus.recv();
274        //     let integrity = await dioxus.recv();
275        //     let crossorigin = await dioxus.recv();
276        //     var newstyle = document.createElement("link"); // Create a new link Tag
277
278        //     newstyle.setAttribute("rel", rel);
279        //     newstyle.setAttribute("type", "text/css");
280        //     newstyle.setAttribute("href", href);
281        //     newstyle.setAttribute("crossorigin", crossorigin);
282        //     newstyle.setAttribute("integrity", integrity);
283        //     document.getElementsByTagName("head")[0].appendChild(newstyle);
284        //     "#,
285        // )
286        // .unwrap();
287
288        // // You can send messages to JavaScript with the send method
289        // eval.send(rel.into()).unwrap();
290        // eval.send(href.into()).unwrap();
291        // eval.send(integrity.into()).unwrap();
292        // eval.send(crossorigin.into()).unwrap();
293    }
294
295    fn el_input_checkbox(
296        self,
297        checked: bool,
298        attributes: ElementAttributes<EventHandler<MouseEvent>>,
299    ) -> Self::View {
300        let class = attributes.classes.join(" ");
301        let style = attributes.style.unwrap_or_default();
302        let onclick = move |e| {
303            if let Some(f) = &attributes.on_click {
304                f.call(e)
305            }
306        };
307        rsx!(input {
308            r#type: "checkbox",
309            checked: checked,
310            style: "{style}",
311            class: "{class}",
312            onclick: onclick
313        })
314    }
315
316    fn props(self) -> MarkdownProps {
317        let props = self.0();
318
319        MarkdownProps {
320            hard_line_breaks: props.hard_line_breaks,
321            wikilinks: props.wikilinks,
322            parse_options: props.parse_options,
323            theme: props.theme,
324        }
325    }
326
327    fn call_handler<T: 'static>(callback: &Self::Handler<T>, input: T) {
328        callback.call(input)
329    }
330
331    fn make_md_handler(
332        self,
333        position: std::ops::Range<usize>,
334        stop_propagation: bool,
335    ) -> Self::Handler<MouseEvent> {
336        let on_click = self.0().on_click.as_ref().cloned();
337
338        EventHandler::new(move |e: MouseEvent| {
339            if stop_propagation {
340                e.stop_propagation()
341            }
342
343            let report = MarkdownMouseEvent {
344                position: position.clone(),
345                mouse_event: e,
346            };
347
348            on_click.map(|x| x.call(report));
349        })
350    }
351
352    fn set_frontmatter(&mut self, frontmatter: String) {
353        self.0().frontmatter.as_mut().map(|x| x.set(frontmatter));
354    }
355
356    fn has_custom_links(self) -> bool {
357        self.0().render_links.is_some()
358    }
359
360    fn render_links(self, link: LinkDescription<Self::View>) -> Result<Self::View, String> {
361        // TODO: remove the unwrap call
362        Ok(self.0().render_links.as_ref().unwrap()(link))
363    }
364
365    fn has_custom_component(self, name: &str) -> bool {
366        self.0().components.read().get_callback(name).is_some()
367    }
368
369    fn render_custom_component(
370        self,
371        name: &str,
372        input: MdComponentProps,
373    ) -> Result<Self::View, ComponentCreationError> {
374        let f: Callback<_, _> = self.0()
375            .components
376            .read()
377            .get_callback(name)
378            .unwrap()
379            .clone();
380        f(input)
381    }
382}
383
384#[allow(non_snake_case)]
385pub fn Markdown(props: MdProps) -> Element {
386    let src: String = props.src.to_string();
387    let signal: Signal<MdProps> = Signal::new(props);
388    render_markdown(MdContext(signal.into()), &src)
389}