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