1use crate::core;
51use crate::core::alignment;
52use crate::core::border;
53use crate::core::font::{self, Font};
54use crate::core::padding;
55use crate::core::text::LineHeight;
56use crate::core::theme;
57use crate::core::{Code, Color, Element, Length, Padding, Pixels, Theme};
58use crate::{checkbox, column, container, rich_text, row, rule, scrollable, span, text};
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).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).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).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 parser: None,
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).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> + '_ {
1004 parse_with(State::default(), markdown).map(|(item, _start, _broken_links)| item)
1005}
1006
1007#[derive(Debug, Default)]
1008struct State {
1009 window: Option<usize>,
1012 references: HashMap<String, String>,
1016 references_staged: HashSet<String>,
1021 images: HashSet<Uri>,
1022 #[cfg(feature = "highlighter")]
1023 parser: Option<code::Parser>,
1024}
1025
1026fn options() -> pulldown_cmark::Options {
1028 pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS
1029 | pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS
1030 | pulldown_cmark::Options::ENABLE_TABLES
1031 | pulldown_cmark::Options::ENABLE_STRIKETHROUGH
1032 | pulldown_cmark::Options::ENABLE_TASKLISTS
1033}
1034
1035fn absorb_references(
1046 markdown: &str,
1047 definitions: &pulldown_cmark::RefDefs<'_>,
1048 references: &mut HashMap<String, String>,
1049 growing_refs: &mut HashSet<String>,
1050) {
1051 for reference in definitions.iter() {
1052 let name = reference.0.to_string();
1053 let dest = reference.1.dest.to_string();
1054
1055 if markdown[reference.1.span.end..].contains('\n') {
1056 if !references.contains_key(&name) || growing_refs.remove(&name) {
1057 let _ = references.insert(name, dest);
1058 }
1059 } else if growing_refs.contains(&name) {
1060 let _ = references.insert(name, dest);
1062 } else if !references.contains_key(&name) {
1063 let _ = growing_refs.insert(name.clone());
1066 let _ = references.insert(name, dest);
1067 }
1068 }
1069}
1070
1071fn parse_with<'a>(
1072 mut state: impl BorrowMut<State> + 'a,
1073 markdown: &'a str,
1074) -> impl Iterator<Item = (Item, usize, HashSet<String>)> + 'a {
1075 enum Scope {
1076 List(List),
1077 Quote(Vec<Item>),
1078 Table {
1079 alignment: Vec<pulldown_cmark::Alignment>,
1080 columns: Vec<Column>,
1081 rows: Vec<Row>,
1082 current: Vec<Item>,
1083 },
1084 }
1085
1086 struct List {
1087 start: Option<u64>,
1088 bullets: Vec<Bullet>,
1089 last_item_start: Option<usize>,
1091 }
1092
1093 let broken_links = Rc::new(RefCell::new(Vec::new()));
1100
1101 let mut spans = Vec::new();
1102 let mut code = String::new();
1103 let mut code_language = None;
1104 let mut code_lines = Vec::new();
1105 let mut strong = false;
1106 let mut emphasis = false;
1107 let mut strikethrough = false;
1108 let mut metadata = false;
1109 let mut code_block = false;
1110 let mut link = None;
1111 let mut image = None;
1112 let mut paragraph_start = None;
1113 let mut stack = Vec::new();
1114
1115 #[cfg(feature = "highlighter")]
1116 let mut code_parser = None;
1117
1118 let parser = pulldown_cmark::Parser::new_with_broken_link_callback(markdown, options(), {
1119 let references = state.borrow().references.clone();
1120 let broken_links = broken_links.clone();
1121
1122 Some(move |broken_link: pulldown_cmark::BrokenLink<'_>| {
1123 if let Some(reference) = references.get(broken_link.reference.as_ref()) {
1124 Some((
1125 pulldown_cmark::CowStr::from(reference.to_owned()),
1126 broken_link.reference.into_static(),
1127 ))
1128 } else {
1129 RefCell::borrow_mut(&broken_links)
1130 .push((broken_link.span, broken_link.reference.into_string()));
1131
1132 None
1133 }
1134 })
1135 });
1136
1137 {
1138 let state = state.borrow_mut();
1139 absorb_references(
1140 markdown,
1141 parser.reference_definitions(),
1142 &mut state.references,
1143 &mut state.references_staged,
1144 );
1145 }
1146
1147 let produce = move |state: &mut State, stack: &mut Vec<Scope>, item, source: Range<usize>| {
1148 if let Some(scope) = stack.last_mut() {
1149 match scope {
1150 Scope::List(list) => {
1151 list.bullets.last_mut().expect("item context").push(item);
1152 }
1153 Scope::Quote(items) => {
1154 items.push(item);
1155 }
1156 Scope::Table { current, .. } => {
1157 current.push(item);
1158 }
1159 }
1160
1161 None
1162 } else {
1163 state.window = Some(source.start);
1164
1165 let mut links = HashSet::new();
1168 for (span, reference) in RefCell::borrow(&broken_links).iter() {
1169 if source.contains(&span.start) {
1170 let _ = links.insert(reference.clone());
1171 }
1172 }
1173
1174 Some((item, source.start, links))
1175 }
1176 };
1177
1178 let resolve_reference = |state: &mut State,
1184 link_type: pulldown_cmark::LinkType,
1185 id: &str,
1186 dest_url: &pulldown_cmark::CowStr<'a>|
1187 -> String {
1188 match link_type {
1189 pulldown_cmark::LinkType::Reference
1190 | pulldown_cmark::LinkType::ReferenceUnknown
1191 | pulldown_cmark::LinkType::Collapsed
1192 | pulldown_cmark::LinkType::CollapsedUnknown
1193 | pulldown_cmark::LinkType::Shortcut
1194 | pulldown_cmark::LinkType::ShortcutUnknown => state
1195 .references
1196 .get(id)
1197 .cloned()
1198 .unwrap_or_else(|| dest_url.to_string()),
1199 _ => dest_url.to_string(),
1200 }
1201 };
1202
1203 let parser = parser.into_offset_iter();
1204
1205 #[allow(clippy::drain_collect)]
1207 parser.filter_map(move |(event, source)| match event {
1208 pulldown_cmark::Event::Start(tag) => match tag {
1209 pulldown_cmark::Tag::Strong if !metadata => {
1210 strong = true;
1211 None
1212 }
1213 pulldown_cmark::Tag::Emphasis if !metadata => {
1214 emphasis = true;
1215 None
1216 }
1217 pulldown_cmark::Tag::Strikethrough if !metadata => {
1218 strikethrough = true;
1219 None
1220 }
1221 pulldown_cmark::Tag::Link {
1222 link_type,
1223 dest_url,
1224 id,
1225 ..
1226 } if !metadata => {
1227 link = Some(resolve_reference(
1228 state.borrow_mut(),
1229 link_type,
1230 &id,
1231 &dest_url,
1232 ));
1233 None
1234 }
1235 pulldown_cmark::Tag::Paragraph if !metadata => {
1236 paragraph_start = Some(source.start);
1237 None
1238 }
1239 pulldown_cmark::Tag::Image {
1240 link_type,
1241 dest_url,
1242 title,
1243 id,
1244 } if !metadata => {
1245 image = Some((
1246 resolve_reference(state.borrow_mut(), link_type, &id, &dest_url),
1247 title.into_string(),
1248 spans.len(),
1249 ));
1250 None
1251 }
1252 pulldown_cmark::Tag::List(first_item) if !metadata => {
1253 let prev = if spans.is_empty() {
1254 None
1255 } else {
1256 produce(
1257 state.borrow_mut(),
1258 &mut stack,
1259 Item::Paragraph(Text::new(spans.drain(..).collect())),
1260 source,
1261 )
1262 };
1263
1264 stack.push(Scope::List(List {
1265 start: first_item,
1266 bullets: Vec::new(),
1267 last_item_start: None,
1268 }));
1269
1270 prev
1271 }
1272 pulldown_cmark::Tag::Item => {
1273 if let Some(Scope::List(list)) = stack.last_mut() {
1274 list.last_item_start = Some(source.start);
1275 list.bullets.push(Bullet::Point { items: Vec::new() });
1276 }
1277
1278 None
1279 }
1280 pulldown_cmark::Tag::BlockQuote(_kind) if !metadata => {
1281 let prev = if spans.is_empty() {
1282 None
1283 } else {
1284 produce(
1285 state.borrow_mut(),
1286 &mut stack,
1287 Item::Paragraph(Text::new(spans.drain(..).collect())),
1288 source,
1289 )
1290 };
1291
1292 stack.push(Scope::Quote(Vec::new()));
1293
1294 prev
1295 }
1296 pulldown_cmark::Tag::CodeBlock(pulldown_cmark::CodeBlockKind::Fenced(language))
1297 if !metadata =>
1298 {
1299 #[cfg(feature = "highlighter")]
1300 {
1301 code_parser = Some({
1302 let mut code_parser = state
1303 .borrow_mut()
1304 .parser
1305 .take()
1306 .filter(|parser| parser.language() == language.as_ref())
1307 .unwrap_or_else(|| {
1308 code::Parser::new(language.split(',').next().unwrap_or_default())
1309 });
1310
1311 code_parser.prepare();
1312
1313 code_parser
1314 });
1315 }
1316
1317 code_block = true;
1318 code_language = (!language.is_empty()).then(|| language.into_string());
1319
1320 if spans.is_empty() {
1321 None
1322 } else {
1323 produce(
1324 state.borrow_mut(),
1325 &mut stack,
1326 Item::Paragraph(Text::new(spans.drain(..).collect())),
1327 source,
1328 )
1329 }
1330 }
1331 pulldown_cmark::Tag::MetadataBlock(_) => {
1332 metadata = true;
1333 None
1334 }
1335 pulldown_cmark::Tag::Table(alignment) => {
1336 stack.push(Scope::Table {
1337 columns: Vec::with_capacity(alignment.len()),
1338 alignment,
1339 current: Vec::new(),
1340 rows: Vec::new(),
1341 });
1342
1343 None
1344 }
1345 pulldown_cmark::Tag::TableHead => {
1346 strong = true;
1347 None
1348 }
1349 pulldown_cmark::Tag::TableRow => {
1350 let Scope::Table { rows, .. } = stack.last_mut()? else {
1351 return None;
1352 };
1353
1354 rows.push(Row { cells: Vec::new() });
1355 None
1356 }
1357 _ => None,
1358 },
1359 pulldown_cmark::Event::End(tag) => match tag {
1360 pulldown_cmark::TagEnd::Heading(level) if !metadata => produce(
1361 state.borrow_mut(),
1362 &mut stack,
1363 Item::Heading(level, Text::new(spans.drain(..).collect())),
1364 source,
1365 ),
1366 pulldown_cmark::TagEnd::Strong if !metadata => {
1367 strong = false;
1368 None
1369 }
1370 pulldown_cmark::TagEnd::Emphasis if !metadata => {
1371 emphasis = false;
1372 None
1373 }
1374 pulldown_cmark::TagEnd::Strikethrough if !metadata => {
1375 strikethrough = false;
1376 None
1377 }
1378 pulldown_cmark::TagEnd::Link if !metadata => {
1379 link = None;
1380 None
1381 }
1382 pulldown_cmark::TagEnd::Paragraph if !metadata => {
1383 paragraph_start = None;
1384
1385 if spans.is_empty() {
1386 None
1387 } else {
1388 produce(
1389 state.borrow_mut(),
1390 &mut stack,
1391 Item::Paragraph(Text::new(spans.drain(..).collect())),
1392 source,
1393 )
1394 }
1395 }
1396 pulldown_cmark::TagEnd::Item if !metadata => {
1397 if spans.is_empty() {
1398 None
1399 } else {
1400 produce(
1401 state.borrow_mut(),
1402 &mut stack,
1403 Item::Paragraph(Text::new(spans.drain(..).collect())),
1404 source,
1405 )
1406 }
1407 }
1408 pulldown_cmark::TagEnd::List(_) if !metadata => {
1409 let scope = stack.pop()?;
1410
1411 let Scope::List(list) = scope else {
1412 return None;
1413 };
1414
1415 let last_item_start = list.last_item_start;
1416 let produced = produce(
1417 state.borrow_mut(),
1418 &mut stack,
1419 Item::List {
1420 start: list.start,
1421 bullets: list.bullets,
1422 },
1423 source,
1424 );
1425
1426 if produced.is_some()
1430 && let Some(start) = last_item_start
1431 {
1432 state.borrow_mut().window = Some(start);
1433 }
1434
1435 produced
1436 }
1437 pulldown_cmark::TagEnd::BlockQuote(_kind) if !metadata => {
1438 let scope = stack.pop()?;
1439
1440 let Scope::Quote(quote) = scope else {
1441 return None;
1442 };
1443
1444 produce(state.borrow_mut(), &mut stack, Item::Quote(quote), source)
1445 }
1446 pulldown_cmark::TagEnd::Image if !metadata => {
1447 let (url, title, start) = image.take()?;
1448 let alt = Text::new(spans.drain(start..).collect());
1449
1450 let state = state.borrow_mut();
1451 let _ = state.images.insert(url.clone());
1452
1453 let produced = produce(state, &mut stack, Item::Image { url, title, alt }, source);
1454
1455 if let Some(start) = paragraph_start.filter(|_| produced.is_some()) {
1459 state.borrow_mut().window = Some(start);
1460 }
1461
1462 produced
1463 }
1464 pulldown_cmark::TagEnd::CodeBlock if !metadata => {
1465 code_block = false;
1466
1467 #[cfg(feature = "highlighter")]
1468 {
1469 state.borrow_mut().parser = code_parser.take();
1470 }
1471
1472 produce(
1473 state.borrow_mut(),
1474 &mut stack,
1475 Item::CodeBlock {
1476 language: code_language.take(),
1477 code: mem::take(&mut code),
1478 lines: code_lines.drain(..).collect(),
1479 },
1480 source,
1481 )
1482 }
1483 pulldown_cmark::TagEnd::MetadataBlock(_) => {
1484 metadata = false;
1485 None
1486 }
1487 pulldown_cmark::TagEnd::Table => {
1488 let scope = stack.pop()?;
1489
1490 let Scope::Table { columns, rows, .. } = scope else {
1491 return None;
1492 };
1493
1494 produce(
1495 state.borrow_mut(),
1496 &mut stack,
1497 Item::Table { columns, rows },
1498 source,
1499 )
1500 }
1501 pulldown_cmark::TagEnd::TableHead => {
1502 strong = false;
1503 None
1504 }
1505 pulldown_cmark::TagEnd::TableCell => {
1506 if !spans.is_empty() {
1507 let _ = produce(
1508 state.borrow_mut(),
1509 &mut stack,
1510 Item::Paragraph(Text::new(spans.drain(..).collect())),
1511 source,
1512 );
1513 }
1514
1515 let Scope::Table {
1516 alignment,
1517 columns,
1518 rows,
1519 current,
1520 } = stack.last_mut()?
1521 else {
1522 return None;
1523 };
1524
1525 if columns.len() < alignment.len() {
1526 columns.push(Column {
1527 header: std::mem::take(current),
1528 alignment: alignment[columns.len()],
1529 });
1530 } else {
1531 rows.last_mut()
1532 .expect("table row")
1533 .cells
1534 .push(std::mem::take(current));
1535 }
1536
1537 None
1538 }
1539 _ => None,
1540 },
1541 pulldown_cmark::Event::Text(text) if !metadata => {
1542 if code_block {
1543 code.push_str(&text);
1544
1545 #[cfg(feature = "highlighter")]
1546 if let Some(highlighter) = &mut code_parser {
1547 for line in text.lines() {
1548 code_lines.push(Text::new(highlighter.parse_line(line).to_vec()));
1549 }
1550 }
1551
1552 #[cfg(not(feature = "highlighter"))]
1553 for line in text.lines() {
1554 code_lines.push(Text::new(vec![Span::Code {
1555 text: line.to_owned(),
1556 code: Code::Other,
1557 }]));
1558 }
1559
1560 return None;
1561 }
1562
1563 let span = Span::Standard {
1564 text: text.into_string(),
1565 strong,
1566 emphasis,
1567 strikethrough,
1568 link: link.clone(),
1569 inline_code: false,
1570 };
1571
1572 spans.push(span);
1573
1574 None
1575 }
1576 pulldown_cmark::Event::Code(code) if !metadata => {
1577 let span = Span::Standard {
1578 text: code.into_string(),
1579 strong,
1580 emphasis,
1581 strikethrough,
1582 link: link.clone(),
1583 inline_code: true,
1584 };
1585
1586 spans.push(span);
1587 None
1588 }
1589 pulldown_cmark::Event::SoftBreak if !metadata => {
1590 spans.push(Span::Standard {
1591 text: String::from(" "),
1592 strikethrough,
1593 strong,
1594 emphasis,
1595 link: link.clone(),
1596 inline_code: false,
1597 });
1598 None
1599 }
1600 pulldown_cmark::Event::HardBreak if !metadata => {
1601 spans.push(Span::Standard {
1602 text: String::from("\n"),
1603 strikethrough,
1604 strong,
1605 emphasis,
1606 link: link.clone(),
1607 inline_code: false,
1608 });
1609 None
1610 }
1611 pulldown_cmark::Event::Rule => produce(state.borrow_mut(), &mut stack, Item::Rule, source),
1612 pulldown_cmark::Event::TaskListMarker(done) => {
1613 if let Some(Scope::List(list)) = stack.last_mut()
1614 && let Some(item) = list.bullets.last_mut()
1615 && let Bullet::Point { items } = item
1616 {
1617 *item = Bullet::Task {
1618 items: std::mem::take(items),
1619 done,
1620 };
1621 }
1622
1623 None
1624 }
1625 _ => None,
1626 })
1627}
1628
1629#[derive(Debug, Clone, Copy, PartialEq)]
1631pub struct Settings {
1632 pub font: Font,
1634 pub inline_code_font: Font,
1636 pub code_block_font: Font,
1638 pub line_height: LineHeight,
1640 pub text_size: Pixels,
1642 pub code_block_size: Pixels,
1644 pub inline_code_size: Pixels,
1646 pub h1_size: Pixels,
1648 pub h2_size: Pixels,
1650 pub h3_size: Pixels,
1652 pub h4_size: Pixels,
1654 pub h5_size: Pixels,
1656 pub h6_size: Pixels,
1658 pub spacing: Pixels,
1660}
1661
1662impl Settings {
1663 pub fn with_text_size(text_size: impl Into<Pixels>) -> Self {
1670 let text_size = text_size.into();
1671 let line_height = LineHeight::default();
1672
1673 Self {
1674 font: Font::DEFAULT,
1675 inline_code_font: Font::MONOSPACE,
1676 code_block_font: Font::MONOSPACE,
1677 line_height,
1678 text_size,
1679 inline_code_size: text_size * 0.85,
1680 code_block_size: text_size * 0.85,
1681 h1_size: text_size * 1.5,
1682 h2_size: text_size * 1.25,
1683 h3_size: text_size * 1.125,
1684 h4_size: text_size,
1685 h5_size: text_size,
1686 h6_size: text_size,
1687 spacing: line_height.to_absolute(text_size) / 1.5,
1688 }
1689 }
1690
1691 pub fn line_height(self, line_height: impl Into<LineHeight>) -> Self {
1693 let line_height = line_height.into();
1694
1695 Self {
1696 line_height,
1697 spacing: line_height.to_absolute(self.text_size) / 1.5,
1698 ..self
1699 }
1700 }
1701}
1702
1703impl Default for Settings {
1704 fn default() -> Self {
1705 Self::with_text_size(16)
1706 }
1707}
1708
1709pub fn view<'a, Theme, Renderer>(
1756 items: &'a [Item],
1757 settings: impl Into<Settings>,
1758 theme: Theme,
1759) -> Element<'a, Uri, Theme, Renderer>
1760where
1761 Theme: Catalog + 'a,
1762 Renderer: core::text::Renderer + 'a,
1763{
1764 view_with(
1765 items,
1766 settings,
1767 &DefaultViewer {
1768 theme,
1769 highlighter: None,
1770 },
1771 )
1772}
1773
1774pub fn view_with<'a, Message, Theme, Renderer>(
1780 items: &'a [Item],
1781 settings: impl Into<Settings>,
1782 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1783) -> Element<'a, Message, Theme, Renderer>
1784where
1785 Message: 'a,
1786 Theme: Catalog + 'a,
1787 Renderer: core::text::Renderer + 'a,
1788{
1789 self::items(viewer, settings.into(), items)
1790}
1791
1792pub fn item<'a, Message, Theme, Renderer>(
1794 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1795 settings: Settings,
1796 item: &'a Item,
1797) -> Element<'a, Message, Theme, Renderer>
1798where
1799 Message: 'a,
1800 Theme: Catalog + 'a,
1801 Renderer: core::text::Renderer + 'a,
1802{
1803 match item {
1804 Item::Image { url, title, alt } => viewer.image(settings, url, title, alt),
1805 Item::Heading(level, text) => viewer.heading(settings, level, text),
1806 Item::Paragraph(text) => viewer.paragraph(settings, text),
1807 Item::CodeBlock {
1808 language,
1809 code,
1810 lines,
1811 } => viewer.code_block(settings, language.as_deref(), code, lines),
1812 Item::List {
1813 start: None,
1814 bullets,
1815 } => viewer.unordered_list(settings, bullets),
1816 Item::List {
1817 start: Some(start),
1818 bullets,
1819 } => viewer.ordered_list(settings, *start, bullets),
1820 Item::Quote(quote) => viewer.quote(settings, quote),
1821 Item::Rule => viewer.rule(),
1822 Item::Table { columns, rows } => viewer.table(settings, columns, rows),
1823 }
1824}
1825
1826pub fn heading<'a, Message, Theme, Renderer>(
1828 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1829 settings: Settings,
1830 level: &'a HeadingLevel,
1831 text: &'a Text,
1832 on_link_click: impl Fn(Uri) -> Message + 'a,
1833) -> Element<'a, Message, Theme, Renderer>
1834where
1835 Message: 'a,
1836 Theme: Catalog + 'a,
1837 Renderer: core::text::Renderer + 'a,
1838{
1839 let Settings {
1840 h1_size,
1841 h2_size,
1842 h3_size,
1843 h4_size,
1844 h5_size,
1845 h6_size,
1846 ..
1847 } = settings;
1848
1849 let size = match level {
1850 pulldown_cmark::HeadingLevel::H1 => h1_size,
1851 pulldown_cmark::HeadingLevel::H2 => h2_size,
1852 pulldown_cmark::HeadingLevel::H3 => h3_size,
1853 pulldown_cmark::HeadingLevel::H4 => h4_size,
1854 pulldown_cmark::HeadingLevel::H5 => h5_size,
1855 pulldown_cmark::HeadingLevel::H6 => h6_size,
1856 };
1857
1858 container(
1859 rich_text(text.spans(
1860 Settings {
1861 font: Font {
1862 weight: font::Weight::Bold,
1863 ..settings.font
1864 },
1865 inline_code_font: Font {
1866 weight: font::Weight::Bold,
1867 ..settings.inline_code_font
1868 },
1869 inline_code_size: size * (settings.inline_code_size / settings.text_size),
1870 ..settings
1871 },
1872 viewer.theme(),
1873 viewer.highlighter(),
1874 ))
1875 .on_link_click(on_link_click)
1876 .size(size)
1877 .line_height(settings.line_height),
1878 )
1879 .into()
1880}
1881
1882pub fn paragraph<'a, Message, Theme, Renderer>(
1884 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1885 settings: Settings,
1886 text: &Text,
1887 on_link_click: impl Fn(Uri) -> Message + 'a,
1888) -> Element<'a, Message, Theme, Renderer>
1889where
1890 Message: 'a,
1891 Theme: Catalog + 'a,
1892 Renderer: core::text::Renderer + 'a,
1893{
1894 rich_text(text.spans(settings, viewer.theme(), viewer.highlighter()))
1895 .size(settings.text_size)
1896 .line_height(settings.line_height)
1897 .on_link_click(on_link_click)
1898 .into()
1899}
1900
1901pub fn unordered_list<'a, Message, Theme, Renderer>(
1904 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1905 settings: Settings,
1906 bullets: &'a [Bullet],
1907) -> Element<'a, Message, Theme, Renderer>
1908where
1909 Message: 'a,
1910 Theme: Catalog + 'a,
1911 Renderer: core::text::Renderer + 'a,
1912{
1913 column(bullets.iter().map(|bullet| {
1914 row![
1915 match bullet {
1916 Bullet::Point { .. } => {
1917 text("•").size(settings.text_size).into()
1918 }
1919 Bullet::Task { done, .. } => {
1920 Element::from(
1921 container(checkbox(*done).size(settings.text_size))
1922 .center_y(text::LineHeight::default().to_absolute(settings.text_size)),
1923 )
1924 }
1925 },
1926 items(
1927 viewer,
1928 Settings {
1929 spacing: settings.spacing / 2.0,
1930 ..settings
1931 },
1932 bullet.items(),
1933 )
1934 ]
1935 .spacing(settings.text_size / 2.0)
1936 .into()
1937 }))
1938 .spacing(settings.spacing / 2.0)
1939 .padding(padding::left(settings.text_size.0))
1940 .into()
1941}
1942
1943pub fn ordered_list<'a, Message, Theme, Renderer>(
1946 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1947 settings: Settings,
1948 start: u64,
1949 bullets: &'a [Bullet],
1950) -> Element<'a, Message, Theme, Renderer>
1951where
1952 Message: 'a,
1953 Theme: Catalog + 'a,
1954 Renderer: core::text::Renderer + 'a,
1955{
1956 let digits = (start + bullets.len() as u64).max(1).ilog10() + 1;
1957
1958 column(bullets.iter().enumerate().map(|(i, bullet)| {
1959 row![
1960 text!("{}.", i as u64 + start)
1961 .size(settings.text_size)
1962 .align_x(alignment::Horizontal::Right)
1963 .width(settings.text_size * ((digits as f32 / 2.0).ceil() + 1.0)),
1964 items(
1965 viewer,
1966 Settings {
1967 spacing: settings.spacing / 2.0,
1968 ..settings
1969 },
1970 bullet.items(),
1971 )
1972 ]
1973 .spacing(settings.text_size / 2.0)
1974 .into()
1975 }))
1976 .spacing(settings.spacing / 2.0)
1977 .into()
1978}
1979
1980pub fn code_block<'a, Message, Theme, Renderer>(
1982 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1983 settings: Settings,
1984 lines: &'a [Text],
1985 on_link_click: impl Fn(Uri) -> Message + Clone + 'a,
1986) -> Element<'a, Message, Theme, Renderer>
1987where
1988 Message: 'a,
1989 Theme: Catalog + 'a,
1990 Renderer: core::text::Renderer + 'a,
1991{
1992 let padding = settings.code_block_size / 0.85 * 0.75;
1993
1994 container(
1995 scrollable(column(lines.iter().map(|line| {
1996 rich_text(line.spans(settings, viewer.theme(), viewer.highlighter()))
1997 .on_link_click(on_link_click.clone())
1998 .font(settings.code_block_font)
1999 .size(settings.code_block_size)
2000 .line_height(settings.line_height)
2001 .into()
2002 })))
2003 .direction(scrollable::Direction::Horizontal(
2004 scrollable::Scrollbar::default()
2005 .width(padding / 2.0)
2006 .scroller_width(padding / 2.0),
2007 ))
2008 .spacing(padding),
2009 )
2010 .width(Length::Fill)
2011 .padding(padding)
2012 .class(Theme::code_block())
2013 .into()
2014}
2015
2016pub fn quote<'a, Message, Theme, Renderer>(
2018 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
2019 settings: Settings,
2020 contents: &'a [Item],
2021) -> Element<'a, Message, Theme, Renderer>
2022where
2023 Message: 'a,
2024 Theme: Catalog + 'a,
2025 Renderer: core::text::Renderer + 'a,
2026{
2027 container(
2028 column(
2029 contents
2030 .iter()
2031 .map(|content| item(viewer, settings, content)),
2032 )
2033 .spacing(settings.spacing.0),
2034 )
2035 .width(Length::Fill)
2036 .padding(settings.spacing.0)
2037 .class(Theme::quote())
2038 .into()
2039}
2040
2041pub fn rule<'a, Message, Theme, Renderer>() -> Element<'a, Message, Theme, Renderer>
2043where
2044 Message: 'a,
2045 Theme: Catalog + 'a,
2046 Renderer: core::text::Renderer + 'a,
2047{
2048 rule::horizontal(2).into()
2049}
2050
2051pub fn table<'a, Message, Theme, Renderer>(
2053 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
2054 settings: Settings,
2055 columns: &'a [Column],
2056 rows: &'a [Row],
2057) -> Element<'a, Message, Theme, Renderer>
2058where
2059 Message: 'a,
2060 Theme: Catalog + 'a,
2061 Renderer: core::text::Renderer + 'a,
2062{
2063 use crate::table;
2064
2065 let table = table(
2066 columns.iter().enumerate().map(move |(i, column)| {
2067 table::column(items(viewer, settings, &column.header), move |row: &Row| {
2068 if let Some(cells) = row.cells.get(i) {
2069 items(viewer, settings, cells)
2070 } else {
2071 text("").into()
2072 }
2073 })
2074 .align_x(match column.alignment {
2075 pulldown_cmark::Alignment::None | pulldown_cmark::Alignment::Left => {
2076 alignment::Horizontal::Left
2077 }
2078 pulldown_cmark::Alignment::Center => alignment::Horizontal::Center,
2079 pulldown_cmark::Alignment::Right => alignment::Horizontal::Right,
2080 })
2081 }),
2082 rows,
2083 )
2084 .padding_x(settings.spacing.0)
2085 .padding_y(settings.spacing.0 / 2.0)
2086 .separator_x(0);
2087
2088 scrollable(table)
2089 .direction(scrollable::Direction::Horizontal(
2090 scrollable::Scrollbar::default(),
2091 ))
2092 .spacing(settings.spacing.0 / 2.0)
2093 .into()
2094}
2095
2096pub fn items<'a, Message, Theme, Renderer>(
2098 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
2099 settings: Settings,
2100 items: &'a [Item],
2101) -> Element<'a, Message, Theme, Renderer>
2102where
2103 Message: 'a,
2104 Theme: Catalog + 'a,
2105 Renderer: core::text::Renderer + 'a,
2106{
2107 column(sections(items).map(|(heading, contents)| {
2108 let contents = column(
2109 contents
2110 .iter()
2111 .map(|content| item(viewer, settings, content)),
2112 )
2113 .spacing(settings.spacing)
2114 .into();
2115
2116 if let Some(heading) = heading {
2117 column![item(viewer, settings, heading), contents]
2118 .spacing(settings.spacing / 2.0)
2119 .into()
2120 } else {
2121 contents
2122 }
2123 }))
2124 .spacing(settings.spacing * 1.5)
2125 .into()
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 ) -> Element<'a, Message, Theme, Renderer> {
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 .into()
2168 }
2169
2170 fn heading(
2174 &self,
2175 settings: Settings,
2176 level: &'a HeadingLevel,
2177 text: &'a Text,
2178 ) -> Element<'a, Message, Theme, Renderer> {
2179 heading(self, settings, level, text, Self::on_link_click)
2180 }
2181
2182 fn paragraph(&self, settings: Settings, text: &Text) -> Element<'a, Message, Theme, Renderer> {
2186 paragraph(self, settings, text, Self::on_link_click)
2187 }
2188
2189 fn code_block(
2193 &self,
2194 settings: Settings,
2195 language: Option<&'a str>,
2196 code: &'a str,
2197 lines: &'a [Text],
2198 ) -> Element<'a, Message, Theme, Renderer> {
2199 let _language = language;
2200 let _code = code;
2201
2202 code_block(self, settings, lines, Self::on_link_click)
2203 }
2204
2205 fn unordered_list(
2209 &self,
2210 settings: Settings,
2211 bullets: &'a [Bullet],
2212 ) -> Element<'a, Message, Theme, Renderer> {
2213 unordered_list(self, settings, bullets)
2214 }
2215
2216 fn ordered_list(
2220 &self,
2221 settings: Settings,
2222 start: u64,
2223 bullets: &'a [Bullet],
2224 ) -> Element<'a, Message, Theme, Renderer> {
2225 ordered_list(self, settings, start, bullets)
2226 }
2227
2228 fn quote(
2232 &self,
2233 settings: Settings,
2234 contents: &'a [Item],
2235 ) -> Element<'a, Message, Theme, Renderer> {
2236 quote(self, settings, contents)
2237 }
2238
2239 fn rule(&self) -> Element<'a, Message, Theme, Renderer> {
2243 rule()
2244 }
2245
2246 fn table(
2250 &self,
2251 settings: Settings,
2252 columns: &'a [Column],
2253 rows: &'a [Row],
2254 ) -> Element<'a, Message, Theme, Renderer> {
2255 table(self, settings, columns, rows)
2256 }
2257}
2258
2259pub struct DefaultViewer<'a, Theme> {
2261 theme: Theme,
2262 highlighter: Option<Box<dyn text::Highlighter<Code, Theme> + 'a>>,
2263}
2264
2265impl<'a, Theme> DefaultViewer<'a, Theme> {
2266 pub fn new(theme: Theme) -> Self {
2268 Self {
2269 theme,
2270 highlighter: None,
2271 }
2272 }
2273
2274 pub fn highlighter(mut self, highlighter: impl text::Highlighter<Code, Theme> + 'a) -> Self {
2276 self.highlighter = Some(Box::new(highlighter));
2277 self
2278 }
2279}
2280
2281impl<'a, Theme, Renderer> Viewer<'a, Uri, Theme, Renderer> for DefaultViewer<'a, Theme>
2282where
2283 Theme: Catalog + 'a,
2284 Renderer: core::text::Renderer + 'a,
2285{
2286 fn theme(&self) -> &Theme {
2287 &self.theme
2288 }
2289
2290 fn highlighter(&self) -> &dyn text::Highlighter<Code, Theme> {
2291 self.highlighter
2292 .as_deref()
2293 .unwrap_or_else(|| self.theme.highlighter())
2294 }
2295
2296 fn on_link_click(url: Uri) -> Uri {
2297 url
2298 }
2299}
2300
2301pub trait Catalog:
2303 container::Catalog
2304 + scrollable::Catalog
2305 + text::Catalog
2306 + crate::rule::Catalog
2307 + checkbox::Catalog
2308 + crate::table::Catalog
2309 + Clone
2310 + PartialEq
2311{
2312 fn id(&self) -> &str;
2316
2317 fn link_color(&self) -> Color;
2319
2320 fn code(&self) -> InlineCode;
2322
2323 fn code_block<'a>() -> <Self as container::Catalog>::Class<'a>;
2325
2326 fn quote<'a>() -> <Self as container::Catalog>::Class<'a>;
2328
2329 fn highlighter(&self) -> &dyn text::Highlighter<Code, Self>;
2331}
2332
2333#[derive(Debug, Clone, Copy, PartialEq)]
2335pub struct InlineCode {
2336 pub padding: Padding,
2338 pub highlight: Highlight,
2340 pub color: Color,
2342}
2343
2344impl Catalog for Theme {
2345 fn id(&self) -> &str {
2346 theme::Base::name(self)
2347 }
2348
2349 fn link_color(&self) -> Color {
2350 self.seed().primary
2351 }
2352
2353 fn code(&self) -> InlineCode {
2354 let palette = self.palette();
2355
2356 InlineCode {
2357 padding: padding::horizontal(4).vertical(1),
2358 highlight: Highlight {
2359 background: palette.background.weaker.color.into(),
2360 border: border::rounded(4),
2361 },
2362 color: palette.background.weaker.text,
2363 }
2364 }
2365
2366 fn code_block<'a>() -> <Self as container::Catalog>::Class<'a> {
2367 Box::new(|theme| container::dark(theme).border(border::rounded(5)))
2368 }
2369
2370 fn quote<'a>() -> <Self as container::Catalog>::Class<'a> {
2371 Box::new(|theme| {
2372 let palette = theme.palette();
2373
2374 container::Style {
2375 text_color: Some(palette.background.weakest.text),
2376 background: Some(palette.background.weakest.color.into()),
2377 border: border::rounded(5),
2378 ..container::Style::default()
2379 }
2380 })
2381 }
2382
2383 fn highlighter(&self) -> &dyn text::Highlighter<Code, Self> {
2384 &Code::highlight
2385 }
2386}
2387
2388#[cfg(feature = "highlighter")]
2389mod code {
2390 use super::Span;
2391
2392 #[derive(Debug)]
2393 pub struct Parser {
2394 lines: Vec<(String, Vec<Span>)>,
2395 language: String,
2396 stream: iced_highlighter::Stream,
2397 current: usize,
2398 }
2399
2400 impl Parser {
2401 pub fn new(language: &str) -> Self {
2402 Self {
2403 lines: Vec::new(),
2404 stream: iced_highlighter::Stream::new(&iced_highlighter::Settings {
2405 token: language.to_owned(),
2406 }),
2407 language: language.to_owned(),
2408 current: 0,
2409 }
2410 }
2411
2412 pub fn language(&self) -> &str {
2413 &self.language
2414 }
2415
2416 pub fn prepare(&mut self) {
2417 self.current = 0;
2418 }
2419
2420 pub fn parse_line(&mut self, text: &str) -> &[Span] {
2421 match self.lines.get(self.current) {
2422 Some(line) if line.0 == text => {
2423 if self.current + 1 == self.lines.len() {
2424 self.stream.commit();
2425 }
2426 }
2427 _ => {
2428 if self.current + 1 < self.lines.len() {
2429 log::debug!("Resetting highlighter...");
2430 self.stream.reset();
2431 self.lines.truncate(self.current);
2432
2433 for line in &self.lines {
2434 log::debug!("Refeeding {n} lines", n = self.lines.len());
2435
2436 let _ = self.stream.parse_line(&line.0);
2437 self.stream.commit();
2438 }
2439 }
2440
2441 log::trace!("Parsing: {text}", text = text.trim_end());
2442
2443 let mut spans = Vec::new();
2444
2445 if self.current == self.lines.len() {
2446 self.stream.commit();
2447 }
2448
2449 for (range, code) in self.stream.parse_line(text) {
2450 spans.push(Span::Code {
2451 text: text[range].to_owned(),
2452 code,
2453 });
2454 }
2455
2456 if self.current == self.lines.len() {
2457 self.lines.push((text.to_owned(), spans));
2458 } else {
2459 self.lines[self.current] = (text.to_owned(), spans);
2460 }
2461 }
2462 }
2463
2464 self.current += 1;
2465
2466 &self
2467 .lines
2468 .get(self.current - 1)
2469 .expect("Line must be parsed")
2470 .1
2471 }
2472 }
2473}