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