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