Skip to main content

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::Uri),
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(
33//!             &self.markdown,
34//!             markdown::Settings::default(),
35//!             Theme::TokyoNight,
36//!         )
37//!             .map(Message::LinkClicked)
38//!             .into()
39//!     }
40//!
41//!     fn update(state: &mut State, message: Message) {
42//!         match message {
43//!             Message::LinkClicked(url) => {
44//!                 println!("The following url was clicked: {url}");
45//!             }
46//!         }
47//!     }
48//! }
49//! ```
50use crate::core;
51use crate::core::alignment;
52use crate::core::border;
53use crate::core::font::{self, Font};
54use crate::core::padding;
55use crate::core::text::LineHeight;
56use crate::core::theme;
57use crate::core::{Code, Color, Element, Length, Padding, Pixels, Theme};
58use crate::{checkbox, column, container, rich_text, row, rule, scrollable, span, text};
59
60use std::borrow::BorrowMut;
61use std::cell::RefCell;
62use std::collections::hash_map::Entry;
63use std::collections::{HashMap, HashSet};
64use std::mem;
65use std::ops::Range;
66use std::rc::Rc;
67use std::sync::Arc;
68
69pub use core::text::{Highlight, Highlighter};
70pub use pulldown_cmark::HeadingLevel;
71
72/// A [`String`] representing a [URI] in a Markdown document
73///
74/// [URI]: https://en.wikipedia.org/wiki/Uniform_Resource_Identifier
75pub type Uri = String;
76
77/// A bunch of Markdown that has been parsed.
78#[derive(Debug, Default)]
79pub struct Content {
80    /// The raw Markdown accumulated so far, shared between all the
81    /// items and sections.
82    raw: String,
83    /// The parsed output.
84    items: Vec<Item>,
85    /// The start of the source that is re-parsed when the item is the
86    /// last one: the start of the item, or the start of its last
87    /// bullet, when it is a list.
88    starts: Vec<usize>,
89    /// The true start of the item's source; for a list, the start of
90    /// the list, unlike `starts`, which is the start of its last
91    /// bullet.
92    base: Vec<usize>,
93    /// The start of the source that will be re-parsed on the next
94    /// push.
95    window: usize,
96    /// Whether a not-yet-settled metadata block was live on the last
97    /// push; when it was, the re-parse starts at the start of the
98    /// block, so that the block is swallowed by the parser once it is
99    /// closed (or re-parsed as the rule it turned out to be).
100    pending_block: bool,
101    /// The start of the not-yet-settled metadata block, if any; the
102    /// re-parse starts there, so that the block (a tentative rule and
103    /// its content, for instance) is re-parsed as a whole.
104    pending_block_start: Option<usize>,
105    incomplete: HashMap<usize, Section>,
106    state: State,
107}
108
109#[derive(Debug)]
110struct Section {
111    /// The start of the source to re-parse when a reference becomes
112    /// available or changes.
113    start: usize,
114    /// The end of the source to re-parse; `None` if the item is still
115    /// the last one, so that the source can still grow.
116    end: Option<usize>,
117    broken_links: HashSet<String>,
118    /// The references that were resolved when the item was last
119    /// re-parsed, along with their destination at that time.
120    references: HashMap<String, String>,
121    /// The index of the item in the re-parse of the source.
122    ///
123    /// A source region can produce more than one item (an image and
124    /// the paragraph it belongs to, for instance); this is the index
125    /// of the item that this section refers to.
126    item: usize,
127}
128
129impl Content {
130    /// Creates a new empty [`Content`].
131    pub fn new() -> Self {
132        Self::default()
133    }
134
135    /// Creates some new [`Content`] by parsing the given Markdown.
136    pub fn parse(markdown: &str) -> Self {
137        let mut content = Self::new();
138        content.push_str(markdown);
139        content
140    }
141
142    /// Pushes more Markdown into the [`Content`]; parsing incrementally!
143    ///
144    /// This is specially useful when you have long streams of Markdown; like
145    /// big files or potentially long replies.
146    ///
147    /// Only the last item is re-parsed on every call; and, when the last
148    /// item is a list, only its last bullet is re-parsed, so that pushing
149    /// new items to a long list stays cheap.
150    ///
151    /// The result converges to the one obtained by parsing the whole
152    /// stream at once, as the stream grows.
153    pub fn push_str(&mut self, markdown: &str) {
154        if markdown.is_empty() {
155            return;
156        }
157
158        self.raw.push_str(markdown);
159
160        // The text to re-parse: from the start of the source of the
161        // last item (or its last bullet, when it is a list) to the
162        // end. Unless a not-yet-settled metadata block is live (or
163        // was live on the last push), in which case the re-parse
164        // starts at the start of the block: it is swallowed by the
165        // parser once it is closed, and its first line (a tentative
166        // rule) must be re-parsed with it.
167        let block_live = self
168            .pending_block_start
169            .map(|start| Self::metadata_block_live(&self.raw[start..]))
170            .unwrap_or(false);
171        // A not-yet-settled metadata block that was live on the last
172        // push is now settled: the references registered while it was
173        // open are swallowed by it, so they must be dropped.
174        let block_closed = self.pending_block && !block_live;
175        let mut input_start = if self.pending_block || block_live {
176            self.pending_block_start.unwrap_or(self.window)
177        } else {
178            self.window
179        };
180
181        // When the last item is a list and the previous item is a list
182        // or a quote, the last list may be a lazy continuation of the
183        // previous item's last bullet once more of it is streamed: an
184        // empty `2. two\n-` bullet list, for instance, becomes the
185        // `--` continuation of the numbered item as the second dash
186        // arrives, and a `-` outside a quoted list likewise becomes a
187        // continuation of the quoted bullet. Re-parse from the
188        // previous item's start so that this collapse is re-evaluated
189        // (and the re-parse yields the merged item, not a trailing
190        // paragraph or list).
191        if !self.pending_block
192            && !block_live
193            && let [.., prev, last] = self.items.as_slice()
194            && matches!(last, Item::List { .. })
195            && matches!(prev, Item::List { .. } | Item::Quote(_))
196        {
197            input_start = input_start.min(self.base[self.base.len() - 2]);
198        }
199        let tail = &self.raw[input_start..];
200        let trimmed = tail.trim_end();
201        let mut input = if trimmed.ends_with('|') {
202            trimmed.trim_end_matches('|')
203        } else {
204            tail
205        };
206
207        // Pop the last item and the items whose source falls within
208        // the text that will be re-parsed (an image and the paragraph
209        // it belongs to, for instance); they will be re-parsed as
210        // well.
211        let last = self.items.pop();
212        let _ = self.starts.pop();
213        // The true start of the source of the last item, if any; it
214        // is the start of the merged list, when the last item is a
215        // list.
216        let old_last_base = self.base.pop();
217        while self
218            .starts
219            .last()
220            .is_some_and(|start| *start >= input_start)
221        {
222            let _ = self.items.pop();
223            let _ = self.starts.pop();
224            let _ = self.base.pop();
225        }
226
227        // Re-parse the last item and the new text
228        let mut items: Vec<_> = parse_with(&mut self.state, input).collect();
229
230        // We only re-parse the last bullet of a list, so merge the
231        // re-parsed list into the old one, keeping the bullets that
232        // were already parsed. This only applies when the re-parse
233        // actually started after the start of the list (at its last
234        // bullet); when it re-parsed the whole list, the re-parsed
235        // list already contains all the bullets, and merging would
236        // duplicate them.
237        let last_is_list = matches!(last.as_ref(), Some(Item::List { .. }));
238        let mut merged = false;
239        if let Some(Item::List { start, bullets, .. }) = last
240            && let Some((first_item, _, _)) = items.first_mut()
241            && let Item::List { bullets: new, .. } = first_item
242            && old_last_base.is_some_and(|base| input_start > base)
243        {
244            // The last bullet of the old list was re-parsed
245            let mut bullets = bullets;
246            let _ = bullets.pop();
247            bullets.extend(mem::take(new));
248            *first_item = Item::List { start, bullets };
249            merged = true;
250        } else if last_is_list {
251            // The re-parse of the last bullet no longer produces a
252            // list (the bullet grew into a rule or a heading, for
253            // instance), so it cannot be merged into the old list:
254            // re-parse from the start of the whole list, so its
255            // source is re-parsed in full.
256            //
257            // The re-parse must not start after a not-yet-settled
258            // metadata block opener (kept in `input_start`), or the
259            // block would be dropped; use the earliest of the two.
260            let base = old_last_base.expect("a list has a base").min(input_start);
261            let tail = &self.raw[base..];
262            let trimmed = tail.trim_end();
263            input = if trimmed.ends_with('|') {
264                trimmed.trim_end_matches('|')
265            } else {
266                tail
267            };
268            input_start = base;
269            items = parse_with(&mut self.state, input).collect();
270        } else if let Some((Item::List { .. }, 0, _)) = items.first()
271            && let Some(Item::List { .. }) = self.items.last()
272            && let Some(base) = self.base.last().copied()
273        {
274            // The re-parse produced a list that starts where the old
275            // last item (a lone paragraph, for instance) used to be,
276            // right after a previous list: the paragraph grew into a
277            // list item that continues the previous list. Re-parse
278            // from the start of the previous list, so that the list
279            // is not split in two; the parser decides whether the
280            // two regions are one list.
281            let tail = &self.raw[base..];
282            let trimmed = tail.trim_end();
283            input = if trimmed.ends_with('|') {
284                trimmed.trim_end_matches('|')
285            } else {
286                tail
287            };
288
289            let reparsed: Vec<_> = parse_with(&mut self.state, input).collect();
290
291            if let Some((Item::List { .. }, _, _)) = reparsed.first() {
292                input_start = base;
293                items = reparsed;
294                // The previous list is covered by the re-parse
295                let _ = self.items.pop();
296                let _ = self.starts.pop();
297                let _ = self.base.pop();
298            }
299        }
300
301        // The start of the most recent `---` or `+++` rule produced
302        // by the re-parse, if any; it is a tentative metadata block
303        // opener.
304        let mut newest_rule_start: Option<usize> = None;
305
306        if items.is_empty() {
307            // The new text did not produce any item (it completed a
308            // reference definition or a metadata block, for
309            // instance), so the last item was replaced by it; the
310            // next push re-parses from the start of the new last
311            // item.
312            self.window = self.starts.last().copied().unwrap_or(input_start);
313        } else {
314            // Remember the start of the source of each re-parsed
315            // item.
316            let starts: Vec<usize> = items
317                .iter()
318                .map(|(_, start, _)| input_start + *start)
319                .collect();
320
321            for (i, (item, _start, broken_links)) in items.into_iter().enumerate() {
322                let start = starts[i];
323                // The merged list is anchored at the start of the
324                // whole list, unlike `start`, which is the start of
325                // its last bullet.
326                let base = if i == 0 && merged {
327                    old_last_base.expect("a merged list has a base")
328                } else {
329                    start
330                };
331
332                if !broken_links.is_empty() {
333                    // The index of the item once it is pushed
334                    let index = self.items.len();
335
336                    // The next item can cover this one (a paragraph
337                    // and the image it contains, for instance); in
338                    // that case, the source to re-parse spans both.
339                    let covers = starts.get(i + 1).is_some_and(|next| *next <= start);
340
341                    // The source to re-parse starts at the start of
342                    // the covered group, if any, and ends where the
343                    // item after the group starts; it grows with the
344                    // source while the group is the last one.
345                    let (section_start, end) = if covers {
346                        (starts[i + 1], starts.get(i + 2).copied())
347                    } else {
348                        (base, starts.get(i + 1).copied())
349                    };
350
351                    // A bullet that was not re-parsed can have broken
352                    // links of its own, so they need to be kept
353                    match self.incomplete.entry(index) {
354                        Entry::Occupied(mut entry) => {
355                            let section = entry.get_mut();
356                            section.broken_links.extend(broken_links);
357                            // The geometry can change (the item was
358                            // not covered when the section was
359                            // created, and its paragraph is now
360                            // re-parsed as well)
361                            section.start = section_start;
362                            section.end = end;
363                        }
364                        Entry::Vacant(entry) => {
365                            // The re-parse of the section's source
366                            // produces the items that fall within
367                            // the section's range (an image and the
368                            // paragraph it belongs to, for instance);
369                            // remember the index of the one this
370                            // section refers to.
371                            let item = starts
372                                .iter()
373                                .take(i)
374                                .copied()
375                                .filter(|start| *start >= section_start)
376                                .count();
377                            let _ = entry.insert(Section {
378                                start: section_start,
379                                end,
380                                broken_links,
381                                references: HashMap::new(),
382                                item,
383                            });
384                        }
385                    }
386                }
387
388                if matches!(item, Item::Rule) && Self::metadata_delimiter(&self.raw, start) {
389                    // A `---` or `+++` rule is a tentative metadata
390                    // block opener; remember where it starts so that
391                    // the block is re-parsed as a whole when it is
392                    // closed.
393                    newest_rule_start = Some(start);
394                }
395
396                self.items.push(item);
397                self.starts.push(start);
398                self.base.push(base);
399            }
400
401            self.window = input_start + self.state.window.unwrap_or(input.len());
402        }
403
404        // Remember the tentative metadata block opener, if any, so
405        // that the block is re-parsed as a whole as it grows, and
406        // swallowed by the parser once it is closed.
407        self.pending_block_start = newest_rule_start;
408        self.pending_block = newest_rule_start
409            .map(|start| Self::metadata_block_live(&self.raw[start..]))
410            .unwrap_or(false);
411
412        // A metadata block that was open on the last push is now
413        // settled: the references registered while it was open are
414        // swallowed by it, so recompute the references from the whole
415        // source, as the one-shot parse does.
416        if block_closed {
417            self.recompute_references();
418        }
419
420        // The sections whose item is not the last one anymore have
421        // a fixed source range
422        self.fix_section_ends();
423
424        // The sections whose broken links became resolvable, or
425        // whose references changed, are re-parsed
426        self.resolve_sections();
427
428        // The images are those present in the items; recompute them,
429        // as an image parsed while a metadata block was still open
430        // can be swallowed by it once the block is closed.
431        self.state.images = self
432            .items
433            .iter()
434            .filter_map(|item| match item {
435                Item::Image { url, .. } => Some(url.clone()),
436                _ => None,
437            })
438            .collect();
439    }
440
441    /// Returns `true` if the source starts with a metadata block that
442    /// is not settled yet: the first line is a complete `---` or
443    /// `+++` delimiter, the second line is not a blank one
444    /// (otherwise the first line is a rule), and the block has not
445    /// been closed.
446    ///
447    /// While such a block is live, its first line is a tentative
448    /// rule that the parser swallows once the block is closed, so
449    /// the re-parse has to cover the block as a whole.
450    fn metadata_block_live(source: &str) -> bool {
451        // The first line, which must be complete
452        let (first, rest) = match source.find('\n') {
453            Some(end) => (&source[..end], &source[end + 1..]),
454            None => return false,
455        };
456
457        // The delimiter line
458        let first = first.trim_end();
459        if first != "---" && first != "+++" {
460            return false;
461        }
462
463        // The second line: a blank one makes the first line a rule,
464        // not a metadata block; an incomplete one could still be
465        // the start of a block
466        let Some(second_end) = rest.find('\n') else {
467            return true;
468        };
469        let second = &rest[..second_end];
470        if second.trim().is_empty() {
471            return false;
472        }
473
474        // The block is closed by a `---` or `...` line, or a `+++`
475        // line, when it is delimited by `+++`
476        let closed = rest.lines().any(|line| {
477            let line = line.trim_end();
478            if first == "+++" {
479                line == "+++"
480            } else {
481                line == "---" || line == "..."
482            }
483        });
484
485        !closed
486    }
487
488    /// Returns `true` if the line starting at `start` is a complete
489    /// `---` or `+++` metadata block delimiter.
490    fn metadata_delimiter(source: &str, start: usize) -> bool {
491        let line = &source[start..];
492        let end = line.find('\n').unwrap_or(line.len());
493        let line = line[..end].trim_end();
494        line == "---" || line == "+++"
495    }
496
497    /// Re-parses the whole source and replaces the reference
498    /// definitions with those of the one-shot parse.
499    ///
500    /// A reference registered while a metadata block was still open
501    /// is swallowed by it once the block is settled, so it must not
502    /// resolve links any more; re-parsing the whole source drops it,
503    /// like the one-shot parse does.
504    fn recompute_references(&mut self) {
505        let parser = pulldown_cmark::Parser::new_ext(&self.raw, options());
506        let definitions = parser.reference_definitions();
507
508        self.state.references.clear();
509        self.state.references_staged.clear();
510
511        absorb_references(
512            &self.raw,
513            definitions,
514            &mut self.state.references,
515            &mut self.state.references_staged,
516        );
517    }
518
519    /// Ends the sections whose item is not the last one anymore:
520    /// their source range is now fixed, and it ends where the next
521    /// item starts.
522    fn fix_section_ends(&mut self) {
523        if self.incomplete.is_empty() {
524            return;
525        }
526
527        for (index, section) in self.incomplete.iter_mut() {
528            if section.end.is_none() && *index + 1 < self.items.len() {
529                // The next item can cover the section's item (a
530                // paragraph and the image it contains), so the end
531                // is the first start that is strictly after the
532                // section's start
533                section.end = self.starts[*index + 1..]
534                    .iter()
535                    .copied()
536                    .find(|end| *end > section.start);
537            }
538        }
539    }
540
541    /// Re-parses the sections whose broken links became resolvable,
542    /// or whose references changed destination; the sections that
543    /// are left with nothing to watch are dropped.
544    fn resolve_sections(&mut self) {
545        if self.incomplete.is_empty() {
546            return;
547        }
548
549        self.incomplete.retain(|index, section| {
550            if self.items.len() <= *index {
551                // The section's item is gone
552                return false;
553            }
554
555            // A link becomes resolvable...
556            let mut newly_resolved = Vec::new();
557            section.broken_links.retain(|link| {
558                if self.state.references.contains_key(link) {
559                    newly_resolved.push(link.clone());
560                    false
561                } else {
562                    true
563                }
564            });
565
566            // ...or the destination of a resolved reference changes,
567            // or the reference is dropped (its definition swallowed
568            // by a metadata block, for instance)
569            let needs_reparse = !newly_resolved.is_empty()
570                || section.references.iter().any(|(link, dest)| {
571                    match self.state.references.get(link) {
572                        Some(new_dest) => new_dest != dest,
573                        None => true,
574                    }
575                });
576
577            if needs_reparse {
578                let mut state = State {
579                    window: None,
580                    references: self.state.references.clone(),
581                    references_staged: HashSet::new(),
582                    images: HashSet::new(),
583                    #[cfg(feature = "highlighter")]
584                    parser: None,
585                };
586
587                let end = section.end.unwrap_or(self.raw.len());
588                let source = &self.raw[section.start..end];
589
590                if let Some((item, _start, broken_links)) =
591                    parse_with(&mut state, source).nth(section.item)
592                {
593                    self.items[*index] = item;
594
595                    // Track the references that were resolved by the
596                    // re-parse, so that a later change of their
597                    // destination triggers a new re-parse
598                    for link in newly_resolved {
599                        if let Some(dest) = self.state.references.get(&link)
600                            && !broken_links.contains(&link)
601                        {
602                            let _ = section.references.insert(link, dest.to_owned());
603                        }
604                    }
605
606                    section.broken_links = broken_links;
607                    section
608                        .references
609                        .retain(|link, _| !section.broken_links.contains(link));
610
611                    for (link, dest) in &mut section.references {
612                        if let Some(new_dest) = self.state.references.get(link) {
613                            *dest = new_dest.clone();
614                        }
615                    }
616                }
617
618                self.state.images.extend(state.images);
619            }
620
621            // The section is kept while something is left to watch
622            !section.broken_links.is_empty() || !section.references.is_empty()
623        });
624    }
625
626    /// Returns the Markdown items, ready to be rendered.
627    ///
628    /// You can use [`view`] to turn them into an [`Element`].
629    pub fn items(&self) -> &[Item] {
630        &self.items
631    }
632
633    /// Returns the URLs of the Markdown images present in the [`Content`].
634    pub fn images(&self) -> &HashSet<Uri> {
635        &self.state.images
636    }
637
638    /// Returns the raw Markdown.
639    pub fn raw(&self) -> &str {
640        &self.raw
641    }
642}
643
644/// Groups the given Markdown [`Item`]s by [`Item::Heading`].
645///
646/// The returned iterator yields a `(Option<&Item>, &[Item])` pair for each
647/// group, without cloning any [`Item`]:
648///
649/// * The first element is the heading that starts the group, if any. It is
650///   [`None`] for the group of items that appears before the first heading,
651///   if there is any;
652/// * The second element is the slice of items that follow the heading, up to
653///   (but not including) the next one.
654///
655/// Every item in the given slice is yielded exactly once: a heading is
656/// returned as the first element of the group it starts, and every other
657/// item is part of the slice that follows the last heading before it.
658///
659/// # Example
660/// ```
661/// use iced_widget::markdown;
662///
663/// let items: Vec<_> = markdown::parse("# Title\n\nHello!\n\n# Subtitle\n\nMore!").collect();
664///
665/// let mut groups = markdown::sections(&items);
666///
667/// let (heading, contents) = groups.next().unwrap();
668/// assert!(heading.is_some());
669/// assert_eq!(contents.len(), 1);
670///
671/// let (heading, contents) = groups.next().unwrap();
672/// assert!(heading.is_some());
673/// assert_eq!(contents.len(), 1);
674///
675/// assert!(groups.next().is_none());
676/// ```
677pub fn sections<'a>(
678    items: &'a [Item],
679) -> impl Iterator<Item = (Option<&'a Item>, &'a [Item])> + 'a {
680    struct Sections<'a> {
681        /// The items being grouped.
682        items: &'a [Item],
683        /// The index of the first item of the next group.
684        ///
685        /// This is always the index of a heading, except for the very first
686        /// group, where it may point at any item (the content before the
687        /// first heading).
688        start: usize,
689    }
690
691    impl<'a> Iterator for Sections<'a> {
692        type Item = (Option<&'a Item>, &'a [Item]);
693
694        fn next(&mut self) -> Option<Self::Item> {
695            let Self { items, start } = self;
696
697            if *start >= items.len() {
698                return None;
699            }
700
701            // The heading of the current group, if any
702            let heading = if matches!(items[*start], Item::Heading(..)) {
703                Some(*start)
704            } else {
705                None
706            };
707
708            // The body of the group: the items after the heading, if any, up to
709            // the next heading
710            let body_start = heading.map_or(*start, |heading| heading + 1);
711            let next_heading = (body_start..items.len())
712                .find(|&index| matches!(items[index], Item::Heading(..)))
713                .unwrap_or(items.len());
714
715            // The next group starts at the next heading, if any
716            *start = next_heading;
717
718            Some((
719                heading.map(|index| &items[index]),
720                &items[body_start..next_heading],
721            ))
722        }
723    }
724
725    Sections { items, start: 0 }
726}
727
728/// A Markdown item.
729#[derive(Debug, Clone)]
730pub enum Item {
731    /// A heading.
732    Heading(pulldown_cmark::HeadingLevel, Text),
733    /// A paragraph.
734    Paragraph(Text),
735    /// A code block.
736    ///
737    /// You can enable the `highlighter` feature for syntax highlighting.
738    CodeBlock {
739        /// The language of the code block, if any.
740        language: Option<String>,
741        /// The raw code of the code block.
742        code: String,
743        /// The styled lines of text in the code block.
744        lines: Vec<Text>,
745    },
746    /// A list.
747    List {
748        /// The first number of the list, if it is ordered.
749        start: Option<u64>,
750        /// The items of the list.
751        bullets: Vec<Bullet>,
752    },
753    /// An image.
754    Image {
755        /// The destination URL of the image.
756        url: Uri,
757        /// The title of the image.
758        title: String,
759        /// The alternative text of the image.
760        alt: Text,
761    },
762    /// A quote.
763    Quote(Vec<Item>),
764    /// A horizontal separator.
765    Rule,
766    /// A table.
767    Table {
768        /// The columns of the table.
769        columns: Vec<Column>,
770        /// The rows of the table.
771        rows: Vec<Row>,
772    },
773}
774
775/// The column of a table.
776#[derive(Debug, Clone)]
777pub struct Column {
778    /// The header of the column.
779    pub header: Vec<Item>,
780    /// The alignment of the column.
781    pub alignment: pulldown_cmark::Alignment,
782}
783
784/// The row of a table.
785#[derive(Debug, Clone)]
786pub struct Row {
787    /// The cells of the row.
788    cells: Vec<Vec<Item>>,
789}
790
791/// A bunch of parsed Markdown text.
792#[derive(Debug, Clone)]
793pub struct Text {
794    spans: Vec<Span>,
795    last_style: RefCell<Option<(Settings, String, String)>>,
796    last_styled_spans: RefCell<Arc<[text::Span<'static, Uri>]>>,
797}
798
799impl Text {
800    fn new(spans: Vec<Span>) -> Self {
801        Self {
802            spans,
803            last_style: RefCell::default(),
804            last_styled_spans: RefCell::default(),
805        }
806    }
807
808    /// Returns the [`rich_text()`] spans ready to be used for the given style.
809    ///
810    /// This method performs caching for you. It will only reallocate if the [`Settings`]
811    /// or the [`Catalog`] provided changes.
812    pub fn spans<Theme: Catalog>(
813        &self,
814        settings: Settings,
815        theme: &Theme,
816        highlighter: &dyn text::Highlighter<Code, Theme>,
817    ) -> Arc<[text::Span<'static, Uri>]> {
818        let is_dirty = self.last_style.borrow().as_ref().is_none_or(
819            |(last_settings, last_theme, last_highlighter)| {
820                &settings != last_settings
821                    || theme.id() != last_theme
822                    || highlighter.id() != last_highlighter
823            },
824        );
825
826        if is_dirty {
827            *self.last_styled_spans.borrow_mut() = self
828                .spans
829                .iter()
830                .map(|span| span.view(&settings, theme, highlighter))
831                .collect();
832
833            *self.last_style.borrow_mut() =
834                Some((settings, theme.id().to_owned(), highlighter.id().to_owned()));
835        }
836
837        self.last_styled_spans.borrow().clone()
838    }
839}
840
841#[derive(Debug, Clone)]
842enum Span {
843    Standard {
844        text: String,
845        strikethrough: bool,
846        link: Option<Uri>,
847        strong: bool,
848        emphasis: bool,
849        inline_code: bool,
850    },
851    Code {
852        text: String,
853        code: Code,
854    },
855}
856
857impl Span {
858    fn view<Theme: Catalog>(
859        &self,
860        settings: &Settings,
861        theme: &Theme,
862        highlighter: &dyn text::Highlighter<Code, Theme>,
863    ) -> text::Span<'static, Uri> {
864        match self {
865            Span::Standard {
866                text,
867                strikethrough,
868                link,
869                strong,
870                emphasis,
871                inline_code,
872            } => {
873                let span = span(text.clone()).strikethrough(*strikethrough);
874
875                let weight = if *strong {
876                    font::Weight::Bold
877                } else {
878                    settings.font.weight
879                };
880
881                let style = if *emphasis {
882                    font::Style::Italic
883                } else {
884                    settings.font.style
885                };
886
887                let span = if *inline_code {
888                    let code = theme.code();
889
890                    span.font(Font {
891                        weight,
892                        style,
893                        ..settings.inline_code_font
894                    })
895                    .size(settings.inline_code_size)
896                    .color(code.color)
897                    .background(code.highlight.background)
898                    .border(code.highlight.border)
899                    .padding(code.padding)
900                } else {
901                    span.font(Font {
902                        weight,
903                        style,
904                        ..settings.font
905                    })
906                };
907
908                if let Some(link) = link.as_ref() {
909                    span.color(theme.link_color()).link(link.clone())
910                } else {
911                    span
912                }
913            }
914            Span::Code { text, code } => {
915                let format = highlighter.highlight(*code, theme);
916
917                span(text.clone())
918                    .color_maybe(format.color)
919                    .font_maybe(format.style.map(|style| Font {
920                        style,
921                        ..settings.code_block_font
922                    }))
923            }
924        }
925    }
926}
927
928/// The item of a list.
929#[derive(Debug, Clone)]
930pub enum Bullet {
931    /// A simple bullet point.
932    Point {
933        /// The contents of the bullet point.
934        items: Vec<Item>,
935    },
936    /// A task.
937    Task {
938        /// The contents of the task.
939        items: Vec<Item>,
940        /// Whether the task is done or not.
941        done: bool,
942    },
943}
944
945impl Bullet {
946    fn items(&self) -> &[Item] {
947        match self {
948            Bullet::Point { items } | Bullet::Task { items, .. } => items,
949        }
950    }
951
952    fn push(&mut self, item: Item) {
953        let (Bullet::Point { items } | Bullet::Task { items, .. }) = self;
954
955        items.push(item);
956    }
957}
958
959/// Parse the given Markdown content.
960///
961/// # Example
962/// ```no_run
963/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
964/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
965/// #
966/// use iced::widget::markdown;
967/// use iced::Theme;
968///
969/// struct State {
970///    markdown: Vec<markdown::Item>,
971/// }
972///
973/// enum Message {
974///     LinkClicked(markdown::Uri),
975/// }
976///
977/// impl State {
978///     pub fn new() -> Self {
979///         Self {
980///             markdown: markdown::parse("This is some **Markdown**!").collect(),
981///         }
982///     }
983///
984///     fn view(&self) -> Element<'_, Message> {
985///         markdown::view(
986///             &self.markdown,
987///             markdown::Settings::default(),
988///             Theme::TokyoNight,
989///         )
990///             .map(Message::LinkClicked)
991///             .into()
992///     }
993///
994///     fn update(state: &mut State, message: Message) {
995///         match message {
996///             Message::LinkClicked(url) => {
997///                 println!("The following url was clicked: {url}");
998///             }
999///         }
1000///     }
1001/// }
1002/// ```
1003pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
1004    parse_with(State::default(), markdown).map(|(item, _start, _broken_links)| item)
1005}
1006
1007#[derive(Debug, Default)]
1008struct State {
1009    /// The start of the source that will be re-parsed next, after the
1010    /// current parse.
1011    window: Option<usize>,
1012    /// The reference definitions, mapping a label to its destination.
1013    ///
1014    /// The first definition of a label wins, like in CommonMark.
1015    references: HashMap<String, String>,
1016    /// The labels whose destination in `references` comes from the
1017    /// definition of the last line, which is not terminated yet and
1018    /// can still grow; their destination is updated on each push,
1019    /// until the line is terminated.
1020    references_staged: HashSet<String>,
1021    images: HashSet<Uri>,
1022    #[cfg(feature = "highlighter")]
1023    parser: Option<code::Parser>,
1024}
1025
1026/// The options used by the parser.
1027fn options() -> pulldown_cmark::Options {
1028    pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS
1029        | pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS
1030        | pulldown_cmark::Options::ENABLE_TABLES
1031        | pulldown_cmark::Options::ENABLE_STRIKETHROUGH
1032        | pulldown_cmark::Options::ENABLE_TASKLISTS
1033}
1034
1035/// Absorbs the reference definitions of a parse into `references`,
1036/// keeping the first definition of a label, like in CommonMark.
1037///
1038/// The definition of the last line, when that line is not terminated
1039/// yet, can still grow, so its destination is updated on each push,
1040/// until the line is terminated; the growing labels are tracked in
1041/// `growing_refs`.
1042///
1043/// `markdown` is the source of the parse, and `definitions` are its
1044/// reference definitions.
1045fn absorb_references(
1046    markdown: &str,
1047    definitions: &pulldown_cmark::RefDefs<'_>,
1048    references: &mut HashMap<String, String>,
1049    growing_refs: &mut HashSet<String>,
1050) {
1051    for reference in definitions.iter() {
1052        let name = reference.0.to_string();
1053        let dest = reference.1.dest.to_string();
1054
1055        if markdown[reference.1.span.end..].contains('\n') {
1056            if !references.contains_key(&name) || growing_refs.remove(&name) {
1057                let _ = references.insert(name, dest);
1058            }
1059        } else if growing_refs.contains(&name) {
1060            // The map's value is the growing one: update it
1061            let _ = references.insert(name, dest);
1062        } else if !references.contains_key(&name) {
1063            // No terminated definition wins: the growing value is
1064            // provisional
1065            let _ = growing_refs.insert(name.clone());
1066            let _ = references.insert(name, dest);
1067        }
1068    }
1069}
1070
1071fn parse_with<'a>(
1072    mut state: impl BorrowMut<State> + 'a,
1073    markdown: &'a str,
1074) -> impl Iterator<Item = (Item, usize, HashSet<String>)> + 'a {
1075    enum Scope {
1076        List(List),
1077        Quote(Vec<Item>),
1078        Table {
1079            alignment: Vec<pulldown_cmark::Alignment>,
1080            columns: Vec<Column>,
1081            rows: Vec<Row>,
1082            current: Vec<Item>,
1083        },
1084    }
1085
1086    struct List {
1087        start: Option<u64>,
1088        bullets: Vec<Bullet>,
1089        /// The start of the last item of the list, if any.
1090        last_item_start: Option<usize>,
1091    }
1092
1093    // The broken links reported by the parser, along with their span
1094    // in the input.
1095    //
1096    // The broken links are reported before the items that contain them
1097    // are produced, so the links are attributed to an item by their
1098    // span.
1099    let broken_links = Rc::new(RefCell::new(Vec::new()));
1100
1101    let mut spans = Vec::new();
1102    let mut code = String::new();
1103    let mut code_language = None;
1104    let mut code_lines = Vec::new();
1105    let mut strong = false;
1106    let mut emphasis = false;
1107    let mut strikethrough = false;
1108    let mut metadata = false;
1109    let mut code_block = false;
1110    let mut link = None;
1111    let mut image = None;
1112    let mut paragraph_start = None;
1113    let mut stack = Vec::new();
1114
1115    #[cfg(feature = "highlighter")]
1116    let mut code_parser = None;
1117
1118    let parser = pulldown_cmark::Parser::new_with_broken_link_callback(markdown, options(), {
1119        let references = state.borrow().references.clone();
1120        let broken_links = broken_links.clone();
1121
1122        Some(move |broken_link: pulldown_cmark::BrokenLink<'_>| {
1123            if let Some(reference) = references.get(broken_link.reference.as_ref()) {
1124                Some((
1125                    pulldown_cmark::CowStr::from(reference.to_owned()),
1126                    broken_link.reference.into_static(),
1127                ))
1128            } else {
1129                RefCell::borrow_mut(&broken_links)
1130                    .push((broken_link.span, broken_link.reference.into_string()));
1131
1132                None
1133            }
1134        })
1135    });
1136
1137    {
1138        let state = state.borrow_mut();
1139        absorb_references(
1140            markdown,
1141            parser.reference_definitions(),
1142            &mut state.references,
1143            &mut state.references_staged,
1144        );
1145    }
1146
1147    let produce = move |state: &mut State, stack: &mut Vec<Scope>, item, source: Range<usize>| {
1148        if let Some(scope) = stack.last_mut() {
1149            match scope {
1150                Scope::List(list) => {
1151                    list.bullets.last_mut().expect("item context").push(item);
1152                }
1153                Scope::Quote(items) => {
1154                    items.push(item);
1155                }
1156                Scope::Table { current, .. } => {
1157                    current.push(item);
1158                }
1159            }
1160
1161            None
1162        } else {
1163            state.window = Some(source.start);
1164
1165            // Attribute the broken links whose span falls within the
1166            // source of the item
1167            let mut links = HashSet::new();
1168            for (span, reference) in RefCell::borrow(&broken_links).iter() {
1169                if source.contains(&span.start) {
1170                    let _ = links.insert(reference.clone());
1171                }
1172            }
1173
1174            Some((item, source.start, links))
1175        }
1176    };
1177
1178    // A reference link or image resolves with the first definition
1179    // of its label in the whole document, like in the one-shot
1180    // parse. A later definition that falls within the input wins
1181    // within the input, so, when known, prefer the global
1182    // definition.
1183    let resolve_reference = |state: &mut State,
1184                             link_type: pulldown_cmark::LinkType,
1185                             id: &str,
1186                             dest_url: &pulldown_cmark::CowStr<'a>|
1187     -> String {
1188        match link_type {
1189            pulldown_cmark::LinkType::Reference
1190            | pulldown_cmark::LinkType::ReferenceUnknown
1191            | pulldown_cmark::LinkType::Collapsed
1192            | pulldown_cmark::LinkType::CollapsedUnknown
1193            | pulldown_cmark::LinkType::Shortcut
1194            | pulldown_cmark::LinkType::ShortcutUnknown => state
1195                .references
1196                .get(id)
1197                .cloned()
1198                .unwrap_or_else(|| dest_url.to_string()),
1199            _ => dest_url.to_string(),
1200        }
1201    };
1202
1203    let parser = parser.into_offset_iter();
1204
1205    // We want to keep the `spans` capacity
1206    #[allow(clippy::drain_collect)]
1207    parser.filter_map(move |(event, source)| match event {
1208        pulldown_cmark::Event::Start(tag) => match tag {
1209            pulldown_cmark::Tag::Strong if !metadata => {
1210                strong = true;
1211                None
1212            }
1213            pulldown_cmark::Tag::Emphasis if !metadata => {
1214                emphasis = true;
1215                None
1216            }
1217            pulldown_cmark::Tag::Strikethrough if !metadata => {
1218                strikethrough = true;
1219                None
1220            }
1221            pulldown_cmark::Tag::Link {
1222                link_type,
1223                dest_url,
1224                id,
1225                ..
1226            } if !metadata => {
1227                link = Some(resolve_reference(
1228                    state.borrow_mut(),
1229                    link_type,
1230                    &id,
1231                    &dest_url,
1232                ));
1233                None
1234            }
1235            pulldown_cmark::Tag::Paragraph if !metadata => {
1236                paragraph_start = Some(source.start);
1237                None
1238            }
1239            pulldown_cmark::Tag::Image {
1240                link_type,
1241                dest_url,
1242                title,
1243                id,
1244            } if !metadata => {
1245                image = Some((
1246                    resolve_reference(state.borrow_mut(), link_type, &id, &dest_url),
1247                    title.into_string(),
1248                    spans.len(),
1249                ));
1250                None
1251            }
1252            pulldown_cmark::Tag::List(first_item) if !metadata => {
1253                let prev = if spans.is_empty() {
1254                    None
1255                } else {
1256                    produce(
1257                        state.borrow_mut(),
1258                        &mut stack,
1259                        Item::Paragraph(Text::new(spans.drain(..).collect())),
1260                        source,
1261                    )
1262                };
1263
1264                stack.push(Scope::List(List {
1265                    start: first_item,
1266                    bullets: Vec::new(),
1267                    last_item_start: None,
1268                }));
1269
1270                prev
1271            }
1272            pulldown_cmark::Tag::Item => {
1273                if let Some(Scope::List(list)) = stack.last_mut() {
1274                    list.last_item_start = Some(source.start);
1275                    list.bullets.push(Bullet::Point { items: Vec::new() });
1276                }
1277
1278                None
1279            }
1280            pulldown_cmark::Tag::BlockQuote(_kind) if !metadata => {
1281                let prev = if spans.is_empty() {
1282                    None
1283                } else {
1284                    produce(
1285                        state.borrow_mut(),
1286                        &mut stack,
1287                        Item::Paragraph(Text::new(spans.drain(..).collect())),
1288                        source,
1289                    )
1290                };
1291
1292                stack.push(Scope::Quote(Vec::new()));
1293
1294                prev
1295            }
1296            pulldown_cmark::Tag::CodeBlock(pulldown_cmark::CodeBlockKind::Fenced(language))
1297                if !metadata =>
1298            {
1299                #[cfg(feature = "highlighter")]
1300                {
1301                    code_parser = Some({
1302                        let mut code_parser = state
1303                            .borrow_mut()
1304                            .parser
1305                            .take()
1306                            .filter(|parser| parser.language() == language.as_ref())
1307                            .unwrap_or_else(|| {
1308                                code::Parser::new(language.split(',').next().unwrap_or_default())
1309                            });
1310
1311                        code_parser.prepare();
1312
1313                        code_parser
1314                    });
1315                }
1316
1317                code_block = true;
1318                code_language = (!language.is_empty()).then(|| language.into_string());
1319
1320                if spans.is_empty() {
1321                    None
1322                } else {
1323                    produce(
1324                        state.borrow_mut(),
1325                        &mut stack,
1326                        Item::Paragraph(Text::new(spans.drain(..).collect())),
1327                        source,
1328                    )
1329                }
1330            }
1331            pulldown_cmark::Tag::MetadataBlock(_) => {
1332                metadata = true;
1333                None
1334            }
1335            pulldown_cmark::Tag::Table(alignment) => {
1336                stack.push(Scope::Table {
1337                    columns: Vec::with_capacity(alignment.len()),
1338                    alignment,
1339                    current: Vec::new(),
1340                    rows: Vec::new(),
1341                });
1342
1343                None
1344            }
1345            pulldown_cmark::Tag::TableHead => {
1346                strong = true;
1347                None
1348            }
1349            pulldown_cmark::Tag::TableRow => {
1350                let Scope::Table { rows, .. } = stack.last_mut()? else {
1351                    return None;
1352                };
1353
1354                rows.push(Row { cells: Vec::new() });
1355                None
1356            }
1357            _ => None,
1358        },
1359        pulldown_cmark::Event::End(tag) => match tag {
1360            pulldown_cmark::TagEnd::Heading(level) if !metadata => produce(
1361                state.borrow_mut(),
1362                &mut stack,
1363                Item::Heading(level, Text::new(spans.drain(..).collect())),
1364                source,
1365            ),
1366            pulldown_cmark::TagEnd::Strong if !metadata => {
1367                strong = false;
1368                None
1369            }
1370            pulldown_cmark::TagEnd::Emphasis if !metadata => {
1371                emphasis = false;
1372                None
1373            }
1374            pulldown_cmark::TagEnd::Strikethrough if !metadata => {
1375                strikethrough = false;
1376                None
1377            }
1378            pulldown_cmark::TagEnd::Link if !metadata => {
1379                link = None;
1380                None
1381            }
1382            pulldown_cmark::TagEnd::Paragraph if !metadata => {
1383                paragraph_start = None;
1384
1385                if spans.is_empty() {
1386                    None
1387                } else {
1388                    produce(
1389                        state.borrow_mut(),
1390                        &mut stack,
1391                        Item::Paragraph(Text::new(spans.drain(..).collect())),
1392                        source,
1393                    )
1394                }
1395            }
1396            pulldown_cmark::TagEnd::Item if !metadata => {
1397                if spans.is_empty() {
1398                    None
1399                } else {
1400                    produce(
1401                        state.borrow_mut(),
1402                        &mut stack,
1403                        Item::Paragraph(Text::new(spans.drain(..).collect())),
1404                        source,
1405                    )
1406                }
1407            }
1408            pulldown_cmark::TagEnd::List(_) if !metadata => {
1409                let scope = stack.pop()?;
1410
1411                let Scope::List(list) = scope else {
1412                    return None;
1413                };
1414
1415                let last_item_start = list.last_item_start;
1416                let produced = produce(
1417                    state.borrow_mut(),
1418                    &mut stack,
1419                    Item::List {
1420                        start: list.start,
1421                        bullets: list.bullets,
1422                    },
1423                    source,
1424                );
1425
1426                // A list is re-parsed only from the start of its last
1427                // item, so that adding new items to a long list does not
1428                // require re-parsing the whole list.
1429                if produced.is_some()
1430                    && let Some(start) = last_item_start
1431                {
1432                    state.borrow_mut().window = Some(start);
1433                }
1434
1435                produced
1436            }
1437            pulldown_cmark::TagEnd::BlockQuote(_kind) if !metadata => {
1438                let scope = stack.pop()?;
1439
1440                let Scope::Quote(quote) = scope else {
1441                    return None;
1442                };
1443
1444                produce(state.borrow_mut(), &mut stack, Item::Quote(quote), source)
1445            }
1446            pulldown_cmark::TagEnd::Image if !metadata => {
1447                let (url, title, start) = image.take()?;
1448                let alt = Text::new(spans.drain(start..).collect());
1449
1450                let state = state.borrow_mut();
1451                let _ = state.images.insert(url.clone());
1452
1453                let produced = produce(state, &mut stack, Item::Image { url, title, alt }, source);
1454
1455                // A top-level image is re-parsed from the start of the
1456                // line that contains it, as the rest of the line can
1457                // change how the image is parsed.
1458                if let Some(start) = paragraph_start.filter(|_| produced.is_some()) {
1459                    state.borrow_mut().window = Some(start);
1460                }
1461
1462                produced
1463            }
1464            pulldown_cmark::TagEnd::CodeBlock if !metadata => {
1465                code_block = false;
1466
1467                #[cfg(feature = "highlighter")]
1468                {
1469                    state.borrow_mut().parser = code_parser.take();
1470                }
1471
1472                produce(
1473                    state.borrow_mut(),
1474                    &mut stack,
1475                    Item::CodeBlock {
1476                        language: code_language.take(),
1477                        code: mem::take(&mut code),
1478                        lines: code_lines.drain(..).collect(),
1479                    },
1480                    source,
1481                )
1482            }
1483            pulldown_cmark::TagEnd::MetadataBlock(_) => {
1484                metadata = false;
1485                None
1486            }
1487            pulldown_cmark::TagEnd::Table => {
1488                let scope = stack.pop()?;
1489
1490                let Scope::Table { columns, rows, .. } = scope else {
1491                    return None;
1492                };
1493
1494                produce(
1495                    state.borrow_mut(),
1496                    &mut stack,
1497                    Item::Table { columns, rows },
1498                    source,
1499                )
1500            }
1501            pulldown_cmark::TagEnd::TableHead => {
1502                strong = false;
1503                None
1504            }
1505            pulldown_cmark::TagEnd::TableCell => {
1506                if !spans.is_empty() {
1507                    let _ = produce(
1508                        state.borrow_mut(),
1509                        &mut stack,
1510                        Item::Paragraph(Text::new(spans.drain(..).collect())),
1511                        source,
1512                    );
1513                }
1514
1515                let Scope::Table {
1516                    alignment,
1517                    columns,
1518                    rows,
1519                    current,
1520                } = stack.last_mut()?
1521                else {
1522                    return None;
1523                };
1524
1525                if columns.len() < alignment.len() {
1526                    columns.push(Column {
1527                        header: std::mem::take(current),
1528                        alignment: alignment[columns.len()],
1529                    });
1530                } else {
1531                    rows.last_mut()
1532                        .expect("table row")
1533                        .cells
1534                        .push(std::mem::take(current));
1535                }
1536
1537                None
1538            }
1539            _ => None,
1540        },
1541        pulldown_cmark::Event::Text(text) if !metadata => {
1542            if code_block {
1543                code.push_str(&text);
1544
1545                #[cfg(feature = "highlighter")]
1546                if let Some(highlighter) = &mut code_parser {
1547                    for line in text.lines() {
1548                        code_lines.push(Text::new(highlighter.parse_line(line).to_vec()));
1549                    }
1550                }
1551
1552                #[cfg(not(feature = "highlighter"))]
1553                for line in text.lines() {
1554                    code_lines.push(Text::new(vec![Span::Code {
1555                        text: line.to_owned(),
1556                        code: Code::Other,
1557                    }]));
1558                }
1559
1560                return None;
1561            }
1562
1563            let span = Span::Standard {
1564                text: text.into_string(),
1565                strong,
1566                emphasis,
1567                strikethrough,
1568                link: link.clone(),
1569                inline_code: false,
1570            };
1571
1572            spans.push(span);
1573
1574            None
1575        }
1576        pulldown_cmark::Event::Code(code) if !metadata => {
1577            let span = Span::Standard {
1578                text: code.into_string(),
1579                strong,
1580                emphasis,
1581                strikethrough,
1582                link: link.clone(),
1583                inline_code: true,
1584            };
1585
1586            spans.push(span);
1587            None
1588        }
1589        pulldown_cmark::Event::SoftBreak if !metadata => {
1590            spans.push(Span::Standard {
1591                text: String::from(" "),
1592                strikethrough,
1593                strong,
1594                emphasis,
1595                link: link.clone(),
1596                inline_code: false,
1597            });
1598            None
1599        }
1600        pulldown_cmark::Event::HardBreak if !metadata => {
1601            spans.push(Span::Standard {
1602                text: String::from("\n"),
1603                strikethrough,
1604                strong,
1605                emphasis,
1606                link: link.clone(),
1607                inline_code: false,
1608            });
1609            None
1610        }
1611        pulldown_cmark::Event::Rule => produce(state.borrow_mut(), &mut stack, Item::Rule, source),
1612        pulldown_cmark::Event::TaskListMarker(done) => {
1613            if let Some(Scope::List(list)) = stack.last_mut()
1614                && let Some(item) = list.bullets.last_mut()
1615                && let Bullet::Point { items } = item
1616            {
1617                *item = Bullet::Task {
1618                    items: std::mem::take(items),
1619                    done,
1620                };
1621            }
1622
1623            None
1624        }
1625        _ => None,
1626    })
1627}
1628
1629/// Configuration controlling Markdown rendering in [`view`].
1630#[derive(Debug, Clone, Copy, PartialEq)]
1631pub struct Settings {
1632    /// The [`Font`] to be applied to basic text.
1633    pub font: Font,
1634    /// The [`Font`] to be applied to inline code.
1635    pub inline_code_font: Font,
1636    /// The [`Font`] to be applied to code blocks.
1637    pub code_block_font: Font,
1638    /// The base line height.
1639    pub line_height: LineHeight,
1640    /// The base text size.
1641    pub text_size: Pixels,
1642    /// The text size used in code blocks.
1643    pub code_block_size: Pixels,
1644    /// The text size used in inline code.
1645    pub inline_code_size: Pixels,
1646    /// The text size of level 1 heading.
1647    pub h1_size: Pixels,
1648    /// The text size of level 2 heading.
1649    pub h2_size: Pixels,
1650    /// The text size of level 3 heading.
1651    pub h3_size: Pixels,
1652    /// The text size of level 4 heading.
1653    pub h4_size: Pixels,
1654    /// The text size of level 5 heading.
1655    pub h5_size: Pixels,
1656    /// The text size of level 6 heading.
1657    pub h6_size: Pixels,
1658    /// The spacing to be used between elements.
1659    pub spacing: Pixels,
1660}
1661
1662impl Settings {
1663    /// Creates new [`Settings`] with the given base text size in [`Pixels`].
1664    ///
1665    /// Heading levels will be adjusted automatically. Specifically,
1666    /// the first level will be 1.5 times the base size, the second
1667    /// 1.25 times, the third 1.125 times, and the remaining levels
1668    /// will use the base size.
1669    pub fn with_text_size(text_size: impl Into<Pixels>) -> Self {
1670        let text_size = text_size.into();
1671        let line_height = LineHeight::default();
1672
1673        Self {
1674            font: Font::DEFAULT,
1675            inline_code_font: Font::MONOSPACE,
1676            code_block_font: Font::MONOSPACE,
1677            line_height,
1678            text_size,
1679            inline_code_size: text_size * 0.85,
1680            code_block_size: text_size * 0.85,
1681            h1_size: text_size * 1.5,
1682            h2_size: text_size * 1.25,
1683            h3_size: text_size * 1.125,
1684            h4_size: text_size,
1685            h5_size: text_size,
1686            h6_size: text_size,
1687            spacing: line_height.to_absolute(text_size) / 1.5,
1688        }
1689    }
1690
1691    /// Sets the [`LineHeight`] of the [`Settings`].
1692    pub fn line_height(self, line_height: impl Into<LineHeight>) -> Self {
1693        let line_height = line_height.into();
1694
1695        Self {
1696            line_height,
1697            spacing: line_height.to_absolute(self.text_size) / 1.5,
1698            ..self
1699        }
1700    }
1701}
1702
1703impl Default for Settings {
1704    fn default() -> Self {
1705        Self::with_text_size(16)
1706    }
1707}
1708
1709/// Display a bunch of Markdown items.
1710///
1711/// You can obtain the items with [`parse`].
1712///
1713/// # Example
1714/// ```no_run
1715/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
1716/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
1717/// #
1718/// use iced::widget::markdown;
1719/// use iced::Theme;
1720///
1721/// struct State {
1722///    markdown: Vec<markdown::Item>,
1723/// }
1724///
1725/// enum Message {
1726///     LinkClicked(markdown::Uri),
1727/// }
1728///
1729/// impl State {
1730///     pub fn new() -> Self {
1731///         Self {
1732///             markdown: markdown::parse("This is some **Markdown**!").collect(),
1733///         }
1734///     }
1735///
1736///     fn view(&self) -> Element<'_, Message> {
1737///         markdown::view(
1738///             &self.markdown,
1739///             markdown::Settings::default(),
1740///             Theme::TokyoNight,
1741///         )
1742///             .map(Message::LinkClicked)
1743///             .into()
1744///     }
1745///
1746///     fn update(state: &mut State, message: Message) {
1747///         match message {
1748///             Message::LinkClicked(url) => {
1749///                 println!("The following url was clicked: {url}");
1750///             }
1751///         }
1752///     }
1753/// }
1754/// ```
1755pub fn view<'a, Theme, Renderer>(
1756    items: &'a [Item],
1757    settings: impl Into<Settings>,
1758    theme: Theme,
1759) -> Element<'a, Uri, Theme, Renderer>
1760where
1761    Theme: Catalog + 'a,
1762    Renderer: core::text::Renderer + 'a,
1763{
1764    view_with(
1765        items,
1766        settings,
1767        &DefaultViewer {
1768            theme,
1769            highlighter: None,
1770        },
1771    )
1772}
1773
1774/// Runs [`view`] but with a custom [`Viewer`] to turn an [`Item`] into
1775/// an [`Element`].
1776///
1777/// This is useful if you want to customize the look of certain Markdown
1778/// elements.
1779pub fn view_with<'a, Message, Theme, Renderer>(
1780    items: &'a [Item],
1781    settings: impl Into<Settings>,
1782    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1783) -> Element<'a, Message, Theme, Renderer>
1784where
1785    Message: 'a,
1786    Theme: Catalog + 'a,
1787    Renderer: core::text::Renderer + 'a,
1788{
1789    self::items(viewer, settings.into(), items)
1790}
1791
1792/// Displays an [`Item`] using the given [`Viewer`].
1793pub fn item<'a, Message, Theme, Renderer>(
1794    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1795    settings: Settings,
1796    item: &'a Item,
1797) -> Element<'a, Message, Theme, Renderer>
1798where
1799    Message: 'a,
1800    Theme: Catalog + 'a,
1801    Renderer: core::text::Renderer + 'a,
1802{
1803    match item {
1804        Item::Image { url, title, alt } => viewer.image(settings, url, title, alt),
1805        Item::Heading(level, text) => viewer.heading(settings, level, text),
1806        Item::Paragraph(text) => viewer.paragraph(settings, text),
1807        Item::CodeBlock {
1808            language,
1809            code,
1810            lines,
1811        } => viewer.code_block(settings, language.as_deref(), code, lines),
1812        Item::List {
1813            start: None,
1814            bullets,
1815        } => viewer.unordered_list(settings, bullets),
1816        Item::List {
1817            start: Some(start),
1818            bullets,
1819        } => viewer.ordered_list(settings, *start, bullets),
1820        Item::Quote(quote) => viewer.quote(settings, quote),
1821        Item::Rule => viewer.rule(),
1822        Item::Table { columns, rows } => viewer.table(settings, columns, rows),
1823    }
1824}
1825
1826/// Displays a heading using the default look.
1827pub fn heading<'a, Message, Theme, Renderer>(
1828    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1829    settings: Settings,
1830    level: &'a HeadingLevel,
1831    text: &'a Text,
1832    on_link_click: impl Fn(Uri) -> Message + 'a,
1833) -> Element<'a, Message, Theme, Renderer>
1834where
1835    Message: 'a,
1836    Theme: Catalog + 'a,
1837    Renderer: core::text::Renderer + 'a,
1838{
1839    let Settings {
1840        h1_size,
1841        h2_size,
1842        h3_size,
1843        h4_size,
1844        h5_size,
1845        h6_size,
1846        ..
1847    } = settings;
1848
1849    let size = match level {
1850        pulldown_cmark::HeadingLevel::H1 => h1_size,
1851        pulldown_cmark::HeadingLevel::H2 => h2_size,
1852        pulldown_cmark::HeadingLevel::H3 => h3_size,
1853        pulldown_cmark::HeadingLevel::H4 => h4_size,
1854        pulldown_cmark::HeadingLevel::H5 => h5_size,
1855        pulldown_cmark::HeadingLevel::H6 => h6_size,
1856    };
1857
1858    container(
1859        rich_text(text.spans(
1860            Settings {
1861                font: Font {
1862                    weight: font::Weight::Bold,
1863                    ..settings.font
1864                },
1865                inline_code_font: Font {
1866                    weight: font::Weight::Bold,
1867                    ..settings.inline_code_font
1868                },
1869                inline_code_size: size * (settings.inline_code_size / settings.text_size),
1870                ..settings
1871            },
1872            viewer.theme(),
1873            viewer.highlighter(),
1874        ))
1875        .on_link_click(on_link_click)
1876        .size(size)
1877        .line_height(settings.line_height),
1878    )
1879    .into()
1880}
1881
1882/// Displays a paragraph using the default look.
1883pub fn paragraph<'a, Message, Theme, Renderer>(
1884    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1885    settings: Settings,
1886    text: &Text,
1887    on_link_click: impl Fn(Uri) -> Message + 'a,
1888) -> Element<'a, Message, Theme, Renderer>
1889where
1890    Message: 'a,
1891    Theme: Catalog + 'a,
1892    Renderer: core::text::Renderer + 'a,
1893{
1894    rich_text(text.spans(settings, viewer.theme(), viewer.highlighter()))
1895        .size(settings.text_size)
1896        .line_height(settings.line_height)
1897        .on_link_click(on_link_click)
1898        .into()
1899}
1900
1901/// Displays an unordered list using the default look and
1902/// calling the [`Viewer`] for each bullet point item.
1903pub fn unordered_list<'a, Message, Theme, Renderer>(
1904    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1905    settings: Settings,
1906    bullets: &'a [Bullet],
1907) -> Element<'a, Message, Theme, Renderer>
1908where
1909    Message: 'a,
1910    Theme: Catalog + 'a,
1911    Renderer: core::text::Renderer + 'a,
1912{
1913    column(bullets.iter().map(|bullet| {
1914        row![
1915            match bullet {
1916                Bullet::Point { .. } => {
1917                    text("•").size(settings.text_size).into()
1918                }
1919                Bullet::Task { done, .. } => {
1920                    Element::from(
1921                        container(checkbox(*done).size(settings.text_size))
1922                            .center_y(text::LineHeight::default().to_absolute(settings.text_size)),
1923                    )
1924                }
1925            },
1926            items(
1927                viewer,
1928                Settings {
1929                    spacing: settings.spacing / 2.0,
1930                    ..settings
1931                },
1932                bullet.items(),
1933            )
1934        ]
1935        .spacing(settings.text_size / 2.0)
1936        .into()
1937    }))
1938    .spacing(settings.spacing / 2.0)
1939    .padding(padding::left(settings.text_size.0))
1940    .into()
1941}
1942
1943/// Displays an ordered list using the default look and
1944/// calling the [`Viewer`] for each numbered item.
1945pub fn ordered_list<'a, Message, Theme, Renderer>(
1946    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1947    settings: Settings,
1948    start: u64,
1949    bullets: &'a [Bullet],
1950) -> Element<'a, Message, Theme, Renderer>
1951where
1952    Message: 'a,
1953    Theme: Catalog + 'a,
1954    Renderer: core::text::Renderer + 'a,
1955{
1956    let digits = (start + bullets.len() as u64).max(1).ilog10() + 1;
1957
1958    column(bullets.iter().enumerate().map(|(i, bullet)| {
1959        row![
1960            text!("{}.", i as u64 + start)
1961                .size(settings.text_size)
1962                .align_x(alignment::Horizontal::Right)
1963                .width(settings.text_size * ((digits as f32 / 2.0).ceil() + 1.0)),
1964            items(
1965                viewer,
1966                Settings {
1967                    spacing: settings.spacing / 2.0,
1968                    ..settings
1969                },
1970                bullet.items(),
1971            )
1972        ]
1973        .spacing(settings.text_size / 2.0)
1974        .into()
1975    }))
1976    .spacing(settings.spacing / 2.0)
1977    .into()
1978}
1979
1980/// Displays a code block using the default look.
1981pub fn code_block<'a, Message, Theme, Renderer>(
1982    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1983    settings: Settings,
1984    lines: &'a [Text],
1985    on_link_click: impl Fn(Uri) -> Message + Clone + 'a,
1986) -> Element<'a, Message, Theme, Renderer>
1987where
1988    Message: 'a,
1989    Theme: Catalog + 'a,
1990    Renderer: core::text::Renderer + 'a,
1991{
1992    let padding = settings.code_block_size / 0.85 * 0.75;
1993
1994    container(
1995        scrollable(column(lines.iter().map(|line| {
1996            rich_text(line.spans(settings, viewer.theme(), viewer.highlighter()))
1997                .on_link_click(on_link_click.clone())
1998                .font(settings.code_block_font)
1999                .size(settings.code_block_size)
2000                .line_height(settings.line_height)
2001                .into()
2002        })))
2003        .direction(scrollable::Direction::Horizontal(
2004            scrollable::Scrollbar::default()
2005                .width(padding / 2.0)
2006                .scroller_width(padding / 2.0),
2007        ))
2008        .spacing(padding),
2009    )
2010    .width(Length::Fill)
2011    .padding(padding)
2012    .class(Theme::code_block())
2013    .into()
2014}
2015
2016/// Displays a quote using the default look.
2017pub fn quote<'a, Message, Theme, Renderer>(
2018    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
2019    settings: Settings,
2020    contents: &'a [Item],
2021) -> Element<'a, Message, Theme, Renderer>
2022where
2023    Message: 'a,
2024    Theme: Catalog + 'a,
2025    Renderer: core::text::Renderer + 'a,
2026{
2027    container(
2028        column(
2029            contents
2030                .iter()
2031                .map(|content| item(viewer, settings, content)),
2032        )
2033        .spacing(settings.spacing.0),
2034    )
2035    .width(Length::Fill)
2036    .padding(settings.spacing.0)
2037    .class(Theme::quote())
2038    .into()
2039}
2040
2041/// Displays a rule using the default look.
2042pub fn rule<'a, Message, Theme, Renderer>() -> Element<'a, Message, Theme, Renderer>
2043where
2044    Message: 'a,
2045    Theme: Catalog + 'a,
2046    Renderer: core::text::Renderer + 'a,
2047{
2048    rule::horizontal(2).into()
2049}
2050
2051/// Displays a table using the default look.
2052pub fn table<'a, Message, Theme, Renderer>(
2053    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
2054    settings: Settings,
2055    columns: &'a [Column],
2056    rows: &'a [Row],
2057) -> Element<'a, Message, Theme, Renderer>
2058where
2059    Message: 'a,
2060    Theme: Catalog + 'a,
2061    Renderer: core::text::Renderer + 'a,
2062{
2063    use crate::table;
2064
2065    let table = table(
2066        columns.iter().enumerate().map(move |(i, column)| {
2067            table::column(items(viewer, settings, &column.header), move |row: &Row| {
2068                if let Some(cells) = row.cells.get(i) {
2069                    items(viewer, settings, cells)
2070                } else {
2071                    text("").into()
2072                }
2073            })
2074            .align_x(match column.alignment {
2075                pulldown_cmark::Alignment::None | pulldown_cmark::Alignment::Left => {
2076                    alignment::Horizontal::Left
2077                }
2078                pulldown_cmark::Alignment::Center => alignment::Horizontal::Center,
2079                pulldown_cmark::Alignment::Right => alignment::Horizontal::Right,
2080            })
2081        }),
2082        rows,
2083    )
2084    .padding_x(settings.spacing.0)
2085    .padding_y(settings.spacing.0 / 2.0)
2086    .separator_x(0);
2087
2088    scrollable(table)
2089        .direction(scrollable::Direction::Horizontal(
2090            scrollable::Scrollbar::default(),
2091        ))
2092        .spacing(settings.spacing.0 / 2.0)
2093        .into()
2094}
2095
2096/// Displays a column of items with the default look.
2097pub fn items<'a, Message, Theme, Renderer>(
2098    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
2099    settings: Settings,
2100    items: &'a [Item],
2101) -> Element<'a, Message, Theme, Renderer>
2102where
2103    Message: 'a,
2104    Theme: Catalog + 'a,
2105    Renderer: core::text::Renderer + 'a,
2106{
2107    column(sections(items).map(|(heading, contents)| {
2108        let contents = column(
2109            contents
2110                .iter()
2111                .map(|content| item(viewer, settings, content)),
2112        )
2113        .spacing(settings.spacing)
2114        .into();
2115
2116        if let Some(heading) = heading {
2117            column![item(viewer, settings, heading), contents]
2118                .spacing(settings.spacing / 2.0)
2119                .into()
2120        } else {
2121            contents
2122        }
2123    }))
2124    .spacing(settings.spacing * 1.5)
2125    .into()
2126}
2127
2128/// A view strategy to display a Markdown [`Item`].
2129///
2130/// A [`Viewer`] is in charge of turning each [`Item`] into an [`Element`]. It
2131/// also provides the [`Theme`] and [`text::Highlighter`] used for rendering.
2132pub trait Viewer<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
2133where
2134    Self: Sized + 'a,
2135    Message: 'a,
2136    Theme: Catalog + 'a,
2137    Renderer: core::text::Renderer + 'a,
2138{
2139    /// The [`Theme`] used for styling the Markdown elements.
2140    fn theme(&self) -> &Theme;
2141
2142    /// The [`text::Highlighter`] used for highligthing [`Code`] regions.
2143    fn highlighter(&self) -> &dyn text::Highlighter<Code, Theme>;
2144
2145    /// Produces a message when a link is clicked with the given [`Uri`].
2146    fn on_link_click(url: Uri) -> Message;
2147
2148    /// Displays an image.
2149    ///
2150    /// By default, it will show a container with the image title.
2151    fn image(
2152        &self,
2153        settings: Settings,
2154        url: &'a Uri,
2155        title: &'a str,
2156        alt: &Text,
2157    ) -> Element<'a, Message, Theme, Renderer> {
2158        let _url = url;
2159        let _title = title;
2160
2161        container(
2162            rich_text(alt.spans(settings, self.theme(), self.highlighter()))
2163                .on_link_click(Self::on_link_click),
2164        )
2165        .padding(settings.spacing.0)
2166        .class(Theme::code_block())
2167        .into()
2168    }
2169
2170    /// Displays a heading.
2171    ///
2172    /// By default, it calls [`heading`].
2173    fn heading(
2174        &self,
2175        settings: Settings,
2176        level: &'a HeadingLevel,
2177        text: &'a Text,
2178    ) -> Element<'a, Message, Theme, Renderer> {
2179        heading(self, settings, level, text, Self::on_link_click)
2180    }
2181
2182    /// Displays a paragraph.
2183    ///
2184    /// By default, it calls [`paragraph`].
2185    fn paragraph(&self, settings: Settings, text: &Text) -> Element<'a, Message, Theme, Renderer> {
2186        paragraph(self, settings, text, Self::on_link_click)
2187    }
2188
2189    /// Displays a code block.
2190    ///
2191    /// By default, it calls [`code_block`].
2192    fn code_block(
2193        &self,
2194        settings: Settings,
2195        language: Option<&'a str>,
2196        code: &'a str,
2197        lines: &'a [Text],
2198    ) -> Element<'a, Message, Theme, Renderer> {
2199        let _language = language;
2200        let _code = code;
2201
2202        code_block(self, settings, lines, Self::on_link_click)
2203    }
2204
2205    /// Displays an unordered list.
2206    ///
2207    /// By default, it calls [`unordered_list`].
2208    fn unordered_list(
2209        &self,
2210        settings: Settings,
2211        bullets: &'a [Bullet],
2212    ) -> Element<'a, Message, Theme, Renderer> {
2213        unordered_list(self, settings, bullets)
2214    }
2215
2216    /// Displays an ordered list.
2217    ///
2218    /// By default, it calls [`ordered_list`].
2219    fn ordered_list(
2220        &self,
2221        settings: Settings,
2222        start: u64,
2223        bullets: &'a [Bullet],
2224    ) -> Element<'a, Message, Theme, Renderer> {
2225        ordered_list(self, settings, start, bullets)
2226    }
2227
2228    /// Displays a quote.
2229    ///
2230    /// By default, it calls [`quote`].
2231    fn quote(
2232        &self,
2233        settings: Settings,
2234        contents: &'a [Item],
2235    ) -> Element<'a, Message, Theme, Renderer> {
2236        quote(self, settings, contents)
2237    }
2238
2239    /// Displays a rule.
2240    ///
2241    /// By default, it calls [`rule`](self::rule()).
2242    fn rule(&self) -> Element<'a, Message, Theme, Renderer> {
2243        rule()
2244    }
2245
2246    /// Displays a table.
2247    ///
2248    /// By default, it calls [`table`].
2249    fn table(
2250        &self,
2251        settings: Settings,
2252        columns: &'a [Column],
2253        rows: &'a [Row],
2254    ) -> Element<'a, Message, Theme, Renderer> {
2255        table(self, settings, columns, rows)
2256    }
2257}
2258
2259/// The default [`Viewer`].
2260pub struct DefaultViewer<'a, Theme> {
2261    theme: Theme,
2262    highlighter: Option<Box<dyn text::Highlighter<Code, Theme> + 'a>>,
2263}
2264
2265impl<'a, Theme> DefaultViewer<'a, Theme> {
2266    /// Creates a new [`DefaultViewer`] with the given [`Theme`].
2267    pub fn new(theme: Theme) -> Self {
2268        Self {
2269            theme,
2270            highlighter: None,
2271        }
2272    }
2273
2274    /// Sets a custom [`text::Highlighter`] for the [`DefaultViewer`].
2275    pub fn highlighter(mut self, highlighter: impl text::Highlighter<Code, Theme> + 'a) -> Self {
2276        self.highlighter = Some(Box::new(highlighter));
2277        self
2278    }
2279}
2280
2281impl<'a, Theme, Renderer> Viewer<'a, Uri, Theme, Renderer> for DefaultViewer<'a, Theme>
2282where
2283    Theme: Catalog + 'a,
2284    Renderer: core::text::Renderer + 'a,
2285{
2286    fn theme(&self) -> &Theme {
2287        &self.theme
2288    }
2289
2290    fn highlighter(&self) -> &dyn text::Highlighter<Code, Theme> {
2291        self.highlighter
2292            .as_deref()
2293            .unwrap_or_else(|| self.theme.highlighter())
2294    }
2295
2296    fn on_link_click(url: Uri) -> Uri {
2297        url
2298    }
2299}
2300
2301/// The theme catalog of Markdown items.
2302pub trait Catalog:
2303    container::Catalog
2304    + scrollable::Catalog
2305    + text::Catalog
2306    + crate::rule::Catalog
2307    + checkbox::Catalog
2308    + crate::table::Catalog
2309    + Clone
2310    + PartialEq
2311{
2312    /// The unique identifier of the [`Catalog`].
2313    ///
2314    /// This will be used to invalidate span styling when a theme changes.
2315    fn id(&self) -> &str;
2316
2317    /// The [`Color`] of some link.
2318    fn link_color(&self) -> Color;
2319
2320    /// The [`InlineCode`] style of some inline code.
2321    fn code(&self) -> InlineCode;
2322
2323    /// The styling class of a code block.
2324    fn code_block<'a>() -> <Self as container::Catalog>::Class<'a>;
2325
2326    /// The styling class of a quote.
2327    fn quote<'a>() -> <Self as container::Catalog>::Class<'a>;
2328
2329    /// The default [`text::Highlighter`] to use to highlight code.
2330    fn highlighter(&self) -> &dyn text::Highlighter<Code, Self>;
2331}
2332
2333/// The style of some inline code.
2334#[derive(Debug, Clone, Copy, PartialEq)]
2335pub struct InlineCode {
2336    /// The [`Padding`] to apply around the code.
2337    pub padding: Padding,
2338    /// The [`Highlight`] of the code.
2339    pub highlight: Highlight,
2340    /// The [`Color`] of the code.
2341    pub color: Color,
2342}
2343
2344impl Catalog for Theme {
2345    fn id(&self) -> &str {
2346        theme::Base::name(self)
2347    }
2348
2349    fn link_color(&self) -> Color {
2350        self.seed().primary
2351    }
2352
2353    fn code(&self) -> InlineCode {
2354        let palette = self.palette();
2355
2356        InlineCode {
2357            padding: padding::horizontal(4).vertical(1),
2358            highlight: Highlight {
2359                background: palette.background.weaker.color.into(),
2360                border: border::rounded(4),
2361            },
2362            color: palette.background.weaker.text,
2363        }
2364    }
2365
2366    fn code_block<'a>() -> <Self as container::Catalog>::Class<'a> {
2367        Box::new(|theme| container::dark(theme).border(border::rounded(5)))
2368    }
2369
2370    fn quote<'a>() -> <Self as container::Catalog>::Class<'a> {
2371        Box::new(|theme| {
2372            let palette = theme.palette();
2373
2374            container::Style {
2375                text_color: Some(palette.background.weakest.text),
2376                background: Some(palette.background.weakest.color.into()),
2377                border: border::rounded(5),
2378                ..container::Style::default()
2379            }
2380        })
2381    }
2382
2383    fn highlighter(&self) -> &dyn text::Highlighter<Code, Self> {
2384        &Code::highlight
2385    }
2386}
2387
2388#[cfg(feature = "highlighter")]
2389mod code {
2390    use super::Span;
2391
2392    #[derive(Debug)]
2393    pub struct Parser {
2394        lines: Vec<(String, Vec<Span>)>,
2395        language: String,
2396        stream: iced_highlighter::Stream,
2397        current: usize,
2398    }
2399
2400    impl Parser {
2401        pub fn new(language: &str) -> Self {
2402            Self {
2403                lines: Vec::new(),
2404                stream: iced_highlighter::Stream::new(&iced_highlighter::Settings {
2405                    token: language.to_owned(),
2406                }),
2407                language: language.to_owned(),
2408                current: 0,
2409            }
2410        }
2411
2412        pub fn language(&self) -> &str {
2413            &self.language
2414        }
2415
2416        pub fn prepare(&mut self) {
2417            self.current = 0;
2418        }
2419
2420        pub fn parse_line(&mut self, text: &str) -> &[Span] {
2421            match self.lines.get(self.current) {
2422                Some(line) if line.0 == text => {
2423                    if self.current + 1 == self.lines.len() {
2424                        self.stream.commit();
2425                    }
2426                }
2427                _ => {
2428                    if self.current + 1 < self.lines.len() {
2429                        log::debug!("Resetting highlighter...");
2430                        self.stream.reset();
2431                        self.lines.truncate(self.current);
2432
2433                        for line in &self.lines {
2434                            log::debug!("Refeeding {n} lines", n = self.lines.len());
2435
2436                            let _ = self.stream.parse_line(&line.0);
2437                            self.stream.commit();
2438                        }
2439                    }
2440
2441                    log::trace!("Parsing: {text}", text = text.trim_end());
2442
2443                    let mut spans = Vec::new();
2444
2445                    if self.current == self.lines.len() {
2446                        self.stream.commit();
2447                    }
2448
2449                    for (range, code) in self.stream.parse_line(text) {
2450                        spans.push(Span::Code {
2451                            text: text[range].to_owned(),
2452                            code,
2453                        });
2454                    }
2455
2456                    if self.current == self.lines.len() {
2457                        self.lines.push((text.to_owned(), spans));
2458                    } else {
2459                        self.lines[self.current] = (text.to_owned(), spans);
2460                    }
2461                }
2462            }
2463
2464            self.current += 1;
2465
2466            &self
2467                .lines
2468                .get(self.current - 1)
2469                .expect("Line must be parsed")
2470                .1
2471        }
2472    }
2473}