Skip to main content

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