iced_widget/
markdown.rs

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