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