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, Widget}; pub use iced_widget::core::*; }
11//! #
12//! use iced::widget::markdown;
13//! use iced::{Theme, Widget};
14//!
15//! struct State {
16//!    markdown: Vec<markdown::Item>,
17//! }
18//!
19//! enum Message {
20//!     LinkClicked(markdown::Uri),
21//! }
22//!
23//! impl State {
24//!     pub fn new() -> Self {
25//!         Self {
26//!             markdown: markdown::parse("This is some **Markdown**!").collect(),
27//!         }
28//!     }
29//!
30//!     fn view(&self) -> impl Widget<Message> {
31//!         markdown::view(
32//!             &self.markdown,
33//!             markdown::Settings::default(),
34//!             Theme::TokyoNight,
35//!         )
36//!             .map(Message::LinkClicked)
37//!     }
38//!
39//!     fn update(state: &mut State, message: Message) {
40//!         match message {
41//!             Message::LinkClicked(url) => {
42//!                 println!("The following url was clicked: {url}");
43//!             }
44//!         }
45//!     }
46//! }
47//! ```
48use crate::core;
49use crate::core::alignment;
50use crate::core::border;
51use crate::core::font::{self, Font};
52use crate::core::padding;
53use crate::core::text::LineHeight;
54use crate::core::theme;
55use crate::core::{Code, Color, Length, Padding, Pixels, Theme};
56use crate::{
57    Widget, center_x, checkbox, column, container, rich_text, row, rule, scrollable, span, text,
58};
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, input_start).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, input_start).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, base).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                    parsers: HashMap::new(),
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, section.start).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 a [`Widget`].
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, Widget}; pub use iced_widget::core::*; }
964/// #
965/// use iced::widget::markdown;
966/// use iced::{Theme, Widget};
967///
968/// struct State {
969///    markdown: Vec<markdown::Item>,
970/// }
971///
972/// enum Message {
973///     LinkClicked(markdown::Uri),
974/// }
975///
976/// impl State {
977///     pub fn new() -> Self {
978///         Self {
979///             markdown: markdown::parse("This is some **Markdown**!").collect(),
980///         }
981///     }
982///
983///     fn view(&self) -> impl Widget<Message> {
984///         markdown::view(
985///             &self.markdown,
986///             markdown::Settings::default(),
987///             Theme::TokyoNight,
988///         )
989///             .map(Message::LinkClicked)
990///     }
991///
992///     fn update(state: &mut State, message: Message) {
993///         match message {
994///             Message::LinkClicked(url) => {
995///                 println!("The following url was clicked: {url}");
996///             }
997///         }
998///     }
999/// }
1000/// ```
1001pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
1002    parse_with(State::default(), markdown, 0).map(|(item, _start, _broken_links)| item)
1003}
1004
1005#[derive(Debug, Default)]
1006struct State {
1007    /// The start of the source that will be re-parsed next, after the
1008    /// current parse.
1009    window: Option<usize>,
1010    /// The reference definitions, mapping a label to its destination.
1011    ///
1012    /// The first definition of a label wins, like in CommonMark.
1013    references: HashMap<String, String>,
1014    /// The labels whose destination in `references` comes from the
1015    /// definition of the last line, which is not terminated yet and
1016    /// can still grow; their destination is updated on each push,
1017    /// until the line is terminated.
1018    references_staged: HashSet<String>,
1019    images: HashSet<Uri>,
1020    /// The highlighters of the code blocks seen in the last re-parses,
1021    /// keyed by the start of the block's source in the outer stream.
1022    ///
1023    /// A highlighter is only reused for the same code block (the same
1024    /// source start and language), so that its line cache stays valid
1025    /// across the re-parses of a block that is still growing.
1026    #[cfg(feature = "highlighter")]
1027    parsers: HashMap<usize, code::Parser>,
1028}
1029
1030/// The options used by the parser.
1031fn options() -> pulldown_cmark::Options {
1032    pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS
1033        | pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS
1034        | pulldown_cmark::Options::ENABLE_TABLES
1035        | pulldown_cmark::Options::ENABLE_STRIKETHROUGH
1036        | pulldown_cmark::Options::ENABLE_TASKLISTS
1037}
1038
1039/// Absorbs the reference definitions of a parse into `references`,
1040/// keeping the first definition of a label, like in CommonMark.
1041///
1042/// The definition of the last line, when that line is not terminated
1043/// yet, can still grow, so its destination is updated on each push,
1044/// until the line is terminated; the growing labels are tracked in
1045/// `growing_refs`.
1046///
1047/// `markdown` is the source of the parse, and `definitions` are its
1048/// reference definitions.
1049fn absorb_references(
1050    markdown: &str,
1051    definitions: &pulldown_cmark::RefDefs<'_>,
1052    references: &mut HashMap<String, String>,
1053    growing_refs: &mut HashSet<String>,
1054) {
1055    for reference in definitions.iter() {
1056        let name = reference.0.to_string();
1057        let dest = reference.1.dest.to_string();
1058
1059        if markdown[reference.1.span.end..].contains('\n') {
1060            if !references.contains_key(&name) || growing_refs.remove(&name) {
1061                let _ = references.insert(name, dest);
1062            }
1063        } else if growing_refs.contains(&name) {
1064            // The map's value is the growing one: update it
1065            let _ = references.insert(name, dest);
1066        } else if !references.contains_key(&name) {
1067            // No terminated definition wins: the growing value is
1068            // provisional
1069            let _ = growing_refs.insert(name.clone());
1070            let _ = references.insert(name, dest);
1071        }
1072    }
1073}
1074
1075fn parse_with<'a>(
1076    mut state: impl BorrowMut<State> + 'a,
1077    markdown: &'a str,
1078    _offset: usize,
1079) -> impl Iterator<Item = (Item, usize, HashSet<String>)> + 'a {
1080    enum Scope {
1081        List(List),
1082        Quote(Vec<Item>),
1083        Table {
1084            alignment: Vec<pulldown_cmark::Alignment>,
1085            columns: Vec<Column>,
1086            rows: Vec<Row>,
1087            current: Vec<Item>,
1088        },
1089    }
1090
1091    struct List {
1092        start: Option<u64>,
1093        bullets: Vec<Bullet>,
1094        /// The start of the last item of the list, if any.
1095        last_item_start: Option<usize>,
1096    }
1097
1098    // The broken links reported by the parser, along with their span
1099    // in the input.
1100    //
1101    // The broken links are reported before the items that contain them
1102    // are produced, so the links are attributed to an item by their
1103    // span.
1104    let broken_links = Rc::new(RefCell::new(Vec::new()));
1105
1106    let mut spans = Vec::new();
1107    let mut code = String::new();
1108    let mut code_language = None;
1109    let mut code_lines = Vec::new();
1110    let mut strong = false;
1111    let mut emphasis = false;
1112    let mut strikethrough = false;
1113    let mut metadata = false;
1114    let mut code_block = false;
1115    let mut link = None;
1116    let mut image = None;
1117    let mut paragraph_start = None;
1118    let mut stack = Vec::new();
1119
1120    #[cfg(feature = "highlighter")]
1121    let mut code_block_key = None;
1122
1123    let parser = pulldown_cmark::Parser::new_with_broken_link_callback(markdown, options(), {
1124        let references = state.borrow().references.clone();
1125        let broken_links = broken_links.clone();
1126
1127        Some(move |broken_link: pulldown_cmark::BrokenLink<'_>| {
1128            if let Some(reference) = references.get(broken_link.reference.as_ref()) {
1129                Some((
1130                    pulldown_cmark::CowStr::from(reference.to_owned()),
1131                    broken_link.reference.into_static(),
1132                ))
1133            } else {
1134                RefCell::borrow_mut(&broken_links)
1135                    .push((broken_link.span, broken_link.reference.into_string()));
1136
1137                None
1138            }
1139        })
1140    });
1141
1142    {
1143        let state = state.borrow_mut();
1144        absorb_references(
1145            markdown,
1146            parser.reference_definitions(),
1147            &mut state.references,
1148            &mut state.references_staged,
1149        );
1150    }
1151
1152    let produce = move |state: &mut State, stack: &mut Vec<Scope>, item, source: Range<usize>| {
1153        if let Some(scope) = stack.last_mut() {
1154            match scope {
1155                Scope::List(list) => {
1156                    list.bullets.last_mut().expect("item context").push(item);
1157                }
1158                Scope::Quote(items) => {
1159                    items.push(item);
1160                }
1161                Scope::Table { current, .. } => {
1162                    current.push(item);
1163                }
1164            }
1165
1166            None
1167        } else {
1168            state.window = Some(source.start);
1169
1170            // Attribute the broken links whose span falls within the
1171            // source of the item
1172            let mut links = HashSet::new();
1173            for (span, reference) in RefCell::borrow(&broken_links).iter() {
1174                if source.contains(&span.start) {
1175                    let _ = links.insert(reference.clone());
1176                }
1177            }
1178
1179            Some((item, source.start, links))
1180        }
1181    };
1182
1183    // A reference link or image resolves with the first definition
1184    // of its label in the whole document, like in the one-shot
1185    // parse. A later definition that falls within the input wins
1186    // within the input, so, when known, prefer the global
1187    // definition.
1188    let resolve_reference = |state: &mut State,
1189                             link_type: pulldown_cmark::LinkType,
1190                             id: &str,
1191                             dest_url: &pulldown_cmark::CowStr<'a>|
1192     -> String {
1193        match link_type {
1194            pulldown_cmark::LinkType::Reference
1195            | pulldown_cmark::LinkType::ReferenceUnknown
1196            | pulldown_cmark::LinkType::Collapsed
1197            | pulldown_cmark::LinkType::CollapsedUnknown
1198            | pulldown_cmark::LinkType::Shortcut
1199            | pulldown_cmark::LinkType::ShortcutUnknown => state
1200                .references
1201                .get(id)
1202                .cloned()
1203                .unwrap_or_else(|| dest_url.to_string()),
1204            _ => dest_url.to_string(),
1205        }
1206    };
1207
1208    let parser = parser.into_offset_iter();
1209
1210    // We want to keep the `spans` capacity
1211    #[allow(clippy::drain_collect)]
1212    parser.filter_map(move |(event, source)| match event {
1213        pulldown_cmark::Event::Start(tag) => match tag {
1214            pulldown_cmark::Tag::Strong if !metadata => {
1215                strong = true;
1216                None
1217            }
1218            pulldown_cmark::Tag::Emphasis if !metadata => {
1219                emphasis = true;
1220                None
1221            }
1222            pulldown_cmark::Tag::Strikethrough if !metadata => {
1223                strikethrough = true;
1224                None
1225            }
1226            pulldown_cmark::Tag::Link {
1227                link_type,
1228                dest_url,
1229                id,
1230                ..
1231            } if !metadata => {
1232                link = Some(resolve_reference(
1233                    state.borrow_mut(),
1234                    link_type,
1235                    &id,
1236                    &dest_url,
1237                ));
1238                None
1239            }
1240            pulldown_cmark::Tag::Paragraph if !metadata => {
1241                paragraph_start = Some(source.start);
1242                None
1243            }
1244            pulldown_cmark::Tag::Image {
1245                link_type,
1246                dest_url,
1247                title,
1248                id,
1249            } if !metadata => {
1250                image = Some((
1251                    resolve_reference(state.borrow_mut(), link_type, &id, &dest_url),
1252                    title.into_string(),
1253                    spans.len(),
1254                ));
1255                None
1256            }
1257            pulldown_cmark::Tag::List(first_item) if !metadata => {
1258                let prev = if spans.is_empty() {
1259                    None
1260                } else {
1261                    produce(
1262                        state.borrow_mut(),
1263                        &mut stack,
1264                        Item::Paragraph(Text::new(spans.drain(..).collect())),
1265                        source,
1266                    )
1267                };
1268
1269                stack.push(Scope::List(List {
1270                    start: first_item,
1271                    bullets: Vec::new(),
1272                    last_item_start: None,
1273                }));
1274
1275                prev
1276            }
1277            pulldown_cmark::Tag::Item => {
1278                if let Some(Scope::List(list)) = stack.last_mut() {
1279                    list.last_item_start = Some(source.start);
1280                    list.bullets.push(Bullet::Point { items: Vec::new() });
1281                }
1282
1283                None
1284            }
1285            pulldown_cmark::Tag::BlockQuote(_kind) if !metadata => {
1286                let prev = if spans.is_empty() {
1287                    None
1288                } else {
1289                    produce(
1290                        state.borrow_mut(),
1291                        &mut stack,
1292                        Item::Paragraph(Text::new(spans.drain(..).collect())),
1293                        source,
1294                    )
1295                };
1296
1297                stack.push(Scope::Quote(Vec::new()));
1298
1299                prev
1300            }
1301            pulldown_cmark::Tag::CodeBlock(pulldown_cmark::CodeBlockKind::Fenced(language))
1302                if !metadata =>
1303            {
1304                #[cfg(feature = "highlighter")]
1305                {
1306                    // The start of the block in the outer stream; the
1307                    // key that identifies the block across re-parses,
1308                    // so that the highlighter's line cache is only
1309                    // reused for the same block.
1310                    let key = _offset + source.start;
1311                    let state = state.borrow_mut();
1312                    let language = language.split(',').next().unwrap_or_default();
1313
1314                    let mut parser = state
1315                        .parsers
1316                        .remove(&key)
1317                        .filter(|parser| parser.language() == language)
1318                        .unwrap_or_else(|| code::Parser::new(language));
1319
1320                    parser.prepare();
1321                    let _ = state.parsers.insert(key, parser);
1322
1323                    code_block_key = Some(key);
1324                }
1325
1326                code_block = true;
1327                code_language = (!language.is_empty()).then(|| language.into_string());
1328
1329                if spans.is_empty() {
1330                    None
1331                } else {
1332                    produce(
1333                        state.borrow_mut(),
1334                        &mut stack,
1335                        Item::Paragraph(Text::new(spans.drain(..).collect())),
1336                        source,
1337                    )
1338                }
1339            }
1340            pulldown_cmark::Tag::MetadataBlock(_) => {
1341                metadata = true;
1342                None
1343            }
1344            pulldown_cmark::Tag::Table(alignment) => {
1345                stack.push(Scope::Table {
1346                    columns: Vec::with_capacity(alignment.len()),
1347                    alignment,
1348                    current: Vec::new(),
1349                    rows: Vec::new(),
1350                });
1351
1352                None
1353            }
1354            pulldown_cmark::Tag::TableHead => {
1355                strong = true;
1356                None
1357            }
1358            pulldown_cmark::Tag::TableRow => {
1359                let Scope::Table { rows, .. } = stack.last_mut()? else {
1360                    return None;
1361                };
1362
1363                rows.push(Row { cells: Vec::new() });
1364                None
1365            }
1366            _ => None,
1367        },
1368        pulldown_cmark::Event::End(tag) => match tag {
1369            pulldown_cmark::TagEnd::Heading(level) if !metadata => produce(
1370                state.borrow_mut(),
1371                &mut stack,
1372                Item::Heading(level, Text::new(spans.drain(..).collect())),
1373                source,
1374            ),
1375            pulldown_cmark::TagEnd::Strong if !metadata => {
1376                strong = false;
1377                None
1378            }
1379            pulldown_cmark::TagEnd::Emphasis if !metadata => {
1380                emphasis = false;
1381                None
1382            }
1383            pulldown_cmark::TagEnd::Strikethrough if !metadata => {
1384                strikethrough = false;
1385                None
1386            }
1387            pulldown_cmark::TagEnd::Link if !metadata => {
1388                link = None;
1389                None
1390            }
1391            pulldown_cmark::TagEnd::Paragraph if !metadata => {
1392                paragraph_start = None;
1393
1394                if spans.is_empty() {
1395                    None
1396                } else {
1397                    produce(
1398                        state.borrow_mut(),
1399                        &mut stack,
1400                        Item::Paragraph(Text::new(spans.drain(..).collect())),
1401                        source,
1402                    )
1403                }
1404            }
1405            pulldown_cmark::TagEnd::Item if !metadata => {
1406                if spans.is_empty() {
1407                    None
1408                } else {
1409                    produce(
1410                        state.borrow_mut(),
1411                        &mut stack,
1412                        Item::Paragraph(Text::new(spans.drain(..).collect())),
1413                        source,
1414                    )
1415                }
1416            }
1417            pulldown_cmark::TagEnd::List(_) if !metadata => {
1418                let scope = stack.pop()?;
1419
1420                let Scope::List(list) = scope else {
1421                    return None;
1422                };
1423
1424                let last_item_start = list.last_item_start;
1425                let produced = produce(
1426                    state.borrow_mut(),
1427                    &mut stack,
1428                    Item::List {
1429                        start: list.start,
1430                        bullets: list.bullets,
1431                    },
1432                    source,
1433                );
1434
1435                // A list is re-parsed only from the start of its last
1436                // item, so that adding new items to a long list does not
1437                // require re-parsing the whole list.
1438                if produced.is_some()
1439                    && let Some(start) = last_item_start
1440                {
1441                    state.borrow_mut().window = Some(start);
1442                }
1443
1444                produced
1445            }
1446            pulldown_cmark::TagEnd::BlockQuote(_kind) if !metadata => {
1447                let scope = stack.pop()?;
1448
1449                let Scope::Quote(quote) = scope else {
1450                    return None;
1451                };
1452
1453                produce(state.borrow_mut(), &mut stack, Item::Quote(quote), source)
1454            }
1455            pulldown_cmark::TagEnd::Image if !metadata => {
1456                let (url, title, start) = image.take()?;
1457                let alt = Text::new(spans.drain(start..).collect());
1458
1459                let state = state.borrow_mut();
1460                let _ = state.images.insert(url.clone());
1461
1462                let produced = produce(state, &mut stack, Item::Image { url, title, alt }, source);
1463
1464                // A top-level image is re-parsed from the start of the
1465                // line that contains it, as the rest of the line can
1466                // change how the image is parsed.
1467                if let Some(start) = paragraph_start.filter(|_| produced.is_some()) {
1468                    state.borrow_mut().window = Some(start);
1469                }
1470
1471                produced
1472            }
1473            pulldown_cmark::TagEnd::CodeBlock if !metadata => {
1474                code_block = false;
1475
1476                produce(
1477                    state.borrow_mut(),
1478                    &mut stack,
1479                    Item::CodeBlock {
1480                        language: code_language.take(),
1481                        code: mem::take(&mut code),
1482                        lines: code_lines.drain(..).collect(),
1483                    },
1484                    source,
1485                )
1486            }
1487            pulldown_cmark::TagEnd::MetadataBlock(_) => {
1488                metadata = false;
1489                None
1490            }
1491            pulldown_cmark::TagEnd::Table => {
1492                let scope = stack.pop()?;
1493
1494                let Scope::Table { columns, rows, .. } = scope else {
1495                    return None;
1496                };
1497
1498                produce(
1499                    state.borrow_mut(),
1500                    &mut stack,
1501                    Item::Table { columns, rows },
1502                    source,
1503                )
1504            }
1505            pulldown_cmark::TagEnd::TableHead => {
1506                strong = false;
1507                None
1508            }
1509            pulldown_cmark::TagEnd::TableCell => {
1510                if !spans.is_empty() {
1511                    let _ = produce(
1512                        state.borrow_mut(),
1513                        &mut stack,
1514                        Item::Paragraph(Text::new(spans.drain(..).collect())),
1515                        source,
1516                    );
1517                }
1518
1519                let Scope::Table {
1520                    alignment,
1521                    columns,
1522                    rows,
1523                    current,
1524                } = stack.last_mut()?
1525                else {
1526                    return None;
1527                };
1528
1529                if columns.len() < alignment.len() {
1530                    columns.push(Column {
1531                        header: std::mem::take(current),
1532                        alignment: alignment[columns.len()],
1533                    });
1534                } else {
1535                    rows.last_mut()
1536                        .expect("table row")
1537                        .cells
1538                        .push(std::mem::take(current));
1539                }
1540
1541                None
1542            }
1543            _ => None,
1544        },
1545        pulldown_cmark::Event::Text(text) if !metadata => {
1546            if code_block {
1547                code.push_str(&text);
1548
1549                #[cfg(feature = "highlighter")]
1550                if let Some(key) = code_block_key
1551                    && let Some(highlighter) = state.borrow_mut().parsers.get_mut(&key)
1552                {
1553                    for line in text.lines() {
1554                        code_lines.push(Text::new(highlighter.parse_line(line).to_vec()));
1555                    }
1556                }
1557
1558                #[cfg(not(feature = "highlighter"))]
1559                for line in text.lines() {
1560                    code_lines.push(Text::new(vec![Span::Code {
1561                        text: line.to_owned(),
1562                        code: Code::Other,
1563                    }]));
1564                }
1565
1566                return None;
1567            }
1568
1569            let span = Span::Standard {
1570                text: text.into_string(),
1571                strong,
1572                emphasis,
1573                strikethrough,
1574                link: link.clone(),
1575                inline_code: false,
1576            };
1577
1578            spans.push(span);
1579
1580            None
1581        }
1582        pulldown_cmark::Event::Code(code) if !metadata => {
1583            let span = Span::Standard {
1584                text: code.into_string(),
1585                strong,
1586                emphasis,
1587                strikethrough,
1588                link: link.clone(),
1589                inline_code: true,
1590            };
1591
1592            spans.push(span);
1593            None
1594        }
1595        pulldown_cmark::Event::SoftBreak if !metadata => {
1596            spans.push(Span::Standard {
1597                text: String::from(" "),
1598                strikethrough,
1599                strong,
1600                emphasis,
1601                link: link.clone(),
1602                inline_code: false,
1603            });
1604            None
1605        }
1606        pulldown_cmark::Event::HardBreak if !metadata => {
1607            spans.push(Span::Standard {
1608                text: String::from("\n"),
1609                strikethrough,
1610                strong,
1611                emphasis,
1612                link: link.clone(),
1613                inline_code: false,
1614            });
1615            None
1616        }
1617        pulldown_cmark::Event::Rule => produce(state.borrow_mut(), &mut stack, Item::Rule, source),
1618        pulldown_cmark::Event::TaskListMarker(done) => {
1619            if let Some(Scope::List(list)) = stack.last_mut()
1620                && let Some(item) = list.bullets.last_mut()
1621                && let Bullet::Point { items } = item
1622            {
1623                *item = Bullet::Task {
1624                    items: std::mem::take(items),
1625                    done,
1626                };
1627            }
1628
1629            None
1630        }
1631        _ => None,
1632    })
1633}
1634
1635/// Configuration controlling Markdown rendering in [`view`].
1636#[derive(Debug, Clone, Copy, PartialEq)]
1637pub struct Settings {
1638    /// The [`Font`] to be applied to basic text.
1639    pub font: Font,
1640    /// The [`Font`] to be applied to inline code.
1641    pub inline_code_font: Font,
1642    /// The [`Font`] to be applied to code blocks.
1643    pub code_block_font: Font,
1644    /// The base line height.
1645    pub line_height: LineHeight,
1646    /// The base text size.
1647    pub text_size: Pixels,
1648    /// The text size used in code blocks.
1649    pub code_block_size: Pixels,
1650    /// The text size used in inline code.
1651    pub inline_code_size: Pixels,
1652    /// The text size of level 1 heading.
1653    pub h1_size: Pixels,
1654    /// The text size of level 2 heading.
1655    pub h2_size: Pixels,
1656    /// The text size of level 3 heading.
1657    pub h3_size: Pixels,
1658    /// The text size of level 4 heading.
1659    pub h4_size: Pixels,
1660    /// The text size of level 5 heading.
1661    pub h5_size: Pixels,
1662    /// The text size of level 6 heading.
1663    pub h6_size: Pixels,
1664    /// The spacing to be used between elements.
1665    pub spacing: Pixels,
1666}
1667
1668impl Settings {
1669    /// Creates new [`Settings`] with the given base text size in [`Pixels`].
1670    ///
1671    /// Heading levels will be adjusted automatically. Specifically,
1672    /// the first level will be 1.5 times the base size, the second
1673    /// 1.25 times, the third 1.125 times, and the remaining levels
1674    /// will use the base size.
1675    pub fn with_text_size(text_size: impl Into<Pixels>) -> Self {
1676        let text_size = text_size.into();
1677        let line_height = LineHeight::default();
1678
1679        Self {
1680            font: Font::DEFAULT,
1681            inline_code_font: Font::MONOSPACE,
1682            code_block_font: Font::MONOSPACE,
1683            line_height,
1684            text_size,
1685            inline_code_size: text_size * 0.85,
1686            code_block_size: text_size * 0.85,
1687            h1_size: text_size * 1.5,
1688            h2_size: text_size * 1.25,
1689            h3_size: text_size * 1.125,
1690            h4_size: text_size,
1691            h5_size: text_size,
1692            h6_size: text_size,
1693            spacing: line_height.to_absolute(text_size) / 1.5,
1694        }
1695    }
1696
1697    /// Sets the [`LineHeight`] of the [`Settings`].
1698    pub fn line_height(self, line_height: impl Into<LineHeight>) -> Self {
1699        let line_height = line_height.into();
1700
1701        Self {
1702            line_height,
1703            spacing: line_height.to_absolute(self.text_size) / 1.5,
1704            ..self
1705        }
1706    }
1707}
1708
1709impl Default for Settings {
1710    fn default() -> Self {
1711        Self::with_text_size(16)
1712    }
1713}
1714
1715/// Display a bunch of Markdown items.
1716///
1717/// You can obtain the items with [`parse`].
1718///
1719/// # Example
1720/// ```no_run
1721/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::{Renderer, Widget}; pub use iced_widget::core::*; }
1722/// #
1723/// use iced::widget::markdown;
1724/// use iced::{Theme, Widget};
1725///
1726/// struct State {
1727///    markdown: Vec<markdown::Item>,
1728/// }
1729///
1730/// enum Message {
1731///     LinkClicked(markdown::Uri),
1732/// }
1733///
1734/// impl State {
1735///     pub fn new() -> Self {
1736///         Self {
1737///             markdown: markdown::parse("This is some **Markdown**!").collect(),
1738///         }
1739///     }
1740///
1741///     fn view(&self) -> impl Widget<Message> {
1742///         markdown::view(
1743///             &self.markdown,
1744///             markdown::Settings::default(),
1745///             Theme::TokyoNight,
1746///         )
1747///             .map(Message::LinkClicked)
1748///     }
1749///
1750///     fn update(state: &mut State, message: Message) {
1751///         match message {
1752///             Message::LinkClicked(url) => {
1753///                 println!("The following url was clicked: {url}");
1754///             }
1755///         }
1756///     }
1757/// }
1758/// ```
1759pub fn view<'a, Theme, Renderer>(
1760    items: &'a [Item],
1761    settings: impl Into<Settings>,
1762    theme: Theme,
1763) -> impl Widget<Uri, Theme, Renderer> + 'a
1764where
1765    Theme: Catalog + 'static,
1766    Renderer: core::text::Renderer,
1767{
1768    view_with(
1769        items,
1770        settings,
1771        &DefaultViewer {
1772            theme,
1773            highlighter: None,
1774        },
1775    )
1776    .boxed()
1777}
1778
1779/// Runs [`view`] but with a custom [`Viewer`] to turn an [`Item`] into a widget.
1780///
1781/// This is useful if you want to customize the look of certain Markdown
1782/// elements.
1783pub fn view_with<'a, Message, Theme, Renderer>(
1784    items: &'a [Item],
1785    settings: impl Into<Settings>,
1786    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1787) -> impl Widget<Message, Theme, Renderer> + 'a
1788where
1789    Message: 'a,
1790    Theme: Catalog + 'a,
1791    Renderer: core::text::Renderer,
1792{
1793    self::items(viewer, settings.into(), items)
1794}
1795
1796/// Displays an [`Item`] using the given [`Viewer`].
1797pub fn item<'a, Message, Theme, Renderer>(
1798    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1799    settings: Settings,
1800    item: &'a Item,
1801) -> impl Widget<Message, Theme, Renderer> + 'a
1802where
1803    Message: 'a,
1804    Theme: Catalog + 'a,
1805    Renderer: core::text::Renderer + 'a,
1806{
1807    match item {
1808        Item::Image { url, title, alt } => viewer.image(settings, url, title, alt).boxed(),
1809        Item::Heading(level, text) => viewer.heading(settings, level, text).boxed(),
1810        Item::Paragraph(text) => viewer.paragraph(settings, text).boxed(),
1811        Item::CodeBlock {
1812            language,
1813            code,
1814            lines,
1815        } => viewer
1816            .code_block(settings, language.as_deref(), code, lines)
1817            .boxed(),
1818        Item::List {
1819            start: None,
1820            bullets,
1821        } => viewer.unordered_list(settings, bullets).boxed(),
1822        Item::List {
1823            start: Some(start),
1824            bullets,
1825        } => viewer.ordered_list(settings, *start, bullets).boxed(),
1826        Item::Quote(quote) => viewer.quote(settings, quote).boxed(),
1827        Item::Rule => viewer.rule().boxed(),
1828        Item::Table { columns, rows } => viewer.table(settings, columns, rows).boxed(),
1829    }
1830}
1831
1832/// Displays a heading using the default look.
1833pub fn heading<'a, Message, Theme, Renderer>(
1834    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1835    settings: Settings,
1836    level: &'a HeadingLevel,
1837    text: &'a Text,
1838    on_link_click: impl Fn(Uri) -> Message + 'a,
1839) -> impl Widget<Message, Theme, Renderer> + 'a
1840where
1841    Message: 'a,
1842    Theme: Catalog + 'a,
1843    Renderer: core::text::Renderer + 'a,
1844{
1845    let Settings {
1846        h1_size,
1847        h2_size,
1848        h3_size,
1849        h4_size,
1850        h5_size,
1851        h6_size,
1852        ..
1853    } = settings;
1854
1855    let size = match level {
1856        pulldown_cmark::HeadingLevel::H1 => h1_size,
1857        pulldown_cmark::HeadingLevel::H2 => h2_size,
1858        pulldown_cmark::HeadingLevel::H3 => h3_size,
1859        pulldown_cmark::HeadingLevel::H4 => h4_size,
1860        pulldown_cmark::HeadingLevel::H5 => h5_size,
1861        pulldown_cmark::HeadingLevel::H6 => h6_size,
1862    };
1863
1864    container(
1865        rich_text(text.spans(
1866            Settings {
1867                font: Font {
1868                    weight: font::Weight::Bold,
1869                    ..settings.font
1870                },
1871                inline_code_font: Font {
1872                    weight: font::Weight::Bold,
1873                    ..settings.inline_code_font
1874                },
1875                inline_code_size: size * (settings.inline_code_size / settings.text_size),
1876                ..settings
1877            },
1878            viewer.theme(),
1879            viewer.highlighter(),
1880        ))
1881        .on_link_click(on_link_click)
1882        .size(size)
1883        .line_height(settings.line_height)
1884        .selectable(true),
1885    )
1886}
1887
1888/// Displays a paragraph using the default look.
1889pub fn paragraph<'a, Message, Theme, Renderer>(
1890    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1891    settings: Settings,
1892    text: &Text,
1893    on_link_click: impl Fn(Uri) -> Message + 'a,
1894) -> impl Widget<Message, Theme, Renderer> + 'a
1895where
1896    Message: 'a,
1897    Theme: Catalog + 'a,
1898    Renderer: core::text::Renderer + 'a,
1899{
1900    rich_text(text.spans(settings, viewer.theme(), viewer.highlighter()))
1901        .size(settings.text_size)
1902        .line_height(settings.line_height)
1903        .on_link_click(on_link_click)
1904        .selectable(true)
1905}
1906
1907/// Displays an unordered list using the default look and
1908/// calling the [`Viewer`] for each bullet point item.
1909pub fn unordered_list<'a, Message, Theme, Renderer>(
1910    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1911    settings: Settings,
1912    bullets: &'a [Bullet],
1913) -> impl Widget<Message, Theme, Renderer> + 'a
1914where
1915    Message: 'a,
1916    Theme: Catalog + 'a,
1917    Renderer: core::text::Renderer + 'a,
1918{
1919    column(bullets.iter().map(|bullet| {
1920        row![
1921            match bullet {
1922                Bullet::Point { .. } => {
1923                    text("•")
1924                        .size(settings.text_size)
1925                        .line_height(settings.line_height)
1926                        .boxed()
1927                }
1928                Bullet::Task { done, .. } => {
1929                    container(checkbox(*done).size(settings.text_size))
1930                        .center_y(settings.line_height.to_absolute(settings.text_size))
1931                        .boxed()
1932                }
1933            },
1934            items(
1935                viewer,
1936                Settings {
1937                    spacing: settings.spacing / 2.0,
1938                    ..settings
1939                },
1940                bullet.items(),
1941            )
1942        ]
1943        .spacing(settings.text_size / 2.0)
1944    }))
1945    .spacing(settings.spacing / 2.0)
1946    .padding(padding::left(settings.text_size.0))
1947}
1948
1949/// Displays an ordered list using the default look and
1950/// calling the [`Viewer`] for each numbered item.
1951pub fn ordered_list<'a, Message, Theme, Renderer>(
1952    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1953    settings: Settings,
1954    start: u64,
1955    bullets: &'a [Bullet],
1956) -> impl Widget<Message, Theme, Renderer> + 'a
1957where
1958    Message: 'a,
1959    Theme: Catalog + 'a,
1960    Renderer: core::text::Renderer + 'a,
1961{
1962    let digits = (start + bullets.len() as u64).max(1).ilog10() + 1;
1963
1964    column(bullets.iter().enumerate().map(|(i, bullet)| {
1965        row![
1966            text!("{}.", i as u64 + start)
1967                .size(settings.text_size)
1968                .align_x(alignment::Horizontal::Right)
1969                .width(settings.text_size * ((digits as f32 / 2.0).ceil() + 1.0)),
1970            items(
1971                viewer,
1972                Settings {
1973                    spacing: settings.spacing / 2.0,
1974                    ..settings
1975                },
1976                bullet.items(),
1977            )
1978        ]
1979        .spacing(settings.text_size / 2.0)
1980    }))
1981    .spacing(settings.spacing / 2.0)
1982}
1983
1984/// Displays a code block using the default look.
1985pub fn code_block<'a, Message, Theme, Renderer>(
1986    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
1987    settings: Settings,
1988    lines: &'a [Text],
1989    on_link_click: impl Fn(Uri) -> Message + Clone + 'a,
1990) -> impl Widget<Message, Theme, Renderer> + 'a
1991where
1992    Message: 'a,
1993    Theme: Catalog + 'a,
1994    Renderer: core::text::Renderer + 'a,
1995{
1996    let padding = settings.code_block_size / 0.85 * 0.75;
1997
1998    container(
1999        scrollable(column(lines.iter().map(|line| {
2000            rich_text(line.spans(settings, viewer.theme(), viewer.highlighter()))
2001                .on_link_click(on_link_click.clone())
2002                .font(settings.code_block_font)
2003                .size(settings.code_block_size)
2004                .line_height(settings.line_height)
2005                .selectable(true)
2006        })))
2007        .direction(scrollable::Direction::Horizontal(
2008            scrollable::Scrollbar::default()
2009                .width(padding / 2.0)
2010                .scroller_width(padding / 2.0),
2011        ))
2012        .spacing(padding),
2013    )
2014    .width(Length::Fill)
2015    .padding(padding)
2016    .class(Theme::code_block())
2017}
2018
2019/// Displays a quote using the default look.
2020pub fn quote<'a, Message, Theme, Renderer>(
2021    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
2022    settings: Settings,
2023    contents: &'a [Item],
2024) -> impl Widget<Message, Theme, Renderer> + 'a
2025where
2026    Message: 'a,
2027    Theme: Catalog + 'a,
2028    Renderer: core::text::Renderer + 'a,
2029{
2030    container(
2031        column(
2032            contents
2033                .iter()
2034                .map(|content| item(viewer, settings, content)),
2035        )
2036        .spacing(settings.spacing.0),
2037    )
2038    .width(Length::Fill)
2039    .padding(settings.spacing.0)
2040    .class(Theme::quote())
2041}
2042
2043/// Displays a rule using the default look.
2044pub fn rule<'a, Message, Theme, Renderer>() -> impl Widget<Message, Theme, Renderer> + 'a
2045where
2046    Message: 'a,
2047    Theme: Catalog + 'a,
2048    Renderer: core::text::Renderer + 'a,
2049{
2050    rule::horizontal(2)
2051}
2052
2053/// Displays a table using the default look.
2054pub fn table<'a, Message, Theme, Renderer>(
2055    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
2056    settings: Settings,
2057    columns: &'a [Column],
2058    rows: &'a [Row],
2059) -> impl Widget<Message, Theme, Renderer> + 'a
2060where
2061    Message: 'a,
2062    Theme: Catalog + 'a,
2063    Renderer: core::text::Renderer + 'a,
2064{
2065    use crate::table;
2066
2067    let table = table(
2068        columns.iter().enumerate().map(move |(i, column)| {
2069            table::column(items(viewer, settings, &column.header), move |row: &Row| {
2070                if let Some(cells) = row.cells.get(i) {
2071                    items(viewer, settings, cells)
2072                } else {
2073                    items(viewer, settings, &[])
2074                }
2075            })
2076            .width(Length::Fit.max(300))
2077            .align_x(match column.alignment {
2078                pulldown_cmark::Alignment::None | pulldown_cmark::Alignment::Left => {
2079                    alignment::Horizontal::Left
2080                }
2081                pulldown_cmark::Alignment::Center => alignment::Horizontal::Center,
2082                pulldown_cmark::Alignment::Right => alignment::Horizontal::Right,
2083            })
2084        }),
2085        rows,
2086    )
2087    .padding_x(settings.spacing.0)
2088    .padding_y(settings.spacing.0 / 2.0)
2089    .separator_x(0);
2090
2091    center_x(
2092        scrollable(table)
2093            .direction(scrollable::Direction::Horizontal(
2094                scrollable::Scrollbar::default(),
2095            ))
2096            .spacing(settings.spacing.0 / 2.0),
2097    )
2098}
2099
2100/// Displays a column of items with the default look.
2101pub fn items<'a, Message, Theme, Renderer>(
2102    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
2103    settings: Settings,
2104    items: &'a [Item],
2105) -> impl Widget<Message, Theme, Renderer> + 'a
2106where
2107    Message: 'a,
2108    Theme: Catalog + 'a,
2109    Renderer: core::text::Renderer + 'a,
2110{
2111    column(sections(items).map(|(heading, contents)| {
2112        let contents = column(
2113            contents
2114                .iter()
2115                .map(|content| item(viewer, settings, content)),
2116        )
2117        .spacing(settings.spacing);
2118
2119        if let Some(heading) = heading {
2120            column![item(viewer, settings, heading), contents].spacing(settings.spacing / 2.0)
2121        } else {
2122            column![contents]
2123        }
2124    }))
2125    .spacing(settings.spacing * 1.5)
2126}
2127
2128/// A view strategy to display a Markdown [`Item`].
2129///
2130/// A [`Viewer`] is in charge of turning each [`Item`] into a widget. 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    ) -> impl Widget<Message, Theme, Renderer> + 'a {
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    }
2168
2169    /// Displays a heading.
2170    ///
2171    /// By default, it calls [`heading`].
2172    fn heading(
2173        &self,
2174        settings: Settings,
2175        level: &'a HeadingLevel,
2176        text: &'a Text,
2177    ) -> impl Widget<Message, Theme, Renderer> + 'a {
2178        heading(self, settings, level, text, Self::on_link_click)
2179    }
2180
2181    /// Displays a paragraph.
2182    ///
2183    /// By default, it calls [`paragraph`].
2184    fn paragraph(
2185        &self,
2186        settings: Settings,
2187        text: &Text,
2188    ) -> impl Widget<Message, Theme, Renderer> + 'a {
2189        paragraph(self, settings, text, Self::on_link_click)
2190    }
2191
2192    /// Displays a code block.
2193    ///
2194    /// By default, it calls [`code_block`].
2195    fn code_block(
2196        &self,
2197        settings: Settings,
2198        language: Option<&'a str>,
2199        code: &'a str,
2200        lines: &'a [Text],
2201    ) -> impl Widget<Message, Theme, Renderer> + 'a {
2202        let _language = language;
2203        let _code = code;
2204
2205        code_block(self, settings, lines, Self::on_link_click)
2206    }
2207
2208    /// Displays an unordered list.
2209    ///
2210    /// By default, it calls [`unordered_list`].
2211    fn unordered_list(
2212        &self,
2213        settings: Settings,
2214        bullets: &'a [Bullet],
2215    ) -> impl Widget<Message, Theme, Renderer> + 'a {
2216        unordered_list(self, settings, bullets)
2217    }
2218
2219    /// Displays an ordered list.
2220    ///
2221    /// By default, it calls [`ordered_list`].
2222    fn ordered_list(
2223        &self,
2224        settings: Settings,
2225        start: u64,
2226        bullets: &'a [Bullet],
2227    ) -> impl Widget<Message, Theme, Renderer> + 'a {
2228        ordered_list(self, settings, start, bullets)
2229    }
2230
2231    /// Displays a quote.
2232    ///
2233    /// By default, it calls [`quote`].
2234    fn quote(
2235        &self,
2236        settings: Settings,
2237        contents: &'a [Item],
2238    ) -> impl Widget<Message, Theme, Renderer> + 'a {
2239        quote(self, settings, contents)
2240    }
2241
2242    /// Displays a rule.
2243    ///
2244    /// By default, it calls [`rule`](self::rule()).
2245    fn rule(&self) -> impl Widget<Message, Theme, Renderer> + 'a {
2246        rule()
2247    }
2248
2249    /// Displays a table.
2250    ///
2251    /// By default, it calls [`table`].
2252    fn table(
2253        &self,
2254        settings: Settings,
2255        columns: &'a [Column],
2256        rows: &'a [Row],
2257    ) -> impl Widget<Message, Theme, Renderer> + 'a {
2258        table(self, settings, columns, rows)
2259    }
2260}
2261
2262/// The default [`Viewer`].
2263pub struct DefaultViewer<'a, Theme> {
2264    theme: Theme,
2265    highlighter: Option<Box<dyn text::Highlighter<Code, Theme> + 'a>>,
2266}
2267
2268impl<'a, Theme> DefaultViewer<'a, Theme> {
2269    /// Creates a new [`DefaultViewer`] with the given [`Theme`].
2270    pub fn new(theme: Theme) -> Self {
2271        Self {
2272            theme,
2273            highlighter: None,
2274        }
2275    }
2276
2277    /// Sets a custom [`text::Highlighter`] for the [`DefaultViewer`].
2278    pub fn highlighter(mut self, highlighter: impl text::Highlighter<Code, Theme> + 'a) -> Self {
2279        self.highlighter = Some(Box::new(highlighter));
2280        self
2281    }
2282}
2283
2284impl<'a, Theme, Renderer> Viewer<'a, Uri, Theme, Renderer> for DefaultViewer<'a, Theme>
2285where
2286    Theme: Catalog + 'a,
2287    Renderer: core::text::Renderer + 'a,
2288{
2289    fn theme(&self) -> &Theme {
2290        &self.theme
2291    }
2292
2293    fn highlighter(&self) -> &dyn text::Highlighter<Code, Theme> {
2294        self.highlighter
2295            .as_deref()
2296            .unwrap_or_else(|| self.theme.highlighter())
2297    }
2298
2299    fn on_link_click(url: Uri) -> Uri {
2300        url
2301    }
2302}
2303
2304/// The theme catalog of Markdown items.
2305pub trait Catalog:
2306    container::Catalog
2307    + scrollable::Catalog
2308    + text::Catalog
2309    + crate::rule::Catalog
2310    + checkbox::Catalog
2311    + crate::table::Catalog
2312    + Clone
2313    + PartialEq
2314{
2315    /// The unique identifier of the [`Catalog`].
2316    ///
2317    /// This will be used to invalidate span styling when a theme changes.
2318    fn id(&self) -> &str;
2319
2320    /// The [`Color`] of some link.
2321    fn link_color(&self) -> Color;
2322
2323    /// The [`InlineCode`] style of some inline code.
2324    fn code(&self) -> InlineCode;
2325
2326    /// The styling class of a code block.
2327    fn code_block<'a>() -> <Self as container::Catalog>::Class<'a>;
2328
2329    /// The styling class of a quote.
2330    fn quote<'a>() -> <Self as container::Catalog>::Class<'a>;
2331
2332    /// The default [`text::Highlighter`] to use to highlight code.
2333    fn highlighter(&self) -> &dyn text::Highlighter<Code, Self>;
2334}
2335
2336/// The style of some inline code.
2337#[derive(Debug, Clone, Copy, PartialEq)]
2338pub struct InlineCode {
2339    /// The [`Padding`] to apply around the code.
2340    pub padding: Padding,
2341    /// The [`Highlight`] of the code.
2342    pub highlight: Highlight,
2343    /// The [`Color`] of the code.
2344    pub color: Color,
2345}
2346
2347impl Catalog for Theme {
2348    fn id(&self) -> &str {
2349        theme::Base::name(self)
2350    }
2351
2352    fn link_color(&self) -> Color {
2353        self.seed().primary
2354    }
2355
2356    fn code(&self) -> InlineCode {
2357        let palette = self.palette();
2358
2359        InlineCode {
2360            padding: padding::horizontal(4).vertical(1),
2361            highlight: Highlight {
2362                background: palette.background.weaker.color.into(),
2363                border: border::rounded(4),
2364            },
2365            color: palette.background.weaker.text,
2366        }
2367    }
2368
2369    fn code_block<'a>() -> <Self as container::Catalog>::Class<'a> {
2370        Box::new(|theme| container::dark(theme).border(border::rounded(5)))
2371    }
2372
2373    fn quote<'a>() -> <Self as container::Catalog>::Class<'a> {
2374        Box::new(|theme| {
2375            let palette = theme.palette();
2376
2377            container::Style {
2378                text_color: Some(palette.background.weakest.text),
2379                background: Some(palette.background.weakest.color.into()),
2380                border: border::rounded(5),
2381                ..container::Style::default()
2382            }
2383        })
2384    }
2385
2386    fn highlighter(&self) -> &dyn text::Highlighter<Code, Self> {
2387        &Code::highlight
2388    }
2389}
2390
2391#[cfg(feature = "highlighter")]
2392mod code {
2393    use super::Span;
2394
2395    #[derive(Debug)]
2396    pub struct Parser {
2397        lines: Vec<(String, Vec<Span>)>,
2398        language: String,
2399        stream: iced_highlighter::Stream,
2400        current: usize,
2401    }
2402
2403    impl Parser {
2404        pub fn new(language: &str) -> Self {
2405            Self {
2406                lines: Vec::new(),
2407                stream: iced_highlighter::Stream::new(&iced_highlighter::Settings {
2408                    token: language.to_owned(),
2409                }),
2410                language: language.to_owned(),
2411                current: 0,
2412            }
2413        }
2414
2415        pub fn language(&self) -> &str {
2416            &self.language
2417        }
2418
2419        pub fn prepare(&mut self) {
2420            self.current = 0;
2421        }
2422
2423        pub fn parse_line(&mut self, text: &str) -> &[Span] {
2424            match self.lines.get(self.current) {
2425                Some(line) if line.0 == text => {}
2426                _ => {
2427                    if self.current + 1 < self.lines.len() {
2428                        log::debug!("Resetting highlighter...");
2429                        self.stream.reset();
2430                        self.lines.truncate(self.current);
2431
2432                        for line in &self.lines {
2433                            log::debug!("Refeeding {n} lines", n = self.lines.len());
2434
2435                            let _ = self.stream.parse_line(&line.0);
2436                            self.stream.commit();
2437                        }
2438                    }
2439
2440                    log::trace!("Parsing: {text}", text = text.trim_end());
2441
2442                    if self.current == self.lines.len() {
2443                        self.stream.commit();
2444                    }
2445
2446                    let mut spans = Vec::new();
2447
2448                    for (range, code) in self.stream.parse_line(text) {
2449                        spans.push(Span::Code {
2450                            text: text[range].to_owned(),
2451                            code,
2452                        });
2453                    }
2454
2455                    if self.current == self.lines.len() {
2456                        self.lines.push((text.to_owned(), spans));
2457                    } else {
2458                        self.lines[self.current] = (text.to_owned(), spans);
2459                    }
2460                }
2461            }
2462
2463            self.current += 1;
2464
2465            &self
2466                .lines
2467                .get(self.current - 1)
2468                .expect("Line must be parsed")
2469                .1
2470        }
2471    }
2472}