1use crate::core::alignment;
47use crate::core::border;
48use crate::core::font::{self, Font};
49use crate::core::padding;
50use crate::core::theme;
51use crate::core::{
52 self, Color, Element, Length, Padding, Pixels, Theme, color,
53};
54use crate::{column, container, rich_text, row, rule, scrollable, span, text};
55
56use std::borrow::BorrowMut;
57use std::cell::{Cell, RefCell};
58use std::collections::{HashMap, HashSet};
59use std::mem;
60use std::ops::Range;
61use std::rc::Rc;
62use std::sync::Arc;
63
64pub use core::text::Highlight;
65pub use pulldown_cmark::HeadingLevel;
66pub use url::Url;
67
68#[derive(Debug, Default)]
70pub struct Content {
71 items: Vec<Item>,
72 incomplete: HashMap<usize, Section>,
73 state: State,
74}
75
76#[derive(Debug)]
77struct Section {
78 content: String,
79 broken_links: HashSet<String>,
80}
81
82impl Content {
83 pub fn new() -> Self {
85 Self::default()
86 }
87
88 pub fn parse(markdown: &str) -> Self {
90 let mut content = Self::new();
91 content.push_str(markdown);
92 content
93 }
94
95 pub fn push_str(&mut self, markdown: &str) {
100 if markdown.is_empty() {
101 return;
102 }
103
104 let mut leftover = std::mem::take(&mut self.state.leftover);
106 leftover.push_str(markdown);
107
108 let input = if leftover.trim_end().ends_with('|') {
109 leftover.trim_end().trim_end_matches('|')
110 } else {
111 leftover.as_str()
112 };
113
114 let _ = self.items.pop();
116
117 for (item, source, broken_links) in parse_with(&mut self.state, input) {
119 if !broken_links.is_empty() {
120 let _ = self.incomplete.insert(
121 self.items.len(),
122 Section {
123 content: source.to_owned(),
124 broken_links,
125 },
126 );
127 }
128
129 self.items.push(item);
130 }
131
132 self.state.leftover.push_str(&leftover[input.len()..]);
133
134 if !self.incomplete.is_empty() {
136 self.incomplete.retain(|index, section| {
137 if self.items.len() <= *index {
138 return false;
139 }
140
141 let broken_links_before = section.broken_links.len();
142
143 section
144 .broken_links
145 .retain(|link| !self.state.references.contains_key(link));
146
147 if broken_links_before != section.broken_links.len() {
148 let mut state = State {
149 leftover: String::new(),
150 references: self.state.references.clone(),
151 images: HashSet::new(),
152 #[cfg(feature = "highlighter")]
153 highlighter: None,
154 };
155
156 if let Some((item, _source, _broken_links)) =
157 parse_with(&mut state, §ion.content).next()
158 {
159 self.items[*index] = item;
160 }
161
162 self.state.images.extend(state.images.drain());
163 drop(state);
164 }
165
166 !section.broken_links.is_empty()
167 });
168 }
169 }
170
171 pub fn items(&self) -> &[Item] {
175 &self.items
176 }
177
178 pub fn images(&self) -> &HashSet<Url> {
180 &self.state.images
181 }
182}
183
184#[derive(Debug, Clone)]
186pub enum Item {
187 Heading(pulldown_cmark::HeadingLevel, Text),
189 Paragraph(Text),
191 CodeBlock {
195 language: Option<String>,
197 code: String,
199 lines: Vec<Text>,
201 },
202 List {
204 start: Option<u64>,
206 items: Vec<Vec<Item>>,
208 },
209 Image {
211 url: Url,
213 title: String,
215 alt: Text,
217 },
218 Quote(Vec<Item>),
220 Rule,
222 Table {
224 columns: Vec<Column>,
226 rows: Vec<Row>,
228 },
229}
230
231#[derive(Debug, Clone)]
233pub struct Column {
234 pub header: Vec<Item>,
236 pub alignment: pulldown_cmark::Alignment,
238}
239
240#[derive(Debug, Clone)]
242pub struct Row {
243 cells: Vec<Vec<Item>>,
245}
246
247#[derive(Debug, Clone)]
249pub struct Text {
250 spans: Vec<Span>,
251 last_style: Cell<Option<Style>>,
252 last_styled_spans: RefCell<Arc<[text::Span<'static, Url>]>>,
253}
254
255impl Text {
256 fn new(spans: Vec<Span>) -> Self {
257 Self {
258 spans,
259 last_style: Cell::default(),
260 last_styled_spans: RefCell::default(),
261 }
262 }
263
264 pub fn spans(&self, style: Style) -> Arc<[text::Span<'static, Url>]> {
269 if Some(style) != self.last_style.get() {
270 *self.last_styled_spans.borrow_mut() =
271 self.spans.iter().map(|span| span.view(&style)).collect();
272
273 self.last_style.set(Some(style));
274 }
275
276 self.last_styled_spans.borrow().clone()
277 }
278}
279
280#[derive(Debug, Clone)]
281enum Span {
282 Standard {
283 text: String,
284 strikethrough: bool,
285 link: Option<Url>,
286 strong: bool,
287 emphasis: bool,
288 code: bool,
289 },
290 #[cfg(feature = "highlighter")]
291 Highlight {
292 text: String,
293 color: Option<Color>,
294 font: Option<Font>,
295 },
296}
297
298impl Span {
299 fn view(&self, style: &Style) -> text::Span<'static, Url> {
300 match self {
301 Span::Standard {
302 text,
303 strikethrough,
304 link,
305 strong,
306 emphasis,
307 code,
308 } => {
309 let span = span(text.clone()).strikethrough(*strikethrough);
310
311 let span = if *code {
312 span.font(Font::MONOSPACE)
313 .color(style.inline_code_color)
314 .background(style.inline_code_highlight.background)
315 .border(style.inline_code_highlight.border)
316 .padding(style.inline_code_padding)
317 } else if *strong || *emphasis {
318 span.font(Font {
319 weight: if *strong {
320 font::Weight::Bold
321 } else {
322 font::Weight::Normal
323 },
324 style: if *emphasis {
325 font::Style::Italic
326 } else {
327 font::Style::Normal
328 },
329 ..Font::default()
330 })
331 } else {
332 span
333 };
334
335 if let Some(link) = link.as_ref() {
336 span.color(style.link_color).link(link.clone())
337 } else {
338 span
339 }
340 }
341 #[cfg(feature = "highlighter")]
342 Span::Highlight { text, color, font } => {
343 span(text.clone()).color_maybe(*color).font_maybe(*font)
344 }
345 }
346 }
347}
348
349pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
390 parse_with(State::default(), markdown)
391 .map(|(item, _source, _broken_links)| item)
392}
393
394#[derive(Debug, Default)]
395struct State {
396 leftover: String,
397 references: HashMap<String, String>,
398 images: HashSet<Url>,
399 #[cfg(feature = "highlighter")]
400 highlighter: Option<Highlighter>,
401}
402
403#[cfg(feature = "highlighter")]
404#[derive(Debug)]
405struct Highlighter {
406 lines: Vec<(String, Vec<Span>)>,
407 language: String,
408 parser: iced_highlighter::Stream,
409 current: usize,
410}
411
412#[cfg(feature = "highlighter")]
413impl Highlighter {
414 pub fn new(language: &str) -> Self {
415 Self {
416 lines: Vec::new(),
417 parser: iced_highlighter::Stream::new(
418 &iced_highlighter::Settings {
419 theme: iced_highlighter::Theme::Base16Ocean,
420 token: language.to_owned(),
421 },
422 ),
423 language: language.to_owned(),
424 current: 0,
425 }
426 }
427
428 pub fn prepare(&mut self) {
429 self.current = 0;
430 }
431
432 pub fn highlight_line(&mut self, text: &str) -> &[Span] {
433 match self.lines.get(self.current) {
434 Some(line) if line.0 == text => {}
435 _ => {
436 if self.current + 1 < self.lines.len() {
437 log::debug!("Resetting highlighter...");
438 self.parser.reset();
439 self.lines.truncate(self.current);
440
441 for line in &self.lines {
442 log::debug!(
443 "Refeeding {n} lines",
444 n = self.lines.len()
445 );
446
447 let _ = self.parser.highlight_line(&line.0);
448 }
449 }
450
451 log::trace!("Parsing: {text}", text = text.trim_end());
452
453 if self.current + 1 < self.lines.len() {
454 self.parser.commit();
455 }
456
457 let mut spans = Vec::new();
458
459 for (range, highlight) in self.parser.highlight_line(text) {
460 spans.push(Span::Highlight {
461 text: text[range].to_owned(),
462 color: highlight.color(),
463 font: highlight.font(),
464 });
465 }
466
467 if self.current + 1 == self.lines.len() {
468 let _ = self.lines.pop();
469 }
470
471 self.lines.push((text.to_owned(), spans));
472 }
473 }
474
475 self.current += 1;
476
477 &self
478 .lines
479 .get(self.current - 1)
480 .expect("Line must be parsed")
481 .1
482 }
483}
484
485fn parse_with<'a>(
486 mut state: impl BorrowMut<State> + 'a,
487 markdown: &'a str,
488) -> impl Iterator<Item = (Item, &'a str, HashSet<String>)> + 'a {
489 enum Scope {
490 List(List),
491 Quote(Vec<Item>),
492 Table {
493 alignment: Vec<pulldown_cmark::Alignment>,
494 columns: Vec<Column>,
495 rows: Vec<Row>,
496 current: Vec<Item>,
497 },
498 }
499
500 struct List {
501 start: Option<u64>,
502 items: Vec<Vec<Item>>,
503 }
504
505 let broken_links = Rc::new(RefCell::new(HashSet::new()));
506
507 let mut spans = Vec::new();
508 let mut code = String::new();
509 let mut code_language = None;
510 let mut code_lines = Vec::new();
511 let mut strong = false;
512 let mut emphasis = false;
513 let mut strikethrough = false;
514 let mut metadata = false;
515 let mut code_block = false;
516 let mut link = None;
517 let mut image = None;
518 let mut stack = Vec::new();
519
520 #[cfg(feature = "highlighter")]
521 let mut highlighter = None;
522
523 let parser = pulldown_cmark::Parser::new_with_broken_link_callback(
524 markdown,
525 pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS
526 | pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS
527 | pulldown_cmark::Options::ENABLE_TABLES
528 | pulldown_cmark::Options::ENABLE_STRIKETHROUGH,
529 {
530 let references = state.borrow().references.clone();
531 let broken_links = broken_links.clone();
532
533 Some(move |broken_link: pulldown_cmark::BrokenLink<'_>| {
534 if let Some(reference) =
535 references.get(broken_link.reference.as_ref())
536 {
537 Some((
538 pulldown_cmark::CowStr::from(reference.to_owned()),
539 broken_link.reference.into_static(),
540 ))
541 } else {
542 let _ = RefCell::borrow_mut(&broken_links)
543 .insert(broken_link.reference.into_string());
544
545 None
546 }
547 })
548 },
549 );
550
551 let references = &mut state.borrow_mut().references;
552
553 for reference in parser.reference_definitions().iter() {
554 let _ = references
555 .insert(reference.0.to_owned(), reference.1.dest.to_string());
556 }
557
558 let produce = move |state: &mut State,
559 stack: &mut Vec<Scope>,
560 item,
561 source: Range<usize>| {
562 if let Some(scope) = stack.last_mut() {
563 match scope {
564 Scope::List(list) => {
565 list.items.last_mut().expect("item context").push(item);
566 }
567 Scope::Quote(items) => {
568 items.push(item);
569 }
570 Scope::Table { current, .. } => {
571 current.push(item);
572 }
573 }
574
575 None
576 } else {
577 state.leftover = markdown[source.start..].to_owned();
578
579 Some((
580 item,
581 &markdown[source.start..source.end],
582 broken_links.take(),
583 ))
584 }
585 };
586
587 let parser = parser.into_offset_iter();
588
589 #[allow(clippy::drain_collect)]
591 parser.filter_map(move |(event, source)| match event {
592 pulldown_cmark::Event::Start(tag) => match tag {
593 pulldown_cmark::Tag::Strong if !metadata => {
594 strong = true;
595 None
596 }
597 pulldown_cmark::Tag::Emphasis if !metadata => {
598 emphasis = true;
599 None
600 }
601 pulldown_cmark::Tag::Strikethrough if !metadata => {
602 strikethrough = true;
603 None
604 }
605 pulldown_cmark::Tag::Link { dest_url, .. } if !metadata => {
606 match Url::parse(&dest_url) {
607 Ok(url)
608 if url.scheme() == "http"
609 || url.scheme() == "https" =>
610 {
611 link = Some(url);
612 }
613 _ => {}
614 }
615
616 None
617 }
618 pulldown_cmark::Tag::Image {
619 dest_url, title, ..
620 } if !metadata => {
621 image = Url::parse(&dest_url)
622 .ok()
623 .map(|url| (url, title.into_string()));
624 None
625 }
626 pulldown_cmark::Tag::List(first_item) if !metadata => {
627 let prev = if spans.is_empty() {
628 None
629 } else {
630 produce(
631 state.borrow_mut(),
632 &mut stack,
633 Item::Paragraph(Text::new(spans.drain(..).collect())),
634 source,
635 )
636 };
637
638 stack.push(Scope::List(List {
639 start: first_item,
640 items: Vec::new(),
641 }));
642
643 prev
644 }
645 pulldown_cmark::Tag::Item => {
646 if let Some(Scope::List(list)) = stack.last_mut() {
647 list.items.push(Vec::new());
648 }
649
650 None
651 }
652 pulldown_cmark::Tag::BlockQuote(_kind) if !metadata => {
653 let prev = if spans.is_empty() {
654 None
655 } else {
656 produce(
657 state.borrow_mut(),
658 &mut stack,
659 Item::Paragraph(Text::new(spans.drain(..).collect())),
660 source,
661 )
662 };
663
664 stack.push(Scope::Quote(Vec::new()));
665
666 prev
667 }
668 pulldown_cmark::Tag::CodeBlock(
669 pulldown_cmark::CodeBlockKind::Fenced(language),
670 ) if !metadata => {
671 #[cfg(feature = "highlighter")]
672 {
673 highlighter = Some({
674 let mut highlighter = state
675 .borrow_mut()
676 .highlighter
677 .take()
678 .filter(|highlighter| {
679 highlighter.language == language.as_ref()
680 })
681 .unwrap_or_else(|| Highlighter::new(&language));
682
683 highlighter.prepare();
684
685 highlighter
686 });
687 }
688
689 code_block = true;
690 code_language =
691 (!language.is_empty()).then(|| language.into_string());
692
693 if spans.is_empty() {
694 None
695 } else {
696 produce(
697 state.borrow_mut(),
698 &mut stack,
699 Item::Paragraph(Text::new(spans.drain(..).collect())),
700 source,
701 )
702 }
703 }
704 pulldown_cmark::Tag::MetadataBlock(_) => {
705 metadata = true;
706 None
707 }
708 pulldown_cmark::Tag::Table(alignment) => {
709 stack.push(Scope::Table {
710 columns: Vec::with_capacity(alignment.len()),
711 alignment,
712 current: Vec::new(),
713 rows: Vec::new(),
714 });
715
716 None
717 }
718 pulldown_cmark::Tag::TableHead => {
719 strong = true;
720 None
721 }
722 pulldown_cmark::Tag::TableRow => {
723 let Scope::Table { rows, .. } = stack.last_mut()? else {
724 return None;
725 };
726
727 rows.push(Row { cells: Vec::new() });
728 None
729 }
730 _ => None,
731 },
732 pulldown_cmark::Event::End(tag) => match tag {
733 pulldown_cmark::TagEnd::Heading(level) if !metadata => produce(
734 state.borrow_mut(),
735 &mut stack,
736 Item::Heading(level, Text::new(spans.drain(..).collect())),
737 source,
738 ),
739 pulldown_cmark::TagEnd::Strong if !metadata => {
740 strong = false;
741 None
742 }
743 pulldown_cmark::TagEnd::Emphasis if !metadata => {
744 emphasis = false;
745 None
746 }
747 pulldown_cmark::TagEnd::Strikethrough if !metadata => {
748 strikethrough = false;
749 None
750 }
751 pulldown_cmark::TagEnd::Link if !metadata => {
752 link = None;
753 None
754 }
755 pulldown_cmark::TagEnd::Paragraph if !metadata => {
756 if spans.is_empty() {
757 None
758 } else {
759 produce(
760 state.borrow_mut(),
761 &mut stack,
762 Item::Paragraph(Text::new(spans.drain(..).collect())),
763 source,
764 )
765 }
766 }
767 pulldown_cmark::TagEnd::Item if !metadata => {
768 if spans.is_empty() {
769 None
770 } else {
771 produce(
772 state.borrow_mut(),
773 &mut stack,
774 Item::Paragraph(Text::new(spans.drain(..).collect())),
775 source,
776 )
777 }
778 }
779 pulldown_cmark::TagEnd::List(_) if !metadata => {
780 let scope = stack.pop()?;
781
782 let Scope::List(list) = scope else {
783 return None;
784 };
785
786 produce(
787 state.borrow_mut(),
788 &mut stack,
789 Item::List {
790 start: list.start,
791 items: list.items,
792 },
793 source,
794 )
795 }
796 pulldown_cmark::TagEnd::BlockQuote(_kind) if !metadata => {
797 let scope = stack.pop()?;
798
799 let Scope::Quote(quote) = scope else {
800 return None;
801 };
802
803 produce(
804 state.borrow_mut(),
805 &mut stack,
806 Item::Quote(quote),
807 source,
808 )
809 }
810 pulldown_cmark::TagEnd::Image if !metadata => {
811 let (url, title) = image.take()?;
812 let alt = Text::new(spans.drain(..).collect());
813
814 let state = state.borrow_mut();
815 let _ = state.images.insert(url.clone());
816
817 produce(
818 state,
819 &mut stack,
820 Item::Image { url, title, alt },
821 source,
822 )
823 }
824 pulldown_cmark::TagEnd::CodeBlock if !metadata => {
825 code_block = false;
826
827 #[cfg(feature = "highlighter")]
828 {
829 state.borrow_mut().highlighter = highlighter.take();
830 }
831
832 produce(
833 state.borrow_mut(),
834 &mut stack,
835 Item::CodeBlock {
836 language: code_language.take(),
837 code: mem::take(&mut code),
838 lines: code_lines.drain(..).collect(),
839 },
840 source,
841 )
842 }
843 pulldown_cmark::TagEnd::MetadataBlock(_) => {
844 metadata = false;
845 None
846 }
847 pulldown_cmark::TagEnd::Table => {
848 let scope = stack.pop()?;
849
850 let Scope::Table { columns, rows, .. } = scope else {
851 return None;
852 };
853
854 produce(
855 state.borrow_mut(),
856 &mut stack,
857 Item::Table { columns, rows },
858 source,
859 )
860 }
861 pulldown_cmark::TagEnd::TableHead => {
862 strong = false;
863 None
864 }
865 pulldown_cmark::TagEnd::TableCell => {
866 if !spans.is_empty() {
867 let _ = produce(
868 state.borrow_mut(),
869 &mut stack,
870 Item::Paragraph(Text::new(spans.drain(..).collect())),
871 source,
872 );
873 }
874
875 let Scope::Table {
876 alignment,
877 columns,
878 rows,
879 current,
880 } = stack.last_mut()?
881 else {
882 return None;
883 };
884
885 if columns.len() < alignment.len() {
886 columns.push(Column {
887 header: std::mem::take(current),
888 alignment: alignment[columns.len()],
889 });
890 } else {
891 rows.last_mut()
892 .expect("table row")
893 .cells
894 .push(std::mem::take(current));
895 }
896
897 None
898 }
899 _ => None,
900 },
901 pulldown_cmark::Event::Text(text) if !metadata => {
902 if code_block {
903 code.push_str(&text);
904
905 #[cfg(feature = "highlighter")]
906 if let Some(highlighter) = &mut highlighter {
907 for line in text.lines() {
908 code_lines.push(Text::new(
909 highlighter.highlight_line(line).to_vec(),
910 ));
911 }
912 }
913
914 #[cfg(not(feature = "highlighter"))]
915 for line in text.lines() {
916 code_lines.push(Text::new(vec![Span::Standard {
917 text: line.to_owned(),
918 strong,
919 emphasis,
920 strikethrough,
921 link: link.clone(),
922 code: false,
923 }]));
924 }
925
926 return None;
927 }
928
929 let span = Span::Standard {
930 text: text.into_string(),
931 strong,
932 emphasis,
933 strikethrough,
934 link: link.clone(),
935 code: false,
936 };
937
938 spans.push(span);
939
940 None
941 }
942 pulldown_cmark::Event::Code(code) if !metadata => {
943 let span = Span::Standard {
944 text: code.into_string(),
945 strong,
946 emphasis,
947 strikethrough,
948 link: link.clone(),
949 code: true,
950 };
951
952 spans.push(span);
953 None
954 }
955 pulldown_cmark::Event::SoftBreak if !metadata => {
956 spans.push(Span::Standard {
957 text: String::from(" "),
958 strikethrough,
959 strong,
960 emphasis,
961 link: link.clone(),
962 code: false,
963 });
964 None
965 }
966 pulldown_cmark::Event::HardBreak if !metadata => {
967 spans.push(Span::Standard {
968 text: String::from("\n"),
969 strikethrough,
970 strong,
971 emphasis,
972 link: link.clone(),
973 code: false,
974 });
975 None
976 }
977 pulldown_cmark::Event::Rule => {
978 produce(state.borrow_mut(), &mut stack, Item::Rule, source)
979 }
980 _ => None,
981 })
982}
983
984#[derive(Debug, Clone, Copy)]
986pub struct Settings {
987 pub text_size: Pixels,
989 pub h1_size: Pixels,
991 pub h2_size: Pixels,
993 pub h3_size: Pixels,
995 pub h4_size: Pixels,
997 pub h5_size: Pixels,
999 pub h6_size: Pixels,
1001 pub code_size: Pixels,
1003 pub spacing: Pixels,
1005 pub style: Style,
1007}
1008
1009impl Settings {
1010 pub fn with_style(style: impl Into<Style>) -> Self {
1012 Self::with_text_size(16, style)
1013 }
1014
1015 pub fn with_text_size(
1021 text_size: impl Into<Pixels>,
1022 style: impl Into<Style>,
1023 ) -> Self {
1024 let text_size = text_size.into();
1025
1026 Self {
1027 text_size,
1028 h1_size: text_size * 2.0,
1029 h2_size: text_size * 1.75,
1030 h3_size: text_size * 1.5,
1031 h4_size: text_size * 1.25,
1032 h5_size: text_size,
1033 h6_size: text_size,
1034 code_size: text_size * 0.75,
1035 spacing: text_size * 0.875,
1036 style: style.into(),
1037 }
1038 }
1039}
1040
1041impl From<&Theme> for Settings {
1042 fn from(theme: &Theme) -> Self {
1043 Self::with_style(Style::from(theme))
1044 }
1045}
1046
1047impl From<Theme> for Settings {
1048 fn from(theme: Theme) -> Self {
1049 Self::with_style(Style::from(theme))
1050 }
1051}
1052
1053#[derive(Debug, Clone, Copy, PartialEq)]
1055pub struct Style {
1056 pub inline_code_highlight: Highlight,
1058 pub inline_code_padding: Padding,
1060 pub inline_code_color: Color,
1062 pub link_color: Color,
1064}
1065
1066impl Style {
1067 pub fn from_palette(palette: theme::Palette) -> Self {
1069 Self {
1070 inline_code_padding: padding::left(1).right(1),
1071 inline_code_highlight: Highlight {
1072 background: color!(0x111111).into(),
1073 border: border::rounded(4),
1074 },
1075 inline_code_color: Color::WHITE,
1076 link_color: palette.primary,
1077 }
1078 }
1079}
1080
1081impl From<theme::Palette> for Style {
1082 fn from(palette: theme::Palette) -> Self {
1083 Self::from_palette(palette)
1084 }
1085}
1086
1087impl From<&Theme> for Style {
1088 fn from(theme: &Theme) -> Self {
1089 Self::from_palette(theme.palette())
1090 }
1091}
1092
1093impl From<Theme> for Style {
1094 fn from(theme: Theme) -> Self {
1095 Self::from_palette(theme.palette())
1096 }
1097}
1098
1099pub fn view<'a, Theme, Renderer>(
1142 items: impl IntoIterator<Item = &'a Item>,
1143 settings: impl Into<Settings>,
1144) -> Element<'a, Url, Theme, Renderer>
1145where
1146 Theme: Catalog + 'a,
1147 Renderer: core::text::Renderer<Font = Font> + 'a,
1148{
1149 view_with(items, settings, &DefaultViewer)
1150}
1151
1152pub fn view_with<'a, Message, Theme, Renderer>(
1158 items: impl IntoIterator<Item = &'a Item>,
1159 settings: impl Into<Settings>,
1160 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1161) -> Element<'a, Message, Theme, Renderer>
1162where
1163 Message: 'a,
1164 Theme: Catalog + 'a,
1165 Renderer: core::text::Renderer<Font = Font> + 'a,
1166{
1167 let settings = settings.into();
1168
1169 let blocks = items
1170 .into_iter()
1171 .enumerate()
1172 .map(|(i, item_)| item(viewer, settings, item_, i));
1173
1174 Element::new(column(blocks).spacing(settings.spacing))
1175}
1176
1177pub fn item<'a, Message, Theme, Renderer>(
1179 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1180 settings: Settings,
1181 item: &'a Item,
1182 index: usize,
1183) -> Element<'a, Message, Theme, Renderer>
1184where
1185 Message: 'a,
1186 Theme: Catalog + 'a,
1187 Renderer: core::text::Renderer<Font = Font> + 'a,
1188{
1189 match item {
1190 Item::Image { url, title, alt } => {
1191 viewer.image(settings, url, title, alt)
1192 }
1193 Item::Heading(level, text) => {
1194 viewer.heading(settings, level, text, index)
1195 }
1196 Item::Paragraph(text) => viewer.paragraph(settings, text),
1197 Item::CodeBlock {
1198 language,
1199 code,
1200 lines,
1201 } => viewer.code_block(settings, language.as_deref(), code, lines),
1202 Item::List { start: None, items } => {
1203 viewer.unordered_list(settings, items)
1204 }
1205 Item::List {
1206 start: Some(start),
1207 items,
1208 } => viewer.ordered_list(settings, *start, items),
1209 Item::Quote(quote) => viewer.quote(settings, quote),
1210 Item::Rule => viewer.rule(settings),
1211 Item::Table { columns, rows } => viewer.table(settings, columns, rows),
1212 }
1213}
1214
1215pub fn heading<'a, Message, Theme, Renderer>(
1217 settings: Settings,
1218 level: &'a HeadingLevel,
1219 text: &'a Text,
1220 index: usize,
1221 on_link_click: impl Fn(Url) -> Message + 'a,
1222) -> Element<'a, Message, Theme, Renderer>
1223where
1224 Message: 'a,
1225 Theme: Catalog + 'a,
1226 Renderer: core::text::Renderer<Font = Font> + 'a,
1227{
1228 let Settings {
1229 h1_size,
1230 h2_size,
1231 h3_size,
1232 h4_size,
1233 h5_size,
1234 h6_size,
1235 text_size,
1236 ..
1237 } = settings;
1238
1239 container(
1240 rich_text(text.spans(settings.style))
1241 .on_link_click(on_link_click)
1242 .size(match level {
1243 pulldown_cmark::HeadingLevel::H1 => h1_size,
1244 pulldown_cmark::HeadingLevel::H2 => h2_size,
1245 pulldown_cmark::HeadingLevel::H3 => h3_size,
1246 pulldown_cmark::HeadingLevel::H4 => h4_size,
1247 pulldown_cmark::HeadingLevel::H5 => h5_size,
1248 pulldown_cmark::HeadingLevel::H6 => h6_size,
1249 }),
1250 )
1251 .padding(padding::top(if index > 0 {
1252 text_size / 2.0
1253 } else {
1254 Pixels::ZERO
1255 }))
1256 .into()
1257}
1258
1259pub fn paragraph<'a, Message, Theme, Renderer>(
1261 settings: Settings,
1262 text: &Text,
1263 on_link_click: impl Fn(Url) -> Message + 'a,
1264) -> Element<'a, Message, Theme, Renderer>
1265where
1266 Message: 'a,
1267 Theme: Catalog + 'a,
1268 Renderer: core::text::Renderer<Font = Font> + 'a,
1269{
1270 rich_text(text.spans(settings.style))
1271 .size(settings.text_size)
1272 .on_link_click(on_link_click)
1273 .into()
1274}
1275
1276pub fn unordered_list<'a, Message, Theme, Renderer>(
1279 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1280 settings: Settings,
1281 items: &'a [Vec<Item>],
1282) -> Element<'a, Message, Theme, Renderer>
1283where
1284 Message: 'a,
1285 Theme: Catalog + 'a,
1286 Renderer: core::text::Renderer<Font = Font> + 'a,
1287{
1288 column(items.iter().map(|items| {
1289 row![
1290 text("•").size(settings.text_size),
1291 view_with(
1292 items,
1293 Settings {
1294 spacing: settings.spacing * 0.6,
1295 ..settings
1296 },
1297 viewer,
1298 )
1299 ]
1300 .spacing(settings.spacing)
1301 .into()
1302 }))
1303 .spacing(settings.spacing * 0.75)
1304 .padding([0.0, settings.spacing.0])
1305 .into()
1306}
1307
1308pub fn ordered_list<'a, Message, Theme, Renderer>(
1311 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1312 settings: Settings,
1313 start: u64,
1314 items: &'a [Vec<Item>],
1315) -> Element<'a, Message, Theme, Renderer>
1316where
1317 Message: 'a,
1318 Theme: Catalog + 'a,
1319 Renderer: core::text::Renderer<Font = Font> + 'a,
1320{
1321 let digits = ((start + items.len() as u64).max(1) as f32).log10().ceil();
1322
1323 column(items.iter().enumerate().map(|(i, items)| {
1324 row![
1325 text!("{}.", i as u64 + start)
1326 .size(settings.text_size)
1327 .align_x(alignment::Horizontal::Right)
1328 .width(settings.text_size * ((digits / 2.0).ceil() + 1.0)),
1329 view_with(
1330 items,
1331 Settings {
1332 spacing: settings.spacing * 0.6,
1333 ..settings
1334 },
1335 viewer,
1336 )
1337 ]
1338 .spacing(settings.spacing)
1339 .into()
1340 }))
1341 .spacing(settings.spacing * 0.75)
1342 .into()
1343}
1344
1345pub fn code_block<'a, Message, Theme, Renderer>(
1347 settings: Settings,
1348 lines: &'a [Text],
1349 on_link_click: impl Fn(Url) -> Message + Clone + 'a,
1350) -> Element<'a, Message, Theme, Renderer>
1351where
1352 Message: 'a,
1353 Theme: Catalog + 'a,
1354 Renderer: core::text::Renderer<Font = Font> + 'a,
1355{
1356 container(
1357 scrollable(
1358 container(column(lines.iter().map(|line| {
1359 rich_text(line.spans(settings.style))
1360 .on_link_click(on_link_click.clone())
1361 .font(Font::MONOSPACE)
1362 .size(settings.code_size)
1363 .into()
1364 })))
1365 .padding(settings.code_size),
1366 )
1367 .direction(scrollable::Direction::Horizontal(
1368 scrollable::Scrollbar::default()
1369 .width(settings.code_size / 2)
1370 .scroller_width(settings.code_size / 2),
1371 )),
1372 )
1373 .width(Length::Fill)
1374 .padding(settings.code_size / 4)
1375 .class(Theme::code_block())
1376 .into()
1377}
1378
1379pub fn quote<'a, Message, Theme, Renderer>(
1381 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1382 settings: Settings,
1383 contents: &'a [Item],
1384) -> Element<'a, Message, Theme, Renderer>
1385where
1386 Message: 'a,
1387 Theme: Catalog + 'a,
1388 Renderer: core::text::Renderer<Font = Font> + 'a,
1389{
1390 row![
1391 rule::vertical(4),
1392 column(
1393 contents
1394 .iter()
1395 .enumerate()
1396 .map(|(i, content)| item(viewer, settings, content, i)),
1397 )
1398 .spacing(settings.spacing.0),
1399 ]
1400 .height(Length::Shrink)
1401 .spacing(settings.spacing.0)
1402 .into()
1403}
1404
1405pub fn rule<'a, Message, Theme, Renderer>()
1407-> Element<'a, Message, Theme, Renderer>
1408where
1409 Message: 'a,
1410 Theme: Catalog + 'a,
1411 Renderer: core::text::Renderer<Font = Font> + 'a,
1412{
1413 rule::horizontal(2).into()
1414}
1415
1416pub fn table<'a, Message, Theme, Renderer>(
1418 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1419 settings: Settings,
1420 columns: &'a [Column],
1421 rows: &'a [Row],
1422) -> Element<'a, Message, Theme, Renderer>
1423where
1424 Message: 'a,
1425 Theme: Catalog + 'a,
1426 Renderer: core::text::Renderer<Font = Font> + 'a,
1427{
1428 use crate::table;
1429
1430 let table = table(
1431 columns.iter().enumerate().map(move |(i, column)| {
1432 table::column(
1433 items(viewer, settings, &column.header),
1434 move |row: &Row| {
1435 if let Some(cells) = row.cells.get(i) {
1436 items(viewer, settings, cells)
1437 } else {
1438 text("").into()
1439 }
1440 },
1441 )
1442 .align_x(match column.alignment {
1443 pulldown_cmark::Alignment::None
1444 | pulldown_cmark::Alignment::Left => {
1445 alignment::Horizontal::Left
1446 }
1447 pulldown_cmark::Alignment::Center => {
1448 alignment::Horizontal::Center
1449 }
1450 pulldown_cmark::Alignment::Right => {
1451 alignment::Horizontal::Right
1452 }
1453 })
1454 }),
1455 rows,
1456 )
1457 .padding_x(settings.spacing.0)
1458 .padding_y(settings.spacing.0 / 2.0)
1459 .separator_x(0);
1460
1461 scrollable(table)
1462 .direction(scrollable::Direction::Horizontal(
1463 scrollable::Scrollbar::default(),
1464 ))
1465 .spacing(settings.spacing.0 / 2.0)
1466 .into()
1467}
1468
1469pub fn items<'a, Message, Theme, Renderer>(
1471 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1472 settings: Settings,
1473 items: &'a [Item],
1474) -> Element<'a, Message, Theme, Renderer>
1475where
1476 Message: 'a,
1477 Theme: Catalog + 'a,
1478 Renderer: core::text::Renderer<Font = Font> + 'a,
1479{
1480 column(
1481 items
1482 .iter()
1483 .enumerate()
1484 .map(|(i, content)| item(viewer, settings, content, i)),
1485 )
1486 .spacing(settings.spacing.0)
1487 .into()
1488}
1489
1490pub trait Viewer<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
1492where
1493 Self: Sized + 'a,
1494 Message: 'a,
1495 Theme: Catalog + 'a,
1496 Renderer: core::text::Renderer<Font = Font> + 'a,
1497{
1498 fn on_link_click(url: Url) -> Message;
1500
1501 fn image(
1505 &self,
1506 settings: Settings,
1507 url: &'a Url,
1508 title: &'a str,
1509 alt: &Text,
1510 ) -> Element<'a, Message, Theme, Renderer> {
1511 let _url = url;
1512 let _title = title;
1513
1514 container(
1515 rich_text(alt.spans(settings.style))
1516 .on_link_click(Self::on_link_click),
1517 )
1518 .padding(settings.spacing.0)
1519 .class(Theme::code_block())
1520 .into()
1521 }
1522
1523 fn heading(
1527 &self,
1528 settings: Settings,
1529 level: &'a HeadingLevel,
1530 text: &'a Text,
1531 index: usize,
1532 ) -> Element<'a, Message, Theme, Renderer> {
1533 heading(settings, level, text, index, Self::on_link_click)
1534 }
1535
1536 fn paragraph(
1540 &self,
1541 settings: Settings,
1542 text: &Text,
1543 ) -> Element<'a, Message, Theme, Renderer> {
1544 paragraph(settings, text, Self::on_link_click)
1545 }
1546
1547 fn code_block(
1551 &self,
1552 settings: Settings,
1553 language: Option<&'a str>,
1554 code: &'a str,
1555 lines: &'a [Text],
1556 ) -> Element<'a, Message, Theme, Renderer> {
1557 let _language = language;
1558 let _code = code;
1559
1560 code_block(settings, lines, Self::on_link_click)
1561 }
1562
1563 fn unordered_list(
1567 &self,
1568 settings: Settings,
1569 items: &'a [Vec<Item>],
1570 ) -> Element<'a, Message, Theme, Renderer> {
1571 unordered_list(self, settings, items)
1572 }
1573
1574 fn ordered_list(
1578 &self,
1579 settings: Settings,
1580 start: u64,
1581 items: &'a [Vec<Item>],
1582 ) -> Element<'a, Message, Theme, Renderer> {
1583 ordered_list(self, settings, start, items)
1584 }
1585
1586 fn quote(
1590 &self,
1591 settings: Settings,
1592 contents: &'a [Item],
1593 ) -> Element<'a, Message, Theme, Renderer> {
1594 quote(self, settings, contents)
1595 }
1596
1597 fn rule(
1601 &self,
1602 _settings: Settings,
1603 ) -> Element<'a, Message, Theme, Renderer> {
1604 rule()
1605 }
1606
1607 fn table(
1611 &self,
1612 settings: Settings,
1613 columns: &'a [Column],
1614 rows: &'a [Row],
1615 ) -> Element<'a, Message, Theme, Renderer> {
1616 table(self, settings, columns, rows)
1617 }
1618}
1619
1620#[derive(Debug, Clone, Copy)]
1621struct DefaultViewer;
1622
1623impl<'a, Theme, Renderer> Viewer<'a, Url, Theme, Renderer> for DefaultViewer
1624where
1625 Theme: Catalog + 'a,
1626 Renderer: core::text::Renderer<Font = Font> + 'a,
1627{
1628 fn on_link_click(url: Url) -> Url {
1629 url
1630 }
1631}
1632
1633pub trait Catalog:
1635 container::Catalog
1636 + scrollable::Catalog
1637 + text::Catalog
1638 + crate::rule::Catalog
1639 + crate::table::Catalog
1640{
1641 fn code_block<'a>() -> <Self as container::Catalog>::Class<'a>;
1643}
1644
1645impl Catalog for Theme {
1646 fn code_block<'a>() -> <Self as container::Catalog>::Class<'a> {
1647 Box::new(container::dark)
1648 }
1649}