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 on_click: Option<EventHandler<MarkdownMouseEvent>>,
33
34 render_links: Option<HtmlCallback<LinkDescription<Element>>>,
36
37 theme: Option<&'static str>,
40
41 #[props(default = false)]
44 wikilinks: bool,
45
46 #[props(default = false)]
48 hard_line_breaks: bool,
49
50 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 pub mouse_event: MouseEvent,
64
65 pub position: Range<usize>,
67 }
70
71#[derive(Clone, Copy)]
72pub struct MdContext(ReadOnlySignal<MdProps>);
73
74#[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 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 }
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 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}