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