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 pub mouse_event: MouseEvent,
23
24 pub position: Range<usize>,
26 }
29
30#[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 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}