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#![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#[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 pub text::txt(txt: impl IntoVar<Txt>);
80
81 pub zng_wgt_text::txt_selectable(enabled: impl IntoVar<bool>);
85 }
86}
87
88pub 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
101pub fn default_popup_context_capture() -> popup::ContextCapture {
107 popup::ContextCapture::context_vars_except(zng_wgt_text::Text::context_vars_set_except_lang)
108}
109
110pub 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
178fn 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 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 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 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 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 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 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!(), },
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}