Skip to main content

zng_wgt_markdown/
lib.rs

1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3//!
4//! Markdown widget, properties and nodes.
5//!
6//! # Crate
7//!
8#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11
12zng_wgt::enable_widget_macros!();
13
14use std::mem;
15
16pub use pulldown_cmark::HeadingLevel;
17
18use zng_wgt::prelude::*;
19use zng_wgt_input::{CursorIcon, cursor};
20
21#[doc(hidden)]
22pub use zng_wgt_text::__formatx;
23
24use zng_wgt_text as text;
25
26mod resolvers;
27mod view_fn;
28
29pub use resolvers::*;
30pub use view_fn::*;
31
32/// Render markdown styled text.
33#[widget($crate::Markdown {
34    ($txt:literal) => {
35        txt = $crate::__formatx!($txt);
36    };
37    ($txt:expr) => {
38        txt = $txt;
39    };
40    ($txt:tt, $($format:tt)*) => {
41        txt = $crate::__formatx!($txt, $($format)*);
42    };
43})]
44#[rustfmt::skip]
45pub struct Markdown(
46    text::FontMix<
47    text::TextSpacingMix<
48    text::ParagraphMix<
49    text::LangMix<
50    WidgetBase
51    >>>>
52);
53impl Markdown {
54    fn widget_intrinsic(&mut self) {
55        widget_set! {
56            self;
57            on_link = hn!(|args| {
58                try_default_link_action(args);
59            });
60            zng_wgt_text::rich_text = true;
61
62            when #txt_selectable {
63                cursor = CursorIcon::Text;
64                zng_wgt_menu::context::context_menu_fn = WidgetFn::new(default_context_menu);
65            }
66        };
67
68        self.widget_builder().push_build_action(|wgt| {
69            let md = wgt.capture_var_or_default(property_id!(text::txt));
70            let child = markdown_node(md);
71            wgt.set_child(child);
72        });
73    }
74
75    widget_impl! {
76        /// Markdown text.
77        pub text::txt(txt: impl IntoVar<Txt>);
78
79        /// Enable text selection, copy.
80        ///
81        /// Note that the copy is only in plain text, without any style.
82        pub zng_wgt_text::txt_selectable(enabled: impl IntoVar<bool>);
83    }
84}
85
86/// Context menu set by the [`Markdown!`] when [`txt_selectable`] is `true`.
87///
88/// [`Markdown!`]: struct@Markdown
89/// [`txt_selectable`]: fn@zng_wgt_text::txt_selectable
90pub fn default_context_menu(args: zng_wgt_menu::context::ContextMenuArgs) -> UiNode {
91    use zng_wgt_button::Button;
92    let id = args.anchor_id;
93    zng_wgt_menu::context::ContextMenu!(ui_vec![
94        Button!(zng_ext_clipboard::COPY_CMD.scoped(id)),
95        Button!(zng_wgt_text::cmd::SELECT_ALL_CMD.scoped(id)),
96    ])
97}
98
99/// Implements the markdown parsing and view generation, configured by contextual properties.
100pub fn markdown_node(md: impl IntoVar<Txt>) -> UiNode {
101    let md = md.into_var();
102    match_node(UiNode::nil(), move |c, op| match op {
103        UiNodeOp::Init => {
104            WIDGET
105                .sub_var(&md)
106                .sub_var(&TEXT_FN_VAR)
107                .sub_var(&LINK_FN_VAR)
108                .sub_var(&CODE_INLINE_FN_VAR)
109                .sub_var(&CODE_BLOCK_FN_VAR)
110                .sub_var(&PARAGRAPH_FN_VAR)
111                .sub_var(&HEADING_FN_VAR)
112                .sub_var(&LIST_FN_VAR)
113                .sub_var(&LIST_ITEM_BULLET_FN_VAR)
114                .sub_var(&LIST_ITEM_FN_VAR)
115                .sub_var(&IMAGE_FN_VAR)
116                .sub_var(&RULE_FN_VAR)
117                .sub_var(&BLOCK_QUOTE_FN_VAR)
118                .sub_var(&TABLE_FN_VAR)
119                .sub_var(&TABLE_CELL_FN_VAR)
120                .sub_var(&PANEL_FN_VAR)
121                .sub_var(&IMAGE_RESOLVER_VAR)
122                .sub_var(&LINK_RESOLVER_VAR);
123
124            *c.node() = md.with(|md| markdown_view_fn(md.as_str()));
125        }
126        UiNodeOp::Deinit => {
127            c.deinit();
128            *c.node() = UiNode::nil();
129        }
130        UiNodeOp::Info { info } => {
131            info.flag_meta(*MARKDOWN_INFO_ID);
132        }
133        UiNodeOp::Update { .. } => {
134            use resolvers::*;
135            use view_fn::*;
136
137            if md.is_new()
138                || TEXT_FN_VAR.is_new()
139                || LINK_FN_VAR.is_new()
140                || CODE_INLINE_FN_VAR.is_new()
141                || CODE_BLOCK_FN_VAR.is_new()
142                || PARAGRAPH_FN_VAR.is_new()
143                || HEADING_FN_VAR.is_new()
144                || LIST_FN_VAR.is_new()
145                || LIST_ITEM_BULLET_FN_VAR.is_new()
146                || LIST_ITEM_FN_VAR.is_new()
147                || IMAGE_FN_VAR.is_new()
148                || RULE_FN_VAR.is_new()
149                || BLOCK_QUOTE_FN_VAR.is_new()
150                || TABLE_FN_VAR.is_new()
151                || TABLE_CELL_FN_VAR.is_new()
152                || PANEL_FN_VAR.is_new()
153                || IMAGE_RESOLVER_VAR.is_new()
154                || LINK_RESOLVER_VAR.is_new()
155            {
156                c.delegated();
157                c.node().deinit();
158                *c.node() = md.with(|md| markdown_view_fn(md.as_str()));
159                c.node().init();
160                WIDGET.update_info().layout().render();
161            }
162        }
163        _ => {}
164    })
165}
166
167/// Parse markdown, with pre-processing, merge texts, collapse white spaces across inline items
168fn markdown_parser<'a>(md: &'a str, mut next_event: impl FnMut(pulldown_cmark::Event<'a>)) {
169    use pulldown_cmark::*;
170
171    let parse_options = Options::ENABLE_TABLES
172        | Options::ENABLE_FOOTNOTES
173        | Options::ENABLE_STRIKETHROUGH
174        | Options::ENABLE_TASKLISTS
175        | Options::ENABLE_SMART_PUNCTUATION
176        | Options::ENABLE_DEFINITION_LIST
177        | Options::ENABLE_SUBSCRIPT
178        | Options::ENABLE_SUPERSCRIPT;
179
180    let mut broken_link_handler = |b: BrokenLink<'a>| Some((b.reference, "".into()));
181    let parser = Parser::new_with_broken_link_callback(md, parse_options, Some(&mut broken_link_handler));
182
183    enum Str<'a> {
184        Md(CowStr<'a>),
185        Buf(String),
186    }
187    impl<'a> Str<'a> {
188        fn buf(&mut self) -> &mut String {
189            if let Str::Md(s) = self {
190                *self = Str::Buf(mem::replace(s, CowStr::Borrowed("")).into_string());
191            }
192            match self {
193                Str::Buf(b) => b,
194                _ => unreachable!(),
195            }
196        }
197
198        fn md(self) -> CowStr<'a> {
199            match self {
200                Str::Md(cow_str) => cow_str,
201                Str::Buf(b) => b.into(),
202            }
203        }
204    }
205    let mut pending_txt: Option<Str<'a>> = None;
206    let mut trim_start = false;
207
208    for event in parser {
209        // resolve breaks
210        let event = match event {
211            Event::SoftBreak => Event::Text(CowStr::Borrowed(" ")),
212            Event::HardBreak => Event::Text(CowStr::Borrowed("\n")),
213            ev => ev,
214        };
215        match event {
216            // merge texts
217            Event::Text(txt) => {
218                if let Some(p) = &mut pending_txt {
219                    p.buf().push_str(&txt);
220                } else if mem::take(&mut trim_start) && txt.starts_with(' ') {
221                    // merge spaces across inline items
222                    pending_txt = Some(match txt {
223                        CowStr::Borrowed(s) => Str::Md(CowStr::Borrowed(s.trim_start())),
224                        CowStr::Boxed(s) => Str::Buf(s.trim_start().to_owned()),
225                        CowStr::Inlined(s) => Str::Buf(s.trim_start().to_owned()),
226                    });
227                } else {
228                    pending_txt = Some(Str::Md(txt));
229                }
230            }
231            // items that don't merge spaces with siblings
232            e @ Event::End(_)
233            | e @ Event::Start(
234                Tag::Paragraph
235                | Tag::Heading { .. }
236                | Tag::Image { .. }
237                | Tag::Item
238                | Tag::List(_)
239                | Tag::CodeBlock(_)
240                | Tag::Table(_)
241                | Tag::TableHead
242                | Tag::TableRow
243                | Tag::TableCell
244                | Tag::BlockQuote(_)
245                | Tag::FootnoteDefinition(_)
246                | Tag::DefinitionList
247                | Tag::DefinitionListTitle
248                | Tag::DefinitionListDefinition
249                | Tag::HtmlBlock
250                | Tag::MetadataBlock(_),
251            )
252            | e @ Event::Code(_)
253            | e @ Event::Rule
254            | e @ Event::TaskListMarker(_)
255            | e @ Event::InlineMath(_)
256            | e @ Event::DisplayMath(_)
257            | e @ Event::Html(_)
258            | e @ Event::InlineHtml(_) => {
259                if let Some(txt) = pending_txt.take() {
260                    next_event(Event::Text(txt.md()));
261                }
262                next_event(e)
263            }
264            // inline items that merge spaces with siblings
265            Event::FootnoteReference(s) => {
266                if let Some(txt) = pending_txt.take() {
267                    let txt = txt.md();
268                    trim_start = txt.ends_with(' ');
269                    next_event(Event::Text(txt));
270                }
271                if mem::take(&mut trim_start) && s.starts_with(' ') {
272                    let s = match s {
273                        CowStr::Borrowed(s) => CowStr::Borrowed(s.trim_start()),
274                        CowStr::Boxed(s) => CowStr::Boxed(s.trim_start().to_owned().into()),
275                        CowStr::Inlined(s) => CowStr::Boxed(s.trim_start().to_owned().into()),
276                    };
277                    next_event(Event::FootnoteReference(s))
278                } else {
279                    next_event(Event::FootnoteReference(s))
280                }
281            }
282            Event::Start(tag) => match tag {
283                t @ Tag::Emphasis
284                | t @ Tag::Strong
285                | t @ Tag::Strikethrough
286                | t @ Tag::Superscript
287                | t @ Tag::Subscript
288                | t @ Tag::Link { .. } => {
289                    if let Some(txt) = pending_txt.take() {
290                        let txt = txt.md();
291                        trim_start = txt.ends_with(' ');
292                        next_event(Event::Text(txt));
293                    }
294                    next_event(Event::Start(t))
295                }
296                t => tracing::error!("unexpected start tag {t:?}"),
297            },
298            // handled early
299            Event::HardBreak | Event::SoftBreak => unreachable!(),
300        }
301        if let Some(txt) = pending_txt.take() {
302            next_event(Event::Text(txt.md()));
303        }
304    }
305}
306
307fn markdown_view_fn(md: &str) -> UiNode {
308    use pulldown_cmark::*;
309    use resolvers::*;
310    use view_fn::*;
311
312    let text_view = TEXT_FN_VAR.get();
313    let link_view = LINK_FN_VAR.get();
314    let code_inline_view = CODE_INLINE_FN_VAR.get();
315    let code_block_view = CODE_BLOCK_FN_VAR.get();
316    let heading_view = HEADING_FN_VAR.get();
317    let paragraph_view = PARAGRAPH_FN_VAR.get();
318    let list_view = LIST_FN_VAR.get();
319    let definition_list_view = DEF_LIST_FN_VAR.get();
320    let list_item_bullet_view = LIST_ITEM_BULLET_FN_VAR.get();
321    let list_item_view = LIST_ITEM_FN_VAR.get();
322    let image_view = IMAGE_FN_VAR.get();
323    let rule_view = RULE_FN_VAR.get();
324    let block_quote_view = BLOCK_QUOTE_FN_VAR.get();
325    let footnote_ref_view = FOOTNOTE_REF_FN_VAR.get();
326    let footnote_def_view = FOOTNOTE_DEF_FN_VAR.get();
327    let def_list_item_title_view = DEF_LIST_ITEM_TITLE_FN_VAR.get();
328    let def_list_item_definition_view = DEF_LIST_ITEM_DEFINITION_FN_VAR.get();
329    let table_view = TABLE_FN_VAR.get();
330    let table_cell_view = TABLE_CELL_FN_VAR.get();
331
332    let image_resolver = IMAGE_RESOLVER_VAR.get();
333    let link_resolver = LINK_RESOLVER_VAR.get();
334
335    #[derive(Default)]
336    struct StyleBuilder {
337        strong: usize,
338        emphasis: usize,
339        strikethrough: usize,
340        superscript: usize,
341        subscript: usize,
342    }
343    impl StyleBuilder {
344        fn build(&self) -> MarkdownStyle {
345            MarkdownStyle {
346                strong: self.strong > 0,
347                emphasis: self.emphasis > 0,
348                strikethrough: self.strikethrough > 0,
349                subscript: self.subscript > self.superscript,
350                superscript: self.superscript > self.subscript,
351            }
352        }
353    }
354    struct ListInfo {
355        block_start: usize,
356        inline_start: usize,
357        first_num: Option<u64>,
358        item_num: Option<u64>,
359        item_checked: Option<bool>,
360    }
361    let mut blocks = vec![];
362    let mut inlines = vec![];
363    let mut txt_style = StyleBuilder::default();
364    let mut link = None;
365    let mut list_info = vec![];
366    let mut list_items = vec![];
367    let mut block_quote_start = vec![];
368    let mut code_block = None;
369    let mut html_block = None;
370    let mut image = None;
371    let mut heading_anchor_txt = None;
372    let mut footnote_def = None;
373    let mut table_cells = vec![];
374    let mut table_cols = vec![];
375    let mut table_col = 0;
376    let mut table_head = false;
377
378    markdown_parser(md, |event| match event {
379        Event::Start(tag) => match tag {
380            Tag::Paragraph => txt_style = StyleBuilder::default(),
381            Tag::Heading { .. } => {
382                txt_style = StyleBuilder::default();
383                heading_anchor_txt = Some(String::new());
384            }
385            Tag::BlockQuote(_) => {
386                txt_style = StyleBuilder::default();
387                block_quote_start.push(blocks.len());
388            }
389            Tag::CodeBlock(kind) => {
390                txt_style = StyleBuilder::default();
391                code_block = Some((String::new(), kind));
392            }
393            Tag::HtmlBlock => {
394                txt_style = StyleBuilder::default();
395                html_block = Some(String::new());
396            }
397            Tag::List(n) => {
398                txt_style = StyleBuilder::default();
399                list_info.push(ListInfo {
400                    block_start: blocks.len(),
401                    inline_start: inlines.len(),
402                    first_num: n,
403                    item_num: n,
404                    item_checked: None,
405                });
406            }
407            Tag::DefinitionList => {
408                txt_style = StyleBuilder::default();
409                list_info.push(ListInfo {
410                    block_start: blocks.len(),
411                    inline_start: inlines.len(),
412                    first_num: None,
413                    item_num: None,
414                    item_checked: None,
415                });
416            }
417            Tag::Item | Tag::DefinitionListTitle | Tag::DefinitionListDefinition => {
418                txt_style = StyleBuilder::default();
419                if let Some(list) = list_info.last_mut() {
420                    list.block_start = blocks.len();
421                }
422            }
423            Tag::FootnoteDefinition(label) => {
424                txt_style = StyleBuilder::default();
425                footnote_def = Some((blocks.len(), label));
426            }
427            Tag::Table(columns) => {
428                txt_style = StyleBuilder::default();
429                table_cols = columns
430                    .into_iter()
431                    .map(|c| match c {
432                        Alignment::None => Align::START,
433                        Alignment::Left => Align::LEFT,
434                        Alignment::Center => Align::CENTER,
435                        Alignment::Right => Align::RIGHT,
436                    })
437                    .collect()
438            }
439            Tag::TableHead => {
440                txt_style = StyleBuilder::default();
441                table_head = true;
442                table_col = 0;
443            }
444            Tag::TableRow => {
445                txt_style = StyleBuilder::default();
446                table_col = 0;
447            }
448            Tag::TableCell => {
449                txt_style = StyleBuilder::default();
450            }
451            Tag::Emphasis => {
452                txt_style.emphasis += 1;
453            }
454            Tag::Strong => {
455                txt_style.strong += 1;
456            }
457            Tag::Strikethrough => {
458                txt_style.strong += 1;
459            }
460            Tag::Superscript => {
461                txt_style.superscript += 1;
462            }
463            Tag::Subscript => {
464                txt_style.subscript += 1;
465            }
466            Tag::Link {
467                link_type,
468                dest_url,
469                title,
470                id,
471            } => {
472                link = Some((inlines.len(), link_type, dest_url, title, id));
473            }
474            Tag::Image { dest_url, title, .. } => {
475                image = Some((String::new(), dest_url, title));
476            }
477            Tag::MetadataBlock(_) => unreachable!(), // not enabled
478        },
479        Event::End(tag_end) => match tag_end {
480            TagEnd::Paragraph => {
481                if !inlines.is_empty() {
482                    blocks.push(paragraph_view(ParagraphFnArgs {
483                        index: blocks.len() as u32,
484                        items: mem::take(&mut inlines).into(),
485                    }));
486                }
487            }
488            TagEnd::Heading(level) => {
489                if !inlines.is_empty() {
490                    blocks.push(heading_view(HeadingFnArgs {
491                        level,
492                        anchor: heading_anchor(heading_anchor_txt.take().unwrap_or_default().as_str()),
493                        items: mem::take(&mut inlines).into(),
494                    }));
495                }
496            }
497            TagEnd::BlockQuote(_) => {
498                if let Some(start) = block_quote_start.pop() {
499                    let items: UiVec = blocks.drain(start..).collect();
500                    if !items.is_empty() {
501                        blocks.push(block_quote_view(BlockQuoteFnArgs {
502                            level: block_quote_start.len() as u32,
503                            items,
504                        }));
505                    }
506                }
507            }
508            TagEnd::CodeBlock => {
509                let (mut txt, kind) = code_block.take().unwrap();
510                if txt.ends_with('\n') {
511                    txt.pop();
512                }
513                blocks.push(code_block_view(CodeBlockFnArgs {
514                    lang: match kind {
515                        CodeBlockKind::Indented => Txt::from_str(""),
516                        CodeBlockKind::Fenced(l) => l.to_txt(),
517                    },
518                    txt: txt.into(),
519                }))
520            }
521            TagEnd::HtmlBlock => {
522                let _html = html_block.take().unwrap();
523            }
524            TagEnd::List(_) => {
525                if let Some(list) = list_info.pop() {
526                    blocks.push(list_view(ListFnArgs {
527                        depth: list_info.len() as u32,
528                        first_num: list.first_num,
529                        items: mem::take(&mut list_items).into(),
530                    }));
531                }
532            }
533            TagEnd::DefinitionList => {
534                if list_info.pop().is_some() {
535                    blocks.push(definition_list_view(DefListArgs {
536                        items: mem::take(&mut list_items).into(),
537                    }));
538                }
539            }
540            TagEnd::Item => {
541                let depth = list_info.len().saturating_sub(1);
542                if let Some(list) = list_info.last_mut() {
543                    let num = match &mut list.item_num {
544                        Some(n) => {
545                            let r = *n;
546                            *n += 1;
547                            Some(r)
548                        }
549                        None => None,
550                    };
551
552                    let bullet_args = ListItemBulletFnArgs {
553                        depth: depth as u32,
554                        num,
555                        checked: list.item_checked.take(),
556                    };
557                    list_items.push(list_item_bullet_view(bullet_args));
558                    list_items.push(list_item_view(ListItemFnArgs {
559                        bullet: bullet_args,
560                        items: inlines.drain(list.inline_start..).collect(),
561                        blocks: blocks.drain(list.block_start..).collect(),
562                    }));
563                }
564            }
565            TagEnd::DefinitionListTitle => {
566                if let Some(list) = list_info.last_mut() {
567                    list_items.push(def_list_item_title_view(DefListItemTitleArgs {
568                        items: inlines.drain(list.inline_start..).collect(),
569                    }));
570                }
571            }
572            TagEnd::DefinitionListDefinition => {
573                if let Some(list) = list_info.last_mut() {
574                    list_items.push(def_list_item_definition_view(DefListItemDefinitionArgs {
575                        items: inlines.drain(list.inline_start..).collect(),
576                    }));
577                }
578            }
579            TagEnd::FootnoteDefinition => {
580                if let Some((i, label)) = footnote_def.take() {
581                    let label = html_escape::decode_html_entities(label.as_ref());
582                    let items = blocks.drain(i..).collect();
583                    blocks.push(footnote_def_view(FootnoteDefFnArgs {
584                        label: label.to_txt(),
585                        items,
586                    }));
587                }
588            }
589            TagEnd::Table => {
590                if !table_cells.is_empty() {
591                    blocks.push(table_view(TableFnArgs {
592                        columns: mem::take(&mut table_cols),
593                        cells: mem::take(&mut table_cells).into(),
594                    }));
595                }
596            }
597            TagEnd::TableHead => {
598                table_head = false;
599            }
600            TagEnd::TableRow => {}
601            TagEnd::TableCell => {
602                table_cells.push(table_cell_view(TableCellFnArgs {
603                    is_heading: table_head,
604                    col_align: table_cols[table_col],
605                    items: mem::take(&mut inlines).into(),
606                }));
607                table_col += 1;
608            }
609            TagEnd::Emphasis => {
610                txt_style.emphasis -= 1;
611            }
612            TagEnd::Strong => {
613                txt_style.strong -= 1;
614            }
615            TagEnd::Strikethrough => {
616                txt_style.strikethrough -= 1;
617            }
618            TagEnd::Superscript => {
619                txt_style.superscript -= 1;
620            }
621            TagEnd::Subscript => txt_style.subscript -= 1,
622            TagEnd::Link => {
623                let (inlines_start, kind, url, title, _id) = link.take().unwrap();
624                let title = html_escape::decode_html_entities(title.as_ref());
625                let url = link_resolver.resolve(url.as_ref());
626                match kind {
627                    LinkType::Autolink | LinkType::Email => {
628                        let url = html_escape::decode_html_entities(&url);
629                        if let Some(txt) = text_view.call_checked(TextFnArgs {
630                            txt: url.to_txt(),
631                            style: txt_style.build(),
632                        }) {
633                            inlines.push(txt);
634                        }
635                    }
636                    LinkType::Inline => {}
637                    LinkType::Reference => {}
638                    LinkType::ReferenceUnknown => {}
639                    LinkType::Collapsed => {}
640                    LinkType::CollapsedUnknown => {}
641                    LinkType::Shortcut => {}
642                    LinkType::ShortcutUnknown => {}
643                    LinkType::WikiLink { .. } => {}
644                }
645                if !inlines.is_empty() {
646                    let items = inlines.drain(inlines_start..).collect();
647                    if let Some(lnk) = link_view.call_checked(LinkFnArgs {
648                        url,
649                        title: title.to_txt(),
650                        items,
651                    }) {
652                        inlines.push(lnk);
653                    }
654                }
655            }
656            TagEnd::Image => {
657                let (alt_txt, url, title) = image.take().unwrap();
658                let title = html_escape::decode_html_entities(title.as_ref());
659                blocks.push(image_view(ImageFnArgs {
660                    source: image_resolver.resolve(&url),
661                    title: title.to_txt(),
662                    alt_items: mem::take(&mut inlines).into(),
663                    alt_txt: alt_txt.into(),
664                }));
665            }
666            TagEnd::MetadataBlock(_) => unreachable!(),
667        },
668        Event::Text(txt) => {
669            if let Some(html) = &mut html_block {
670                html.push_str(&txt);
671            } else {
672                let txt = html_escape::decode_html_entities(txt.as_ref());
673                if let Some((code, _)) = &mut code_block {
674                    code.push_str(&txt);
675                } else if !txt.is_empty() {
676                    if let Some(anchor_txt) = &mut heading_anchor_txt {
677                        anchor_txt.push_str(&txt);
678                    }
679                    if let Some((alt_txt, _, _)) = &mut image {
680                        alt_txt.push_str(&txt);
681                    }
682                    if let Some(txt) = text_view.call_checked(TextFnArgs {
683                        txt: Txt::from_str(&txt),
684                        style: txt_style.build(),
685                    }) {
686                        inlines.push(txt);
687                    }
688                }
689            }
690        }
691        Event::Code(txt) => {
692            let txt = html_escape::decode_html_entities(txt.as_ref());
693            if let Some(txt) = code_inline_view.call_checked(CodeInlineFnArgs {
694                txt: txt.to_txt(),
695                style: txt_style.build(),
696            }) {
697                inlines.push(txt);
698            }
699        }
700        Event::Html(h) => {
701            if let Some(html) = &mut html_block {
702                html.push_str(&h);
703            }
704        }
705        Event::InlineHtml(tag) => match tag.as_ref() {
706            "<b>" => txt_style.strong += 1,
707            "</b>" => txt_style.strong -= 1,
708            "<em>" => txt_style.emphasis += 1,
709            "</em>" => txt_style.emphasis -= 1,
710            "<s>" => txt_style.strikethrough += 1,
711            "</s>" => txt_style.strikethrough -= 1,
712            _ => {}
713        },
714        Event::FootnoteReference(label) => {
715            let label = html_escape::decode_html_entities(label.as_ref());
716            if let Some(txt) = footnote_ref_view.call_checked(FootnoteRefFnArgs { label: label.to_txt() }) {
717                inlines.push(txt);
718            }
719        }
720        Event::Rule => {
721            blocks.push(rule_view(RuleFnArgs {}));
722        }
723        Event::TaskListMarker(c) => {
724            if let Some(l) = &mut list_info.last_mut() {
725                l.item_checked = Some(c);
726            }
727        }
728
729        Event::InlineMath(_) => {}
730        Event::DisplayMath(_) => {}
731        Event::SoftBreak | Event::HardBreak => unreachable!(),
732    });
733
734    PANEL_FN_VAR.get()(PanelFnArgs { items: blocks.into() })
735}