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