1#![allow(dead_code)]
2
3extern crate alloc;
104
105#[cfg_attr(all(), allow(clippy::mem_forget))]
106mod parser_cell {
107 use self_cell::self_cell;
108
109 use super::MarkdownParser;
110
111 self_cell!(
112 pub(super) struct ParserCell {
113 owner: String,
114 #[covariant]
115 dependent: MarkdownParser,
116 }
117 );
118}
119
120mod html;
121
122use alloc::collections::VecDeque;
123use std::io::{Read, Seek};
124
125pub use docspec_core::EventSource;
126use docspec_core::{Event, ImageSource, ListStyleType, Result, TableHeaderScope, TextStyleKind};
127use parser_cell::ParserCell;
128use pulldown_cmark::{CodeBlockKind, CowStr, HeadingLevel, Options, Parser, Tag, TagEnd};
129
130struct MarkdownParser<'a>(Parser<'a>);
131
132#[derive(Clone, Copy, PartialEq, Eq)]
134enum BlockState {
135 AutoParagraph,
137 Explicit,
139 None,
141 PendingExplicit,
143}
144
145#[derive(Clone, Copy, PartialEq, Eq)]
147enum Phase {
148 Finished,
150 NotStarted,
152 Running,
154}
155
156struct ListContext {
158 item_open: bool,
160 ordered: bool,
162 pending_start: Option<u64>,
165}
166
167struct ImageBuffer {
169 alt_buf: String,
171 title: Option<String>,
173 url: String,
175}
176
177enum MarkdownPulldownEvent {
178 BlockHtml(String),
179 Code(String),
180 End(TagEnd),
181 HardBreak,
182 InlineHtml(String),
183 Ignored,
184 Rule,
185 SoftBreak,
186 Start(MarkdownStartTag),
187 Text(String),
188}
189
190enum MarkdownStartTag {
191 BlockQuote,
192 CodeBlock {
193 syntax: Option<String>,
194 },
195 Emphasis,
196 Heading {
197 level: HeadingLevel,
198 },
199 Image {
200 dest_url: String,
201 title: Option<String>,
202 },
203 Item,
204 Link {
205 dest_url: String,
206 title: Option<String>,
207 },
208 HtmlBlock,
209 List(Option<u64>),
210 Paragraph,
211 Strikethrough,
212 Strong,
213 Table,
214 TableCell,
215 TableHead,
216 TableRow,
217}
218
219struct LinkBuffer {
221 href: String,
223 started: bool,
225 title: Option<String>,
227}
228
229pub struct MarkdownReader {
247 block_state: BlockState,
249 cell: ParserCell,
251 code_block_buffer: Option<String>,
253 image: Option<ImageBuffer>,
255 html_block_heading_acc: crate::html::translator::BlockHeadingAccumulator,
257 html_block_inline_stack: docspec_core::StyleStack,
259 in_html_block: bool,
261 in_preformatted: bool,
263 in_table_head: bool,
265 link: Option<LinkBuffer>,
267 list_stack: alloc::vec::Vec<ListContext>,
270 inline_style_stack: docspec_core::StyleStack,
272 phase: Phase,
274 queue: VecDeque<Event>,
276}
277
278impl MarkdownReader {
279 fn close_current_item_if_open(&mut self) {
280 let Some(ctx) = self.list_stack.last() else {
281 return;
282 };
283 if !ctx.item_open {
284 return;
285 }
286
287 let ordered = ctx.ordered;
288 self.flush_html_styles();
289 if ordered {
290 self.queue.push_back(Event::EndOrderedListItem);
291 } else {
292 self.queue.push_back(Event::EndUnorderedListItem);
293 }
294 if let Some(current_ctx) = self.list_stack.last_mut() {
295 current_ctx.item_open = false;
296 }
297 self.block_state = BlockState::None;
298 }
299
300 fn close_style(&mut self, kind: &TextStyleKind) {
301 if self.in_preformatted {
302 return;
303 }
304
305 for event in self.inline_style_stack.close(kind) {
306 self.queue.push_back(event);
307 }
308 }
309
310 fn open_style(&mut self, kind: &TextStyleKind) {
311 if !self.in_preformatted {
312 for event in self.inline_style_stack.open(kind.clone()) {
313 self.queue.push_back(event);
314 }
315 }
316 }
317
318 fn enqueue_text(&mut self, content: String) {
319 for event in self.inline_style_stack.note_text() {
320 self.queue.push_back(event);
321 }
322 let text_event = Event::Text { content };
323 self.queue.push_back(text_event);
324 }
325
326 fn flush_html_styles(&mut self) {
327 for event in self.inline_style_stack.close_all() {
328 self.queue.push_back(event);
329 }
330 }
331
332 fn emit_pending_link_start(&mut self) {
335 self.flush_pending_paragraph_start();
336 if let Some(link) = self.link.as_mut() {
337 if !link.started {
338 self.queue.push_back(Event::StartLink {
339 href: link.href.clone(),
340 id: None,
341 title: link.title.clone(),
342 });
343 link.started = true;
344 }
345 }
346 }
347
348 fn flush_pending_paragraph_start(&mut self) {
351 if self.block_state == BlockState::PendingExplicit {
352 self.queue.push_back(Event::StartParagraph {
353 alignment: None,
354 id: None,
355 });
356 self.block_state = BlockState::Explicit;
357 }
358 }
359
360 fn from_owned_string(source: String) -> Self {
361 let options = Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH;
362 let cell = ParserCell::new(source, |s| MarkdownParser(Parser::new_ext(s, options)));
363 Self {
364 block_state: BlockState::None,
365 cell,
366 code_block_buffer: None,
367 image: None,
368 html_block_heading_acc: crate::html::translator::BlockHeadingAccumulator::default(),
369 html_block_inline_stack: docspec_core::StyleStack::default(),
370 in_html_block: false,
371 in_preformatted: false,
372 in_table_head: false,
373 link: None,
374 list_stack: Vec::new(),
375 inline_style_stack: docspec_core::StyleStack::default(),
376 phase: Phase::NotStarted,
377 queue: VecDeque::new(),
378 }
379 }
380
381 #[inline]
390 pub fn from_reader<R>(mut reader: R) -> Result<Self>
391 where
392 R: Read + Seek + Send + 'static,
393 {
394 let mut source = String::new();
395 reader.read_to_string(&mut source)?;
396 Ok(Self::from_owned_string(source))
397 }
398
399 #[inline]
411 #[must_use]
412 #[expect(
413 clippy::should_implement_trait,
414 reason = "constructor name is required for reader API consistency"
415 )]
416 pub fn from_str(input: &str) -> Self {
417 Self::from_owned_string(input.to_owned())
418 }
419
420 fn handle_code(&mut self, content: String) {
421 if let Some(img) = &mut self.image {
422 img.alt_buf.push_str(&content);
423 } else {
424 self.emit_pending_link_start();
425 if self.block_state == BlockState::None {
426 self.queue.push_back(Event::StartParagraph {
427 alignment: None,
428 id: None,
429 });
430 self.block_state = BlockState::AutoParagraph;
431 }
432 self.open_style(&TextStyleKind::Code);
433 self.enqueue_text(content);
434 self.close_style(&TextStyleKind::Code);
435 }
436 }
437
438 fn handle_end_code_block(&mut self) {
441 if let Some(buf) = self.code_block_buffer.take() {
442 let content = buf.strip_suffix('\n').unwrap_or(&buf).to_owned();
443 if !content.is_empty() {
444 self.enqueue_text(content);
445 }
446 }
447 self.in_preformatted = false;
448 self.push_event_end(Event::EndPreformatted);
449 }
450
451 fn handle_end_image(&mut self) {
455 let Some(img) = self.image.take() else { return };
456 self.flush_pending_paragraph_start();
457 let trimmed = img.alt_buf.trim();
458 let alt = if trimmed.is_empty() {
459 None
460 } else {
461 Some(trimmed.to_owned())
462 };
463 let decorative = alt.is_none();
464 self.queue.push_back(Event::Image {
465 source: ImageSource::Uri { uri: img.url },
466 alt,
467 title: img.title,
468 decorative,
469 id: None,
470 });
471 }
472
473 fn handle_end_item(&mut self) {
476 if self.block_state == BlockState::AutoParagraph {
477 self.flush_html_styles();
478 self.queue.push_back(Event::EndParagraph);
479 }
480 self.close_current_item_if_open();
481 self.block_state = BlockState::None;
482 }
483
484 fn handle_end_link(&mut self) {
486 let Some(link) = self.link.take() else { return };
487 if link.started {
488 self.queue.push_back(Event::EndLink);
489 } else {
490 self.flush_pending_paragraph_start();
491 self.queue.push_back(Event::StartLink {
492 href: link.href,
493 id: None,
494 title: link.title,
495 });
496 self.queue.push_back(Event::EndLink);
497 }
498 }
499
500 fn handle_end_list(&mut self) {
502 self.close_current_item_if_open();
503 self.list_stack.pop();
504 self.block_state = BlockState::None;
505 }
506
507 fn handle_end_table_cell(&mut self) {
510 if self.in_table_head {
511 self.push_event_end(Event::EndTableHeader);
512 } else {
513 self.push_event_end(Event::EndTableCell);
514 }
515 }
516
517 fn handle_end_table_head(&mut self) {
519 self.push_event_end(Event::EndTableRow);
520 self.in_table_head = false;
521 }
522
523 fn handle_end_tag(&mut self, tag_end: TagEnd) {
529 match tag_end {
530 TagEnd::BlockQuote(_) => self.push_event_end(Event::EndBlockQuote),
531 TagEnd::CodeBlock => self.handle_end_code_block(),
532 TagEnd::Emphasis => self.close_style(&TextStyleKind::Italic),
533 TagEnd::Heading(_) => self.push_event_end(Event::EndHeading),
534 TagEnd::HtmlBlock => {
535 self.in_html_block = false;
536 for event in self.html_block_inline_stack.close_all() {
537 self.queue.push_back(event);
538 }
539 if let Some(event) = self.html_block_heading_acc.finish_block() {
540 self.queue.push_back(event);
541 }
542 }
543 TagEnd::Image => self.handle_end_image(),
544 TagEnd::Item => self.handle_end_item(),
545 TagEnd::Link => self.handle_end_link(),
546 TagEnd::List(_) => self.handle_end_list(),
547 TagEnd::Paragraph => {
548 if self.block_state == BlockState::PendingExplicit {
549 self.flush_html_styles();
550 self.block_state = BlockState::None;
551 } else {
552 self.push_event_end(Event::EndParagraph);
553 }
554 }
555 TagEnd::Strikethrough => self.close_style(&TextStyleKind::Strikethrough),
556 TagEnd::Strong => self.close_style(&TextStyleKind::Bold),
557 TagEnd::Table => self.push_event_end(Event::EndTable),
558 TagEnd::TableCell => self.handle_end_table_cell(),
559 TagEnd::TableHead => self.handle_end_table_head(),
560 TagEnd::TableRow => self.push_event_end(Event::EndTableRow),
561 TagEnd::DefinitionList
563 | TagEnd::DefinitionListDefinition
564 | TagEnd::DefinitionListTitle
565 | TagEnd::FootnoteDefinition
566 | TagEnd::MetadataBlock(_)
567 | TagEnd::Subscript
568 | TagEnd::Superscript => {}
569 }
570 }
571
572 fn handle_item_start(&mut self) {
573 let depth = self.list_stack.len().saturating_sub(1);
574 let level = u32::try_from(depth).map_or(u32::MAX, |v| v);
575 if let Some(ctx) = self.list_stack.last_mut() {
576 if ctx.ordered {
577 self.queue.push_back(Event::StartOrderedListItem {
578 start: ctx.pending_start.take(),
579 style_type: ListStyleType::Decimal,
580 level,
581 id: None,
582 });
583 } else {
584 self.queue.push_back(Event::StartUnorderedListItem {
585 style_type: ListStyleType::Disc,
586 level,
587 id: None,
588 });
589 }
590 ctx.item_open = true;
591 self.block_state = BlockState::Explicit;
592 }
593 }
594
595 fn handle_list_start(&mut self, start_opt: Option<u64>) {
596 self.list_stack.push(ListContext {
597 item_open: false,
598 ordered: start_opt.is_some(),
599 pending_start: start_opt,
600 });
601 }
602
603 fn handle_start_code_block(&mut self, syntax: Option<String>) {
606 self.code_block_buffer = Some(String::new());
607 self.in_preformatted = true;
608 self.push_event_start(Event::StartPreformatted { id: None, syntax });
609 }
610
611 fn handle_start_heading(&mut self, level: HeadingLevel) {
613 let level_u8 = match level {
614 HeadingLevel::H1 => 1,
615 HeadingLevel::H2 => 2,
616 HeadingLevel::H3 => 3,
617 HeadingLevel::H4 => 4,
618 HeadingLevel::H5 => 5,
619 HeadingLevel::H6 => 6,
620 };
621 self.push_event_start(Event::StartHeading {
622 level: level_u8,
623 id: None,
624 });
625 }
626
627 fn handle_start_image(&mut self, dest_url: String, title: Option<String>) {
631 self.flush_pending_paragraph_start();
640 if let Some(link) = self.link.take() {
641 if link.started {
642 self.queue.push_back(Event::EndLink);
643 } else {
644 self.queue.push_back(Event::StartLink {
645 href: link.href,
646 id: None,
647 title: link.title,
648 });
649 self.queue.push_back(Event::EndLink);
650 }
651 }
652
653 self.image = Some(ImageBuffer {
654 alt_buf: String::new(),
655 title,
656 url: dest_url,
657 });
658 }
659
660 fn handle_start_link(&mut self, dest_url: String, title: Option<String>) {
665 self.link = Some(LinkBuffer {
666 href: dest_url,
667 started: false,
668 title,
669 });
670 }
671
672 fn handle_start_table_cell(&mut self) {
675 if self.in_table_head {
676 self.push_event_start(Event::StartTableHeader {
677 scope: Some(TableHeaderScope::Column),
678 abbr: None,
679 colspan: None,
680 rowspan: None,
681 id: None,
682 });
683 } else {
684 self.push_event_start(Event::StartTableCell {
685 colspan: None,
686 rowspan: None,
687 id: None,
688 });
689 }
690 }
691
692 fn handle_start_table_head(&mut self) {
694 self.in_table_head = true;
695 self.push_event_start(Event::StartTableRow { id: None });
696 }
697
698 fn handle_start_tag(&mut self, tag: MarkdownStartTag) {
704 match tag {
705 MarkdownStartTag::BlockQuote => {
706 self.push_event_start(Event::StartBlockQuote { id: None });
707 }
708 MarkdownStartTag::CodeBlock { syntax } => self.handle_start_code_block(syntax),
709 MarkdownStartTag::Emphasis => self.open_style(&TextStyleKind::Italic),
710 MarkdownStartTag::Heading { level } => self.handle_start_heading(level),
711 MarkdownStartTag::HtmlBlock => self.in_html_block = true,
712 MarkdownStartTag::Image { dest_url, title } => self.handle_start_image(dest_url, title),
713 MarkdownStartTag::Item => self.handle_item_start(),
714 MarkdownStartTag::Link { dest_url, title } => self.handle_start_link(dest_url, title),
715 MarkdownStartTag::List(start_opt) => self.handle_list_start(start_opt),
716 MarkdownStartTag::Paragraph => self.block_state = BlockState::PendingExplicit,
717 MarkdownStartTag::Strikethrough => self.open_style(&TextStyleKind::Strikethrough),
718 MarkdownStartTag::Strong => self.open_style(&TextStyleKind::Bold),
719 MarkdownStartTag::Table => self.push_event_start(Event::StartTable { id: None }),
720 MarkdownStartTag::TableCell => self.handle_start_table_cell(),
721 MarkdownStartTag::TableHead => self.handle_start_table_head(),
722 MarkdownStartTag::TableRow => self.push_event_start(Event::StartTableRow { id: None }),
723 }
724 }
725
726 fn handle_text(&mut self, content: String) {
727 if let Some(img) = &mut self.image {
728 img.alt_buf.push_str(&content);
729 } else if let Some(buf) = &mut self.code_block_buffer {
730 buf.push_str(&content);
731 } else {
732 self.emit_pending_link_start();
733 if self.block_state == BlockState::None {
734 self.queue.push_back(Event::StartParagraph {
735 alignment: None,
736 id: None,
737 });
738 self.block_state = BlockState::AutoParagraph;
739 }
740 self.enqueue_text(content);
741 }
742 }
743
744 fn next_pulldown_event(&mut self) -> Option<MarkdownPulldownEvent> {
745 self.cell.with_dependent_mut(|_, dep| {
746 dep.0.next().map(|event| match event {
747 pulldown_cmark::Event::Start(tag) => markdown_start_tag(tag)
748 .map_or(MarkdownPulldownEvent::Ignored, MarkdownPulldownEvent::Start),
749 pulldown_cmark::Event::End(tag_end) => MarkdownPulldownEvent::End(tag_end),
750 pulldown_cmark::Event::Text(text) => {
751 MarkdownPulldownEvent::Text(text.into_string())
752 }
753 pulldown_cmark::Event::Code(code) => {
754 MarkdownPulldownEvent::Code(code.into_string())
755 }
756 pulldown_cmark::Event::HardBreak => MarkdownPulldownEvent::HardBreak,
757 pulldown_cmark::Event::SoftBreak => MarkdownPulldownEvent::SoftBreak,
758 pulldown_cmark::Event::Rule => MarkdownPulldownEvent::Rule,
759 pulldown_cmark::Event::InlineHtml(tag_str) => {
760 MarkdownPulldownEvent::InlineHtml(tag_str.into_string())
761 }
762 pulldown_cmark::Event::Html(fragment) => {
763 MarkdownPulldownEvent::BlockHtml(fragment.into_string())
764 }
765 pulldown_cmark::Event::DisplayMath(_)
766 | pulldown_cmark::Event::FootnoteReference(_)
767 | pulldown_cmark::Event::InlineMath(_)
768 | pulldown_cmark::Event::TaskListMarker(_) => MarkdownPulldownEvent::Ignored,
769 })
770 })
771 }
772
773 fn process_next_pulldown_event(&mut self) {
774 let Some(pm_event) = self.next_pulldown_event() else {
775 if self.phase != Phase::Finished {
776 self.phase = Phase::Finished;
777 self.flush_html_styles();
778 self.queue.push_back(Event::EndDocument);
779 }
780 return;
781 };
782
783 match pm_event {
784 MarkdownPulldownEvent::BlockHtml(fragment) => {
785 let events = crate::html::translator::translate_block(
786 &fragment,
787 &mut self.html_block_heading_acc,
788 &mut self.html_block_inline_stack,
789 self.in_preformatted,
790 );
791 for event in events {
792 match event {
793 Event::Text { content } => self.enqueue_text(content),
794 other => self.queue.push_back(other),
795 }
796 }
797 }
798 MarkdownPulldownEvent::Start(tag) => self.handle_start_tag(tag),
799 MarkdownPulldownEvent::End(tag_end) => self.handle_end_tag(tag_end),
800 MarkdownPulldownEvent::Text(text) => self.handle_text(text),
801 MarkdownPulldownEvent::Code(code) => self.handle_code(code),
802 MarkdownPulldownEvent::InlineHtml(fragment) => {
803 let events = crate::html::translator::translate_inline(
804 &fragment,
805 &mut self.inline_style_stack,
806 self.in_preformatted,
807 );
808 for event in events {
809 match event {
810 Event::Text { content } => self.enqueue_text(content),
811 other => self.queue.push_back(other),
812 }
813 }
814 }
815 MarkdownPulldownEvent::HardBreak => {
816 if let Some(img) = &mut self.image {
817 img.alt_buf.push(' ');
818 } else if self.block_state == BlockState::PendingExplicit {
819 } else {
821 self.emit_pending_link_start();
822 self.queue.push_back(Event::LineBreak);
823 }
824 }
825 MarkdownPulldownEvent::SoftBreak => {
826 if let Some(img) = &mut self.image {
827 img.alt_buf.push(' ');
828 } else if self.block_state == BlockState::PendingExplicit {
829 } else {
831 self.emit_pending_link_start();
832 self.queue.push_back(Event::SoftBreak);
833 }
834 }
835 MarkdownPulldownEvent::Rule => {
836 self.queue.push_back(Event::ThematicBreak { id: None });
837 }
838 MarkdownPulldownEvent::Ignored => {}
839 }
840 }
841
842 fn push_event(&mut self, event: Event, state: BlockState) {
843 self.queue.push_back(event);
844 self.block_state = state;
845 }
846
847 fn push_event_end(&mut self, event: Event) {
848 self.flush_html_styles();
849 self.push_event(event, BlockState::None);
850 }
851
852 fn push_event_start(&mut self, event: Event) {
853 self.push_event(event, BlockState::Explicit);
854 }
855}
856
857impl EventSource for MarkdownReader {
858 #[inline]
859 fn next_event(&mut self) -> Result<Option<Event>> {
860 if self.phase == Phase::NotStarted {
861 self.phase = Phase::Running;
862 return Ok(Some(Event::StartDocument {
863 id: None,
864 language: None,
865 metadata: None,
866 }));
867 }
868
869 if self.phase == Phase::Finished && self.queue.is_empty() {
870 return Ok(None);
871 }
872
873 while self.queue.is_empty() && self.phase != Phase::Finished {
874 self.process_next_pulldown_event();
875 }
876
877 Ok(self.queue.pop_front())
878 }
879}
880
881fn markdown_start_tag(tag: Tag<'_>) -> Option<MarkdownStartTag> {
882 match tag {
883 Tag::BlockQuote(_) => Some(MarkdownStartTag::BlockQuote),
884 Tag::CodeBlock(kind) => Some(MarkdownStartTag::CodeBlock {
885 syntax: code_block_syntax(kind),
886 }),
887 Tag::Emphasis => Some(MarkdownStartTag::Emphasis),
888 Tag::Heading { level, .. } => Some(MarkdownStartTag::Heading { level }),
889 Tag::Image {
890 dest_url, title, ..
891 } => Some(MarkdownStartTag::Image {
892 dest_url: dest_url.into_string(),
893 title: cow_to_optional_string(title),
894 }),
895 Tag::HtmlBlock => Some(MarkdownStartTag::HtmlBlock),
896 Tag::Item => Some(MarkdownStartTag::Item),
897 Tag::Link {
898 dest_url, title, ..
899 } => Some(MarkdownStartTag::Link {
900 dest_url: dest_url.into_string(),
901 title: cow_to_optional_string(title),
902 }),
903 Tag::List(start_opt) => Some(MarkdownStartTag::List(start_opt)),
904 Tag::Paragraph => Some(MarkdownStartTag::Paragraph),
905 Tag::Strikethrough => Some(MarkdownStartTag::Strikethrough),
906 Tag::Strong => Some(MarkdownStartTag::Strong),
907 Tag::Table(_) => Some(MarkdownStartTag::Table),
908 Tag::TableCell => Some(MarkdownStartTag::TableCell),
909 Tag::TableHead => Some(MarkdownStartTag::TableHead),
910 Tag::TableRow => Some(MarkdownStartTag::TableRow),
911 Tag::DefinitionList
912 | Tag::DefinitionListDefinition
913 | Tag::DefinitionListTitle
914 | Tag::FootnoteDefinition(_)
915 | Tag::MetadataBlock(_)
916 | Tag::Subscript
917 | Tag::Superscript => None,
918 }
919}
920
921fn code_block_syntax(kind: CodeBlockKind<'_>) -> Option<String> {
922 match kind {
923 CodeBlockKind::Fenced(lang) if !lang.is_empty() => Some(lang.into_string()),
924 CodeBlockKind::Fenced(_) | CodeBlockKind::Indented => None,
925 }
926}
927
928fn cow_to_optional_string(value: CowStr<'_>) -> Option<String> {
929 if value.is_empty() {
930 None
931 } else {
932 Some(value.into_string())
933 }
934}
935
936#[cfg(test)]
937mod tests {
938 use super::*;
939
940 #[test]
941 fn handle_code_without_open_block_auto_opens_paragraph() {
942 let mut reader = MarkdownReader::from_str("");
943 reader.handle_code("code".to_string());
944
945 assert_eq!(reader.queue.len(), 4);
946 assert_eq!(
947 reader.queue.front(),
948 Some(&Event::StartParagraph {
949 alignment: None,
950 id: None,
951 })
952 );
953 assert_eq!(
954 reader.queue.get(1),
955 Some(&Event::StartTextStyle {
956 kind: TextStyleKind::Code,
957 id: None,
958 })
959 );
960 assert_eq!(
961 reader.queue.get(2),
962 Some(&Event::Text {
963 content: "code".to_string(),
964 })
965 );
966 assert_eq!(reader.queue.get(3), Some(&Event::EndTextStyle));
967 }
968
969 #[test]
970 fn handle_text_without_open_block_auto_opens_paragraph() {
971 let mut reader = MarkdownReader::from_str("");
972 reader.handle_text("hello".to_string());
973
974 assert_eq!(reader.queue.len(), 2);
975 assert_eq!(
976 reader.queue.front(),
977 Some(&Event::StartParagraph {
978 alignment: None,
979 id: None,
980 })
981 );
982 assert_eq!(
983 reader.queue.get(1),
984 Some(&Event::Text {
985 content: "hello".to_string(),
986 })
987 );
988 }
989}
990
991#[cfg(test)]
992mod send_static_assertions {
993 fn assert_send_static<T>()
994 where
995 T: Send + 'static,
996 {
997 }
998
999 #[test]
1000 fn markdown_reader_is_send_static() {
1001 assert_send_static::<crate::MarkdownReader>();
1002 }
1003}