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
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#[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 pub text::txt(txt: impl IntoVar<Txt>);
78
79 pub zng_wgt_text::txt_selectable(enabled: impl IntoVar<bool>);
83 }
84}
85
86pub 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
99pub 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
167fn 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 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 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 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 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 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 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!(), },
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}