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