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