1use crate::core::border;
47use crate::core::font::{self, Font};
48use crate::core::padding;
49use crate::core::theme;
50use crate::core::{
51 self, color, Color, Element, Length, Padding, Pixels, Theme,
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 let span = if let Some(link) = link.as_ref() {
302 span.color(style.link_color).link(link.clone())
303 } else {
304 span
305 };
306
307 span
308 }
309 #[cfg(feature = "highlighter")]
310 Span::Highlight { text, color, font } => {
311 span(text.clone()).color_maybe(*color).font_maybe(*font)
312 }
313 }
314 }
315}
316
317pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
358 parse_with(State::default(), markdown)
359 .map(|(item, _source, _broken_links)| item)
360}
361
362#[derive(Debug, Default)]
363struct State {
364 leftover: String,
365 references: HashMap<String, String>,
366 images: HashSet<Url>,
367 #[cfg(feature = "highlighter")]
368 highlighter: Option<Highlighter>,
369}
370
371#[cfg(feature = "highlighter")]
372#[derive(Debug)]
373struct Highlighter {
374 lines: Vec<(String, Vec<Span>)>,
375 language: String,
376 parser: iced_highlighter::Stream,
377 current: usize,
378}
379
380#[cfg(feature = "highlighter")]
381impl Highlighter {
382 pub fn new(language: &str) -> Self {
383 Self {
384 lines: Vec::new(),
385 parser: iced_highlighter::Stream::new(
386 &iced_highlighter::Settings {
387 theme: iced_highlighter::Theme::Base16Ocean,
388 token: language.to_owned(),
389 },
390 ),
391 language: language.to_owned(),
392 current: 0,
393 }
394 }
395
396 pub fn prepare(&mut self) {
397 self.current = 0;
398 }
399
400 pub fn highlight_line(&mut self, text: &str) -> &[Span] {
401 match self.lines.get(self.current) {
402 Some(line) if line.0 == text => {}
403 _ => {
404 if self.current + 1 < self.lines.len() {
405 log::debug!("Resetting highlighter...");
406 self.parser.reset();
407 self.lines.truncate(self.current);
408
409 for line in &self.lines {
410 log::debug!(
411 "Refeeding {n} lines",
412 n = self.lines.len()
413 );
414
415 let _ = self.parser.highlight_line(&line.0);
416 }
417 }
418
419 log::trace!("Parsing: {text}", text = text.trim_end());
420
421 if self.current + 1 < self.lines.len() {
422 self.parser.commit();
423 }
424
425 let mut spans = Vec::new();
426
427 for (range, highlight) in self.parser.highlight_line(text) {
428 spans.push(Span::Highlight {
429 text: text[range].to_owned(),
430 color: highlight.color(),
431 font: highlight.font(),
432 });
433 }
434
435 if self.current + 1 == self.lines.len() {
436 let _ = self.lines.pop();
437 }
438
439 self.lines.push((text.to_owned(), spans));
440 }
441 }
442
443 self.current += 1;
444
445 &self
446 .lines
447 .get(self.current - 1)
448 .expect("Line must be parsed")
449 .1
450 }
451}
452
453fn parse_with<'a>(
454 mut state: impl BorrowMut<State> + 'a,
455 markdown: &'a str,
456) -> impl Iterator<Item = (Item, &'a str, HashSet<String>)> + 'a {
457 enum Scope {
458 List(List),
459 }
460
461 struct List {
462 start: Option<u64>,
463 items: Vec<Vec<Item>>,
464 }
465
466 let broken_links = Rc::new(RefCell::new(HashSet::new()));
467
468 let mut spans = Vec::new();
469 let mut code = String::new();
470 let mut code_language = None;
471 let mut code_lines = Vec::new();
472 let mut strong = false;
473 let mut emphasis = false;
474 let mut strikethrough = false;
475 let mut metadata = false;
476 let mut table = false;
477 let mut link = None;
478 let mut image = None;
479 let mut stack = Vec::new();
480
481 #[cfg(feature = "highlighter")]
482 let mut highlighter = None;
483
484 let parser = pulldown_cmark::Parser::new_with_broken_link_callback(
485 markdown,
486 pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS
487 | pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS
488 | pulldown_cmark::Options::ENABLE_TABLES
489 | pulldown_cmark::Options::ENABLE_STRIKETHROUGH,
490 {
491 let references = state.borrow().references.clone();
492 let broken_links = broken_links.clone();
493
494 Some(move |broken_link: pulldown_cmark::BrokenLink<'_>| {
495 if let Some(reference) =
496 references.get(broken_link.reference.as_ref())
497 {
498 Some((
499 pulldown_cmark::CowStr::from(reference.to_owned()),
500 broken_link.reference.into_static(),
501 ))
502 } else {
503 let _ = RefCell::borrow_mut(&broken_links)
504 .insert(broken_link.reference.into_string());
505
506 None
507 }
508 })
509 },
510 );
511
512 let references = &mut state.borrow_mut().references;
513
514 for reference in parser.reference_definitions().iter() {
515 let _ = references
516 .insert(reference.0.to_owned(), reference.1.dest.to_string());
517 }
518
519 let produce = move |state: &mut State,
520 stack: &mut Vec<Scope>,
521 item,
522 source: Range<usize>| {
523 if let Some(scope) = stack.last_mut() {
524 match scope {
525 Scope::List(list) => {
526 list.items.last_mut().expect("item context").push(item);
527 }
528 }
529
530 None
531 } else {
532 state.leftover = markdown[source.start..].to_owned();
533
534 Some((
535 item,
536 &markdown[source.start..source.end],
537 broken_links.take(),
538 ))
539 }
540 };
541
542 let parser = parser.into_offset_iter();
543
544 #[allow(clippy::drain_collect)]
546 parser.filter_map(move |(event, source)| match event {
547 pulldown_cmark::Event::Start(tag) => match tag {
548 pulldown_cmark::Tag::Strong if !metadata && !table => {
549 strong = true;
550 None
551 }
552 pulldown_cmark::Tag::Emphasis if !metadata && !table => {
553 emphasis = true;
554 None
555 }
556 pulldown_cmark::Tag::Strikethrough if !metadata && !table => {
557 strikethrough = true;
558 None
559 }
560 pulldown_cmark::Tag::Link { dest_url, .. }
561 if !metadata && !table =>
562 {
563 match Url::parse(&dest_url) {
564 Ok(url)
565 if url.scheme() == "http"
566 || url.scheme() == "https" =>
567 {
568 link = Some(url);
569 }
570 _ => {}
571 }
572
573 None
574 }
575 pulldown_cmark::Tag::Image {
576 dest_url, title, ..
577 } if !metadata && !table => {
578 image = Url::parse(&dest_url)
579 .ok()
580 .map(|url| (url, title.into_string()));
581 None
582 }
583 pulldown_cmark::Tag::List(first_item) if !metadata && !table => {
584 let prev = if spans.is_empty() {
585 None
586 } else {
587 produce(
588 state.borrow_mut(),
589 &mut stack,
590 Item::Paragraph(Text::new(spans.drain(..).collect())),
591 source,
592 )
593 };
594
595 stack.push(Scope::List(List {
596 start: first_item,
597 items: Vec::new(),
598 }));
599
600 prev
601 }
602 pulldown_cmark::Tag::Item => {
603 if let Some(Scope::List(list)) = stack.last_mut() {
604 list.items.push(Vec::new());
605 }
606
607 None
608 }
609 pulldown_cmark::Tag::CodeBlock(
610 pulldown_cmark::CodeBlockKind::Fenced(language),
611 ) if !metadata && !table => {
612 #[cfg(feature = "highlighter")]
613 {
614 highlighter = Some({
615 let mut highlighter = state
616 .borrow_mut()
617 .highlighter
618 .take()
619 .filter(|highlighter| {
620 highlighter.language == language.as_ref()
621 })
622 .unwrap_or_else(|| Highlighter::new(&language));
623
624 highlighter.prepare();
625
626 highlighter
627 });
628 }
629
630 code_language =
631 (!language.is_empty()).then(|| language.into_string());
632
633 let prev = 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 prev
645 }
646 pulldown_cmark::Tag::MetadataBlock(_) => {
647 metadata = true;
648 None
649 }
650 pulldown_cmark::Tag::Table(_) => {
651 table = true;
652 None
653 }
654 _ => None,
655 },
656 pulldown_cmark::Event::End(tag) => match tag {
657 pulldown_cmark::TagEnd::Heading(level) if !metadata && !table => {
658 produce(
659 state.borrow_mut(),
660 &mut stack,
661 Item::Heading(level, Text::new(spans.drain(..).collect())),
662 source,
663 )
664 }
665 pulldown_cmark::TagEnd::Strong if !metadata && !table => {
666 strong = false;
667 None
668 }
669 pulldown_cmark::TagEnd::Emphasis if !metadata && !table => {
670 emphasis = false;
671 None
672 }
673 pulldown_cmark::TagEnd::Strikethrough if !metadata && !table => {
674 strikethrough = false;
675 None
676 }
677 pulldown_cmark::TagEnd::Link if !metadata && !table => {
678 link = None;
679 None
680 }
681 pulldown_cmark::TagEnd::Paragraph if !metadata && !table => {
682 if spans.is_empty() {
683 None
684 } else {
685 produce(
686 state.borrow_mut(),
687 &mut stack,
688 Item::Paragraph(Text::new(spans.drain(..).collect())),
689 source,
690 )
691 }
692 }
693 pulldown_cmark::TagEnd::Item if !metadata && !table => {
694 if spans.is_empty() {
695 None
696 } else {
697 produce(
698 state.borrow_mut(),
699 &mut stack,
700 Item::Paragraph(Text::new(spans.drain(..).collect())),
701 source,
702 )
703 }
704 }
705 pulldown_cmark::TagEnd::List(_) if !metadata && !table => {
706 let scope = stack.pop()?;
707
708 let Scope::List(list) = scope;
709
710 produce(
711 state.borrow_mut(),
712 &mut stack,
713 Item::List {
714 start: list.start,
715 items: list.items,
716 },
717 source,
718 )
719 }
720 pulldown_cmark::TagEnd::Image if !metadata && !table => {
721 let (url, title) = image.take()?;
722 let alt = Text::new(spans.drain(..).collect());
723
724 let state = state.borrow_mut();
725 let _ = state.images.insert(url.clone());
726
727 produce(
728 state,
729 &mut stack,
730 Item::Image { url, title, alt },
731 source,
732 )
733 }
734 pulldown_cmark::TagEnd::CodeBlock if !metadata && !table => {
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 #[cfg(feature = "highlighter")]
763 if let Some(highlighter) = &mut highlighter {
764 code.push_str(&text);
765
766 for line in text.lines() {
767 code_lines.push(Text::new(
768 highlighter.highlight_line(line).to_vec(),
769 ));
770 }
771
772 return None;
773 }
774
775 let span = Span::Standard {
776 text: text.into_string(),
777 strong,
778 emphasis,
779 strikethrough,
780 link: link.clone(),
781 code: false,
782 };
783
784 spans.push(span);
785
786 None
787 }
788 pulldown_cmark::Event::Code(code) if !metadata && !table => {
789 let span = Span::Standard {
790 text: code.into_string(),
791 strong,
792 emphasis,
793 strikethrough,
794 link: link.clone(),
795 code: true,
796 };
797
798 spans.push(span);
799 None
800 }
801 pulldown_cmark::Event::SoftBreak if !metadata && !table => {
802 spans.push(Span::Standard {
803 text: String::from(" "),
804 strikethrough,
805 strong,
806 emphasis,
807 link: link.clone(),
808 code: false,
809 });
810 None
811 }
812 pulldown_cmark::Event::HardBreak if !metadata && !table => {
813 spans.push(Span::Standard {
814 text: String::from("\n"),
815 strikethrough,
816 strong,
817 emphasis,
818 link: link.clone(),
819 code: false,
820 });
821 None
822 }
823 _ => None,
824 })
825}
826
827#[derive(Debug, Clone, Copy)]
829pub struct Settings {
830 pub text_size: Pixels,
832 pub h1_size: Pixels,
834 pub h2_size: Pixels,
836 pub h3_size: Pixels,
838 pub h4_size: Pixels,
840 pub h5_size: Pixels,
842 pub h6_size: Pixels,
844 pub code_size: Pixels,
846 pub spacing: Pixels,
848 pub style: Style,
850}
851
852impl Settings {
853 pub fn with_style(style: impl Into<Style>) -> Self {
855 Self::with_text_size(16, style)
856 }
857
858 pub fn with_text_size(
864 text_size: impl Into<Pixels>,
865 style: impl Into<Style>,
866 ) -> Self {
867 let text_size = text_size.into();
868
869 Self {
870 text_size,
871 h1_size: text_size * 2.0,
872 h2_size: text_size * 1.75,
873 h3_size: text_size * 1.5,
874 h4_size: text_size * 1.25,
875 h5_size: text_size,
876 h6_size: text_size,
877 code_size: text_size * 0.75,
878 spacing: text_size * 0.875,
879 style: style.into(),
880 }
881 }
882}
883
884impl From<&Theme> for Settings {
885 fn from(theme: &Theme) -> Self {
886 Self::with_style(Style::from(theme))
887 }
888}
889
890impl From<Theme> for Settings {
891 fn from(theme: Theme) -> Self {
892 Self::with_style(Style::from(theme))
893 }
894}
895
896#[derive(Debug, Clone, Copy, PartialEq)]
898pub struct Style {
899 pub inline_code_highlight: Highlight,
901 pub inline_code_padding: Padding,
903 pub inline_code_color: Color,
905 pub link_color: Color,
907}
908
909impl Style {
910 pub fn from_palette(palette: theme::Palette) -> Self {
912 Self {
913 inline_code_padding: padding::left(1).right(1),
914 inline_code_highlight: Highlight {
915 background: color!(0x111).into(),
916 border: border::rounded(2),
917 },
918 inline_code_color: Color::WHITE,
919 link_color: palette.primary,
920 }
921 }
922}
923
924impl From<theme::Palette> for Style {
925 fn from(palette: theme::Palette) -> Self {
926 Self::from_palette(palette)
927 }
928}
929
930impl From<&Theme> for Style {
931 fn from(theme: &Theme) -> Self {
932 Self::from_palette(theme.palette())
933 }
934}
935
936impl From<Theme> for Style {
937 fn from(theme: Theme) -> Self {
938 Self::from_palette(theme.palette())
939 }
940}
941
942pub fn view<'a, Theme, Renderer>(
985 items: impl IntoIterator<Item = &'a Item>,
986 settings: impl Into<Settings>,
987) -> Element<'a, Url, Theme, Renderer>
988where
989 Theme: Catalog + 'a,
990 Renderer: core::text::Renderer<Font = Font> + 'a,
991{
992 view_with(items, settings, &DefaultViewer)
993}
994
995pub fn view_with<'a, Message, Theme, Renderer>(
1001 items: impl IntoIterator<Item = &'a Item>,
1002 settings: impl Into<Settings>,
1003 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1004) -> Element<'a, Message, Theme, Renderer>
1005where
1006 Message: 'a,
1007 Theme: Catalog + 'a,
1008 Renderer: core::text::Renderer<Font = Font> + 'a,
1009{
1010 let settings = settings.into();
1011
1012 let blocks = items
1013 .into_iter()
1014 .enumerate()
1015 .map(|(i, item_)| item(viewer, settings, item_, i));
1016
1017 Element::new(column(blocks).spacing(settings.spacing))
1018}
1019
1020pub fn item<'a, Message, Theme, Renderer>(
1022 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1023 settings: Settings,
1024 item: &'a Item,
1025 index: usize,
1026) -> Element<'a, Message, Theme, Renderer>
1027where
1028 Message: 'a,
1029 Theme: Catalog + 'a,
1030 Renderer: core::text::Renderer<Font = Font> + 'a,
1031{
1032 match item {
1033 Item::Image { url, title, alt } => {
1034 viewer.image(settings, url, title, alt)
1035 }
1036 Item::Heading(level, text) => {
1037 viewer.heading(settings, level, text, index)
1038 }
1039 Item::Paragraph(text) => viewer.paragraph(settings, text),
1040 Item::CodeBlock {
1041 language,
1042 code,
1043 lines,
1044 } => viewer.code_block(settings, language.as_deref(), code, lines),
1045 Item::List { start: None, items } => {
1046 viewer.unordered_list(settings, items)
1047 }
1048 Item::List {
1049 start: Some(start),
1050 items,
1051 } => viewer.ordered_list(settings, *start, items),
1052 }
1053}
1054
1055pub fn heading<'a, Message, Theme, Renderer>(
1057 settings: Settings,
1058 level: &'a HeadingLevel,
1059 text: &'a Text,
1060 index: usize,
1061 on_link_click: impl Fn(Url) -> Message + 'a,
1062) -> Element<'a, Message, Theme, Renderer>
1063where
1064 Message: 'a,
1065 Theme: Catalog + 'a,
1066 Renderer: core::text::Renderer<Font = Font> + 'a,
1067{
1068 let Settings {
1069 h1_size,
1070 h2_size,
1071 h3_size,
1072 h4_size,
1073 h5_size,
1074 h6_size,
1075 text_size,
1076 ..
1077 } = settings;
1078
1079 container(
1080 rich_text(text.spans(settings.style))
1081 .on_link_click(on_link_click)
1082 .size(match level {
1083 pulldown_cmark::HeadingLevel::H1 => h1_size,
1084 pulldown_cmark::HeadingLevel::H2 => h2_size,
1085 pulldown_cmark::HeadingLevel::H3 => h3_size,
1086 pulldown_cmark::HeadingLevel::H4 => h4_size,
1087 pulldown_cmark::HeadingLevel::H5 => h5_size,
1088 pulldown_cmark::HeadingLevel::H6 => h6_size,
1089 }),
1090 )
1091 .padding(padding::top(if index > 0 {
1092 text_size / 2.0
1093 } else {
1094 Pixels::ZERO
1095 }))
1096 .into()
1097}
1098
1099pub fn paragraph<'a, Message, Theme, Renderer>(
1101 settings: Settings,
1102 text: &'a Text,
1103 on_link_click: impl Fn(Url) -> Message + 'a,
1104) -> Element<'a, Message, Theme, Renderer>
1105where
1106 Message: 'a,
1107 Theme: Catalog + 'a,
1108 Renderer: core::text::Renderer<Font = Font> + 'a,
1109{
1110 rich_text(text.spans(settings.style))
1111 .size(settings.text_size)
1112 .on_link_click(on_link_click)
1113 .into()
1114}
1115
1116pub fn unordered_list<'a, Message, Theme, Renderer>(
1119 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1120 settings: Settings,
1121 items: &'a [Vec<Item>],
1122) -> Element<'a, Message, Theme, Renderer>
1123where
1124 Message: 'a,
1125 Theme: Catalog + 'a,
1126 Renderer: core::text::Renderer<Font = Font> + 'a,
1127{
1128 column(items.iter().map(|items| {
1129 row![
1130 text("•").size(settings.text_size),
1131 view_with(
1132 items,
1133 Settings {
1134 spacing: settings.spacing * 0.6,
1135 ..settings
1136 },
1137 viewer,
1138 )
1139 ]
1140 .spacing(settings.spacing)
1141 .into()
1142 }))
1143 .spacing(settings.spacing * 0.75)
1144 .padding([0.0, settings.spacing.0])
1145 .into()
1146}
1147
1148pub fn ordered_list<'a, Message, Theme, Renderer>(
1151 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1152 settings: Settings,
1153 start: u64,
1154 items: &'a [Vec<Item>],
1155) -> Element<'a, Message, Theme, Renderer>
1156where
1157 Message: 'a,
1158 Theme: Catalog + 'a,
1159 Renderer: core::text::Renderer<Font = Font> + 'a,
1160{
1161 column(items.iter().enumerate().map(|(i, items)| {
1162 row![
1163 text!("{}.", i as u64 + start).size(settings.text_size),
1164 view_with(
1165 items,
1166 Settings {
1167 spacing: settings.spacing * 0.6,
1168 ..settings
1169 },
1170 viewer,
1171 )
1172 ]
1173 .spacing(settings.spacing)
1174 .into()
1175 }))
1176 .spacing(settings.spacing * 0.75)
1177 .padding([0.0, settings.spacing.0])
1178 .into()
1179}
1180
1181pub fn code_block<'a, Message, Theme, Renderer>(
1183 settings: Settings,
1184 lines: &'a [Text],
1185 on_link_click: impl Fn(Url) -> Message + Clone + 'a,
1186) -> Element<'a, Message, Theme, Renderer>
1187where
1188 Message: 'a,
1189 Theme: Catalog + 'a,
1190 Renderer: core::text::Renderer<Font = Font> + 'a,
1191{
1192 container(
1193 scrollable(
1194 container(column(lines.iter().map(|line| {
1195 rich_text(line.spans(settings.style))
1196 .on_link_click(on_link_click.clone())
1197 .font(Font::MONOSPACE)
1198 .size(settings.code_size)
1199 .into()
1200 })))
1201 .padding(settings.code_size),
1202 )
1203 .direction(scrollable::Direction::Horizontal(
1204 scrollable::Scrollbar::default()
1205 .width(settings.code_size / 2)
1206 .scroller_width(settings.code_size / 2),
1207 )),
1208 )
1209 .width(Length::Fill)
1210 .padding(settings.code_size / 4)
1211 .class(Theme::code_block())
1212 .into()
1213}
1214
1215pub trait Viewer<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
1217where
1218 Self: Sized + 'a,
1219 Message: 'a,
1220 Theme: Catalog + 'a,
1221 Renderer: core::text::Renderer<Font = Font> + 'a,
1222{
1223 fn on_link_click(url: Url) -> Message;
1225
1226 fn image(
1230 &self,
1231 settings: Settings,
1232 url: &'a Url,
1233 title: &'a str,
1234 alt: &Text,
1235 ) -> Element<'a, Message, Theme, Renderer> {
1236 let _url = url;
1237 let _title = title;
1238
1239 container(
1240 rich_text(alt.spans(settings.style))
1241 .on_link_click(Self::on_link_click),
1242 )
1243 .padding(settings.spacing.0)
1244 .class(Theme::code_block())
1245 .into()
1246 }
1247
1248 fn heading(
1252 &self,
1253 settings: Settings,
1254 level: &'a HeadingLevel,
1255 text: &'a Text,
1256 index: usize,
1257 ) -> Element<'a, Message, Theme, Renderer> {
1258 heading(settings, level, text, index, Self::on_link_click)
1259 }
1260
1261 fn paragraph(
1265 &self,
1266 settings: Settings,
1267 text: &'a Text,
1268 ) -> Element<'a, Message, Theme, Renderer> {
1269 paragraph(settings, text, Self::on_link_click)
1270 }
1271
1272 fn code_block(
1276 &self,
1277 settings: Settings,
1278 language: Option<&'a str>,
1279 code: &'a str,
1280 lines: &'a [Text],
1281 ) -> Element<'a, Message, Theme, Renderer> {
1282 let _language = language;
1283 let _code = code;
1284
1285 code_block(settings, lines, Self::on_link_click)
1286 }
1287
1288 fn unordered_list(
1292 &self,
1293 settings: Settings,
1294 items: &'a [Vec<Item>],
1295 ) -> Element<'a, Message, Theme, Renderer> {
1296 unordered_list(self, settings, items)
1297 }
1298
1299 fn ordered_list(
1303 &self,
1304 settings: Settings,
1305 start: u64,
1306 items: &'a [Vec<Item>],
1307 ) -> Element<'a, Message, Theme, Renderer> {
1308 ordered_list(self, settings, start, items)
1309 }
1310}
1311
1312#[derive(Debug, Clone, Copy)]
1313struct DefaultViewer;
1314
1315impl<'a, Theme, Renderer> Viewer<'a, Url, Theme, Renderer> for DefaultViewer
1316where
1317 Theme: Catalog + 'a,
1318 Renderer: core::text::Renderer<Font = Font> + 'a,
1319{
1320 fn on_link_click(url: Url) -> Url {
1321 url
1322 }
1323}
1324
1325pub trait Catalog:
1327 container::Catalog + scrollable::Catalog + text::Catalog
1328{
1329 fn code_block<'a>() -> <Self as container::Catalog>::Class<'a>;
1331}
1332
1333impl Catalog for Theme {
1334 fn code_block<'a>() -> <Self as container::Catalog>::Class<'a> {
1335 Box::new(container::dark)
1336 }
1337}