1use std::{
2 borrow::Cow,
3 mem,
4};
5
6#[cfg(feature = "remote-asset")]
7use freya_components::Url;
8#[cfg(feature = "remote-asset")]
9use freya_components::image_viewer::ImageViewer;
10#[cfg(feature = "router")]
11use freya_components::link::{
12 Link,
13 LinkTooltip,
14};
15use freya_components::{
16 define_theme,
17 get_theme_or_default,
18 table::{
19 Table,
20 TableBody,
21 TableCell,
22 TableHead,
23 TableRow,
24 },
25 theming::macros::Preference,
26};
27use freya_core::{
28 elements::rect::Rect,
29 prelude::*,
30};
31use pulldown_cmark::{
32 Event,
33 HeadingLevel,
34 Options,
35 Parser,
36 Tag,
37 TagEnd,
38};
39use torin::prelude::*;
40
41#[cfg(feature = "code-editor")]
42mod code_editor;
43#[cfg(feature = "code-editor")]
44use code_editor::CodeBlockEditor;
45
46define_theme! {
47 %[component]
48 pub MarkdownViewer {
49 %[fields]
50 color: Color,
51 color_link: Color,
52 background_code: Color,
53 color_code: Color,
54 background_blockquote: Color,
55 border_blockquote: Color,
56 background_divider: Color,
57 heading_h1: f32,
58 heading_h2: f32,
59 heading_h3: f32,
60 heading_h4: f32,
61 heading_h5: f32,
62 heading_h6: f32,
63 paragraph_size: f32,
64 code_font_size: f32,
65 table_font_size: f32,
66 }
67}
68
69fn markdown_theme_preference() -> MarkdownViewerThemePreference {
70 MarkdownViewerThemePreference {
71 color: Preference::Reference("text_primary"),
72 color_link: Preference::Reference("text_highlight"),
73 background_code: Preference::Reference("surface_tertiary"),
74 color_code: Preference::Reference("text_primary"),
75 background_blockquote: Preference::Reference("surface_tertiary"),
76 border_blockquote: Preference::Reference("surface_primary"),
77 background_divider: Preference::Reference("border"),
78 heading_h1: Preference::Specific(32.0),
79 heading_h2: Preference::Specific(28.0),
80 heading_h3: Preference::Specific(24.0),
81 heading_h4: Preference::Specific(20.0),
82 heading_h5: Preference::Specific(18.0),
83 heading_h6: Preference::Specific(16.0),
84 paragraph_size: Preference::Specific(16.0),
85 code_font_size: Preference::Specific(14.0),
86 table_font_size: Preference::Specific(14.0),
87 }
88}
89
90#[derive(PartialEq)]
118pub struct MarkdownViewer {
119 content: Cow<'static, str>,
120 layout: LayoutData,
121 key: DiffKey,
122 pub(crate) theme: Option<MarkdownViewerThemePartial>,
123 inline_element: Option<Callback<String, Option<Element>>>,
124 code_editor_font_family: Cow<'static, str>,
125 #[cfg(feature = "code-editor")]
126 language_resolver: Option<code_editor::LanguageResolver>,
127}
128
129impl MarkdownViewer {
130 pub fn new(content: impl Into<Cow<'static, str>>) -> Self {
131 Self {
132 content: content.into(),
133 layout: LayoutData::default(),
134 key: DiffKey::None,
135 theme: None,
136 inline_element: None,
137 code_editor_font_family: Cow::Borrowed("Jetbrains Mono"),
138 #[cfg(feature = "code-editor")]
139 language_resolver: None,
140 }
141 }
142
143 pub fn inline_element<ReturnedElement: IntoElement + 'static>(
156 mut self,
157 handler: impl Into<Callback<String, Option<ReturnedElement>>>,
158 ) -> Self {
159 let handler = handler.into();
160 self.inline_element = Some(Callback::new(move |html| {
161 handler.call(html).map(IntoElement::into_element)
162 }));
163 self
164 }
165
166 pub fn code_editor_font_family(mut self, font_family: impl Into<Cow<'static, str>>) -> Self {
168 self.code_editor_font_family = font_family.into();
169 self
170 }
171
172 #[cfg(feature = "code-editor")]
174 pub fn code_editor_language(
175 mut self,
176 resolver: impl Into<code_editor::LanguageResolver>,
177 ) -> Self {
178 self.language_resolver = Some(resolver.into());
179 self
180 }
181}
182
183impl KeyExt for MarkdownViewer {
184 fn write_key(&mut self) -> &mut DiffKey {
185 &mut self.key
186 }
187}
188
189impl LayoutExt for MarkdownViewer {
190 fn get_layout(&mut self) -> &mut LayoutData {
191 &mut self.layout
192 }
193}
194
195impl ContainerExt for MarkdownViewer {}
196
197#[allow(dead_code)]
198#[derive(Clone)]
199enum MarkdownElement {
200 Heading {
201 level: HeadingLevel,
202 spans: Vec<TextSpan>,
203 },
204 Paragraph {
205 content: Vec<Inline>,
206 },
207 CodeBlock {
208 code: String,
209 language: Option<String>,
210 },
211 List(List),
212 Image {
213 url: String,
214 alt: String,
215 },
216 Link {
217 url: String,
218 title: Option<String>,
219 content: Vec<Inline>,
220 },
221 Blockquote {
222 content: Vec<Inline>,
223 },
224 Table {
225 headers: Vec<Vec<TextSpan>>,
226 rows: Vec<Vec<Vec<TextSpan>>>,
227 },
228 HorizontalRule,
229}
230
231#[derive(Clone)]
233struct List {
234 start: Option<u64>,
235 items: Vec<ListItem>,
236}
237
238#[derive(Clone)]
240struct ListItem {
241 content: Vec<Inline>,
242 nested_lists: Vec<List>,
243}
244
245#[derive(Clone)]
247enum Inline {
248 Span(TextSpan),
249 Image {
250 url: String,
251 alt: String,
252 },
253 #[cfg_attr(not(feature = "router"), allow(dead_code))]
254 Link {
255 url: String,
256 title: Option<String>,
257 content: Vec<Inline>,
258 },
259 Html(String),
261}
262
263#[derive(Clone, Debug)]
265struct TextSpan {
266 text: String,
267 bold: bool,
268 italic: bool,
269 #[allow(dead_code)]
270 strikethrough: bool,
271 code: bool,
272}
273
274impl TextSpan {
275 fn new(text: impl Into<String>) -> Self {
276 Self {
277 text: text.into(),
278 bold: false,
279 italic: false,
280 strikethrough: false,
281 code: false,
282 }
283 }
284}
285
286fn parse_markdown(content: &str) -> Vec<MarkdownElement> {
287 let mut options = Options::empty();
288 options.insert(Options::ENABLE_STRIKETHROUGH);
289 options.insert(Options::ENABLE_TABLES);
290
291 let parser = Parser::new_ext(content, options);
292 let mut elements = Vec::new();
293 let mut current_spans: Vec<TextSpan> = Vec::new();
294 let mut current_content: Vec<Inline> = Vec::new();
295 let mut list_stack: Vec<List> = Vec::new();
296 let mut item_stack: Vec<ListItem> = Vec::new();
297
298 let mut in_heading: Option<HeadingLevel> = None;
299 let mut in_paragraph = false;
300 let mut in_code_block = false;
301 let mut code_block_content = String::new();
302 let mut code_block_language: Option<String> = None;
303 let mut in_blockquote = false;
304 let mut blockquote_content: Vec<Inline> = Vec::new();
305
306 let mut in_table_cell = false;
307 let mut table_headers: Vec<Vec<TextSpan>> = Vec::new();
308 let mut table_rows: Vec<Vec<Vec<TextSpan>>> = Vec::new();
309 let mut current_table_row: Vec<Vec<TextSpan>> = Vec::new();
310 let mut current_cell_spans: Vec<TextSpan> = Vec::new();
311
312 let mut in_link = false;
313 let mut link_url: Option<String> = None;
314 let mut link_title: Option<String> = None;
315 let mut link_content: Vec<Inline> = Vec::new();
316
317 let mut in_image = false;
318 let mut image_url = String::new();
319 let mut image_title = String::new();
320 let mut image_alt = String::new();
321
322 let mut bold = false;
323 let mut italic = false;
324 let mut strikethrough = false;
325
326 for event in parser {
327 match event {
328 Event::Start(tag) => match tag {
329 Tag::Heading { level, .. } => {
330 in_heading = Some(level);
331 current_spans.clear();
332 }
333 Tag::Paragraph => {
334 if in_blockquote {
335 } else if !item_stack.is_empty() {
337 } else {
339 in_paragraph = true;
340 current_spans.clear();
341 current_content.clear();
342 }
343 }
344 Tag::CodeBlock(kind) => {
345 in_code_block = true;
346 code_block_content.clear();
347 code_block_language = match kind {
348 pulldown_cmark::CodeBlockKind::Fenced(lang) => {
349 let lang_str = lang.to_string();
350 if lang_str.is_empty() {
351 None
352 } else {
353 Some(lang_str)
354 }
355 }
356 pulldown_cmark::CodeBlockKind::Indented => None,
357 };
358 }
359 Tag::List(start) => {
360 list_stack.push(List {
361 start,
362 items: Vec::new(),
363 });
364 }
365 Tag::Item => {
366 item_stack.push(ListItem {
367 content: Vec::new(),
368 nested_lists: Vec::new(),
369 });
370 }
371 Tag::Strong => bold = true,
372 Tag::Emphasis => italic = true,
373 Tag::Strikethrough => strikethrough = true,
374 Tag::BlockQuote(_) => {
375 in_blockquote = true;
376 blockquote_content.clear();
377 }
378 Tag::Image {
379 dest_url, title, ..
380 } => {
381 in_image = true;
382 image_url = dest_url.to_string();
383 image_title = title.to_string();
384 image_alt.clear();
385 }
386 Tag::Link {
387 dest_url, title, ..
388 } => {
389 in_link = true;
390 link_url = Some(dest_url.to_string());
391 link_title = Some(title.to_string());
392 link_content.clear();
393 }
394 Tag::Table(_) => {
395 table_headers.clear();
396 table_rows.clear();
397 current_table_row.clear();
398 }
399 Tag::TableHead => {}
400 Tag::TableRow => {
401 current_table_row.clear();
402 }
403 Tag::TableCell => {
404 in_table_cell = true;
405 current_cell_spans.clear();
406 }
407 _ => {}
408 },
409 Event::End(tag_end) => match tag_end {
410 TagEnd::Heading(_) => {
411 if let Some(level) = in_heading.take() {
412 elements.push(MarkdownElement::Heading {
413 level,
414 spans: mem::take(&mut current_spans),
415 });
416 }
417 }
418 TagEnd::Paragraph => {
419 if in_blockquote {
420 blockquote_content.extend(current_spans.drain(..).map(Inline::Span))
421 } else if let Some(item) = item_stack.last_mut() {
422 item.content
423 .extend(current_spans.drain(..).map(Inline::Span))
424 } else if in_paragraph {
425 in_paragraph = false;
426 current_content.extend(current_spans.drain(..).map(Inline::Span));
427 elements.push(MarkdownElement::Paragraph {
428 content: mem::take(&mut current_content),
429 });
430 }
431 }
432 TagEnd::CodeBlock => {
433 in_code_block = false;
434 elements.push(MarkdownElement::CodeBlock {
435 code: mem::take(&mut code_block_content),
436 language: code_block_language.take(),
437 });
438 }
439 TagEnd::List(_) => {
440 if let Some(list) = list_stack.pop() {
441 if let Some(item) = item_stack.last_mut() {
442 item.nested_lists.push(list);
443 } else {
444 elements.push(MarkdownElement::List(list));
445 }
446 }
447 }
448 TagEnd::Item => {
449 if let (Some(item), Some(list)) = (item_stack.pop(), list_stack.last_mut()) {
450 list.items.push(item);
451 }
452 }
453 TagEnd::Strong => bold = false,
454 TagEnd::Emphasis => italic = false,
455 TagEnd::Strikethrough => strikethrough = false,
456 TagEnd::BlockQuote(_) => {
457 in_blockquote = false;
458 elements.push(MarkdownElement::Blockquote {
459 content: mem::take(&mut blockquote_content),
460 });
461 }
462 TagEnd::Table => {
463 elements.push(MarkdownElement::Table {
464 headers: mem::take(&mut table_headers),
465 rows: mem::take(&mut table_rows),
466 });
467 }
468 TagEnd::TableHead => {
469 table_headers = mem::take(&mut current_table_row);
471 }
472 TagEnd::TableRow => {
473 table_rows.push(mem::take(&mut current_table_row));
475 }
476 TagEnd::TableCell => {
477 in_table_cell = false;
478 current_table_row.push(mem::take(&mut current_cell_spans));
479 }
480 TagEnd::Image => {
481 in_image = false;
482 let url = mem::take(&mut image_url);
483 let alt = if image_alt.is_empty() {
484 mem::take(&mut image_title)
485 } else {
486 mem::take(&mut image_alt)
487 };
488 if in_link {
489 link_content.push(Inline::Image { url, alt });
490 } else if in_blockquote {
491 blockquote_content.push(Inline::Image { url, alt });
492 } else if let Some(item) = item_stack.last_mut() {
493 item.content.push(Inline::Image { url, alt });
494 } else if in_paragraph {
495 current_content.extend(current_spans.drain(..).map(Inline::Span));
496 current_content.push(Inline::Image { url, alt });
497 } else {
498 elements.push(MarkdownElement::Image { url, alt });
499 }
500 }
501 TagEnd::Link => {
502 in_link = false;
503 if let Some(url) = link_url.take() {
504 let title = link_title.take();
505 let content = mem::take(&mut link_content);
506 if in_blockquote {
507 blockquote_content.push(Inline::Link {
508 url,
509 title,
510 content,
511 });
512 } else if let Some(item) = item_stack.last_mut() {
513 item.content.push(Inline::Link {
514 url,
515 title,
516 content,
517 });
518 } else if in_paragraph {
519 current_content.extend(current_spans.drain(..).map(Inline::Span));
520 current_content.push(Inline::Link {
521 url,
522 title,
523 content,
524 });
525 } else {
526 elements.push(MarkdownElement::Link {
527 url,
528 title,
529 content,
530 });
531 }
532 }
533 }
534 _ => {}
535 },
536 Event::Text(text) => {
537 if in_code_block {
538 code_block_content.push_str(text.trim());
539 } else if in_image {
540 image_alt.push_str(&text);
541 } else if in_table_cell {
542 let span = TextSpan {
543 text: text.to_string(),
544 bold,
545 italic,
546 strikethrough,
547 code: false,
548 };
549 current_cell_spans.push(span);
550 } else {
551 let span = TextSpan {
552 text: text.to_string(),
553 bold,
554 italic,
555 strikethrough,
556 code: false,
557 };
558 if in_link {
559 link_content.push(Inline::Span(span));
560 } else if in_blockquote && !in_paragraph {
561 blockquote_content.push(Inline::Span(span));
562 } else if let Some(item) = item_stack.last_mut()
563 && !in_paragraph
564 {
565 item.content.push(Inline::Span(span));
566 } else {
567 current_spans.push(span);
568 }
569 }
570 }
571 Event::Code(code) => {
572 if in_image {
573 image_alt.push_str(&code);
574 continue;
575 }
576 let span = TextSpan {
577 text: code.to_string(),
578 bold,
579 italic,
580 strikethrough,
581 code: true,
582 };
583 if in_table_cell {
584 current_cell_spans.push(span);
585 } else if in_link {
586 link_content.push(Inline::Span(span));
587 } else if in_blockquote {
588 blockquote_content.push(Inline::Span(span));
589 } else if let Some(item) = item_stack.last_mut() {
590 item.content.push(Inline::Span(span));
591 } else {
592 current_spans.push(span);
593 }
594 }
595 Event::SoftBreak | Event::HardBreak => {
596 if in_image {
597 image_alt.push(' ');
598 continue;
599 }
600 let span = TextSpan::new(" ");
601 if in_link {
602 link_content.push(Inline::Span(span));
603 } else if in_blockquote {
604 blockquote_content.push(Inline::Span(span));
605 } else if let Some(item) = item_stack.last_mut() {
606 item.content.push(Inline::Span(span));
607 } else {
608 current_spans.push(span);
609 }
610 }
611 Event::InlineHtml(html) => {
612 if in_paragraph && !in_link {
613 current_content.extend(current_spans.drain(..).map(Inline::Span));
614 current_content.push(Inline::Html(html.to_string()));
615 }
616 }
617 Event::Rule => {
618 elements.push(MarkdownElement::HorizontalRule);
619 }
620 _ => {}
621 }
622 }
623
624 elements
625}
626
627fn styled_span(span: &TextSpan, text_color: Color, code_color: Color) -> Span<'static> {
629 let mut styled = Span::new(span.text.clone());
630 if span.bold {
631 styled = styled.font_weight(FontWeight::BOLD);
632 }
633 if span.italic {
634 styled = styled.font_slant(FontSlant::Italic);
635 }
636 if span.code {
637 styled.font_family("monospace").color(code_color)
638 } else {
639 styled.color(text_color)
640 }
641}
642
643fn render_spans(
645 spans: &[TextSpan],
646 base_font_size: f32,
647 text_color: Color,
648 code_color: Color,
649) -> Paragraph {
650 paragraph().font_size(base_font_size).spans_iter(
651 spans
652 .iter()
653 .map(|span| styled_span(span, text_color, code_color)),
654 )
655}
656
657fn render_list(
659 list: &List,
660 paragraph_size: f32,
661 color: Color,
662 color_link: Color,
663 color_code: Color,
664 inline_element: Option<&Callback<String, Option<Element>>>,
665) -> Rect {
666 rect()
667 .vertical()
668 .spacing(4.)
669 .padding(Gaps::new(0., 0., 0., 20.))
670 .children(list.items.iter().enumerate().map(|(item_idx, item)| {
671 rect()
672 .key(item_idx)
673 .horizontal()
674 .cross_align(Alignment::Start)
675 .spacing(8.)
676 .child(
677 label()
678 .text(match list.start {
679 Some(start) => format!("{}.", start + item_idx as u64),
680 None => "•".to_string(),
681 })
682 .font_size(paragraph_size)
683 .color(color),
684 )
685 .child(
686 rect()
687 .vertical()
688 .spacing(4.)
689 .child(render_content(
690 &item.content,
691 paragraph_size,
692 color,
693 color_link,
694 color_code,
695 inline_element,
696 ))
697 .children(item.nested_lists.iter().map(|nested_list| {
698 render_list(
699 nested_list,
700 paragraph_size,
701 color,
702 color_link,
703 color_code,
704 inline_element,
705 )
706 .into()
707 })),
708 )
709 .into()
710 }))
711}
712
713#[cfg(feature = "remote-asset")]
715fn render_image(url: &str, alt: &str, text_color: Color) -> Element {
716 match url.parse::<Url>() {
717 Ok(uri) => ImageViewer::new(uri)
718 .a11y_alt(alt)
719 .aspect_ratio(AspectRatio::Fit)
720 .into(),
721 Err(_) => label()
722 .text(format!("[Invalid image URL: {}]", url))
723 .color(text_color)
724 .into(),
725 }
726}
727
728#[cfg(not(feature = "remote-asset"))]
730fn render_image(_url: &str, alt: &str, text_color: Color) -> Element {
731 label()
732 .text(format!("[Image: {}]", alt))
733 .color(text_color)
734 .into()
735}
736
737fn render_content(
740 content: &[Inline],
741 base_font_size: f32,
742 text_color: Color,
743 link_color: Color,
744 code_color: Color,
745 inline_element: Option<&Callback<String, Option<Element>>>,
746) -> Paragraph {
747 let mut result = paragraph().font_size(base_font_size);
748 for item in content {
749 result = match item {
750 Inline::Span(span) => result.span(styled_span(span, text_color, code_color)),
751 Inline::Image { url, alt } => result.child(render_image(url, alt, text_color)),
752 Inline::Html(raw) => {
753 match inline_element.and_then(|handler| handler.call(raw.clone())) {
754 Some(element) => result.child(element),
755 None => result.span(Span::new(raw.clone()).color(text_color)),
756 }
757 }
758 #[cfg(feature = "router")]
759 Inline::Link {
760 url,
761 title,
762 content,
763 } => {
764 let mut tooltip = LinkTooltip::Default;
765 if let Some(title) = title
766 && !title.is_empty()
767 {
768 tooltip = LinkTooltip::Custom(title.clone());
769 }
770 result.child(
771 Link::new(url.clone())
772 .tooltip(tooltip)
773 .child(render_content(
774 content,
775 base_font_size,
776 link_color,
777 link_color,
778 code_color,
779 inline_element,
780 )),
781 )
782 }
783 #[cfg(not(feature = "router"))]
784 Inline::Link { content, .. } => {
785 content.iter().fold(result, |paragraph, item| match item {
786 Inline::Span(span) => paragraph.span(styled_span(span, link_color, code_color)),
787 Inline::Image { url, alt } => {
788 paragraph.child(render_image(url, alt, text_color))
789 }
790 _ => paragraph,
791 })
792 }
793 };
794 }
795 result
796}
797
798impl Component for MarkdownViewer {
799 fn render(&self) -> impl IntoElement {
800 let elements = parse_markdown(&self.content);
801
802 let MarkdownViewerTheme {
803 color,
804 color_link,
805 #[cfg(not(feature = "code-editor"))]
806 background_code,
807 #[cfg(feature = "code-editor")]
808 background_code: _,
809 color_code,
810 background_blockquote,
811 border_blockquote,
812 background_divider,
813 heading_h1,
814 heading_h2,
815 heading_h3,
816 heading_h4,
817 heading_h5,
818 heading_h6,
819 paragraph_size,
820 code_font_size,
821 table_font_size,
822 } = get_theme_or_default!(
823 &self.theme,
824 MarkdownViewerThemePreference,
825 "markdown_viewer",
826 markdown_theme_preference
827 );
828
829 let mut container = rect().vertical().layout(self.layout.clone()).spacing(12.);
830
831 for (idx, element) in elements.into_iter().enumerate() {
832 let child: Element = match element {
833 MarkdownElement::Heading { level, spans } => {
834 let font_size = match level {
835 HeadingLevel::H1 => heading_h1,
836 HeadingLevel::H2 => heading_h2,
837 HeadingLevel::H3 => heading_h3,
838 HeadingLevel::H4 => heading_h4,
839 HeadingLevel::H5 => heading_h5,
840 HeadingLevel::H6 => heading_h6,
841 };
842 render_spans(&spans, font_size, color, color_code)
843 .font_weight(FontWeight::BOLD)
844 .key(idx)
845 .into()
846 }
847 MarkdownElement::Paragraph { content } => render_content(
848 &content,
849 paragraph_size,
850 color,
851 color_link,
852 color_code,
853 self.inline_element.as_ref(),
854 )
855 .key(idx)
856 .into(),
857 MarkdownElement::CodeBlock {
858 code,
859 #[cfg(feature = "code-editor")]
860 language,
861 #[cfg(not(feature = "code-editor"))]
862 language: _,
863 } => {
864 #[cfg(feature = "code-editor")]
865 let element = CodeBlockEditor::new(
866 move || Cow::Owned(code.clone()),
867 language,
868 self.language_resolver.clone(),
869 code_font_size,
870 self.code_editor_font_family.clone(),
871 )
872 .key(idx)
873 .into();
874
875 #[cfg(not(feature = "code-editor"))]
876 let element = rect()
877 .key(idx)
878 .width(Size::fill())
879 .background(background_code)
880 .corner_radius(6.)
881 .padding(Gaps::new_all(12.))
882 .child(
883 label()
884 .text(code)
885 .font_family(self.code_editor_font_family.clone())
886 .font_size(code_font_size)
887 .color(color_code),
888 )
889 .into();
890
891 element
892 }
893 MarkdownElement::List(list) => render_list(
894 &list,
895 paragraph_size,
896 color,
897 color_link,
898 color_code,
899 self.inline_element.as_ref(),
900 )
901 .key(idx)
902 .into(),
903 MarkdownElement::Image { url, alt } => rect()
904 .key(idx)
905 .child(render_image(&url, &alt, color))
906 .into(),
907 #[cfg(feature = "router")]
908 MarkdownElement::Link {
909 url,
910 title,
911 content,
912 } => {
913 let mut tooltip = LinkTooltip::Default;
914 if let Some(title) = title
915 && !title.is_empty()
916 {
917 tooltip = LinkTooltip::Custom(title);
918 }
919
920 Link::new(url)
921 .tooltip(tooltip)
922 .child(render_content(
923 &content,
924 paragraph_size,
925 color_link,
926 color_link,
927 color_code,
928 self.inline_element.as_ref(),
929 ))
930 .key(idx)
931 .into()
932 }
933 #[cfg(not(feature = "router"))]
934 MarkdownElement::Link { content, .. } => render_content(
935 &content,
936 paragraph_size,
937 color,
938 color_link,
939 color_code,
940 self.inline_element.as_ref(),
941 )
942 .key(idx)
943 .into(),
944 MarkdownElement::Blockquote { content } => rect()
945 .key(idx)
946 .width(Size::fill())
947 .padding(Gaps::new(12., 12., 12., 16.))
948 .border(
949 Border::new()
950 .width(4.)
951 .fill(border_blockquote)
952 .alignment(BorderAlignment::Inner),
953 )
954 .background(background_blockquote)
955 .child(
956 render_content(
957 &content,
958 paragraph_size,
959 color,
960 color_link,
961 color_code,
962 self.inline_element.as_ref(),
963 )
964 .font_slant(FontSlant::Italic),
965 )
966 .into(),
967 MarkdownElement::HorizontalRule => rect()
968 .key(idx)
969 .width(Size::fill())
970 .height(Size::px(1.))
971 .background(background_divider)
972 .into(),
973 MarkdownElement::Table { headers, rows } => {
974 let mut head = TableHead::new();
975 let mut header_row = TableRow::new();
976 for (col_idx, header_spans) in headers.into_iter().enumerate() {
977 header_row = header_row.child(
978 TableCell::new().key(col_idx).child(
979 render_spans(&header_spans, table_font_size, color, color_code)
980 .font_weight(FontWeight::BOLD),
981 ),
982 );
983 }
984 head = head.child(header_row);
985
986 let mut body = TableBody::new();
987 for (row_idx, row) in rows.into_iter().enumerate() {
988 let mut table_row = TableRow::new().key(row_idx);
989 for (col_idx, cell_spans) in row.into_iter().enumerate() {
990 table_row = table_row.child(TableCell::new().key(col_idx).child(
991 render_spans(&cell_spans, table_font_size, color, color_code),
992 ));
993 }
994 body = body.child(table_row);
995 }
996
997 Table::new().key(idx).child(head).child(body).into()
998 }
999 };
1000
1001 container = container.child(child);
1002 }
1003
1004 container
1005 }
1006
1007 fn render_key(&self) -> DiffKey {
1008 self.key.clone().or(self.default_key())
1009 }
1010}