1use crate::core::border;
47use crate::core::font::{self, Font};
48use crate::core::padding;
49use crate::core::theme;
50use crate::core::{
51 self, Color, Element, Length, Padding, Pixels, Theme, color,
52};
53use crate::{column, container, rich_text, row, scrollable, span, text};
54
55use std::borrow::BorrowMut;
56use std::cell::{Cell, RefCell};
57use std::collections::{HashMap, HashSet};
58use std::mem;
59use std::ops::Range;
60use std::rc::Rc;
61use std::sync::Arc;
62
63pub use core::text::Highlight;
64pub use pulldown_cmark::HeadingLevel;
65pub use url::Url;
66
67#[derive(Debug, Default)]
69pub struct Content {
70 items: Vec<Item>,
71 incomplete: HashMap<usize, Section>,
72 state: State,
73}
74
75#[derive(Debug)]
76struct Section {
77 content: String,
78 broken_links: HashSet<String>,
79}
80
81impl Content {
82 pub fn new() -> Self {
84 Self::default()
85 }
86
87 pub fn parse(markdown: &str) -> Self {
89 let mut content = Self::new();
90 content.push_str(markdown);
91 content
92 }
93
94 pub fn push_str(&mut self, markdown: &str) {
99 if markdown.is_empty() {
100 return;
101 }
102
103 let mut leftover = std::mem::take(&mut self.state.leftover);
105 leftover.push_str(markdown);
106
107 let _ = self.items.pop();
109
110 for (item, source, broken_links) in
112 parse_with(&mut self.state, &leftover)
113 {
114 if !broken_links.is_empty() {
115 let _ = self.incomplete.insert(
116 self.items.len(),
117 Section {
118 content: source.to_owned(),
119 broken_links,
120 },
121 );
122 }
123
124 self.items.push(item);
125 }
126
127 if !self.incomplete.is_empty() {
129 self.incomplete.retain(|index, section| {
130 if self.items.len() <= *index {
131 return false;
132 }
133
134 let broken_links_before = section.broken_links.len();
135
136 section
137 .broken_links
138 .retain(|link| !self.state.references.contains_key(link));
139
140 if broken_links_before != section.broken_links.len() {
141 let mut state = State {
142 leftover: String::new(),
143 references: self.state.references.clone(),
144 images: HashSet::new(),
145 #[cfg(feature = "highlighter")]
146 highlighter: None,
147 };
148
149 if let Some((item, _source, _broken_links)) =
150 parse_with(&mut state, §ion.content).next()
151 {
152 self.items[*index] = item;
153 }
154
155 self.state.images.extend(state.images.drain());
156 drop(state);
157 }
158
159 !section.broken_links.is_empty()
160 });
161 }
162 }
163
164 pub fn items(&self) -> &[Item] {
168 &self.items
169 }
170
171 pub fn images(&self) -> &HashSet<Url> {
173 &self.state.images
174 }
175}
176
177#[derive(Debug, Clone)]
179pub enum Item {
180 Heading(pulldown_cmark::HeadingLevel, Text),
182 Paragraph(Text),
184 CodeBlock {
188 language: Option<String>,
190 code: String,
192 lines: Vec<Text>,
194 },
195 List {
197 start: Option<u64>,
199 items: Vec<Vec<Item>>,
201 },
202 Image {
204 url: Url,
206 title: String,
208 alt: Text,
210 },
211}
212
213#[derive(Debug, Clone)]
215pub struct Text {
216 spans: Vec<Span>,
217 last_style: Cell<Option<Style>>,
218 last_styled_spans: RefCell<Arc<[text::Span<'static, Url>]>>,
219}
220
221impl Text {
222 fn new(spans: Vec<Span>) -> Self {
223 Self {
224 spans,
225 last_style: Cell::default(),
226 last_styled_spans: RefCell::default(),
227 }
228 }
229
230 pub fn spans(&self, style: Style) -> Arc<[text::Span<'static, Url>]> {
235 if Some(style) != self.last_style.get() {
236 *self.last_styled_spans.borrow_mut() =
237 self.spans.iter().map(|span| span.view(&style)).collect();
238
239 self.last_style.set(Some(style));
240 }
241
242 self.last_styled_spans.borrow().clone()
243 }
244}
245
246#[derive(Debug, Clone)]
247enum Span {
248 Standard {
249 text: String,
250 strikethrough: bool,
251 link: Option<Url>,
252 strong: bool,
253 emphasis: bool,
254 code: bool,
255 },
256 #[cfg(feature = "highlighter")]
257 Highlight {
258 text: String,
259 color: Option<Color>,
260 font: Option<Font>,
261 },
262}
263
264impl Span {
265 fn view(&self, style: &Style) -> text::Span<'static, Url> {
266 match self {
267 Span::Standard {
268 text,
269 strikethrough,
270 link,
271 strong,
272 emphasis,
273 code,
274 } => {
275 let span = span(text.clone()).strikethrough(*strikethrough);
276
277 let span = if *code {
278 span.font(Font::MONOSPACE)
279 .color(style.inline_code_color)
280 .background(style.inline_code_highlight.background)
281 .border(style.inline_code_highlight.border)
282 .padding(style.inline_code_padding)
283 } else if *strong || *emphasis {
284 span.font(Font {
285 weight: if *strong {
286 font::Weight::Bold
287 } else {
288 font::Weight::Normal
289 },
290 style: if *emphasis {
291 font::Style::Italic
292 } else {
293 font::Style::Normal
294 },
295 ..Font::default()
296 })
297 } else {
298 span
299 };
300
301 if let Some(link) = link.as_ref() {
302 span.color(style.link_color).link(link.clone())
303 } else {
304 span
305 }
306 }
307 #[cfg(feature = "highlighter")]
308 Span::Highlight { text, color, font } => {
309 span(text.clone()).color_maybe(*color).font_maybe(*font)
310 }
311 }
312 }
313}
314
315pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
356 parse_with(State::default(), markdown)
357 .map(|(item, _source, _broken_links)| item)
358}
359
360#[derive(Debug, Default)]
361struct State {
362 leftover: String,
363 references: HashMap<String, String>,
364 images: HashSet<Url>,
365 #[cfg(feature = "highlighter")]
366 highlighter: Option<Highlighter>,
367}
368
369#[cfg(feature = "highlighter")]
370#[derive(Debug)]
371struct Highlighter {
372 lines: Vec<(String, Vec<Span>)>,
373 language: String,
374 parser: iced_highlighter::Stream,
375 current: usize,
376}
377
378#[cfg(feature = "highlighter")]
379impl Highlighter {
380 pub fn new(language: &str) -> Self {
381 Self {
382 lines: Vec::new(),
383 parser: iced_highlighter::Stream::new(
384 &iced_highlighter::Settings {
385 theme: iced_highlighter::Theme::Base16Ocean,
386 token: language.to_owned(),
387 },
388 ),
389 language: language.to_owned(),
390 current: 0,
391 }
392 }
393
394 pub fn prepare(&mut self) {
395 self.current = 0;
396 }
397
398 pub fn highlight_line(&mut self, text: &str) -> &[Span] {
399 match self.lines.get(self.current) {
400 Some(line) if line.0 == text => {}
401 _ => {
402 if self.current + 1 < self.lines.len() {
403 log::debug!("Resetting highlighter...");
404 self.parser.reset();
405 self.lines.truncate(self.current);
406
407 for line in &self.lines {
408 log::debug!(
409 "Refeeding {n} lines",
410 n = self.lines.len()
411 );
412
413 let _ = self.parser.highlight_line(&line.0);
414 }
415 }
416
417 log::trace!("Parsing: {text}", text = text.trim_end());
418
419 if self.current + 1 < self.lines.len() {
420 self.parser.commit();
421 }
422
423 let mut spans = Vec::new();
424
425 for (range, highlight) in self.parser.highlight_line(text) {
426 spans.push(Span::Highlight {
427 text: text[range].to_owned(),
428 color: highlight.color(),
429 font: highlight.font(),
430 });
431 }
432
433 if self.current + 1 == self.lines.len() {
434 let _ = self.lines.pop();
435 }
436
437 self.lines.push((text.to_owned(), spans));
438 }
439 }
440
441 self.current += 1;
442
443 &self
444 .lines
445 .get(self.current - 1)
446 .expect("Line must be parsed")
447 .1
448 }
449}
450
451fn parse_with<'a>(
452 mut state: impl BorrowMut<State> + 'a,
453 markdown: &'a str,
454) -> impl Iterator<Item = (Item, &'a str, HashSet<String>)> + 'a {
455 enum Scope {
456 List(List),
457 }
458
459 struct List {
460 start: Option<u64>,
461 items: Vec<Vec<Item>>,
462 }
463
464 let broken_links = Rc::new(RefCell::new(HashSet::new()));
465
466 let mut spans = Vec::new();
467 let mut code = String::new();
468 let mut code_language = None;
469 let mut code_lines = Vec::new();
470 let mut strong = false;
471 let mut emphasis = false;
472 let mut strikethrough = false;
473 let mut metadata = false;
474 let mut table = false;
475 let mut code_block = false;
476 let mut link = None;
477 let mut image = None;
478 let mut stack = Vec::new();
479
480 #[cfg(feature = "highlighter")]
481 let mut highlighter = None;
482
483 let parser = pulldown_cmark::Parser::new_with_broken_link_callback(
484 markdown,
485 pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS
486 | pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS
487 | pulldown_cmark::Options::ENABLE_TABLES
488 | pulldown_cmark::Options::ENABLE_STRIKETHROUGH,
489 {
490 let references = state.borrow().references.clone();
491 let broken_links = broken_links.clone();
492
493 Some(move |broken_link: pulldown_cmark::BrokenLink<'_>| {
494 if let Some(reference) =
495 references.get(broken_link.reference.as_ref())
496 {
497 Some((
498 pulldown_cmark::CowStr::from(reference.to_owned()),
499 broken_link.reference.into_static(),
500 ))
501 } else {
502 let _ = RefCell::borrow_mut(&broken_links)
503 .insert(broken_link.reference.into_string());
504
505 None
506 }
507 })
508 },
509 );
510
511 let references = &mut state.borrow_mut().references;
512
513 for reference in parser.reference_definitions().iter() {
514 let _ = references
515 .insert(reference.0.to_owned(), reference.1.dest.to_string());
516 }
517
518 let produce = move |state: &mut State,
519 stack: &mut Vec<Scope>,
520 item,
521 source: Range<usize>| {
522 if let Some(scope) = stack.last_mut() {
523 match scope {
524 Scope::List(list) => {
525 list.items.last_mut().expect("item context").push(item);
526 }
527 }
528
529 None
530 } else {
531 state.leftover = markdown[source.start..].to_owned();
532
533 Some((
534 item,
535 &markdown[source.start..source.end],
536 broken_links.take(),
537 ))
538 }
539 };
540
541 let parser = parser.into_offset_iter();
542
543 #[allow(clippy::drain_collect)]
545 parser.filter_map(move |(event, source)| match event {
546 pulldown_cmark::Event::Start(tag) => match tag {
547 pulldown_cmark::Tag::Strong if !metadata && !table => {
548 strong = true;
549 None
550 }
551 pulldown_cmark::Tag::Emphasis if !metadata && !table => {
552 emphasis = true;
553 None
554 }
555 pulldown_cmark::Tag::Strikethrough if !metadata && !table => {
556 strikethrough = true;
557 None
558 }
559 pulldown_cmark::Tag::Link { dest_url, .. }
560 if !metadata && !table =>
561 {
562 match Url::parse(&dest_url) {
563 Ok(url)
564 if url.scheme() == "http"
565 || url.scheme() == "https" =>
566 {
567 link = Some(url);
568 }
569 _ => {}
570 }
571
572 None
573 }
574 pulldown_cmark::Tag::Image {
575 dest_url, title, ..
576 } if !metadata && !table => {
577 image = Url::parse(&dest_url)
578 .ok()
579 .map(|url| (url, title.into_string()));
580 None
581 }
582 pulldown_cmark::Tag::List(first_item) if !metadata && !table => {
583 let prev = if spans.is_empty() {
584 None
585 } else {
586 produce(
587 state.borrow_mut(),
588 &mut stack,
589 Item::Paragraph(Text::new(spans.drain(..).collect())),
590 source,
591 )
592 };
593
594 stack.push(Scope::List(List {
595 start: first_item,
596 items: Vec::new(),
597 }));
598
599 prev
600 }
601 pulldown_cmark::Tag::Item => {
602 if let Some(Scope::List(list)) = stack.last_mut() {
603 list.items.push(Vec::new());
604 }
605
606 None
607 }
608 pulldown_cmark::Tag::CodeBlock(
609 pulldown_cmark::CodeBlockKind::Fenced(language),
610 ) if !metadata && !table => {
611 #[cfg(feature = "highlighter")]
612 {
613 highlighter = Some({
614 let mut highlighter = state
615 .borrow_mut()
616 .highlighter
617 .take()
618 .filter(|highlighter| {
619 highlighter.language == language.as_ref()
620 })
621 .unwrap_or_else(|| Highlighter::new(&language));
622
623 highlighter.prepare();
624
625 highlighter
626 });
627 }
628
629 code_block = true;
630 code_language =
631 (!language.is_empty()).then(|| language.into_string());
632
633 if spans.is_empty() {
634 None
635 } else {
636 produce(
637 state.borrow_mut(),
638 &mut stack,
639 Item::Paragraph(Text::new(spans.drain(..).collect())),
640 source,
641 )
642 }
643 }
644 pulldown_cmark::Tag::MetadataBlock(_) => {
645 metadata = true;
646 None
647 }
648 pulldown_cmark::Tag::Table(_) => {
649 table = true;
650 None
651 }
652 _ => None,
653 },
654 pulldown_cmark::Event::End(tag) => match tag {
655 pulldown_cmark::TagEnd::Heading(level) if !metadata && !table => {
656 produce(
657 state.borrow_mut(),
658 &mut stack,
659 Item::Heading(level, Text::new(spans.drain(..).collect())),
660 source,
661 )
662 }
663 pulldown_cmark::TagEnd::Strong if !metadata && !table => {
664 strong = false;
665 None
666 }
667 pulldown_cmark::TagEnd::Emphasis if !metadata && !table => {
668 emphasis = false;
669 None
670 }
671 pulldown_cmark::TagEnd::Strikethrough if !metadata && !table => {
672 strikethrough = false;
673 None
674 }
675 pulldown_cmark::TagEnd::Link if !metadata && !table => {
676 link = None;
677 None
678 }
679 pulldown_cmark::TagEnd::Paragraph if !metadata && !table => {
680 if spans.is_empty() {
681 None
682 } else {
683 produce(
684 state.borrow_mut(),
685 &mut stack,
686 Item::Paragraph(Text::new(spans.drain(..).collect())),
687 source,
688 )
689 }
690 }
691 pulldown_cmark::TagEnd::Item if !metadata && !table => {
692 if spans.is_empty() {
693 None
694 } else {
695 produce(
696 state.borrow_mut(),
697 &mut stack,
698 Item::Paragraph(Text::new(spans.drain(..).collect())),
699 source,
700 )
701 }
702 }
703 pulldown_cmark::TagEnd::List(_) if !metadata && !table => {
704 let scope = stack.pop()?;
705
706 let Scope::List(list) = scope;
707
708 produce(
709 state.borrow_mut(),
710 &mut stack,
711 Item::List {
712 start: list.start,
713 items: list.items,
714 },
715 source,
716 )
717 }
718 pulldown_cmark::TagEnd::Image if !metadata && !table => {
719 let (url, title) = image.take()?;
720 let alt = Text::new(spans.drain(..).collect());
721
722 let state = state.borrow_mut();
723 let _ = state.images.insert(url.clone());
724
725 produce(
726 state,
727 &mut stack,
728 Item::Image { url, title, alt },
729 source,
730 )
731 }
732 pulldown_cmark::TagEnd::CodeBlock if !metadata && !table => {
733 code_block = false;
734
735 #[cfg(feature = "highlighter")]
736 {
737 state.borrow_mut().highlighter = highlighter.take();
738 }
739
740 produce(
741 state.borrow_mut(),
742 &mut stack,
743 Item::CodeBlock {
744 language: code_language.take(),
745 code: mem::take(&mut code),
746 lines: code_lines.drain(..).collect(),
747 },
748 source,
749 )
750 }
751 pulldown_cmark::TagEnd::MetadataBlock(_) => {
752 metadata = false;
753 None
754 }
755 pulldown_cmark::TagEnd::Table => {
756 table = false;
757 None
758 }
759 _ => None,
760 },
761 pulldown_cmark::Event::Text(text) if !metadata && !table => {
762 if code_block {
763 code.push_str(&text);
764
765 #[cfg(feature = "highlighter")]
766 if let Some(highlighter) = &mut highlighter {
767 for line in text.lines() {
768 code_lines.push(Text::new(
769 highlighter.highlight_line(line).to_vec(),
770 ));
771 }
772 }
773
774 #[cfg(not(feature = "highlighter"))]
775 for line in text.lines() {
776 code_lines.push(Text::new(vec![Span::Standard {
777 text: line.to_owned(),
778 strong,
779 emphasis,
780 strikethrough,
781 link: link.clone(),
782 code: false,
783 }]));
784 }
785
786 return None;
787 }
788
789 let span = Span::Standard {
790 text: text.into_string(),
791 strong,
792 emphasis,
793 strikethrough,
794 link: link.clone(),
795 code: false,
796 };
797
798 spans.push(span);
799
800 None
801 }
802 pulldown_cmark::Event::Code(code) if !metadata && !table => {
803 let span = Span::Standard {
804 text: code.into_string(),
805 strong,
806 emphasis,
807 strikethrough,
808 link: link.clone(),
809 code: true,
810 };
811
812 spans.push(span);
813 None
814 }
815 pulldown_cmark::Event::SoftBreak if !metadata && !table => {
816 spans.push(Span::Standard {
817 text: String::from(" "),
818 strikethrough,
819 strong,
820 emphasis,
821 link: link.clone(),
822 code: false,
823 });
824 None
825 }
826 pulldown_cmark::Event::HardBreak if !metadata && !table => {
827 spans.push(Span::Standard {
828 text: String::from("\n"),
829 strikethrough,
830 strong,
831 emphasis,
832 link: link.clone(),
833 code: false,
834 });
835 None
836 }
837 _ => None,
838 })
839}
840
841#[derive(Debug, Clone, Copy)]
843pub struct Settings {
844 pub text_size: Pixels,
846 pub h1_size: Pixels,
848 pub h2_size: Pixels,
850 pub h3_size: Pixels,
852 pub h4_size: Pixels,
854 pub h5_size: Pixels,
856 pub h6_size: Pixels,
858 pub code_size: Pixels,
860 pub spacing: Pixels,
862 pub style: Style,
864}
865
866impl Settings {
867 pub fn with_style(style: impl Into<Style>) -> Self {
869 Self::with_text_size(16, style)
870 }
871
872 pub fn with_text_size(
878 text_size: impl Into<Pixels>,
879 style: impl Into<Style>,
880 ) -> Self {
881 let text_size = text_size.into();
882
883 Self {
884 text_size,
885 h1_size: text_size * 2.0,
886 h2_size: text_size * 1.75,
887 h3_size: text_size * 1.5,
888 h4_size: text_size * 1.25,
889 h5_size: text_size,
890 h6_size: text_size,
891 code_size: text_size * 0.75,
892 spacing: text_size * 0.875,
893 style: style.into(),
894 }
895 }
896}
897
898impl From<&Theme> for Settings {
899 fn from(theme: &Theme) -> Self {
900 Self::with_style(Style::from(theme))
901 }
902}
903
904impl From<Theme> for Settings {
905 fn from(theme: Theme) -> Self {
906 Self::with_style(Style::from(theme))
907 }
908}
909
910#[derive(Debug, Clone, Copy, PartialEq)]
912pub struct Style {
913 pub inline_code_highlight: Highlight,
915 pub inline_code_padding: Padding,
917 pub inline_code_color: Color,
919 pub link_color: Color,
921}
922
923impl Style {
924 pub fn from_palette(palette: theme::Palette) -> Self {
926 Self {
927 inline_code_padding: padding::left(1).right(1),
928 inline_code_highlight: Highlight {
929 background: color!(0x111).into(),
930 border: border::rounded(2),
931 },
932 inline_code_color: Color::WHITE,
933 link_color: palette.primary,
934 }
935 }
936}
937
938impl From<theme::Palette> for Style {
939 fn from(palette: theme::Palette) -> Self {
940 Self::from_palette(palette)
941 }
942}
943
944impl From<&Theme> for Style {
945 fn from(theme: &Theme) -> Self {
946 Self::from_palette(theme.palette())
947 }
948}
949
950impl From<Theme> for Style {
951 fn from(theme: Theme) -> Self {
952 Self::from_palette(theme.palette())
953 }
954}
955
956pub fn view<'a, Theme, Renderer>(
999 items: impl IntoIterator<Item = &'a Item>,
1000 settings: impl Into<Settings>,
1001) -> Element<'a, Url, Theme, Renderer>
1002where
1003 Theme: Catalog + 'a,
1004 Renderer: core::text::Renderer<Font = Font> + 'a,
1005{
1006 view_with(items, settings, &DefaultViewer)
1007}
1008
1009pub fn view_with<'a, Message, Theme, Renderer>(
1015 items: impl IntoIterator<Item = &'a Item>,
1016 settings: impl Into<Settings>,
1017 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1018) -> Element<'a, Message, Theme, Renderer>
1019where
1020 Message: 'a,
1021 Theme: Catalog + 'a,
1022 Renderer: core::text::Renderer<Font = Font> + 'a,
1023{
1024 let settings = settings.into();
1025
1026 let blocks = items
1027 .into_iter()
1028 .enumerate()
1029 .map(|(i, item_)| item(viewer, settings, item_, i));
1030
1031 Element::new(column(blocks).spacing(settings.spacing))
1032}
1033
1034pub fn item<'a, Message, Theme, Renderer>(
1036 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1037 settings: Settings,
1038 item: &'a Item,
1039 index: usize,
1040) -> Element<'a, Message, Theme, Renderer>
1041where
1042 Message: 'a,
1043 Theme: Catalog + 'a,
1044 Renderer: core::text::Renderer<Font = Font> + 'a,
1045{
1046 match item {
1047 Item::Image { url, title, alt } => {
1048 viewer.image(settings, url, title, alt)
1049 }
1050 Item::Heading(level, text) => {
1051 viewer.heading(settings, level, text, index)
1052 }
1053 Item::Paragraph(text) => viewer.paragraph(settings, text),
1054 Item::CodeBlock {
1055 language,
1056 code,
1057 lines,
1058 } => viewer.code_block(settings, language.as_deref(), code, lines),
1059 Item::List { start: None, items } => {
1060 viewer.unordered_list(settings, items)
1061 }
1062 Item::List {
1063 start: Some(start),
1064 items,
1065 } => viewer.ordered_list(settings, *start, items),
1066 }
1067}
1068
1069pub fn heading<'a, Message, Theme, Renderer>(
1071 settings: Settings,
1072 level: &'a HeadingLevel,
1073 text: &'a Text,
1074 index: usize,
1075 on_link_click: impl Fn(Url) -> Message + 'a,
1076) -> Element<'a, Message, Theme, Renderer>
1077where
1078 Message: 'a,
1079 Theme: Catalog + 'a,
1080 Renderer: core::text::Renderer<Font = Font> + 'a,
1081{
1082 let Settings {
1083 h1_size,
1084 h2_size,
1085 h3_size,
1086 h4_size,
1087 h5_size,
1088 h6_size,
1089 text_size,
1090 ..
1091 } = settings;
1092
1093 container(
1094 rich_text(text.spans(settings.style))
1095 .on_link_click(on_link_click)
1096 .size(match level {
1097 pulldown_cmark::HeadingLevel::H1 => h1_size,
1098 pulldown_cmark::HeadingLevel::H2 => h2_size,
1099 pulldown_cmark::HeadingLevel::H3 => h3_size,
1100 pulldown_cmark::HeadingLevel::H4 => h4_size,
1101 pulldown_cmark::HeadingLevel::H5 => h5_size,
1102 pulldown_cmark::HeadingLevel::H6 => h6_size,
1103 }),
1104 )
1105 .padding(padding::top(if index > 0 {
1106 text_size / 2.0
1107 } else {
1108 Pixels::ZERO
1109 }))
1110 .into()
1111}
1112
1113pub fn paragraph<'a, Message, Theme, Renderer>(
1115 settings: Settings,
1116 text: &Text,
1117 on_link_click: impl Fn(Url) -> Message + 'a,
1118) -> Element<'a, Message, Theme, Renderer>
1119where
1120 Message: 'a,
1121 Theme: Catalog + 'a,
1122 Renderer: core::text::Renderer<Font = Font> + 'a,
1123{
1124 rich_text(text.spans(settings.style))
1125 .size(settings.text_size)
1126 .on_link_click(on_link_click)
1127 .into()
1128}
1129
1130pub fn unordered_list<'a, Message, Theme, Renderer>(
1133 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1134 settings: Settings,
1135 items: &'a [Vec<Item>],
1136) -> Element<'a, Message, Theme, Renderer>
1137where
1138 Message: 'a,
1139 Theme: Catalog + 'a,
1140 Renderer: core::text::Renderer<Font = Font> + 'a,
1141{
1142 column(items.iter().map(|items| {
1143 row![
1144 text("•").size(settings.text_size),
1145 view_with(
1146 items,
1147 Settings {
1148 spacing: settings.spacing * 0.6,
1149 ..settings
1150 },
1151 viewer,
1152 )
1153 ]
1154 .spacing(settings.spacing)
1155 .into()
1156 }))
1157 .spacing(settings.spacing * 0.75)
1158 .padding([0.0, settings.spacing.0])
1159 .into()
1160}
1161
1162pub fn ordered_list<'a, Message, Theme, Renderer>(
1165 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1166 settings: Settings,
1167 start: u64,
1168 items: &'a [Vec<Item>],
1169) -> Element<'a, Message, Theme, Renderer>
1170where
1171 Message: 'a,
1172 Theme: Catalog + 'a,
1173 Renderer: core::text::Renderer<Font = Font> + 'a,
1174{
1175 column(items.iter().enumerate().map(|(i, items)| {
1176 row![
1177 text!("{}.", i as u64 + start).size(settings.text_size),
1178 view_with(
1179 items,
1180 Settings {
1181 spacing: settings.spacing * 0.6,
1182 ..settings
1183 },
1184 viewer,
1185 )
1186 ]
1187 .spacing(settings.spacing)
1188 .into()
1189 }))
1190 .spacing(settings.spacing * 0.75)
1191 .padding([0.0, settings.spacing.0])
1192 .into()
1193}
1194
1195pub fn code_block<'a, Message, Theme, Renderer>(
1197 settings: Settings,
1198 lines: &'a [Text],
1199 on_link_click: impl Fn(Url) -> Message + Clone + 'a,
1200) -> Element<'a, Message, Theme, Renderer>
1201where
1202 Message: 'a,
1203 Theme: Catalog + 'a,
1204 Renderer: core::text::Renderer<Font = Font> + 'a,
1205{
1206 container(
1207 scrollable(
1208 container(column(lines.iter().map(|line| {
1209 rich_text(line.spans(settings.style))
1210 .on_link_click(on_link_click.clone())
1211 .font(Font::MONOSPACE)
1212 .size(settings.code_size)
1213 .into()
1214 })))
1215 .padding(settings.code_size),
1216 )
1217 .direction(scrollable::Direction::Horizontal(
1218 scrollable::Scrollbar::default()
1219 .width(settings.code_size / 2)
1220 .scroller_width(settings.code_size / 2),
1221 )),
1222 )
1223 .width(Length::Fill)
1224 .padding(settings.code_size / 4)
1225 .class(Theme::code_block())
1226 .into()
1227}
1228
1229pub trait Viewer<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
1231where
1232 Self: Sized + 'a,
1233 Message: 'a,
1234 Theme: Catalog + 'a,
1235 Renderer: core::text::Renderer<Font = Font> + 'a,
1236{
1237 fn on_link_click(url: Url) -> Message;
1239
1240 fn image(
1244 &self,
1245 settings: Settings,
1246 url: &'a Url,
1247 title: &'a str,
1248 alt: &Text,
1249 ) -> Element<'a, Message, Theme, Renderer> {
1250 let _url = url;
1251 let _title = title;
1252
1253 container(
1254 rich_text(alt.spans(settings.style))
1255 .on_link_click(Self::on_link_click),
1256 )
1257 .padding(settings.spacing.0)
1258 .class(Theme::code_block())
1259 .into()
1260 }
1261
1262 fn heading(
1266 &self,
1267 settings: Settings,
1268 level: &'a HeadingLevel,
1269 text: &'a Text,
1270 index: usize,
1271 ) -> Element<'a, Message, Theme, Renderer> {
1272 heading(settings, level, text, index, Self::on_link_click)
1273 }
1274
1275 fn paragraph(
1279 &self,
1280 settings: Settings,
1281 text: &Text,
1282 ) -> Element<'a, Message, Theme, Renderer> {
1283 paragraph(settings, text, Self::on_link_click)
1284 }
1285
1286 fn code_block(
1290 &self,
1291 settings: Settings,
1292 language: Option<&'a str>,
1293 code: &'a str,
1294 lines: &'a [Text],
1295 ) -> Element<'a, Message, Theme, Renderer> {
1296 let _language = language;
1297 let _code = code;
1298
1299 code_block(settings, lines, Self::on_link_click)
1300 }
1301
1302 fn unordered_list(
1306 &self,
1307 settings: Settings,
1308 items: &'a [Vec<Item>],
1309 ) -> Element<'a, Message, Theme, Renderer> {
1310 unordered_list(self, settings, items)
1311 }
1312
1313 fn ordered_list(
1317 &self,
1318 settings: Settings,
1319 start: u64,
1320 items: &'a [Vec<Item>],
1321 ) -> Element<'a, Message, Theme, Renderer> {
1322 ordered_list(self, settings, start, items)
1323 }
1324}
1325
1326#[derive(Debug, Clone, Copy)]
1327struct DefaultViewer;
1328
1329impl<'a, Theme, Renderer> Viewer<'a, Url, Theme, Renderer> for DefaultViewer
1330where
1331 Theme: Catalog + 'a,
1332 Renderer: core::text::Renderer<Font = Font> + 'a,
1333{
1334 fn on_link_click(url: Url) -> Url {
1335 url
1336 }
1337}
1338
1339pub trait Catalog:
1341 container::Catalog + scrollable::Catalog + text::Catalog
1342{
1343 fn code_block<'a>() -> <Self as container::Catalog>::Class<'a>;
1345}
1346
1347impl Catalog for Theme {
1348 fn code_block<'a>() -> <Self as container::Catalog>::Class<'a> {
1349 Box::new(container::dark)
1350 }
1351}