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, Element, Length, Padding, Pixels, Theme, color,
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 code_block = false;
478    let mut link = None;
479    let mut image = None;
480    let mut stack = Vec::new();
481
482    #[cfg(feature = "highlighter")]
483    let mut highlighter = None;
484
485    let parser = pulldown_cmark::Parser::new_with_broken_link_callback(
486        markdown,
487        pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS
488            | pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS
489            | pulldown_cmark::Options::ENABLE_TABLES
490            | pulldown_cmark::Options::ENABLE_STRIKETHROUGH,
491        {
492            let references = state.borrow().references.clone();
493            let broken_links = broken_links.clone();
494
495            Some(move |broken_link: pulldown_cmark::BrokenLink<'_>| {
496                if let Some(reference) =
497                    references.get(broken_link.reference.as_ref())
498                {
499                    Some((
500                        pulldown_cmark::CowStr::from(reference.to_owned()),
501                        broken_link.reference.into_static(),
502                    ))
503                } else {
504                    let _ = RefCell::borrow_mut(&broken_links)
505                        .insert(broken_link.reference.into_string());
506
507                    None
508                }
509            })
510        },
511    );
512
513    let references = &mut state.borrow_mut().references;
514
515    for reference in parser.reference_definitions().iter() {
516        let _ = references
517            .insert(reference.0.to_owned(), reference.1.dest.to_string());
518    }
519
520    let produce = move |state: &mut State,
521                        stack: &mut Vec<Scope>,
522                        item,
523                        source: Range<usize>| {
524        if let Some(scope) = stack.last_mut() {
525            match scope {
526                Scope::List(list) => {
527                    list.items.last_mut().expect("item context").push(item);
528                }
529            }
530
531            None
532        } else {
533            state.leftover = markdown[source.start..].to_owned();
534
535            Some((
536                item,
537                &markdown[source.start..source.end],
538                broken_links.take(),
539            ))
540        }
541    };
542
543    let parser = parser.into_offset_iter();
544
545    // We want to keep the `spans` capacity
546    #[allow(clippy::drain_collect)]
547    parser.filter_map(move |(event, source)| match event {
548        pulldown_cmark::Event::Start(tag) => match tag {
549            pulldown_cmark::Tag::Strong if !metadata && !table => {
550                strong = true;
551                None
552            }
553            pulldown_cmark::Tag::Emphasis if !metadata && !table => {
554                emphasis = true;
555                None
556            }
557            pulldown_cmark::Tag::Strikethrough if !metadata && !table => {
558                strikethrough = true;
559                None
560            }
561            pulldown_cmark::Tag::Link { dest_url, .. }
562                if !metadata && !table =>
563            {
564                match Url::parse(&dest_url) {
565                    Ok(url)
566                        if url.scheme() == "http"
567                            || url.scheme() == "https" =>
568                    {
569                        link = Some(url);
570                    }
571                    _ => {}
572                }
573
574                None
575            }
576            pulldown_cmark::Tag::Image {
577                dest_url, title, ..
578            } if !metadata && !table => {
579                image = Url::parse(&dest_url)
580                    .ok()
581                    .map(|url| (url, title.into_string()));
582                None
583            }
584            pulldown_cmark::Tag::List(first_item) if !metadata && !table => {
585                let prev = if spans.is_empty() {
586                    None
587                } else {
588                    produce(
589                        state.borrow_mut(),
590                        &mut stack,
591                        Item::Paragraph(Text::new(spans.drain(..).collect())),
592                        source,
593                    )
594                };
595
596                stack.push(Scope::List(List {
597                    start: first_item,
598                    items: Vec::new(),
599                }));
600
601                prev
602            }
603            pulldown_cmark::Tag::Item => {
604                if let Some(Scope::List(list)) = stack.last_mut() {
605                    list.items.push(Vec::new());
606                }
607
608                None
609            }
610            pulldown_cmark::Tag::CodeBlock(
611                pulldown_cmark::CodeBlockKind::Fenced(language),
612            ) if !metadata && !table => {
613                #[cfg(feature = "highlighter")]
614                {
615                    highlighter = Some({
616                        let mut highlighter = state
617                            .borrow_mut()
618                            .highlighter
619                            .take()
620                            .filter(|highlighter| {
621                                highlighter.language == language.as_ref()
622                            })
623                            .unwrap_or_else(|| Highlighter::new(&language));
624
625                        highlighter.prepare();
626
627                        highlighter
628                    });
629                }
630
631                code_block = true;
632                code_language =
633                    (!language.is_empty()).then(|| language.into_string());
634
635                let prev = if spans.is_empty() {
636                    None
637                } else {
638                    produce(
639                        state.borrow_mut(),
640                        &mut stack,
641                        Item::Paragraph(Text::new(spans.drain(..).collect())),
642                        source,
643                    )
644                };
645
646                prev
647            }
648            pulldown_cmark::Tag::MetadataBlock(_) => {
649                metadata = true;
650                None
651            }
652            pulldown_cmark::Tag::Table(_) => {
653                table = true;
654                None
655            }
656            _ => None,
657        },
658        pulldown_cmark::Event::End(tag) => match tag {
659            pulldown_cmark::TagEnd::Heading(level) if !metadata && !table => {
660                produce(
661                    state.borrow_mut(),
662                    &mut stack,
663                    Item::Heading(level, Text::new(spans.drain(..).collect())),
664                    source,
665                )
666            }
667            pulldown_cmark::TagEnd::Strong if !metadata && !table => {
668                strong = false;
669                None
670            }
671            pulldown_cmark::TagEnd::Emphasis if !metadata && !table => {
672                emphasis = false;
673                None
674            }
675            pulldown_cmark::TagEnd::Strikethrough if !metadata && !table => {
676                strikethrough = false;
677                None
678            }
679            pulldown_cmark::TagEnd::Link if !metadata && !table => {
680                link = None;
681                None
682            }
683            pulldown_cmark::TagEnd::Paragraph if !metadata && !table => {
684                if spans.is_empty() {
685                    None
686                } else {
687                    produce(
688                        state.borrow_mut(),
689                        &mut stack,
690                        Item::Paragraph(Text::new(spans.drain(..).collect())),
691                        source,
692                    )
693                }
694            }
695            pulldown_cmark::TagEnd::Item if !metadata && !table => {
696                if spans.is_empty() {
697                    None
698                } else {
699                    produce(
700                        state.borrow_mut(),
701                        &mut stack,
702                        Item::Paragraph(Text::new(spans.drain(..).collect())),
703                        source,
704                    )
705                }
706            }
707            pulldown_cmark::TagEnd::List(_) if !metadata && !table => {
708                let scope = stack.pop()?;
709
710                let Scope::List(list) = scope;
711
712                produce(
713                    state.borrow_mut(),
714                    &mut stack,
715                    Item::List {
716                        start: list.start,
717                        items: list.items,
718                    },
719                    source,
720                )
721            }
722            pulldown_cmark::TagEnd::Image if !metadata && !table => {
723                let (url, title) = image.take()?;
724                let alt = Text::new(spans.drain(..).collect());
725
726                let state = state.borrow_mut();
727                let _ = state.images.insert(url.clone());
728
729                produce(
730                    state,
731                    &mut stack,
732                    Item::Image { url, title, alt },
733                    source,
734                )
735            }
736            pulldown_cmark::TagEnd::CodeBlock if !metadata && !table => {
737                code_block = false;
738
739                #[cfg(feature = "highlighter")]
740                {
741                    state.borrow_mut().highlighter = highlighter.take();
742                }
743
744                produce(
745                    state.borrow_mut(),
746                    &mut stack,
747                    Item::CodeBlock {
748                        language: code_language.take(),
749                        code: mem::take(&mut code),
750                        lines: code_lines.drain(..).collect(),
751                    },
752                    source,
753                )
754            }
755            pulldown_cmark::TagEnd::MetadataBlock(_) => {
756                metadata = false;
757                None
758            }
759            pulldown_cmark::TagEnd::Table => {
760                table = false;
761                None
762            }
763            _ => None,
764        },
765        pulldown_cmark::Event::Text(text) if !metadata && !table => {
766            if code_block {
767                code.push_str(&text);
768
769                #[cfg(feature = "highlighter")]
770                if let Some(highlighter) = &mut highlighter {
771                    for line in text.lines() {
772                        code_lines.push(Text::new(
773                            highlighter.highlight_line(line).to_vec(),
774                        ));
775                    }
776                }
777
778                #[cfg(not(feature = "highlighter"))]
779                for line in text.lines() {
780                    code_lines.push(Text::new(vec![Span::Standard {
781                        text: line.to_owned(),
782                        strong,
783                        emphasis,
784                        strikethrough,
785                        link: link.clone(),
786                        code: false,
787                    }]));
788                }
789
790                return None;
791            }
792
793            let span = Span::Standard {
794                text: text.into_string(),
795                strong,
796                emphasis,
797                strikethrough,
798                link: link.clone(),
799                code: false,
800            };
801
802            spans.push(span);
803
804            None
805        }
806        pulldown_cmark::Event::Code(code) if !metadata && !table => {
807            let span = Span::Standard {
808                text: code.into_string(),
809                strong,
810                emphasis,
811                strikethrough,
812                link: link.clone(),
813                code: true,
814            };
815
816            spans.push(span);
817            None
818        }
819        pulldown_cmark::Event::SoftBreak if !metadata && !table => {
820            spans.push(Span::Standard {
821                text: String::from(" "),
822                strikethrough,
823                strong,
824                emphasis,
825                link: link.clone(),
826                code: false,
827            });
828            None
829        }
830        pulldown_cmark::Event::HardBreak if !metadata && !table => {
831            spans.push(Span::Standard {
832                text: String::from("\n"),
833                strikethrough,
834                strong,
835                emphasis,
836                link: link.clone(),
837                code: false,
838            });
839            None
840        }
841        _ => None,
842    })
843}
844
845/// Configuration controlling Markdown rendering in [`view`].
846#[derive(Debug, Clone, Copy)]
847pub struct Settings {
848    /// The base text size.
849    pub text_size: Pixels,
850    /// The text size of level 1 heading.
851    pub h1_size: Pixels,
852    /// The text size of level 2 heading.
853    pub h2_size: Pixels,
854    /// The text size of level 3 heading.
855    pub h3_size: Pixels,
856    /// The text size of level 4 heading.
857    pub h4_size: Pixels,
858    /// The text size of level 5 heading.
859    pub h5_size: Pixels,
860    /// The text size of level 6 heading.
861    pub h6_size: Pixels,
862    /// The text size used in code blocks.
863    pub code_size: Pixels,
864    /// The spacing to be used between elements.
865    pub spacing: Pixels,
866    /// The styling of the Markdown.
867    pub style: Style,
868}
869
870impl Settings {
871    /// Creates new [`Settings`] with default text size and the given [`Style`].
872    pub fn with_style(style: impl Into<Style>) -> Self {
873        Self::with_text_size(16, style)
874    }
875
876    /// Creates new [`Settings`] with the given base text size in [`Pixels`].
877    ///
878    /// Heading levels will be adjusted automatically. Specifically,
879    /// the first level will be twice the base size, and then every level
880    /// after that will be 25% smaller.
881    pub fn with_text_size(
882        text_size: impl Into<Pixels>,
883        style: impl Into<Style>,
884    ) -> Self {
885        let text_size = text_size.into();
886
887        Self {
888            text_size,
889            h1_size: text_size * 2.0,
890            h2_size: text_size * 1.75,
891            h3_size: text_size * 1.5,
892            h4_size: text_size * 1.25,
893            h5_size: text_size,
894            h6_size: text_size,
895            code_size: text_size * 0.75,
896            spacing: text_size * 0.875,
897            style: style.into(),
898        }
899    }
900}
901
902impl From<&Theme> for Settings {
903    fn from(theme: &Theme) -> Self {
904        Self::with_style(Style::from(theme))
905    }
906}
907
908impl From<Theme> for Settings {
909    fn from(theme: Theme) -> Self {
910        Self::with_style(Style::from(theme))
911    }
912}
913
914/// The text styling of some Markdown rendering in [`view`].
915#[derive(Debug, Clone, Copy, PartialEq)]
916pub struct Style {
917    /// The [`Highlight`] to be applied to the background of inline code.
918    pub inline_code_highlight: Highlight,
919    /// The [`Padding`] to be applied to the background of inline code.
920    pub inline_code_padding: Padding,
921    /// The [`Color`] to be applied to inline code.
922    pub inline_code_color: Color,
923    /// The [`Color`] to be applied to links.
924    pub link_color: Color,
925}
926
927impl Style {
928    /// Creates a new [`Style`] from the given [`theme::Palette`].
929    pub fn from_palette(palette: theme::Palette) -> Self {
930        Self {
931            inline_code_padding: padding::left(1).right(1),
932            inline_code_highlight: Highlight {
933                background: color!(0x111).into(),
934                border: border::rounded(2),
935            },
936            inline_code_color: Color::WHITE,
937            link_color: palette.primary,
938        }
939    }
940}
941
942impl From<theme::Palette> for Style {
943    fn from(palette: theme::Palette) -> Self {
944        Self::from_palette(palette)
945    }
946}
947
948impl From<&Theme> for Style {
949    fn from(theme: &Theme) -> Self {
950        Self::from_palette(theme.palette())
951    }
952}
953
954impl From<Theme> for Style {
955    fn from(theme: Theme) -> Self {
956        Self::from_palette(theme.palette())
957    }
958}
959
960/// Display a bunch of Markdown items.
961///
962/// You can obtain the items with [`parse`].
963///
964/// # Example
965/// ```no_run
966/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
967/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
968/// #
969/// use iced::widget::markdown;
970/// use iced::Theme;
971///
972/// struct State {
973///    markdown: Vec<markdown::Item>,
974/// }
975///
976/// enum Message {
977///     LinkClicked(markdown::Url),
978/// }
979///
980/// impl State {
981///     pub fn new() -> Self {
982///         Self {
983///             markdown: markdown::parse("This is some **Markdown**!").collect(),
984///         }
985///     }
986///
987///     fn view(&self) -> Element<'_, Message> {
988///         markdown::view(&self.markdown, Theme::TokyoNight)
989///             .map(Message::LinkClicked)
990///             .into()
991///     }
992///
993///     fn update(state: &mut State, message: Message) {
994///         match message {
995///             Message::LinkClicked(url) => {
996///                 println!("The following url was clicked: {url}");
997///             }
998///         }
999///     }
1000/// }
1001/// ```
1002pub fn view<'a, Theme, Renderer>(
1003    items: impl IntoIterator<Item = &'a Item>,
1004    settings: impl Into<Settings>,
1005) -> Element<'a, Url, Theme, Renderer>
1006where
1007    Theme: Catalog + 'a,
1008    Renderer: core::text::Renderer<Font = Font> + 'a,
1009{
1010    view_with(items, settings, &DefaultViewer)
1011}
1012
1013/// Runs [`view`] but with a custom [`Viewer`] to turn an [`Item`] into
1014/// an [`Element`].
1015///
1016/// This is useful if you want to customize the look of certain Markdown
1017/// elements.
1018pub fn view_with<'a, Message, Theme, Renderer>(
1019    items: impl IntoIterator<Item = &'a Item>,
1020    settings: impl Into<Settings>,
1021    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1022) -> Element<'a, Message, Theme, Renderer>
1023where
1024    Message: 'a,
1025    Theme: Catalog + 'a,
1026    Renderer: core::text::Renderer<Font = Font> + 'a,
1027{
1028    let settings = settings.into();
1029
1030    let blocks = items
1031        .into_iter()
1032        .enumerate()
1033        .map(|(i, item_)| item(viewer, settings, item_, i));
1034
1035    Element::new(column(blocks).spacing(settings.spacing))
1036}
1037
1038/// Displays an [`Item`] using the given [`Viewer`].
1039pub fn item<'a, Message, Theme, Renderer>(
1040    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1041    settings: Settings,
1042    item: &'a Item,
1043    index: usize,
1044) -> Element<'a, Message, Theme, Renderer>
1045where
1046    Message: 'a,
1047    Theme: Catalog + 'a,
1048    Renderer: core::text::Renderer<Font = Font> + 'a,
1049{
1050    match item {
1051        Item::Image { url, title, alt } => {
1052            viewer.image(settings, url, title, alt)
1053        }
1054        Item::Heading(level, text) => {
1055            viewer.heading(settings, level, text, index)
1056        }
1057        Item::Paragraph(text) => viewer.paragraph(settings, text),
1058        Item::CodeBlock {
1059            language,
1060            code,
1061            lines,
1062        } => viewer.code_block(settings, language.as_deref(), code, lines),
1063        Item::List { start: None, items } => {
1064            viewer.unordered_list(settings, items)
1065        }
1066        Item::List {
1067            start: Some(start),
1068            items,
1069        } => viewer.ordered_list(settings, *start, items),
1070    }
1071}
1072
1073/// Displays a heading using the default look.
1074pub fn heading<'a, Message, Theme, Renderer>(
1075    settings: Settings,
1076    level: &'a HeadingLevel,
1077    text: &'a Text,
1078    index: usize,
1079    on_link_click: impl Fn(Url) -> Message + 'a,
1080) -> Element<'a, Message, Theme, Renderer>
1081where
1082    Message: 'a,
1083    Theme: Catalog + 'a,
1084    Renderer: core::text::Renderer<Font = Font> + 'a,
1085{
1086    let Settings {
1087        h1_size,
1088        h2_size,
1089        h3_size,
1090        h4_size,
1091        h5_size,
1092        h6_size,
1093        text_size,
1094        ..
1095    } = settings;
1096
1097    container(
1098        rich_text(text.spans(settings.style))
1099            .on_link_click(on_link_click)
1100            .size(match level {
1101                pulldown_cmark::HeadingLevel::H1 => h1_size,
1102                pulldown_cmark::HeadingLevel::H2 => h2_size,
1103                pulldown_cmark::HeadingLevel::H3 => h3_size,
1104                pulldown_cmark::HeadingLevel::H4 => h4_size,
1105                pulldown_cmark::HeadingLevel::H5 => h5_size,
1106                pulldown_cmark::HeadingLevel::H6 => h6_size,
1107            }),
1108    )
1109    .padding(padding::top(if index > 0 {
1110        text_size / 2.0
1111    } else {
1112        Pixels::ZERO
1113    }))
1114    .into()
1115}
1116
1117/// Displays a paragraph using the default look.
1118pub fn paragraph<'a, Message, Theme, Renderer>(
1119    settings: Settings,
1120    text: &Text,
1121    on_link_click: impl Fn(Url) -> Message + 'a,
1122) -> Element<'a, Message, Theme, Renderer>
1123where
1124    Message: 'a,
1125    Theme: Catalog + 'a,
1126    Renderer: core::text::Renderer<Font = Font> + 'a,
1127{
1128    rich_text(text.spans(settings.style))
1129        .size(settings.text_size)
1130        .on_link_click(on_link_click)
1131        .into()
1132}
1133
1134/// Displays an unordered list using the default look and
1135/// calling the [`Viewer`] for each bullet point item.
1136pub fn unordered_list<'a, Message, Theme, Renderer>(
1137    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1138    settings: Settings,
1139    items: &'a [Vec<Item>],
1140) -> Element<'a, Message, Theme, Renderer>
1141where
1142    Message: 'a,
1143    Theme: Catalog + 'a,
1144    Renderer: core::text::Renderer<Font = Font> + 'a,
1145{
1146    column(items.iter().map(|items| {
1147        row![
1148            text("•").size(settings.text_size),
1149            view_with(
1150                items,
1151                Settings {
1152                    spacing: settings.spacing * 0.6,
1153                    ..settings
1154                },
1155                viewer,
1156            )
1157        ]
1158        .spacing(settings.spacing)
1159        .into()
1160    }))
1161    .spacing(settings.spacing * 0.75)
1162    .padding([0.0, settings.spacing.0])
1163    .into()
1164}
1165
1166/// Displays an ordered list using the default look and
1167/// calling the [`Viewer`] for each numbered item.
1168pub fn ordered_list<'a, Message, Theme, Renderer>(
1169    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1170    settings: Settings,
1171    start: u64,
1172    items: &'a [Vec<Item>],
1173) -> Element<'a, Message, Theme, Renderer>
1174where
1175    Message: 'a,
1176    Theme: Catalog + 'a,
1177    Renderer: core::text::Renderer<Font = Font> + 'a,
1178{
1179    column(items.iter().enumerate().map(|(i, items)| {
1180        row![
1181            text!("{}.", i as u64 + start).size(settings.text_size),
1182            view_with(
1183                items,
1184                Settings {
1185                    spacing: settings.spacing * 0.6,
1186                    ..settings
1187                },
1188                viewer,
1189            )
1190        ]
1191        .spacing(settings.spacing)
1192        .into()
1193    }))
1194    .spacing(settings.spacing * 0.75)
1195    .padding([0.0, settings.spacing.0])
1196    .into()
1197}
1198
1199/// Displays a code block using the default look.
1200pub fn code_block<'a, Message, Theme, Renderer>(
1201    settings: Settings,
1202    lines: &'a [Text],
1203    on_link_click: impl Fn(Url) -> Message + Clone + 'a,
1204) -> Element<'a, Message, Theme, Renderer>
1205where
1206    Message: 'a,
1207    Theme: Catalog + 'a,
1208    Renderer: core::text::Renderer<Font = Font> + 'a,
1209{
1210    container(
1211        scrollable(
1212            container(column(lines.iter().map(|line| {
1213                rich_text(line.spans(settings.style))
1214                    .on_link_click(on_link_click.clone())
1215                    .font(Font::MONOSPACE)
1216                    .size(settings.code_size)
1217                    .into()
1218            })))
1219            .padding(settings.code_size),
1220        )
1221        .direction(scrollable::Direction::Horizontal(
1222            scrollable::Scrollbar::default()
1223                .width(settings.code_size / 2)
1224                .scroller_width(settings.code_size / 2),
1225        )),
1226    )
1227    .width(Length::Fill)
1228    .padding(settings.code_size / 4)
1229    .class(Theme::code_block())
1230    .into()
1231}
1232
1233/// A view strategy to display a Markdown [`Item`].j
1234pub trait Viewer<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
1235where
1236    Self: Sized + 'a,
1237    Message: 'a,
1238    Theme: Catalog + 'a,
1239    Renderer: core::text::Renderer<Font = Font> + 'a,
1240{
1241    /// Produces a message when a link is clicked with the given [`Url`].
1242    fn on_link_click(url: Url) -> Message;
1243
1244    /// Displays an image.
1245    ///
1246    /// By default, it will show a container with the image title.
1247    fn image(
1248        &self,
1249        settings: Settings,
1250        url: &'a Url,
1251        title: &'a str,
1252        alt: &Text,
1253    ) -> Element<'a, Message, Theme, Renderer> {
1254        let _url = url;
1255        let _title = title;
1256
1257        container(
1258            rich_text(alt.spans(settings.style))
1259                .on_link_click(Self::on_link_click),
1260        )
1261        .padding(settings.spacing.0)
1262        .class(Theme::code_block())
1263        .into()
1264    }
1265
1266    /// Displays a heading.
1267    ///
1268    /// By default, it calls [`heading`].
1269    fn heading(
1270        &self,
1271        settings: Settings,
1272        level: &'a HeadingLevel,
1273        text: &'a Text,
1274        index: usize,
1275    ) -> Element<'a, Message, Theme, Renderer> {
1276        heading(settings, level, text, index, Self::on_link_click)
1277    }
1278
1279    /// Displays a paragraph.
1280    ///
1281    /// By default, it calls [`paragraph`].
1282    fn paragraph(
1283        &self,
1284        settings: Settings,
1285        text: &Text,
1286    ) -> Element<'a, Message, Theme, Renderer> {
1287        paragraph(settings, text, Self::on_link_click)
1288    }
1289
1290    /// Displays a code block.
1291    ///
1292    /// By default, it calls [`code_block`].
1293    fn code_block(
1294        &self,
1295        settings: Settings,
1296        language: Option<&'a str>,
1297        code: &'a str,
1298        lines: &'a [Text],
1299    ) -> Element<'a, Message, Theme, Renderer> {
1300        let _language = language;
1301        let _code = code;
1302
1303        code_block(settings, lines, Self::on_link_click)
1304    }
1305
1306    /// Displays an unordered list.
1307    ///
1308    /// By default, it calls [`unordered_list`].
1309    fn unordered_list(
1310        &self,
1311        settings: Settings,
1312        items: &'a [Vec<Item>],
1313    ) -> Element<'a, Message, Theme, Renderer> {
1314        unordered_list(self, settings, items)
1315    }
1316
1317    /// Displays an ordered list.
1318    ///
1319    /// By default, it calls [`ordered_list`].
1320    fn ordered_list(
1321        &self,
1322        settings: Settings,
1323        start: u64,
1324        items: &'a [Vec<Item>],
1325    ) -> Element<'a, Message, Theme, Renderer> {
1326        ordered_list(self, settings, start, items)
1327    }
1328}
1329
1330#[derive(Debug, Clone, Copy)]
1331struct DefaultViewer;
1332
1333impl<'a, Theme, Renderer> Viewer<'a, Url, Theme, Renderer> for DefaultViewer
1334where
1335    Theme: Catalog + 'a,
1336    Renderer: core::text::Renderer<Font = Font> + 'a,
1337{
1338    fn on_link_click(url: Url) -> Url {
1339        url
1340    }
1341}
1342
1343/// The theme catalog of Markdown items.
1344pub trait Catalog:
1345    container::Catalog + scrollable::Catalog + text::Catalog
1346{
1347    /// The styling class of a Markdown code block.
1348    fn code_block<'a>() -> <Self as container::Catalog>::Class<'a>;
1349}
1350
1351impl Catalog for Theme {
1352    fn code_block<'a>() -> <Self as container::Catalog>::Class<'a> {
1353        Box::new(container::dark)
1354    }
1355}