1use crate::core;
49use crate::core::alignment;
50use crate::core::border;
51use crate::core::font::{self, Font};
52use crate::core::padding;
53use crate::core::text::LineHeight;
54use crate::core::theme;
55use crate::core::{Code, Color, Length, Padding, Pixels, Theme};
56use crate::{
57 Widget, center_x, checkbox, column, container, rich_text, row, rule, scrollable, span, text,
58};
59
60use std::borrow::BorrowMut;
61use std::cell::RefCell;
62use std::collections::hash_map::Entry;
63use std::collections::{HashMap, HashSet};
64use std::mem;
65use std::ops::Range;
66use std::rc::Rc;
67use std::sync::Arc;
68
69pub use core::text::{Highlight, Highlighter};
70pub use pulldown_cmark::HeadingLevel;
71
72pub type Uri = String;
76
77#[derive(Debug, Default)]
79pub struct Content {
80 raw: String,
83 items: Vec<Item>,
85 starts: Vec<usize>,
89 base: Vec<usize>,
93 window: usize,
96 pending_block: bool,
101 pending_block_start: Option<usize>,
105 incomplete: HashMap<usize, Section>,
106 state: State,
107}
108
109#[derive(Debug)]
110struct Section {
111 start: usize,
114 end: Option<usize>,
117 broken_links: HashSet<String>,
118 references: HashMap<String, String>,
121 item: usize,
127}
128
129impl Content {
130 pub fn new() -> Self {
132 Self::default()
133 }
134
135 pub fn parse(markdown: &str) -> Self {
137 let mut content = Self::new();
138 content.push_str(markdown);
139 content
140 }
141
142 pub fn push_str(&mut self, markdown: &str) {
154 if markdown.is_empty() {
155 return;
156 }
157
158 self.raw.push_str(markdown);
159
160 let block_live = self
168 .pending_block_start
169 .map(|start| Self::metadata_block_live(&self.raw[start..]))
170 .unwrap_or(false);
171 let block_closed = self.pending_block && !block_live;
175 let mut input_start = if self.pending_block || block_live {
176 self.pending_block_start.unwrap_or(self.window)
177 } else {
178 self.window
179 };
180
181 if !self.pending_block
192 && !block_live
193 && let [.., prev, last] = self.items.as_slice()
194 && matches!(last, Item::List { .. })
195 && matches!(prev, Item::List { .. } | Item::Quote(_))
196 {
197 input_start = input_start.min(self.base[self.base.len() - 2]);
198 }
199 let tail = &self.raw[input_start..];
200 let trimmed = tail.trim_end();
201 let mut input = if trimmed.ends_with('|') {
202 trimmed.trim_end_matches('|')
203 } else {
204 tail
205 };
206
207 let last = self.items.pop();
212 let _ = self.starts.pop();
213 let old_last_base = self.base.pop();
217 while self
218 .starts
219 .last()
220 .is_some_and(|start| *start >= input_start)
221 {
222 let _ = self.items.pop();
223 let _ = self.starts.pop();
224 let _ = self.base.pop();
225 }
226
227 let mut items: Vec<_> = parse_with(&mut self.state, input, input_start).collect();
229
230 let last_is_list = matches!(last.as_ref(), Some(Item::List { .. }));
238 let mut merged = false;
239 if let Some(Item::List { start, bullets, .. }) = last
240 && let Some((first_item, _, _)) = items.first_mut()
241 && let Item::List { bullets: new, .. } = first_item
242 && old_last_base.is_some_and(|base| input_start > base)
243 {
244 let mut bullets = bullets;
246 let _ = bullets.pop();
247 bullets.extend(mem::take(new));
248 *first_item = Item::List { start, bullets };
249 merged = true;
250 } else if last_is_list {
251 let base = old_last_base.expect("a list has a base").min(input_start);
261 let tail = &self.raw[base..];
262 let trimmed = tail.trim_end();
263 input = if trimmed.ends_with('|') {
264 trimmed.trim_end_matches('|')
265 } else {
266 tail
267 };
268 input_start = base;
269 items = parse_with(&mut self.state, input, input_start).collect();
270 } else if let Some((Item::List { .. }, 0, _)) = items.first()
271 && let Some(Item::List { .. }) = self.items.last()
272 && let Some(base) = self.base.last().copied()
273 {
274 let tail = &self.raw[base..];
282 let trimmed = tail.trim_end();
283 input = if trimmed.ends_with('|') {
284 trimmed.trim_end_matches('|')
285 } else {
286 tail
287 };
288
289 let reparsed: Vec<_> = parse_with(&mut self.state, input, base).collect();
290
291 if let Some((Item::List { .. }, _, _)) = reparsed.first() {
292 input_start = base;
293 items = reparsed;
294 let _ = self.items.pop();
296 let _ = self.starts.pop();
297 let _ = self.base.pop();
298 }
299 }
300
301 let mut newest_rule_start: Option<usize> = None;
305
306 if items.is_empty() {
307 self.window = self.starts.last().copied().unwrap_or(input_start);
313 } else {
314 let starts: Vec<usize> = items
317 .iter()
318 .map(|(_, start, _)| input_start + *start)
319 .collect();
320
321 for (i, (item, _start, broken_links)) in items.into_iter().enumerate() {
322 let start = starts[i];
323 let base = if i == 0 && merged {
327 old_last_base.expect("a merged list has a base")
328 } else {
329 start
330 };
331
332 if !broken_links.is_empty() {
333 let index = self.items.len();
335
336 let covers = starts.get(i + 1).is_some_and(|next| *next <= start);
340
341 let (section_start, end) = if covers {
346 (starts[i + 1], starts.get(i + 2).copied())
347 } else {
348 (base, starts.get(i + 1).copied())
349 };
350
351 match self.incomplete.entry(index) {
354 Entry::Occupied(mut entry) => {
355 let section = entry.get_mut();
356 section.broken_links.extend(broken_links);
357 section.start = section_start;
362 section.end = end;
363 }
364 Entry::Vacant(entry) => {
365 let item = starts
372 .iter()
373 .take(i)
374 .copied()
375 .filter(|start| *start >= section_start)
376 .count();
377 let _ = entry.insert(Section {
378 start: section_start,
379 end,
380 broken_links,
381 references: HashMap::new(),
382 item,
383 });
384 }
385 }
386 }
387
388 if matches!(item, Item::Rule) && Self::metadata_delimiter(&self.raw, start) {
389 newest_rule_start = Some(start);
394 }
395
396 self.items.push(item);
397 self.starts.push(start);
398 self.base.push(base);
399 }
400
401 self.window = input_start + self.state.window.unwrap_or(input.len());
402 }
403
404 self.pending_block_start = newest_rule_start;
408 self.pending_block = newest_rule_start
409 .map(|start| Self::metadata_block_live(&self.raw[start..]))
410 .unwrap_or(false);
411
412 if block_closed {
417 self.recompute_references();
418 }
419
420 self.fix_section_ends();
423
424 self.resolve_sections();
427
428 self.state.images = self
432 .items
433 .iter()
434 .filter_map(|item| match item {
435 Item::Image { url, .. } => Some(url.clone()),
436 _ => None,
437 })
438 .collect();
439 }
440
441 fn metadata_block_live(source: &str) -> bool {
451 let (first, rest) = match source.find('\n') {
453 Some(end) => (&source[..end], &source[end + 1..]),
454 None => return false,
455 };
456
457 let first = first.trim_end();
459 if first != "---" && first != "+++" {
460 return false;
461 }
462
463 let Some(second_end) = rest.find('\n') else {
467 return true;
468 };
469 let second = &rest[..second_end];
470 if second.trim().is_empty() {
471 return false;
472 }
473
474 let closed = rest.lines().any(|line| {
477 let line = line.trim_end();
478 if first == "+++" {
479 line == "+++"
480 } else {
481 line == "---" || line == "..."
482 }
483 });
484
485 !closed
486 }
487
488 fn metadata_delimiter(source: &str, start: usize) -> bool {
491 let line = &source[start..];
492 let end = line.find('\n').unwrap_or(line.len());
493 let line = line[..end].trim_end();
494 line == "---" || line == "+++"
495 }
496
497 fn recompute_references(&mut self) {
505 let parser = pulldown_cmark::Parser::new_ext(&self.raw, options());
506 let definitions = parser.reference_definitions();
507
508 self.state.references.clear();
509 self.state.references_staged.clear();
510
511 absorb_references(
512 &self.raw,
513 definitions,
514 &mut self.state.references,
515 &mut self.state.references_staged,
516 );
517 }
518
519 fn fix_section_ends(&mut self) {
523 if self.incomplete.is_empty() {
524 return;
525 }
526
527 for (index, section) in self.incomplete.iter_mut() {
528 if section.end.is_none() && *index + 1 < self.items.len() {
529 section.end = self.starts[*index + 1..]
534 .iter()
535 .copied()
536 .find(|end| *end > section.start);
537 }
538 }
539 }
540
541 fn resolve_sections(&mut self) {
545 if self.incomplete.is_empty() {
546 return;
547 }
548
549 self.incomplete.retain(|index, section| {
550 if self.items.len() <= *index {
551 return false;
553 }
554
555 let mut newly_resolved = Vec::new();
557 section.broken_links.retain(|link| {
558 if self.state.references.contains_key(link) {
559 newly_resolved.push(link.clone());
560 false
561 } else {
562 true
563 }
564 });
565
566 let needs_reparse = !newly_resolved.is_empty()
570 || section.references.iter().any(|(link, dest)| {
571 match self.state.references.get(link) {
572 Some(new_dest) => new_dest != dest,
573 None => true,
574 }
575 });
576
577 if needs_reparse {
578 let mut state = State {
579 window: None,
580 references: self.state.references.clone(),
581 references_staged: HashSet::new(),
582 images: HashSet::new(),
583 #[cfg(feature = "highlighter")]
584 parsers: HashMap::new(),
585 };
586
587 let end = section.end.unwrap_or(self.raw.len());
588 let source = &self.raw[section.start..end];
589
590 if let Some((item, _start, broken_links)) =
591 parse_with(&mut state, source, section.start).nth(section.item)
592 {
593 self.items[*index] = item;
594
595 for link in newly_resolved {
599 if let Some(dest) = self.state.references.get(&link)
600 && !broken_links.contains(&link)
601 {
602 let _ = section.references.insert(link, dest.to_owned());
603 }
604 }
605
606 section.broken_links = broken_links;
607 section
608 .references
609 .retain(|link, _| !section.broken_links.contains(link));
610
611 for (link, dest) in &mut section.references {
612 if let Some(new_dest) = self.state.references.get(link) {
613 *dest = new_dest.clone();
614 }
615 }
616 }
617
618 self.state.images.extend(state.images);
619 }
620
621 !section.broken_links.is_empty() || !section.references.is_empty()
623 });
624 }
625
626 pub fn items(&self) -> &[Item] {
630 &self.items
631 }
632
633 pub fn images(&self) -> &HashSet<Uri> {
635 &self.state.images
636 }
637
638 pub fn raw(&self) -> &str {
640 &self.raw
641 }
642}
643
644pub fn sections<'a>(
678 items: &'a [Item],
679) -> impl Iterator<Item = (Option<&'a Item>, &'a [Item])> + 'a {
680 struct Sections<'a> {
681 items: &'a [Item],
683 start: usize,
689 }
690
691 impl<'a> Iterator for Sections<'a> {
692 type Item = (Option<&'a Item>, &'a [Item]);
693
694 fn next(&mut self) -> Option<Self::Item> {
695 let Self { items, start } = self;
696
697 if *start >= items.len() {
698 return None;
699 }
700
701 let heading = if matches!(items[*start], Item::Heading(..)) {
703 Some(*start)
704 } else {
705 None
706 };
707
708 let body_start = heading.map_or(*start, |heading| heading + 1);
711 let next_heading = (body_start..items.len())
712 .find(|&index| matches!(items[index], Item::Heading(..)))
713 .unwrap_or(items.len());
714
715 *start = next_heading;
717
718 Some((
719 heading.map(|index| &items[index]),
720 &items[body_start..next_heading],
721 ))
722 }
723 }
724
725 Sections { items, start: 0 }
726}
727
728#[derive(Debug, Clone)]
730pub enum Item {
731 Heading(pulldown_cmark::HeadingLevel, Text),
733 Paragraph(Text),
735 CodeBlock {
739 language: Option<String>,
741 code: String,
743 lines: Vec<Text>,
745 },
746 List {
748 start: Option<u64>,
750 bullets: Vec<Bullet>,
752 },
753 Image {
755 url: Uri,
757 title: String,
759 alt: Text,
761 },
762 Quote(Vec<Item>),
764 Rule,
766 Table {
768 columns: Vec<Column>,
770 rows: Vec<Row>,
772 },
773}
774
775#[derive(Debug, Clone)]
777pub struct Column {
778 pub header: Vec<Item>,
780 pub alignment: pulldown_cmark::Alignment,
782}
783
784#[derive(Debug, Clone)]
786pub struct Row {
787 cells: Vec<Vec<Item>>,
789}
790
791#[derive(Debug, Clone)]
793pub struct Text {
794 spans: Vec<Span>,
795 last_style: RefCell<Option<(Settings, String, String)>>,
796 last_styled_spans: RefCell<Arc<[text::Span<'static, Uri>]>>,
797}
798
799impl Text {
800 fn new(spans: Vec<Span>) -> Self {
801 Self {
802 spans,
803 last_style: RefCell::default(),
804 last_styled_spans: RefCell::default(),
805 }
806 }
807
808 pub fn spans<Theme: Catalog>(
813 &self,
814 settings: Settings,
815 theme: &Theme,
816 highlighter: &dyn text::Highlighter<Code, Theme>,
817 ) -> Arc<[text::Span<'static, Uri>]> {
818 let is_dirty = self.last_style.borrow().as_ref().is_none_or(
819 |(last_settings, last_theme, last_highlighter)| {
820 &settings != last_settings
821 || theme.id() != last_theme
822 || highlighter.id() != last_highlighter
823 },
824 );
825
826 if is_dirty {
827 *self.last_styled_spans.borrow_mut() = self
828 .spans
829 .iter()
830 .map(|span| span.view(&settings, theme, highlighter))
831 .collect();
832
833 *self.last_style.borrow_mut() =
834 Some((settings, theme.id().to_owned(), highlighter.id().to_owned()));
835 }
836
837 self.last_styled_spans.borrow().clone()
838 }
839}
840
841#[derive(Debug, Clone)]
842enum Span {
843 Standard {
844 text: String,
845 strikethrough: bool,
846 link: Option<Uri>,
847 strong: bool,
848 emphasis: bool,
849 inline_code: bool,
850 },
851 Code {
852 text: String,
853 code: Code,
854 },
855}
856
857impl Span {
858 fn view<Theme: Catalog>(
859 &self,
860 settings: &Settings,
861 theme: &Theme,
862 highlighter: &dyn text::Highlighter<Code, Theme>,
863 ) -> text::Span<'static, Uri> {
864 match self {
865 Span::Standard {
866 text,
867 strikethrough,
868 link,
869 strong,
870 emphasis,
871 inline_code,
872 } => {
873 let span = span(text.clone()).strikethrough(*strikethrough);
874
875 let weight = if *strong {
876 font::Weight::Bold
877 } else {
878 settings.font.weight
879 };
880
881 let style = if *emphasis {
882 font::Style::Italic
883 } else {
884 settings.font.style
885 };
886
887 let span = if *inline_code {
888 let code = theme.code();
889
890 span.font(Font {
891 weight,
892 style,
893 ..settings.inline_code_font
894 })
895 .size(settings.inline_code_size)
896 .color(code.color)
897 .background(code.highlight.background)
898 .border(code.highlight.border)
899 .padding(code.padding)
900 } else {
901 span.font(Font {
902 weight,
903 style,
904 ..settings.font
905 })
906 };
907
908 if let Some(link) = link.as_ref() {
909 span.color(theme.link_color()).link(link.clone())
910 } else {
911 span
912 }
913 }
914 Span::Code { text, code } => {
915 let format = highlighter.highlight(*code, theme);
916
917 span(text.clone())
918 .color_maybe(format.color)
919 .font_maybe(format.style.map(|style| Font {
920 style,
921 ..settings.code_block_font
922 }))
923 }
924 }
925 }
926}
927
928#[derive(Debug, Clone)]
930pub enum Bullet {
931 Point {
933 items: Vec<Item>,
935 },
936 Task {
938 items: Vec<Item>,
940 done: bool,
942 },
943}
944
945impl Bullet {
946 fn items(&self) -> &[Item] {
947 match self {
948 Bullet::Point { items } | Bullet::Task { items, .. } => items,
949 }
950 }
951
952 fn push(&mut self, item: Item) {
953 let (Bullet::Point { items } | Bullet::Task { items, .. }) = self;
954
955 items.push(item);
956 }
957}
958
959pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
1002 parse_with(State::default(), markdown, 0).map(|(item, _start, _broken_links)| item)
1003}
1004
1005#[derive(Debug, Default)]
1006struct State {
1007 window: Option<usize>,
1010 references: HashMap<String, String>,
1014 references_staged: HashSet<String>,
1019 images: HashSet<Uri>,
1020 #[cfg(feature = "highlighter")]
1027 parsers: HashMap<usize, code::Parser>,
1028}
1029
1030fn options() -> pulldown_cmark::Options {
1032 pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS
1033 | pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS
1034 | pulldown_cmark::Options::ENABLE_TABLES
1035 | pulldown_cmark::Options::ENABLE_STRIKETHROUGH
1036 | pulldown_cmark::Options::ENABLE_TASKLISTS
1037}
1038
1039fn absorb_references(
1050 markdown: &str,
1051 definitions: &pulldown_cmark::RefDefs<'_>,
1052 references: &mut HashMap<String, String>,
1053 growing_refs: &mut HashSet<String>,
1054) {
1055 for reference in definitions.iter() {
1056 let name = reference.0.to_string();
1057 let dest = reference.1.dest.to_string();
1058
1059 if markdown[reference.1.span.end..].contains('\n') {
1060 if !references.contains_key(&name) || growing_refs.remove(&name) {
1061 let _ = references.insert(name, dest);
1062 }
1063 } else if growing_refs.contains(&name) {
1064 let _ = references.insert(name, dest);
1066 } else if !references.contains_key(&name) {
1067 let _ = growing_refs.insert(name.clone());
1070 let _ = references.insert(name, dest);
1071 }
1072 }
1073}
1074
1075fn parse_with<'a>(
1076 mut state: impl BorrowMut<State> + 'a,
1077 markdown: &'a str,
1078 _offset: usize,
1079) -> impl Iterator<Item = (Item, usize, HashSet<String>)> + 'a {
1080 enum Scope {
1081 List(List),
1082 Quote(Vec<Item>),
1083 Table {
1084 alignment: Vec<pulldown_cmark::Alignment>,
1085 columns: Vec<Column>,
1086 rows: Vec<Row>,
1087 current: Vec<Item>,
1088 },
1089 }
1090
1091 struct List {
1092 start: Option<u64>,
1093 bullets: Vec<Bullet>,
1094 last_item_start: Option<usize>,
1096 }
1097
1098 let broken_links = Rc::new(RefCell::new(Vec::new()));
1105
1106 let mut spans = Vec::new();
1107 let mut code = String::new();
1108 let mut code_language = None;
1109 let mut code_lines = Vec::new();
1110 let mut strong = false;
1111 let mut emphasis = false;
1112 let mut strikethrough = false;
1113 let mut metadata = false;
1114 let mut code_block = false;
1115 let mut link = None;
1116 let mut image = None;
1117 let mut paragraph_start = None;
1118 let mut stack = Vec::new();
1119
1120 #[cfg(feature = "highlighter")]
1121 let mut code_block_key = None;
1122
1123 let parser = pulldown_cmark::Parser::new_with_broken_link_callback(markdown, options(), {
1124 let references = state.borrow().references.clone();
1125 let broken_links = broken_links.clone();
1126
1127 Some(move |broken_link: pulldown_cmark::BrokenLink<'_>| {
1128 if let Some(reference) = references.get(broken_link.reference.as_ref()) {
1129 Some((
1130 pulldown_cmark::CowStr::from(reference.to_owned()),
1131 broken_link.reference.into_static(),
1132 ))
1133 } else {
1134 RefCell::borrow_mut(&broken_links)
1135 .push((broken_link.span, broken_link.reference.into_string()));
1136
1137 None
1138 }
1139 })
1140 });
1141
1142 {
1143 let state = state.borrow_mut();
1144 absorb_references(
1145 markdown,
1146 parser.reference_definitions(),
1147 &mut state.references,
1148 &mut state.references_staged,
1149 );
1150 }
1151
1152 let produce = move |state: &mut State, stack: &mut Vec<Scope>, item, source: Range<usize>| {
1153 if let Some(scope) = stack.last_mut() {
1154 match scope {
1155 Scope::List(list) => {
1156 list.bullets.last_mut().expect("item context").push(item);
1157 }
1158 Scope::Quote(items) => {
1159 items.push(item);
1160 }
1161 Scope::Table { current, .. } => {
1162 current.push(item);
1163 }
1164 }
1165
1166 None
1167 } else {
1168 state.window = Some(source.start);
1169
1170 let mut links = HashSet::new();
1173 for (span, reference) in RefCell::borrow(&broken_links).iter() {
1174 if source.contains(&span.start) {
1175 let _ = links.insert(reference.clone());
1176 }
1177 }
1178
1179 Some((item, source.start, links))
1180 }
1181 };
1182
1183 let resolve_reference = |state: &mut State,
1189 link_type: pulldown_cmark::LinkType,
1190 id: &str,
1191 dest_url: &pulldown_cmark::CowStr<'a>|
1192 -> String {
1193 match link_type {
1194 pulldown_cmark::LinkType::Reference
1195 | pulldown_cmark::LinkType::ReferenceUnknown
1196 | pulldown_cmark::LinkType::Collapsed
1197 | pulldown_cmark::LinkType::CollapsedUnknown
1198 | pulldown_cmark::LinkType::Shortcut
1199 | pulldown_cmark::LinkType::ShortcutUnknown => state
1200 .references
1201 .get(id)
1202 .cloned()
1203 .unwrap_or_else(|| dest_url.to_string()),
1204 _ => dest_url.to_string(),
1205 }
1206 };
1207
1208 let parser = parser.into_offset_iter();
1209
1210 #[allow(clippy::drain_collect)]
1212 parser.filter_map(move |(event, source)| match event {
1213 pulldown_cmark::Event::Start(tag) => match tag {
1214 pulldown_cmark::Tag::Strong if !metadata => {
1215 strong = true;
1216 None
1217 }
1218 pulldown_cmark::Tag::Emphasis if !metadata => {
1219 emphasis = true;
1220 None
1221 }
1222 pulldown_cmark::Tag::Strikethrough if !metadata => {
1223 strikethrough = true;
1224 None
1225 }
1226 pulldown_cmark::Tag::Link {
1227 link_type,
1228 dest_url,
1229 id,
1230 ..
1231 } if !metadata => {
1232 link = Some(resolve_reference(
1233 state.borrow_mut(),
1234 link_type,
1235 &id,
1236 &dest_url,
1237 ));
1238 None
1239 }
1240 pulldown_cmark::Tag::Paragraph if !metadata => {
1241 paragraph_start = Some(source.start);
1242 None
1243 }
1244 pulldown_cmark::Tag::Image {
1245 link_type,
1246 dest_url,
1247 title,
1248 id,
1249 } if !metadata => {
1250 image = Some((
1251 resolve_reference(state.borrow_mut(), link_type, &id, &dest_url),
1252 title.into_string(),
1253 spans.len(),
1254 ));
1255 None
1256 }
1257 pulldown_cmark::Tag::List(first_item) if !metadata => {
1258 let prev = if spans.is_empty() {
1259 None
1260 } else {
1261 produce(
1262 state.borrow_mut(),
1263 &mut stack,
1264 Item::Paragraph(Text::new(spans.drain(..).collect())),
1265 source,
1266 )
1267 };
1268
1269 stack.push(Scope::List(List {
1270 start: first_item,
1271 bullets: Vec::new(),
1272 last_item_start: None,
1273 }));
1274
1275 prev
1276 }
1277 pulldown_cmark::Tag::Item => {
1278 if let Some(Scope::List(list)) = stack.last_mut() {
1279 list.last_item_start = Some(source.start);
1280 list.bullets.push(Bullet::Point { items: Vec::new() });
1281 }
1282
1283 None
1284 }
1285 pulldown_cmark::Tag::BlockQuote(_kind) if !metadata => {
1286 let prev = if spans.is_empty() {
1287 None
1288 } else {
1289 produce(
1290 state.borrow_mut(),
1291 &mut stack,
1292 Item::Paragraph(Text::new(spans.drain(..).collect())),
1293 source,
1294 )
1295 };
1296
1297 stack.push(Scope::Quote(Vec::new()));
1298
1299 prev
1300 }
1301 pulldown_cmark::Tag::CodeBlock(pulldown_cmark::CodeBlockKind::Fenced(language))
1302 if !metadata =>
1303 {
1304 #[cfg(feature = "highlighter")]
1305 {
1306 let key = _offset + source.start;
1311 let state = state.borrow_mut();
1312 let language = language.split(',').next().unwrap_or_default();
1313
1314 let mut parser = state
1315 .parsers
1316 .remove(&key)
1317 .filter(|parser| parser.language() == language)
1318 .unwrap_or_else(|| code::Parser::new(language));
1319
1320 parser.prepare();
1321 let _ = state.parsers.insert(key, parser);
1322
1323 code_block_key = Some(key);
1324 }
1325
1326 code_block = true;
1327 code_language = (!language.is_empty()).then(|| language.into_string());
1328
1329 if spans.is_empty() {
1330 None
1331 } else {
1332 produce(
1333 state.borrow_mut(),
1334 &mut stack,
1335 Item::Paragraph(Text::new(spans.drain(..).collect())),
1336 source,
1337 )
1338 }
1339 }
1340 pulldown_cmark::Tag::MetadataBlock(_) => {
1341 metadata = true;
1342 None
1343 }
1344 pulldown_cmark::Tag::Table(alignment) => {
1345 stack.push(Scope::Table {
1346 columns: Vec::with_capacity(alignment.len()),
1347 alignment,
1348 current: Vec::new(),
1349 rows: Vec::new(),
1350 });
1351
1352 None
1353 }
1354 pulldown_cmark::Tag::TableHead => {
1355 strong = true;
1356 None
1357 }
1358 pulldown_cmark::Tag::TableRow => {
1359 let Scope::Table { rows, .. } = stack.last_mut()? else {
1360 return None;
1361 };
1362
1363 rows.push(Row { cells: Vec::new() });
1364 None
1365 }
1366 _ => None,
1367 },
1368 pulldown_cmark::Event::End(tag) => match tag {
1369 pulldown_cmark::TagEnd::Heading(level) if !metadata => produce(
1370 state.borrow_mut(),
1371 &mut stack,
1372 Item::Heading(level, Text::new(spans.drain(..).collect())),
1373 source,
1374 ),
1375 pulldown_cmark::TagEnd::Strong if !metadata => {
1376 strong = false;
1377 None
1378 }
1379 pulldown_cmark::TagEnd::Emphasis if !metadata => {
1380 emphasis = false;
1381 None
1382 }
1383 pulldown_cmark::TagEnd::Strikethrough if !metadata => {
1384 strikethrough = false;
1385 None
1386 }
1387 pulldown_cmark::TagEnd::Link if !metadata => {
1388 link = None;
1389 None
1390 }
1391 pulldown_cmark::TagEnd::Paragraph if !metadata => {
1392 paragraph_start = None;
1393
1394 if spans.is_empty() {
1395 None
1396 } else {
1397 produce(
1398 state.borrow_mut(),
1399 &mut stack,
1400 Item::Paragraph(Text::new(spans.drain(..).collect())),
1401 source,
1402 )
1403 }
1404 }
1405 pulldown_cmark::TagEnd::Item if !metadata => {
1406 if spans.is_empty() {
1407 None
1408 } else {
1409 produce(
1410 state.borrow_mut(),
1411 &mut stack,
1412 Item::Paragraph(Text::new(spans.drain(..).collect())),
1413 source,
1414 )
1415 }
1416 }
1417 pulldown_cmark::TagEnd::List(_) if !metadata => {
1418 let scope = stack.pop()?;
1419
1420 let Scope::List(list) = scope else {
1421 return None;
1422 };
1423
1424 let last_item_start = list.last_item_start;
1425 let produced = produce(
1426 state.borrow_mut(),
1427 &mut stack,
1428 Item::List {
1429 start: list.start,
1430 bullets: list.bullets,
1431 },
1432 source,
1433 );
1434
1435 if produced.is_some()
1439 && let Some(start) = last_item_start
1440 {
1441 state.borrow_mut().window = Some(start);
1442 }
1443
1444 produced
1445 }
1446 pulldown_cmark::TagEnd::BlockQuote(_kind) if !metadata => {
1447 let scope = stack.pop()?;
1448
1449 let Scope::Quote(quote) = scope else {
1450 return None;
1451 };
1452
1453 produce(state.borrow_mut(), &mut stack, Item::Quote(quote), source)
1454 }
1455 pulldown_cmark::TagEnd::Image if !metadata => {
1456 let (url, title, start) = image.take()?;
1457 let alt = Text::new(spans.drain(start..).collect());
1458
1459 let state = state.borrow_mut();
1460 let _ = state.images.insert(url.clone());
1461
1462 let produced = produce(state, &mut stack, Item::Image { url, title, alt }, source);
1463
1464 if let Some(start) = paragraph_start.filter(|_| produced.is_some()) {
1468 state.borrow_mut().window = Some(start);
1469 }
1470
1471 produced
1472 }
1473 pulldown_cmark::TagEnd::CodeBlock if !metadata => {
1474 code_block = false;
1475
1476 produce(
1477 state.borrow_mut(),
1478 &mut stack,
1479 Item::CodeBlock {
1480 language: code_language.take(),
1481 code: mem::take(&mut code),
1482 lines: code_lines.drain(..).collect(),
1483 },
1484 source,
1485 )
1486 }
1487 pulldown_cmark::TagEnd::MetadataBlock(_) => {
1488 metadata = false;
1489 None
1490 }
1491 pulldown_cmark::TagEnd::Table => {
1492 let scope = stack.pop()?;
1493
1494 let Scope::Table { columns, rows, .. } = scope else {
1495 return None;
1496 };
1497
1498 produce(
1499 state.borrow_mut(),
1500 &mut stack,
1501 Item::Table { columns, rows },
1502 source,
1503 )
1504 }
1505 pulldown_cmark::TagEnd::TableHead => {
1506 strong = false;
1507 None
1508 }
1509 pulldown_cmark::TagEnd::TableCell => {
1510 if !spans.is_empty() {
1511 let _ = produce(
1512 state.borrow_mut(),
1513 &mut stack,
1514 Item::Paragraph(Text::new(spans.drain(..).collect())),
1515 source,
1516 );
1517 }
1518
1519 let Scope::Table {
1520 alignment,
1521 columns,
1522 rows,
1523 current,
1524 } = stack.last_mut()?
1525 else {
1526 return None;
1527 };
1528
1529 if columns.len() < alignment.len() {
1530 columns.push(Column {
1531 header: std::mem::take(current),
1532 alignment: alignment[columns.len()],
1533 });
1534 } else {
1535 rows.last_mut()
1536 .expect("table row")
1537 .cells
1538 .push(std::mem::take(current));
1539 }
1540
1541 None
1542 }
1543 _ => None,
1544 },
1545 pulldown_cmark::Event::Text(text) if !metadata => {
1546 if code_block {
1547 code.push_str(&text);
1548
1549 #[cfg(feature = "highlighter")]
1550 if let Some(key) = code_block_key
1551 && let Some(highlighter) = state.borrow_mut().parsers.get_mut(&key)
1552 {
1553 for line in text.lines() {
1554 code_lines.push(Text::new(highlighter.parse_line(line).to_vec()));
1555 }
1556 }
1557
1558 #[cfg(not(feature = "highlighter"))]
1559 for line in text.lines() {
1560 code_lines.push(Text::new(vec![Span::Code {
1561 text: line.to_owned(),
1562 code: Code::Other,
1563 }]));
1564 }
1565
1566 return None;
1567 }
1568
1569 let span = Span::Standard {
1570 text: text.into_string(),
1571 strong,
1572 emphasis,
1573 strikethrough,
1574 link: link.clone(),
1575 inline_code: false,
1576 };
1577
1578 spans.push(span);
1579
1580 None
1581 }
1582 pulldown_cmark::Event::Code(code) if !metadata => {
1583 let span = Span::Standard {
1584 text: code.into_string(),
1585 strong,
1586 emphasis,
1587 strikethrough,
1588 link: link.clone(),
1589 inline_code: true,
1590 };
1591
1592 spans.push(span);
1593 None
1594 }
1595 pulldown_cmark::Event::SoftBreak if !metadata => {
1596 spans.push(Span::Standard {
1597 text: String::from(" "),
1598 strikethrough,
1599 strong,
1600 emphasis,
1601 link: link.clone(),
1602 inline_code: false,
1603 });
1604 None
1605 }
1606 pulldown_cmark::Event::HardBreak if !metadata => {
1607 spans.push(Span::Standard {
1608 text: String::from("\n"),
1609 strikethrough,
1610 strong,
1611 emphasis,
1612 link: link.clone(),
1613 inline_code: false,
1614 });
1615 None
1616 }
1617 pulldown_cmark::Event::Rule => produce(state.borrow_mut(), &mut stack, Item::Rule, source),
1618 pulldown_cmark::Event::TaskListMarker(done) => {
1619 if let Some(Scope::List(list)) = stack.last_mut()
1620 && let Some(item) = list.bullets.last_mut()
1621 && let Bullet::Point { items } = item
1622 {
1623 *item = Bullet::Task {
1624 items: std::mem::take(items),
1625 done,
1626 };
1627 }
1628
1629 None
1630 }
1631 _ => None,
1632 })
1633}
1634
1635#[derive(Debug, Clone, Copy, PartialEq)]
1637pub struct Settings {
1638 pub font: Font,
1640 pub inline_code_font: Font,
1642 pub code_block_font: Font,
1644 pub line_height: LineHeight,
1646 pub text_size: Pixels,
1648 pub code_block_size: Pixels,
1650 pub inline_code_size: Pixels,
1652 pub h1_size: Pixels,
1654 pub h2_size: Pixels,
1656 pub h3_size: Pixels,
1658 pub h4_size: Pixels,
1660 pub h5_size: Pixels,
1662 pub h6_size: Pixels,
1664 pub spacing: Pixels,
1666}
1667
1668impl Settings {
1669 pub fn with_text_size(text_size: impl Into<Pixels>) -> Self {
1676 let text_size = text_size.into();
1677 let line_height = LineHeight::default();
1678
1679 Self {
1680 font: Font::DEFAULT,
1681 inline_code_font: Font::MONOSPACE,
1682 code_block_font: Font::MONOSPACE,
1683 line_height,
1684 text_size,
1685 inline_code_size: text_size * 0.85,
1686 code_block_size: text_size * 0.85,
1687 h1_size: text_size * 1.5,
1688 h2_size: text_size * 1.25,
1689 h3_size: text_size * 1.125,
1690 h4_size: text_size,
1691 h5_size: text_size,
1692 h6_size: text_size,
1693 spacing: line_height.to_absolute(text_size) / 1.5,
1694 }
1695 }
1696
1697 pub fn line_height(self, line_height: impl Into<LineHeight>) -> Self {
1699 let line_height = line_height.into();
1700
1701 Self {
1702 line_height,
1703 spacing: line_height.to_absolute(self.text_size) / 1.5,
1704 ..self
1705 }
1706 }
1707}
1708
1709impl Default for Settings {
1710 fn default() -> Self {
1711 Self::with_text_size(16)
1712 }
1713}
1714
1715pub fn view<'a, Theme, Renderer>(
1760 items: &'a [Item],
1761 settings: impl Into<Settings>,
1762 theme: Theme,
1763) -> impl Widget<Uri, Theme, Renderer> + 'a
1764where
1765 Theme: Catalog + 'static,
1766 Renderer: core::text::Renderer,
1767{
1768 view_with(
1769 items,
1770 settings,
1771 &DefaultViewer {
1772 theme,
1773 highlighter: None,
1774 },
1775 )
1776 .boxed()
1777}
1778
1779pub fn view_with<'a, Message, Theme, Renderer>(
1784 items: &'a [Item],
1785 settings: impl Into<Settings>,
1786 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1787) -> impl Widget<Message, Theme, Renderer> + 'a
1788where
1789 Message: 'a,
1790 Theme: Catalog + 'a,
1791 Renderer: core::text::Renderer,
1792{
1793 self::items(viewer, settings.into(), items)
1794}
1795
1796pub fn item<'a, Message, Theme, Renderer>(
1798 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1799 settings: Settings,
1800 item: &'a Item,
1801) -> impl Widget<Message, Theme, Renderer> + 'a
1802where
1803 Message: 'a,
1804 Theme: Catalog + 'a,
1805 Renderer: core::text::Renderer + 'a,
1806{
1807 match item {
1808 Item::Image { url, title, alt } => viewer.image(settings, url, title, alt).boxed(),
1809 Item::Heading(level, text) => viewer.heading(settings, level, text).boxed(),
1810 Item::Paragraph(text) => viewer.paragraph(settings, text).boxed(),
1811 Item::CodeBlock {
1812 language,
1813 code,
1814 lines,
1815 } => viewer
1816 .code_block(settings, language.as_deref(), code, lines)
1817 .boxed(),
1818 Item::List {
1819 start: None,
1820 bullets,
1821 } => viewer.unordered_list(settings, bullets).boxed(),
1822 Item::List {
1823 start: Some(start),
1824 bullets,
1825 } => viewer.ordered_list(settings, *start, bullets).boxed(),
1826 Item::Quote(quote) => viewer.quote(settings, quote).boxed(),
1827 Item::Rule => viewer.rule().boxed(),
1828 Item::Table { columns, rows } => viewer.table(settings, columns, rows).boxed(),
1829 }
1830}
1831
1832pub fn heading<'a, Message, Theme, Renderer>(
1834 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1835 settings: Settings,
1836 level: &'a HeadingLevel,
1837 text: &'a Text,
1838 on_link_click: impl Fn(Uri) -> Message + 'a,
1839) -> impl Widget<Message, Theme, Renderer> + 'a
1840where
1841 Message: 'a,
1842 Theme: Catalog + 'a,
1843 Renderer: core::text::Renderer + 'a,
1844{
1845 let Settings {
1846 h1_size,
1847 h2_size,
1848 h3_size,
1849 h4_size,
1850 h5_size,
1851 h6_size,
1852 ..
1853 } = settings;
1854
1855 let size = match level {
1856 pulldown_cmark::HeadingLevel::H1 => h1_size,
1857 pulldown_cmark::HeadingLevel::H2 => h2_size,
1858 pulldown_cmark::HeadingLevel::H3 => h3_size,
1859 pulldown_cmark::HeadingLevel::H4 => h4_size,
1860 pulldown_cmark::HeadingLevel::H5 => h5_size,
1861 pulldown_cmark::HeadingLevel::H6 => h6_size,
1862 };
1863
1864 container(
1865 rich_text(text.spans(
1866 Settings {
1867 font: Font {
1868 weight: font::Weight::Bold,
1869 ..settings.font
1870 },
1871 inline_code_font: Font {
1872 weight: font::Weight::Bold,
1873 ..settings.inline_code_font
1874 },
1875 inline_code_size: size * (settings.inline_code_size / settings.text_size),
1876 ..settings
1877 },
1878 viewer.theme(),
1879 viewer.highlighter(),
1880 ))
1881 .on_link_click(on_link_click)
1882 .size(size)
1883 .line_height(settings.line_height)
1884 .selectable(true),
1885 )
1886}
1887
1888pub fn paragraph<'a, Message, Theme, Renderer>(
1890 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1891 settings: Settings,
1892 text: &Text,
1893 on_link_click: impl Fn(Uri) -> Message + 'a,
1894) -> impl Widget<Message, Theme, Renderer> + 'a
1895where
1896 Message: 'a,
1897 Theme: Catalog + 'a,
1898 Renderer: core::text::Renderer + 'a,
1899{
1900 rich_text(text.spans(settings, viewer.theme(), viewer.highlighter()))
1901 .size(settings.text_size)
1902 .line_height(settings.line_height)
1903 .on_link_click(on_link_click)
1904 .selectable(true)
1905}
1906
1907pub fn unordered_list<'a, Message, Theme, Renderer>(
1910 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1911 settings: Settings,
1912 bullets: &'a [Bullet],
1913) -> impl Widget<Message, Theme, Renderer> + 'a
1914where
1915 Message: 'a,
1916 Theme: Catalog + 'a,
1917 Renderer: core::text::Renderer + 'a,
1918{
1919 column(bullets.iter().map(|bullet| {
1920 row![
1921 match bullet {
1922 Bullet::Point { .. } => {
1923 text("•")
1924 .size(settings.text_size)
1925 .line_height(settings.line_height)
1926 .boxed()
1927 }
1928 Bullet::Task { done, .. } => {
1929 container(checkbox(*done).size(settings.text_size))
1930 .center_y(settings.line_height.to_absolute(settings.text_size))
1931 .boxed()
1932 }
1933 },
1934 items(
1935 viewer,
1936 Settings {
1937 spacing: settings.spacing / 2.0,
1938 ..settings
1939 },
1940 bullet.items(),
1941 )
1942 ]
1943 .spacing(settings.text_size / 2.0)
1944 }))
1945 .spacing(settings.spacing / 2.0)
1946 .padding(padding::left(settings.text_size.0))
1947}
1948
1949pub fn ordered_list<'a, Message, Theme, Renderer>(
1952 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1953 settings: Settings,
1954 start: u64,
1955 bullets: &'a [Bullet],
1956) -> impl Widget<Message, Theme, Renderer> + 'a
1957where
1958 Message: 'a,
1959 Theme: Catalog + 'a,
1960 Renderer: core::text::Renderer + 'a,
1961{
1962 let digits = (start + bullets.len() as u64).max(1).ilog10() + 1;
1963
1964 column(bullets.iter().enumerate().map(|(i, bullet)| {
1965 row![
1966 text!("{}.", i as u64 + start)
1967 .size(settings.text_size)
1968 .align_x(alignment::Horizontal::Right)
1969 .width(settings.text_size * ((digits as f32 / 2.0).ceil() + 1.0)),
1970 items(
1971 viewer,
1972 Settings {
1973 spacing: settings.spacing / 2.0,
1974 ..settings
1975 },
1976 bullet.items(),
1977 )
1978 ]
1979 .spacing(settings.text_size / 2.0)
1980 }))
1981 .spacing(settings.spacing / 2.0)
1982}
1983
1984pub fn code_block<'a, Message, Theme, Renderer>(
1986 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1987 settings: Settings,
1988 lines: &'a [Text],
1989 on_link_click: impl Fn(Uri) -> Message + Clone + 'a,
1990) -> impl Widget<Message, Theme, Renderer> + 'a
1991where
1992 Message: 'a,
1993 Theme: Catalog + 'a,
1994 Renderer: core::text::Renderer + 'a,
1995{
1996 let padding = settings.code_block_size / 0.85 * 0.75;
1997
1998 container(
1999 scrollable(column(lines.iter().map(|line| {
2000 rich_text(line.spans(settings, viewer.theme(), viewer.highlighter()))
2001 .on_link_click(on_link_click.clone())
2002 .font(settings.code_block_font)
2003 .size(settings.code_block_size)
2004 .line_height(settings.line_height)
2005 .selectable(true)
2006 })))
2007 .direction(scrollable::Direction::Horizontal(
2008 scrollable::Scrollbar::default()
2009 .width(padding / 2.0)
2010 .scroller_width(padding / 2.0),
2011 ))
2012 .spacing(padding),
2013 )
2014 .width(Length::Fill)
2015 .padding(padding)
2016 .class(Theme::code_block())
2017}
2018
2019pub fn quote<'a, Message, Theme, Renderer>(
2021 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
2022 settings: Settings,
2023 contents: &'a [Item],
2024) -> impl Widget<Message, Theme, Renderer> + 'a
2025where
2026 Message: 'a,
2027 Theme: Catalog + 'a,
2028 Renderer: core::text::Renderer + 'a,
2029{
2030 container(
2031 column(
2032 contents
2033 .iter()
2034 .map(|content| item(viewer, settings, content)),
2035 )
2036 .spacing(settings.spacing.0),
2037 )
2038 .width(Length::Fill)
2039 .padding(settings.spacing.0)
2040 .class(Theme::quote())
2041}
2042
2043pub fn rule<'a, Message, Theme, Renderer>() -> impl Widget<Message, Theme, Renderer> + 'a
2045where
2046 Message: 'a,
2047 Theme: Catalog + 'a,
2048 Renderer: core::text::Renderer + 'a,
2049{
2050 rule::horizontal(2)
2051}
2052
2053pub fn table<'a, Message, Theme, Renderer>(
2055 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
2056 settings: Settings,
2057 columns: &'a [Column],
2058 rows: &'a [Row],
2059) -> impl Widget<Message, Theme, Renderer> + 'a
2060where
2061 Message: 'a,
2062 Theme: Catalog + 'a,
2063 Renderer: core::text::Renderer + 'a,
2064{
2065 use crate::table;
2066
2067 let table = table(
2068 columns.iter().enumerate().map(move |(i, column)| {
2069 table::column(items(viewer, settings, &column.header), move |row: &Row| {
2070 if let Some(cells) = row.cells.get(i) {
2071 items(viewer, settings, cells)
2072 } else {
2073 items(viewer, settings, &[])
2074 }
2075 })
2076 .width(Length::Fit.max(300))
2077 .align_x(match column.alignment {
2078 pulldown_cmark::Alignment::None | pulldown_cmark::Alignment::Left => {
2079 alignment::Horizontal::Left
2080 }
2081 pulldown_cmark::Alignment::Center => alignment::Horizontal::Center,
2082 pulldown_cmark::Alignment::Right => alignment::Horizontal::Right,
2083 })
2084 }),
2085 rows,
2086 )
2087 .padding_x(settings.spacing.0)
2088 .padding_y(settings.spacing.0 / 2.0)
2089 .separator_x(0);
2090
2091 center_x(
2092 scrollable(table)
2093 .direction(scrollable::Direction::Horizontal(
2094 scrollable::Scrollbar::default(),
2095 ))
2096 .spacing(settings.spacing.0 / 2.0),
2097 )
2098}
2099
2100pub fn items<'a, Message, Theme, Renderer>(
2102 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
2103 settings: Settings,
2104 items: &'a [Item],
2105) -> impl Widget<Message, Theme, Renderer> + 'a
2106where
2107 Message: 'a,
2108 Theme: Catalog + 'a,
2109 Renderer: core::text::Renderer + 'a,
2110{
2111 column(sections(items).map(|(heading, contents)| {
2112 let contents = column(
2113 contents
2114 .iter()
2115 .map(|content| item(viewer, settings, content)),
2116 )
2117 .spacing(settings.spacing);
2118
2119 if let Some(heading) = heading {
2120 column![item(viewer, settings, heading), contents].spacing(settings.spacing / 2.0)
2121 } else {
2122 column![contents]
2123 }
2124 }))
2125 .spacing(settings.spacing * 1.5)
2126}
2127
2128pub trait Viewer<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
2133where
2134 Self: Sized + 'a,
2135 Message: 'a,
2136 Theme: Catalog + 'a,
2137 Renderer: core::text::Renderer + 'a,
2138{
2139 fn theme(&self) -> &Theme;
2141
2142 fn highlighter(&self) -> &dyn text::Highlighter<Code, Theme>;
2144
2145 fn on_link_click(url: Uri) -> Message;
2147
2148 fn image(
2152 &self,
2153 settings: Settings,
2154 url: &'a Uri,
2155 title: &'a str,
2156 alt: &Text,
2157 ) -> impl Widget<Message, Theme, Renderer> + 'a {
2158 let _url = url;
2159 let _title = title;
2160
2161 container(
2162 rich_text(alt.spans(settings, self.theme(), self.highlighter()))
2163 .on_link_click(Self::on_link_click),
2164 )
2165 .padding(settings.spacing.0)
2166 .class(Theme::code_block())
2167 }
2168
2169 fn heading(
2173 &self,
2174 settings: Settings,
2175 level: &'a HeadingLevel,
2176 text: &'a Text,
2177 ) -> impl Widget<Message, Theme, Renderer> + 'a {
2178 heading(self, settings, level, text, Self::on_link_click)
2179 }
2180
2181 fn paragraph(
2185 &self,
2186 settings: Settings,
2187 text: &Text,
2188 ) -> impl Widget<Message, Theme, Renderer> + 'a {
2189 paragraph(self, settings, text, Self::on_link_click)
2190 }
2191
2192 fn code_block(
2196 &self,
2197 settings: Settings,
2198 language: Option<&'a str>,
2199 code: &'a str,
2200 lines: &'a [Text],
2201 ) -> impl Widget<Message, Theme, Renderer> + 'a {
2202 let _language = language;
2203 let _code = code;
2204
2205 code_block(self, settings, lines, Self::on_link_click)
2206 }
2207
2208 fn unordered_list(
2212 &self,
2213 settings: Settings,
2214 bullets: &'a [Bullet],
2215 ) -> impl Widget<Message, Theme, Renderer> + 'a {
2216 unordered_list(self, settings, bullets)
2217 }
2218
2219 fn ordered_list(
2223 &self,
2224 settings: Settings,
2225 start: u64,
2226 bullets: &'a [Bullet],
2227 ) -> impl Widget<Message, Theme, Renderer> + 'a {
2228 ordered_list(self, settings, start, bullets)
2229 }
2230
2231 fn quote(
2235 &self,
2236 settings: Settings,
2237 contents: &'a [Item],
2238 ) -> impl Widget<Message, Theme, Renderer> + 'a {
2239 quote(self, settings, contents)
2240 }
2241
2242 fn rule(&self) -> impl Widget<Message, Theme, Renderer> + 'a {
2246 rule()
2247 }
2248
2249 fn table(
2253 &self,
2254 settings: Settings,
2255 columns: &'a [Column],
2256 rows: &'a [Row],
2257 ) -> impl Widget<Message, Theme, Renderer> + 'a {
2258 table(self, settings, columns, rows)
2259 }
2260}
2261
2262pub struct DefaultViewer<'a, Theme> {
2264 theme: Theme,
2265 highlighter: Option<Box<dyn text::Highlighter<Code, Theme> + 'a>>,
2266}
2267
2268impl<'a, Theme> DefaultViewer<'a, Theme> {
2269 pub fn new(theme: Theme) -> Self {
2271 Self {
2272 theme,
2273 highlighter: None,
2274 }
2275 }
2276
2277 pub fn highlighter(mut self, highlighter: impl text::Highlighter<Code, Theme> + 'a) -> Self {
2279 self.highlighter = Some(Box::new(highlighter));
2280 self
2281 }
2282}
2283
2284impl<'a, Theme, Renderer> Viewer<'a, Uri, Theme, Renderer> for DefaultViewer<'a, Theme>
2285where
2286 Theme: Catalog + 'a,
2287 Renderer: core::text::Renderer + 'a,
2288{
2289 fn theme(&self) -> &Theme {
2290 &self.theme
2291 }
2292
2293 fn highlighter(&self) -> &dyn text::Highlighter<Code, Theme> {
2294 self.highlighter
2295 .as_deref()
2296 .unwrap_or_else(|| self.theme.highlighter())
2297 }
2298
2299 fn on_link_click(url: Uri) -> Uri {
2300 url
2301 }
2302}
2303
2304pub trait Catalog:
2306 container::Catalog
2307 + scrollable::Catalog
2308 + text::Catalog
2309 + crate::rule::Catalog
2310 + checkbox::Catalog
2311 + crate::table::Catalog
2312 + Clone
2313 + PartialEq
2314{
2315 fn id(&self) -> &str;
2319
2320 fn link_color(&self) -> Color;
2322
2323 fn code(&self) -> InlineCode;
2325
2326 fn code_block<'a>() -> <Self as container::Catalog>::Class<'a>;
2328
2329 fn quote<'a>() -> <Self as container::Catalog>::Class<'a>;
2331
2332 fn highlighter(&self) -> &dyn text::Highlighter<Code, Self>;
2334}
2335
2336#[derive(Debug, Clone, Copy, PartialEq)]
2338pub struct InlineCode {
2339 pub padding: Padding,
2341 pub highlight: Highlight,
2343 pub color: Color,
2345}
2346
2347impl Catalog for Theme {
2348 fn id(&self) -> &str {
2349 theme::Base::name(self)
2350 }
2351
2352 fn link_color(&self) -> Color {
2353 self.seed().primary
2354 }
2355
2356 fn code(&self) -> InlineCode {
2357 let palette = self.palette();
2358
2359 InlineCode {
2360 padding: padding::horizontal(4).vertical(1),
2361 highlight: Highlight {
2362 background: palette.background.weaker.color.into(),
2363 border: border::rounded(4),
2364 },
2365 color: palette.background.weaker.text,
2366 }
2367 }
2368
2369 fn code_block<'a>() -> <Self as container::Catalog>::Class<'a> {
2370 Box::new(|theme| container::dark(theme).border(border::rounded(5)))
2371 }
2372
2373 fn quote<'a>() -> <Self as container::Catalog>::Class<'a> {
2374 Box::new(|theme| {
2375 let palette = theme.palette();
2376
2377 container::Style {
2378 text_color: Some(palette.background.weakest.text),
2379 background: Some(palette.background.weakest.color.into()),
2380 border: border::rounded(5),
2381 ..container::Style::default()
2382 }
2383 })
2384 }
2385
2386 fn highlighter(&self) -> &dyn text::Highlighter<Code, Self> {
2387 &Code::highlight
2388 }
2389}
2390
2391#[cfg(feature = "highlighter")]
2392mod code {
2393 use super::Span;
2394
2395 #[derive(Debug)]
2396 pub struct Parser {
2397 lines: Vec<(String, Vec<Span>)>,
2398 language: String,
2399 stream: iced_highlighter::Stream,
2400 current: usize,
2401 }
2402
2403 impl Parser {
2404 pub fn new(language: &str) -> Self {
2405 Self {
2406 lines: Vec::new(),
2407 stream: iced_highlighter::Stream::new(&iced_highlighter::Settings {
2408 token: language.to_owned(),
2409 }),
2410 language: language.to_owned(),
2411 current: 0,
2412 }
2413 }
2414
2415 pub fn language(&self) -> &str {
2416 &self.language
2417 }
2418
2419 pub fn prepare(&mut self) {
2420 self.current = 0;
2421 }
2422
2423 pub fn parse_line(&mut self, text: &str) -> &[Span] {
2424 match self.lines.get(self.current) {
2425 Some(line) if line.0 == text => {}
2426 _ => {
2427 if self.current + 1 < self.lines.len() {
2428 log::debug!("Resetting highlighter...");
2429 self.stream.reset();
2430 self.lines.truncate(self.current);
2431
2432 for line in &self.lines {
2433 log::debug!("Refeeding {n} lines", n = self.lines.len());
2434
2435 let _ = self.stream.parse_line(&line.0);
2436 self.stream.commit();
2437 }
2438 }
2439
2440 log::trace!("Parsing: {text}", text = text.trim_end());
2441
2442 if self.current == self.lines.len() {
2443 self.stream.commit();
2444 }
2445
2446 let mut spans = Vec::new();
2447
2448 for (range, code) in self.stream.parse_line(text) {
2449 spans.push(Span::Code {
2450 text: text[range].to_owned(),
2451 code,
2452 });
2453 }
2454
2455 if self.current == self.lines.len() {
2456 self.lines.push((text.to_owned(), spans));
2457 } else {
2458 self.lines[self.current] = (text.to_owned(), spans);
2459 }
2460 }
2461 }
2462
2463 self.current += 1;
2464
2465 &self
2466 .lines
2467 .get(self.current - 1)
2468 .expect("Line must be parsed")
2469 .1
2470 }
2471 }
2472}