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