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