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 checkbox, column, container, rich_text, row, rule, scrollable, span, text,
56};
57
58use std::borrow::BorrowMut;
59use std::cell::{Cell, RefCell};
60use std::collections::{HashMap, HashSet};
61use std::mem;
62use std::ops::Range;
63use std::rc::Rc;
64use std::sync::Arc;
65
66pub use core::text::Highlight;
67pub use pulldown_cmark::HeadingLevel;
68
69pub type Uri = String;
73
74#[derive(Debug, Default)]
76pub struct Content {
77 items: Vec<Item>,
78 incomplete: HashMap<usize, Section>,
79 state: State,
80}
81
82#[derive(Debug)]
83struct Section {
84 content: String,
85 broken_links: HashSet<String>,
86}
87
88impl Content {
89 pub fn new() -> Self {
91 Self::default()
92 }
93
94 pub fn parse(markdown: &str) -> Self {
96 let mut content = Self::new();
97 content.push_str(markdown);
98 content
99 }
100
101 pub fn push_str(&mut self, markdown: &str) {
106 if markdown.is_empty() {
107 return;
108 }
109
110 let mut leftover = std::mem::take(&mut self.state.leftover);
112 leftover.push_str(markdown);
113
114 let input = if leftover.trim_end().ends_with('|') {
115 leftover.trim_end().trim_end_matches('|')
116 } else {
117 leftover.as_str()
118 };
119
120 let _ = self.items.pop();
122
123 for (item, source, broken_links) in parse_with(&mut self.state, input) {
125 if !broken_links.is_empty() {
126 let _ = self.incomplete.insert(
127 self.items.len(),
128 Section {
129 content: source.to_owned(),
130 broken_links,
131 },
132 );
133 }
134
135 self.items.push(item);
136 }
137
138 self.state.leftover.push_str(&leftover[input.len()..]);
139
140 if !self.incomplete.is_empty() {
142 self.incomplete.retain(|index, section| {
143 if self.items.len() <= *index {
144 return false;
145 }
146
147 let broken_links_before = section.broken_links.len();
148
149 section
150 .broken_links
151 .retain(|link| !self.state.references.contains_key(link));
152
153 if broken_links_before != section.broken_links.len() {
154 let mut state = State {
155 leftover: String::new(),
156 references: self.state.references.clone(),
157 images: HashSet::new(),
158 #[cfg(feature = "highlighter")]
159 highlighter: None,
160 };
161
162 if let Some((item, _source, _broken_links)) =
163 parse_with(&mut state, §ion.content).next()
164 {
165 self.items[*index] = item;
166 }
167
168 self.state.images.extend(state.images.drain());
169 drop(state);
170 }
171
172 !section.broken_links.is_empty()
173 });
174 }
175 }
176
177 pub fn items(&self) -> &[Item] {
181 &self.items
182 }
183
184 pub fn images(&self) -> &HashSet<Uri> {
186 &self.state.images
187 }
188}
189
190#[derive(Debug, Clone)]
192pub enum Item {
193 Heading(pulldown_cmark::HeadingLevel, Text),
195 Paragraph(Text),
197 CodeBlock {
201 language: Option<String>,
203 code: String,
205 lines: Vec<Text>,
207 },
208 List {
210 start: Option<u64>,
212 bullets: Vec<Bullet>,
214 },
215 Image {
217 url: Uri,
219 title: String,
221 alt: Text,
223 },
224 Quote(Vec<Item>),
226 Rule,
228 Table {
230 columns: Vec<Column>,
232 rows: Vec<Row>,
234 },
235}
236
237#[derive(Debug, Clone)]
239pub struct Column {
240 pub header: Vec<Item>,
242 pub alignment: pulldown_cmark::Alignment,
244}
245
246#[derive(Debug, Clone)]
248pub struct Row {
249 cells: Vec<Vec<Item>>,
251}
252
253#[derive(Debug, Clone)]
255pub struct Text {
256 spans: Vec<Span>,
257 last_style: Cell<Option<Style>>,
258 last_styled_spans: RefCell<Arc<[text::Span<'static, Uri>]>>,
259}
260
261impl Text {
262 fn new(spans: Vec<Span>) -> Self {
263 Self {
264 spans,
265 last_style: Cell::default(),
266 last_styled_spans: RefCell::default(),
267 }
268 }
269
270 pub fn spans(&self, style: Style) -> Arc<[text::Span<'static, Uri>]> {
275 if Some(style) != self.last_style.get() {
276 *self.last_styled_spans.borrow_mut() =
277 self.spans.iter().map(|span| span.view(&style)).collect();
278
279 self.last_style.set(Some(style));
280 }
281
282 self.last_styled_spans.borrow().clone()
283 }
284}
285
286#[derive(Debug, Clone)]
287enum Span {
288 Standard {
289 text: String,
290 strikethrough: bool,
291 link: Option<Uri>,
292 strong: bool,
293 emphasis: bool,
294 code: bool,
295 },
296 #[cfg(feature = "highlighter")]
297 Highlight {
298 text: String,
299 color: Option<Color>,
300 font: Option<Font>,
301 },
302}
303
304impl Span {
305 fn view(&self, style: &Style) -> text::Span<'static, Uri> {
306 match self {
307 Span::Standard {
308 text,
309 strikethrough,
310 link,
311 strong,
312 emphasis,
313 code,
314 } => {
315 let span = span(text.clone()).strikethrough(*strikethrough);
316
317 let span = if *code {
318 span.font(style.inline_code_font)
319 .color(style.inline_code_color)
320 .background(style.inline_code_highlight.background)
321 .border(style.inline_code_highlight.border)
322 .padding(style.inline_code_padding)
323 } else if *strong || *emphasis {
324 span.font(Font {
325 weight: if *strong {
326 font::Weight::Bold
327 } else {
328 font::Weight::Normal
329 },
330 style: if *emphasis {
331 font::Style::Italic
332 } else {
333 font::Style::Normal
334 },
335 ..style.font
336 })
337 } else {
338 span.font(style.font)
339 };
340
341 if let Some(link) = link.as_ref() {
342 span.color(style.link_color).link(link.clone())
343 } else {
344 span
345 }
346 }
347 #[cfg(feature = "highlighter")]
348 Span::Highlight { text, color, font } => {
349 span(text.clone()).color_maybe(*color).font_maybe(*font)
350 }
351 }
352 }
353}
354
355#[derive(Debug, Clone)]
357pub enum Bullet {
358 Point {
360 items: Vec<Item>,
362 },
363 Task {
365 items: Vec<Item>,
367 done: bool,
369 },
370}
371
372impl Bullet {
373 fn items(&self) -> &[Item] {
374 match self {
375 Bullet::Point { items } | Bullet::Task { items, .. } => items,
376 }
377 }
378
379 fn push(&mut self, item: Item) {
380 let (Bullet::Point { items } | Bullet::Task { items, .. }) = self;
381
382 items.push(item);
383 }
384}
385
386pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
427 parse_with(State::default(), markdown)
428 .map(|(item, _source, _broken_links)| item)
429}
430
431#[derive(Debug, Default)]
432struct State {
433 leftover: String,
434 references: HashMap<String, String>,
435 images: HashSet<Uri>,
436 #[cfg(feature = "highlighter")]
437 highlighter: Option<Highlighter>,
438}
439
440#[cfg(feature = "highlighter")]
441#[derive(Debug)]
442struct Highlighter {
443 lines: Vec<(String, Vec<Span>)>,
444 language: String,
445 parser: iced_highlighter::Stream,
446 current: usize,
447}
448
449#[cfg(feature = "highlighter")]
450impl Highlighter {
451 pub fn new(language: &str) -> Self {
452 Self {
453 lines: Vec::new(),
454 parser: iced_highlighter::Stream::new(
455 &iced_highlighter::Settings {
456 theme: iced_highlighter::Theme::Base16Ocean,
457 token: language.to_owned(),
458 },
459 ),
460 language: language.to_owned(),
461 current: 0,
462 }
463 }
464
465 pub fn prepare(&mut self) {
466 self.current = 0;
467 }
468
469 pub fn highlight_line(&mut self, text: &str) -> &[Span] {
470 match self.lines.get(self.current) {
471 Some(line) if line.0 == text => {}
472 _ => {
473 if self.current + 1 < self.lines.len() {
474 log::debug!("Resetting highlighter...");
475 self.parser.reset();
476 self.lines.truncate(self.current);
477
478 for line in &self.lines {
479 log::debug!(
480 "Refeeding {n} lines",
481 n = self.lines.len()
482 );
483
484 let _ = self.parser.highlight_line(&line.0);
485 }
486 }
487
488 log::trace!("Parsing: {text}", text = text.trim_end());
489
490 if self.current + 1 < self.lines.len() {
491 self.parser.commit();
492 }
493
494 let mut spans = Vec::new();
495
496 for (range, highlight) in self.parser.highlight_line(text) {
497 spans.push(Span::Highlight {
498 text: text[range].to_owned(),
499 color: highlight.color(),
500 font: highlight.font(),
501 });
502 }
503
504 if self.current + 1 == self.lines.len() {
505 let _ = self.lines.pop();
506 }
507
508 self.lines.push((text.to_owned(), spans));
509 }
510 }
511
512 self.current += 1;
513
514 &self
515 .lines
516 .get(self.current - 1)
517 .expect("Line must be parsed")
518 .1
519 }
520}
521
522fn parse_with<'a>(
523 mut state: impl BorrowMut<State> + 'a,
524 markdown: &'a str,
525) -> impl Iterator<Item = (Item, &'a str, HashSet<String>)> + 'a {
526 enum Scope {
527 List(List),
528 Quote(Vec<Item>),
529 Table {
530 alignment: Vec<pulldown_cmark::Alignment>,
531 columns: Vec<Column>,
532 rows: Vec<Row>,
533 current: Vec<Item>,
534 },
535 }
536
537 struct List {
538 start: Option<u64>,
539 bullets: Vec<Bullet>,
540 }
541
542 let broken_links = Rc::new(RefCell::new(HashSet::new()));
543
544 let mut spans = Vec::new();
545 let mut code = String::new();
546 let mut code_language = None;
547 let mut code_lines = Vec::new();
548 let mut strong = false;
549 let mut emphasis = false;
550 let mut strikethrough = false;
551 let mut metadata = false;
552 let mut code_block = false;
553 let mut link = None;
554 let mut image = None;
555 let mut stack = Vec::new();
556
557 #[cfg(feature = "highlighter")]
558 let mut highlighter = None;
559
560 let parser = pulldown_cmark::Parser::new_with_broken_link_callback(
561 markdown,
562 pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS
563 | pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS
564 | pulldown_cmark::Options::ENABLE_TABLES
565 | pulldown_cmark::Options::ENABLE_STRIKETHROUGH
566 | pulldown_cmark::Options::ENABLE_TASKLISTS,
567 {
568 let references = state.borrow().references.clone();
569 let broken_links = broken_links.clone();
570
571 Some(move |broken_link: pulldown_cmark::BrokenLink<'_>| {
572 if let Some(reference) =
573 references.get(broken_link.reference.as_ref())
574 {
575 Some((
576 pulldown_cmark::CowStr::from(reference.to_owned()),
577 broken_link.reference.into_static(),
578 ))
579 } else {
580 let _ = RefCell::borrow_mut(&broken_links)
581 .insert(broken_link.reference.into_string());
582
583 None
584 }
585 })
586 },
587 );
588
589 let references = &mut state.borrow_mut().references;
590
591 for reference in parser.reference_definitions().iter() {
592 let _ = references
593 .insert(reference.0.to_owned(), reference.1.dest.to_string());
594 }
595
596 let produce = move |state: &mut State,
597 stack: &mut Vec<Scope>,
598 item,
599 source: Range<usize>| {
600 if let Some(scope) = stack.last_mut() {
601 match scope {
602 Scope::List(list) => {
603 list.bullets.last_mut().expect("item context").push(item);
604 }
605 Scope::Quote(items) => {
606 items.push(item);
607 }
608 Scope::Table { current, .. } => {
609 current.push(item);
610 }
611 }
612
613 None
614 } else {
615 state.leftover = markdown[source.start..].to_owned();
616
617 Some((
618 item,
619 &markdown[source.start..source.end],
620 broken_links.take(),
621 ))
622 }
623 };
624
625 let parser = parser.into_offset_iter();
626
627 #[allow(clippy::drain_collect)]
629 parser.filter_map(move |(event, source)| match event {
630 pulldown_cmark::Event::Start(tag) => match tag {
631 pulldown_cmark::Tag::Strong if !metadata => {
632 strong = true;
633 None
634 }
635 pulldown_cmark::Tag::Emphasis if !metadata => {
636 emphasis = true;
637 None
638 }
639 pulldown_cmark::Tag::Strikethrough if !metadata => {
640 strikethrough = true;
641 None
642 }
643 pulldown_cmark::Tag::Link { dest_url, .. } if !metadata => {
644 link = Some(dest_url.into_string());
645 None
646 }
647 pulldown_cmark::Tag::Image {
648 dest_url, title, ..
649 } if !metadata => {
650 image = Some((dest_url.into_string(), title.into_string()));
651 None
652 }
653 pulldown_cmark::Tag::List(first_item) if !metadata => {
654 let prev = if spans.is_empty() {
655 None
656 } else {
657 produce(
658 state.borrow_mut(),
659 &mut stack,
660 Item::Paragraph(Text::new(spans.drain(..).collect())),
661 source,
662 )
663 };
664
665 stack.push(Scope::List(List {
666 start: first_item,
667 bullets: Vec::new(),
668 }));
669
670 prev
671 }
672 pulldown_cmark::Tag::Item => {
673 if let Some(Scope::List(list)) = stack.last_mut() {
674 list.bullets.push(Bullet::Point { items: Vec::new() });
675 }
676
677 None
678 }
679 pulldown_cmark::Tag::BlockQuote(_kind) if !metadata => {
680 let prev = 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 stack.push(Scope::Quote(Vec::new()));
692
693 prev
694 }
695 pulldown_cmark::Tag::CodeBlock(
696 pulldown_cmark::CodeBlockKind::Fenced(language),
697 ) if !metadata => {
698 #[cfg(feature = "highlighter")]
699 {
700 highlighter = Some({
701 let mut highlighter = state
702 .borrow_mut()
703 .highlighter
704 .take()
705 .filter(|highlighter| {
706 highlighter.language == language.as_ref()
707 })
708 .unwrap_or_else(|| {
709 Highlighter::new(
710 language
711 .split(',')
712 .next()
713 .unwrap_or_default(),
714 )
715 });
716
717 highlighter.prepare();
718
719 highlighter
720 });
721 }
722
723 code_block = true;
724 code_language =
725 (!language.is_empty()).then(|| language.into_string());
726
727 if spans.is_empty() {
728 None
729 } else {
730 produce(
731 state.borrow_mut(),
732 &mut stack,
733 Item::Paragraph(Text::new(spans.drain(..).collect())),
734 source,
735 )
736 }
737 }
738 pulldown_cmark::Tag::MetadataBlock(_) => {
739 metadata = true;
740 None
741 }
742 pulldown_cmark::Tag::Table(alignment) => {
743 stack.push(Scope::Table {
744 columns: Vec::with_capacity(alignment.len()),
745 alignment,
746 current: Vec::new(),
747 rows: Vec::new(),
748 });
749
750 None
751 }
752 pulldown_cmark::Tag::TableHead => {
753 strong = true;
754 None
755 }
756 pulldown_cmark::Tag::TableRow => {
757 let Scope::Table { rows, .. } = stack.last_mut()? else {
758 return None;
759 };
760
761 rows.push(Row { cells: Vec::new() });
762 None
763 }
764 _ => None,
765 },
766 pulldown_cmark::Event::End(tag) => match tag {
767 pulldown_cmark::TagEnd::Heading(level) if !metadata => produce(
768 state.borrow_mut(),
769 &mut stack,
770 Item::Heading(level, Text::new(spans.drain(..).collect())),
771 source,
772 ),
773 pulldown_cmark::TagEnd::Strong if !metadata => {
774 strong = false;
775 None
776 }
777 pulldown_cmark::TagEnd::Emphasis if !metadata => {
778 emphasis = false;
779 None
780 }
781 pulldown_cmark::TagEnd::Strikethrough if !metadata => {
782 strikethrough = false;
783 None
784 }
785 pulldown_cmark::TagEnd::Link if !metadata => {
786 link = None;
787 None
788 }
789 pulldown_cmark::TagEnd::Paragraph if !metadata => {
790 if spans.is_empty() {
791 None
792 } else {
793 produce(
794 state.borrow_mut(),
795 &mut stack,
796 Item::Paragraph(Text::new(spans.drain(..).collect())),
797 source,
798 )
799 }
800 }
801 pulldown_cmark::TagEnd::Item if !metadata => {
802 if spans.is_empty() {
803 None
804 } else {
805 produce(
806 state.borrow_mut(),
807 &mut stack,
808 Item::Paragraph(Text::new(spans.drain(..).collect())),
809 source,
810 )
811 }
812 }
813 pulldown_cmark::TagEnd::List(_) if !metadata => {
814 let scope = stack.pop()?;
815
816 let Scope::List(list) = scope else {
817 return None;
818 };
819
820 produce(
821 state.borrow_mut(),
822 &mut stack,
823 Item::List {
824 start: list.start,
825 bullets: list.bullets,
826 },
827 source,
828 )
829 }
830 pulldown_cmark::TagEnd::BlockQuote(_kind) if !metadata => {
831 let scope = stack.pop()?;
832
833 let Scope::Quote(quote) = scope else {
834 return None;
835 };
836
837 produce(
838 state.borrow_mut(),
839 &mut stack,
840 Item::Quote(quote),
841 source,
842 )
843 }
844 pulldown_cmark::TagEnd::Image if !metadata => {
845 let (url, title) = image.take()?;
846 let alt = Text::new(spans.drain(..).collect());
847
848 let state = state.borrow_mut();
849 let _ = state.images.insert(url.clone());
850
851 produce(
852 state,
853 &mut stack,
854 Item::Image { url, title, alt },
855 source,
856 )
857 }
858 pulldown_cmark::TagEnd::CodeBlock if !metadata => {
859 code_block = false;
860
861 #[cfg(feature = "highlighter")]
862 {
863 state.borrow_mut().highlighter = highlighter.take();
864 }
865
866 produce(
867 state.borrow_mut(),
868 &mut stack,
869 Item::CodeBlock {
870 language: code_language.take(),
871 code: mem::take(&mut code),
872 lines: code_lines.drain(..).collect(),
873 },
874 source,
875 )
876 }
877 pulldown_cmark::TagEnd::MetadataBlock(_) => {
878 metadata = false;
879 None
880 }
881 pulldown_cmark::TagEnd::Table => {
882 let scope = stack.pop()?;
883
884 let Scope::Table { columns, rows, .. } = scope else {
885 return None;
886 };
887
888 produce(
889 state.borrow_mut(),
890 &mut stack,
891 Item::Table { columns, rows },
892 source,
893 )
894 }
895 pulldown_cmark::TagEnd::TableHead => {
896 strong = false;
897 None
898 }
899 pulldown_cmark::TagEnd::TableCell => {
900 if !spans.is_empty() {
901 let _ = produce(
902 state.borrow_mut(),
903 &mut stack,
904 Item::Paragraph(Text::new(spans.drain(..).collect())),
905 source,
906 );
907 }
908
909 let Scope::Table {
910 alignment,
911 columns,
912 rows,
913 current,
914 } = stack.last_mut()?
915 else {
916 return None;
917 };
918
919 if columns.len() < alignment.len() {
920 columns.push(Column {
921 header: std::mem::take(current),
922 alignment: alignment[columns.len()],
923 });
924 } else {
925 rows.last_mut()
926 .expect("table row")
927 .cells
928 .push(std::mem::take(current));
929 }
930
931 None
932 }
933 _ => None,
934 },
935 pulldown_cmark::Event::Text(text) if !metadata => {
936 if code_block {
937 code.push_str(&text);
938
939 #[cfg(feature = "highlighter")]
940 if let Some(highlighter) = &mut highlighter {
941 for line in text.lines() {
942 code_lines.push(Text::new(
943 highlighter.highlight_line(line).to_vec(),
944 ));
945 }
946 }
947
948 #[cfg(not(feature = "highlighter"))]
949 for line in text.lines() {
950 code_lines.push(Text::new(vec![Span::Standard {
951 text: line.to_owned(),
952 strong,
953 emphasis,
954 strikethrough,
955 link: link.clone(),
956 code: false,
957 }]));
958 }
959
960 return None;
961 }
962
963 let span = Span::Standard {
964 text: text.into_string(),
965 strong,
966 emphasis,
967 strikethrough,
968 link: link.clone(),
969 code: false,
970 };
971
972 spans.push(span);
973
974 None
975 }
976 pulldown_cmark::Event::Code(code) if !metadata => {
977 let span = Span::Standard {
978 text: code.into_string(),
979 strong,
980 emphasis,
981 strikethrough,
982 link: link.clone(),
983 code: true,
984 };
985
986 spans.push(span);
987 None
988 }
989 pulldown_cmark::Event::SoftBreak if !metadata => {
990 spans.push(Span::Standard {
991 text: String::from(" "),
992 strikethrough,
993 strong,
994 emphasis,
995 link: link.clone(),
996 code: false,
997 });
998 None
999 }
1000 pulldown_cmark::Event::HardBreak if !metadata => {
1001 spans.push(Span::Standard {
1002 text: String::from("\n"),
1003 strikethrough,
1004 strong,
1005 emphasis,
1006 link: link.clone(),
1007 code: false,
1008 });
1009 None
1010 }
1011 pulldown_cmark::Event::Rule => {
1012 produce(state.borrow_mut(), &mut stack, Item::Rule, source)
1013 }
1014 pulldown_cmark::Event::TaskListMarker(done) => {
1015 if let Some(Scope::List(list)) = stack.last_mut()
1016 && let Some(item) = list.bullets.last_mut()
1017 && let Bullet::Point { items } = item
1018 {
1019 *item = Bullet::Task {
1020 items: std::mem::take(items),
1021 done,
1022 };
1023 }
1024
1025 None
1026 }
1027 _ => None,
1028 })
1029}
1030
1031#[derive(Debug, Clone, Copy)]
1033pub struct Settings {
1034 pub text_size: Pixels,
1036 pub h1_size: Pixels,
1038 pub h2_size: Pixels,
1040 pub h3_size: Pixels,
1042 pub h4_size: Pixels,
1044 pub h5_size: Pixels,
1046 pub h6_size: Pixels,
1048 pub code_size: Pixels,
1050 pub spacing: Pixels,
1052 pub style: Style,
1054}
1055
1056impl Settings {
1057 pub fn with_style(style: impl Into<Style>) -> Self {
1059 Self::with_text_size(16, style)
1060 }
1061
1062 pub fn with_text_size(
1068 text_size: impl Into<Pixels>,
1069 style: impl Into<Style>,
1070 ) -> Self {
1071 let text_size = text_size.into();
1072
1073 Self {
1074 text_size,
1075 h1_size: text_size * 2.0,
1076 h2_size: text_size * 1.75,
1077 h3_size: text_size * 1.5,
1078 h4_size: text_size * 1.25,
1079 h5_size: text_size,
1080 h6_size: text_size,
1081 code_size: text_size * 0.75,
1082 spacing: text_size * 0.875,
1083 style: style.into(),
1084 }
1085 }
1086}
1087
1088impl From<&Theme> for Settings {
1089 fn from(theme: &Theme) -> Self {
1090 Self::with_style(Style::from(theme))
1091 }
1092}
1093
1094impl From<Theme> for Settings {
1095 fn from(theme: Theme) -> Self {
1096 Self::with_style(Style::from(theme))
1097 }
1098}
1099
1100#[derive(Debug, Clone, Copy, PartialEq)]
1102pub struct Style {
1103 pub font: Font,
1105 pub inline_code_highlight: Highlight,
1107 pub inline_code_padding: Padding,
1109 pub inline_code_color: Color,
1111 pub inline_code_font: Font,
1113 pub code_block_font: Font,
1115 pub link_color: Color,
1117}
1118
1119impl Style {
1120 pub fn from_palette(palette: theme::Palette) -> Self {
1122 Self {
1123 font: Font::default(),
1124 inline_code_padding: padding::left(1).right(1),
1125 inline_code_highlight: Highlight {
1126 background: color!(0x111111).into(),
1127 border: border::rounded(4),
1128 },
1129 inline_code_color: Color::WHITE,
1130 inline_code_font: Font::MONOSPACE,
1131 code_block_font: Font::MONOSPACE,
1132 link_color: palette.primary,
1133 }
1134 }
1135}
1136
1137impl From<theme::Palette> for Style {
1138 fn from(palette: theme::Palette) -> Self {
1139 Self::from_palette(palette)
1140 }
1141}
1142
1143impl From<&Theme> for Style {
1144 fn from(theme: &Theme) -> Self {
1145 Self::from_palette(theme.palette())
1146 }
1147}
1148
1149impl From<Theme> for Style {
1150 fn from(theme: Theme) -> Self {
1151 Self::from_palette(theme.palette())
1152 }
1153}
1154
1155pub fn view<'a, Theme, Renderer>(
1198 items: impl IntoIterator<Item = &'a Item>,
1199 settings: impl Into<Settings>,
1200) -> Element<'a, Uri, Theme, Renderer>
1201where
1202 Theme: Catalog + 'a,
1203 Renderer: core::text::Renderer<Font = Font> + 'a,
1204{
1205 view_with(items, settings, &DefaultViewer)
1206}
1207
1208pub fn view_with<'a, Message, Theme, Renderer>(
1214 items: impl IntoIterator<Item = &'a Item>,
1215 settings: impl Into<Settings>,
1216 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1217) -> Element<'a, Message, Theme, Renderer>
1218where
1219 Message: 'a,
1220 Theme: Catalog + 'a,
1221 Renderer: core::text::Renderer<Font = Font> + 'a,
1222{
1223 let settings = settings.into();
1224
1225 let blocks = items
1226 .into_iter()
1227 .enumerate()
1228 .map(|(i, item_)| item(viewer, settings, item_, i));
1229
1230 Element::new(column(blocks).spacing(settings.spacing))
1231}
1232
1233pub fn item<'a, Message, Theme, Renderer>(
1235 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1236 settings: Settings,
1237 item: &'a Item,
1238 index: usize,
1239) -> Element<'a, Message, Theme, Renderer>
1240where
1241 Message: 'a,
1242 Theme: Catalog + 'a,
1243 Renderer: core::text::Renderer<Font = Font> + 'a,
1244{
1245 match item {
1246 Item::Image { url, title, alt } => {
1247 viewer.image(settings, url, title, alt)
1248 }
1249 Item::Heading(level, text) => {
1250 viewer.heading(settings, level, text, index)
1251 }
1252 Item::Paragraph(text) => viewer.paragraph(settings, text),
1253 Item::CodeBlock {
1254 language,
1255 code,
1256 lines,
1257 } => viewer.code_block(settings, language.as_deref(), code, lines),
1258 Item::List {
1259 start: None,
1260 bullets,
1261 } => viewer.unordered_list(settings, bullets),
1262 Item::List {
1263 start: Some(start),
1264 bullets,
1265 } => viewer.ordered_list(settings, *start, bullets),
1266 Item::Quote(quote) => viewer.quote(settings, quote),
1267 Item::Rule => viewer.rule(settings),
1268 Item::Table { columns, rows } => viewer.table(settings, columns, rows),
1269 }
1270}
1271
1272pub fn heading<'a, Message, Theme, Renderer>(
1274 settings: Settings,
1275 level: &'a HeadingLevel,
1276 text: &'a Text,
1277 index: usize,
1278 on_link_click: impl Fn(Uri) -> Message + 'a,
1279) -> Element<'a, Message, Theme, Renderer>
1280where
1281 Message: 'a,
1282 Theme: Catalog + 'a,
1283 Renderer: core::text::Renderer<Font = Font> + 'a,
1284{
1285 let Settings {
1286 h1_size,
1287 h2_size,
1288 h3_size,
1289 h4_size,
1290 h5_size,
1291 h6_size,
1292 text_size,
1293 ..
1294 } = settings;
1295
1296 container(
1297 rich_text(text.spans(settings.style))
1298 .on_link_click(on_link_click)
1299 .size(match level {
1300 pulldown_cmark::HeadingLevel::H1 => h1_size,
1301 pulldown_cmark::HeadingLevel::H2 => h2_size,
1302 pulldown_cmark::HeadingLevel::H3 => h3_size,
1303 pulldown_cmark::HeadingLevel::H4 => h4_size,
1304 pulldown_cmark::HeadingLevel::H5 => h5_size,
1305 pulldown_cmark::HeadingLevel::H6 => h6_size,
1306 }),
1307 )
1308 .padding(padding::top(if index > 0 {
1309 text_size / 2.0
1310 } else {
1311 Pixels::ZERO
1312 }))
1313 .into()
1314}
1315
1316pub fn paragraph<'a, Message, Theme, Renderer>(
1318 settings: Settings,
1319 text: &Text,
1320 on_link_click: impl Fn(Uri) -> Message + 'a,
1321) -> Element<'a, Message, Theme, Renderer>
1322where
1323 Message: 'a,
1324 Theme: Catalog + 'a,
1325 Renderer: core::text::Renderer<Font = Font> + 'a,
1326{
1327 rich_text(text.spans(settings.style))
1328 .size(settings.text_size)
1329 .on_link_click(on_link_click)
1330 .into()
1331}
1332
1333pub fn unordered_list<'a, Message, Theme, Renderer>(
1336 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1337 settings: Settings,
1338 bullets: &'a [Bullet],
1339) -> Element<'a, Message, Theme, Renderer>
1340where
1341 Message: 'a,
1342 Theme: Catalog + 'a,
1343 Renderer: core::text::Renderer<Font = Font> + 'a,
1344{
1345 column(bullets.iter().map(|bullet| {
1346 row![
1347 match bullet {
1348 Bullet::Point { .. } => {
1349 text("•").size(settings.text_size).into()
1350 }
1351 Bullet::Task { done, .. } => {
1352 Element::from(
1353 container(checkbox(*done).size(settings.text_size))
1354 .center_y(
1355 text::LineHeight::default()
1356 .to_absolute(settings.text_size),
1357 ),
1358 )
1359 }
1360 },
1361 view_with(
1362 bullet.items(),
1363 Settings {
1364 spacing: settings.spacing * 0.6,
1365 ..settings
1366 },
1367 viewer,
1368 )
1369 ]
1370 .spacing(settings.spacing)
1371 .into()
1372 }))
1373 .spacing(settings.spacing * 0.75)
1374 .padding([0.0, settings.spacing.0])
1375 .into()
1376}
1377
1378pub fn ordered_list<'a, Message, Theme, Renderer>(
1381 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1382 settings: Settings,
1383 start: u64,
1384 bullets: &'a [Bullet],
1385) -> Element<'a, Message, Theme, Renderer>
1386where
1387 Message: 'a,
1388 Theme: Catalog + 'a,
1389 Renderer: core::text::Renderer<Font = Font> + 'a,
1390{
1391 let digits = ((start + bullets.len() as u64).max(1) as f32)
1392 .log10()
1393 .ceil();
1394
1395 column(bullets.iter().enumerate().map(|(i, bullet)| {
1396 row![
1397 text!("{}.", i as u64 + start)
1398 .size(settings.text_size)
1399 .align_x(alignment::Horizontal::Right)
1400 .width(settings.text_size * ((digits / 2.0).ceil() + 1.0)),
1401 view_with(
1402 bullet.items(),
1403 Settings {
1404 spacing: settings.spacing * 0.6,
1405 ..settings
1406 },
1407 viewer,
1408 )
1409 ]
1410 .spacing(settings.spacing)
1411 .into()
1412 }))
1413 .spacing(settings.spacing * 0.75)
1414 .into()
1415}
1416
1417pub fn code_block<'a, Message, Theme, Renderer>(
1419 settings: Settings,
1420 lines: &'a [Text],
1421 on_link_click: impl Fn(Uri) -> Message + Clone + 'a,
1422) -> Element<'a, Message, Theme, Renderer>
1423where
1424 Message: 'a,
1425 Theme: Catalog + 'a,
1426 Renderer: core::text::Renderer<Font = Font> + 'a,
1427{
1428 container(
1429 scrollable(
1430 container(column(lines.iter().map(|line| {
1431 rich_text(line.spans(settings.style))
1432 .on_link_click(on_link_click.clone())
1433 .font(settings.style.code_block_font)
1434 .size(settings.code_size)
1435 .into()
1436 })))
1437 .padding(settings.code_size),
1438 )
1439 .direction(scrollable::Direction::Horizontal(
1440 scrollable::Scrollbar::default()
1441 .width(settings.code_size / 2)
1442 .scroller_width(settings.code_size / 2),
1443 )),
1444 )
1445 .width(Length::Fill)
1446 .padding(settings.code_size / 4)
1447 .class(Theme::code_block())
1448 .into()
1449}
1450
1451pub fn quote<'a, Message, Theme, Renderer>(
1453 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1454 settings: Settings,
1455 contents: &'a [Item],
1456) -> Element<'a, Message, Theme, Renderer>
1457where
1458 Message: 'a,
1459 Theme: Catalog + 'a,
1460 Renderer: core::text::Renderer<Font = Font> + 'a,
1461{
1462 row![
1463 rule::vertical(4),
1464 column(
1465 contents
1466 .iter()
1467 .enumerate()
1468 .map(|(i, content)| item(viewer, settings, content, i)),
1469 )
1470 .spacing(settings.spacing.0),
1471 ]
1472 .height(Length::Shrink)
1473 .spacing(settings.spacing.0)
1474 .into()
1475}
1476
1477pub fn rule<'a, Message, Theme, Renderer>()
1479-> Element<'a, Message, Theme, Renderer>
1480where
1481 Message: 'a,
1482 Theme: Catalog + 'a,
1483 Renderer: core::text::Renderer<Font = Font> + 'a,
1484{
1485 rule::horizontal(2).into()
1486}
1487
1488pub fn table<'a, Message, Theme, Renderer>(
1490 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1491 settings: Settings,
1492 columns: &'a [Column],
1493 rows: &'a [Row],
1494) -> Element<'a, Message, Theme, Renderer>
1495where
1496 Message: 'a,
1497 Theme: Catalog + 'a,
1498 Renderer: core::text::Renderer<Font = Font> + 'a,
1499{
1500 use crate::table;
1501
1502 let table = table(
1503 columns.iter().enumerate().map(move |(i, column)| {
1504 table::column(
1505 items(viewer, settings, &column.header),
1506 move |row: &Row| {
1507 if let Some(cells) = row.cells.get(i) {
1508 items(viewer, settings, cells)
1509 } else {
1510 text("").into()
1511 }
1512 },
1513 )
1514 .align_x(match column.alignment {
1515 pulldown_cmark::Alignment::None
1516 | pulldown_cmark::Alignment::Left => {
1517 alignment::Horizontal::Left
1518 }
1519 pulldown_cmark::Alignment::Center => {
1520 alignment::Horizontal::Center
1521 }
1522 pulldown_cmark::Alignment::Right => {
1523 alignment::Horizontal::Right
1524 }
1525 })
1526 }),
1527 rows,
1528 )
1529 .padding_x(settings.spacing.0)
1530 .padding_y(settings.spacing.0 / 2.0)
1531 .separator_x(0);
1532
1533 scrollable(table)
1534 .direction(scrollable::Direction::Horizontal(
1535 scrollable::Scrollbar::default(),
1536 ))
1537 .spacing(settings.spacing.0 / 2.0)
1538 .into()
1539}
1540
1541pub fn items<'a, Message, Theme, Renderer>(
1543 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1544 settings: Settings,
1545 items: &'a [Item],
1546) -> Element<'a, Message, Theme, Renderer>
1547where
1548 Message: 'a,
1549 Theme: Catalog + 'a,
1550 Renderer: core::text::Renderer<Font = Font> + 'a,
1551{
1552 column(
1553 items
1554 .iter()
1555 .enumerate()
1556 .map(|(i, content)| item(viewer, settings, content, i)),
1557 )
1558 .spacing(settings.spacing.0)
1559 .into()
1560}
1561
1562pub trait Viewer<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
1564where
1565 Self: Sized + 'a,
1566 Message: 'a,
1567 Theme: Catalog + 'a,
1568 Renderer: core::text::Renderer<Font = Font> + 'a,
1569{
1570 fn on_link_click(url: Uri) -> Message;
1572
1573 fn image(
1577 &self,
1578 settings: Settings,
1579 url: &'a Uri,
1580 title: &'a str,
1581 alt: &Text,
1582 ) -> Element<'a, Message, Theme, Renderer> {
1583 let _url = url;
1584 let _title = title;
1585
1586 container(
1587 rich_text(alt.spans(settings.style))
1588 .on_link_click(Self::on_link_click),
1589 )
1590 .padding(settings.spacing.0)
1591 .class(Theme::code_block())
1592 .into()
1593 }
1594
1595 fn heading(
1599 &self,
1600 settings: Settings,
1601 level: &'a HeadingLevel,
1602 text: &'a Text,
1603 index: usize,
1604 ) -> Element<'a, Message, Theme, Renderer> {
1605 heading(settings, level, text, index, Self::on_link_click)
1606 }
1607
1608 fn paragraph(
1612 &self,
1613 settings: Settings,
1614 text: &Text,
1615 ) -> Element<'a, Message, Theme, Renderer> {
1616 paragraph(settings, text, Self::on_link_click)
1617 }
1618
1619 fn code_block(
1623 &self,
1624 settings: Settings,
1625 language: Option<&'a str>,
1626 code: &'a str,
1627 lines: &'a [Text],
1628 ) -> Element<'a, Message, Theme, Renderer> {
1629 let _language = language;
1630 let _code = code;
1631
1632 code_block(settings, lines, Self::on_link_click)
1633 }
1634
1635 fn unordered_list(
1639 &self,
1640 settings: Settings,
1641 bullets: &'a [Bullet],
1642 ) -> Element<'a, Message, Theme, Renderer> {
1643 unordered_list(self, settings, bullets)
1644 }
1645
1646 fn ordered_list(
1650 &self,
1651 settings: Settings,
1652 start: u64,
1653 bullets: &'a [Bullet],
1654 ) -> Element<'a, Message, Theme, Renderer> {
1655 ordered_list(self, settings, start, bullets)
1656 }
1657
1658 fn quote(
1662 &self,
1663 settings: Settings,
1664 contents: &'a [Item],
1665 ) -> Element<'a, Message, Theme, Renderer> {
1666 quote(self, settings, contents)
1667 }
1668
1669 fn rule(
1673 &self,
1674 _settings: Settings,
1675 ) -> Element<'a, Message, Theme, Renderer> {
1676 rule()
1677 }
1678
1679 fn table(
1683 &self,
1684 settings: Settings,
1685 columns: &'a [Column],
1686 rows: &'a [Row],
1687 ) -> Element<'a, Message, Theme, Renderer> {
1688 table(self, settings, columns, rows)
1689 }
1690}
1691
1692#[derive(Debug, Clone, Copy)]
1693struct DefaultViewer;
1694
1695impl<'a, Theme, Renderer> Viewer<'a, Uri, Theme, Renderer> for DefaultViewer
1696where
1697 Theme: Catalog + 'a,
1698 Renderer: core::text::Renderer<Font = Font> + 'a,
1699{
1700 fn on_link_click(url: Uri) -> Uri {
1701 url
1702 }
1703}
1704
1705pub trait Catalog:
1707 container::Catalog
1708 + scrollable::Catalog
1709 + text::Catalog
1710 + crate::rule::Catalog
1711 + checkbox::Catalog
1712 + crate::table::Catalog
1713{
1714 fn code_block<'a>() -> <Self as container::Catalog>::Class<'a>;
1716}
1717
1718impl Catalog for Theme {
1719 fn code_block<'a>() -> <Self as container::Catalog>::Class<'a> {
1720 Box::new(container::dark)
1721 }
1722}