1pub mod palette;
163
164use std::io::Write;
165
166use docspec_core::{
167 BlockKind, Depth, Error, Event, EventSink, ImageSource, Result, TextAlignment, TextStyleKind,
168};
169use docspec_json::{JsonEmitter, Null, StrusonBackend};
170
171macro_rules! close_text_block {
172 ($writer:expr) => {{
173 $writer.close_open_link_if_any()?;
174 $writer.close_content_block()?;
175 $writer.context.in_text_block = false;
176 Ok(())
177 }};
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182enum ListKind {
183 Ordered,
185 Unordered,
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190enum ListContentState {
191 Pending,
192 Open,
193 Closed,
194}
195
196#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
197enum BlockquoteContentState {
198 #[default]
201 Pending,
202 InContent,
204 InChildren,
207}
208
209#[derive(Debug)]
210enum DrainDestination {
211 DocumentRoot,
212 Ancestor {
213 ancestor_index: usize,
214 kind: BlockKind,
215 },
216}
217
218#[derive(Copy, Clone, Debug, Eq, PartialEq)]
220enum BlockLeafKind {
221 Image,
222 Divider,
223}
224
225#[derive(Copy, Clone, Debug, Eq, PartialEq)]
227enum ChildKind {
228 Container(BlockKind),
229 Leaf(BlockLeafKind),
230}
231
232fn blocknote_accepts_child(parent: Option<BlockKind>, child: ChildKind) -> bool {
235 match parent {
236 None => true,
237 Some(
238 BlockKind::Blockquote
239 | BlockKind::Heading
240 | BlockKind::OrderedListItem
241 | BlockKind::UnorderedListItem,
242 ) => !matches!(
243 child,
244 ChildKind::Container(
245 BlockKind::TableRow | BlockKind::TableCell | BlockKind::TableHeader
246 )
247 ),
248 Some(_) => false,
249 }
250}
251
252const _: BlockLeafKind = BlockLeafKind::Image;
253const _: BlockLeafKind = BlockLeafKind::Divider;
254const _: ChildKind = ChildKind::Container(BlockKind::Paragraph);
255const _: ChildKind = ChildKind::Leaf(BlockLeafKind::Image);
256const _: fn(Option<BlockKind>, ChildKind) -> bool = blocknote_accepts_child;
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260struct ListStackEntry {
261 children_array_open: bool,
263 content_state: ListContentState,
265 first_paragraph_consumed: bool,
267 kind: ListKind,
269 level: u32,
271 start: Option<u32>,
273}
274
275#[derive(Default)]
276struct BlockContext {
277 blockquote_has_content: bool,
278 in_table_cell: bool,
279 in_text_block: bool,
280}
281
282fn non_default_alignment_value(alignment: Option<&TextAlignment>) -> Option<&'static str> {
283 match alignment {
284 Some(TextAlignment::Center) => Some("center"),
285 Some(TextAlignment::Right) => Some("right"),
286 Some(TextAlignment::Justify) => Some("justify"),
287 _ => None,
288 }
289}
290
291pub struct BlockNoteWriter<W: Write> {
300 blockquote_depth: Depth,
301 blockquote_content_states: Vec<BlockquoteContentState>,
306 context: BlockContext,
307 in_link: bool,
309 json: JsonEmitter<StrusonBackend<W>>,
310 link_emitted_styled_text: bool,
312 lifted_nested_events: Vec<Event>,
317 lifted_subtree_depth: Depth,
322 list_stack: Vec<ListStackEntry>,
323 open_block_ancestors: Vec<BlockKind>,
329 open_styles: Vec<TextStyleKind>,
330 table_depth: Depth,
331}
332
333impl<W: Write> BlockNoteWriter<W> {
334 fn close_content_block(&mut self) -> Result<()> {
335 self.json.close_array()?;
336 self.json.key("children").array(|_| Ok(()))?;
337 self.json.close_object()
338 }
339
340 fn close_current_list_item_object(&mut self) -> Result<()> {
341 if self
342 .list_stack
343 .last()
344 .is_some_and(|entry| entry.content_state == ListContentState::Pending)
345 {
346 self.initialize_current_list_item_content(None)?;
347 }
348 let popped_entry = self.list_stack.pop();
349 if let Some(list_entry) = popped_entry {
350 let ancestor_kind = match list_entry.kind {
351 ListKind::Ordered => BlockKind::OrderedListItem,
352 ListKind::Unordered => BlockKind::UnorderedListItem,
353 };
354 if list_entry.content_state == ListContentState::Open {
355 self.close_open_link_if_any()?;
356 self.json.close_array()?;
357 }
358 if list_entry.children_array_open {
359 self.json.close_array()?;
360 } else {
361 self.json.key("children").array(|_| Ok(()))?;
362 }
363 self.json.close_object()?;
364 self.pop_ancestor(ancestor_kind);
365 }
366 Ok(())
367 }
368
369 fn close_for_block_sibling(&mut self) -> Result<()> {
370 if !self.list_stack.is_empty() {
371 self.close_open_list_items()?;
372 }
373 if self.blockquote_depth.is_positive() {
374 return self.prepare_blockquote_children();
375 }
376 if self.context.in_text_block {
377 self.close_open_link_if_any()?;
378 self.close_content_block()?;
379 self.context.in_text_block = false;
380 }
381 Ok(())
382 }
383
384 fn close_open_link_if_any(&mut self) -> Result<()> {
390 if self.in_link {
391 self.handle_end_link()?;
392 }
393 Ok(())
394 }
395
396 fn close_open_child_text_block(&mut self) -> Result<()> {
406 if self.context.in_text_block {
407 return close_text_block!(self);
408 }
409 Ok(())
410 }
411
412 fn close_open_list_items(&mut self) -> Result<()> {
413 while !self.list_stack.is_empty() {
414 self.close_current_list_item_object()?;
415 }
416 Ok(())
417 }
418
419 fn handle_blockquote(&mut self, id: Option<&str>) -> Result<()> {
420 self.json.open_object()?;
421 self.json.key("type").value("quote")?;
422 self.write_id(id)?;
423 self.blockquote_depth.inc();
424 self.blockquote_content_states
425 .push(BlockquoteContentState::Pending);
426 self.open_block_ancestors.push(BlockKind::Blockquote);
427 self.context.blockquote_has_content = false;
428 Ok(())
429 }
430
431 fn prepare_blockquote_inline_content(&mut self) -> Result<()> {
432 if self.blockquote_content_states.last() == Some(&BlockquoteContentState::Pending) {
433 self.json.key("content").open_array()?;
434 if let Some(state) = self.blockquote_content_states.last_mut() {
435 *state = BlockquoteContentState::InContent;
436 }
437 self.context.in_text_block = true;
438 }
439 Ok(())
440 }
441
442 fn prepare_blockquote_children(&mut self) -> Result<()> {
443 match self.blockquote_content_states.last().copied() {
444 Some(BlockquoteContentState::Pending) => {
445 self.json.key("content").open_array()?;
446 self.json.close_array()?;
447 self.json.key("children").open_array()?;
448 if let Some(state) = self.blockquote_content_states.last_mut() {
449 *state = BlockquoteContentState::InChildren;
450 }
451 }
452 Some(BlockquoteContentState::InContent) => {
453 self.close_open_link_if_any()?;
454 self.json.close_array()?;
455 self.context.in_text_block = false;
456 self.json.key("children").open_array()?;
457 if let Some(state) = self.blockquote_content_states.last_mut() {
458 *state = BlockquoteContentState::InChildren;
459 }
460 }
461 Some(BlockquoteContentState::InChildren) => self.close_open_child_text_block()?,
462 None => {}
463 }
464 Ok(())
465 }
466
467 fn handle_divider(&mut self, id: Option<&str>) -> Result<()> {
468 self.json.object(|j| {
469 j.key("type").value("divider")?;
470 if let Some(id_val) = id {
471 j.key("id").value(id_val)?;
472 }
473 Ok(())
474 })
475 }
476
477 fn handle_end_link(&mut self) -> Result<()> {
483 if !self.in_link {
484 return Ok(());
485 }
486 if !self.link_emitted_styled_text {
487 self.json.open_object()?;
488 self.json.key("type").value("text")?;
489 self.json.key("text").value("")?;
490 self.json.key("styles").open_object()?;
491 self.json.close_object()?;
492 self.json.close_object()?;
493 }
494 self.json.close_array()?;
495 self.json.close_object()?;
496 self.in_link = false;
497 self.link_emitted_styled_text = false;
498 Ok(())
499 }
500
501 fn handle_end_list_item(&mut self) -> Result<()> {
502 if self.list_stack.is_empty() {
503 return Ok(());
504 }
505 self.close_current_list_item_object()
506 }
507
508 fn handle_end_paragraph(&mut self) -> Result<()> {
509 let in_current_blockquote_list_item =
510 self.blockquote_depth.is_positive() && self.current_blockquote_contains_list_item();
511 if !self.list_stack.is_empty()
512 && (self.blockquote_depth.is_zero() || in_current_blockquote_list_item)
513 && self
514 .list_stack
515 .last()
516 .is_some_and(|e| e.first_paragraph_consumed)
517 && self.context.in_text_block
518 {
519 self.close_open_link_if_any()?;
520 self.json.close_array()?;
521 self.json.key("children").array(|_| Ok(()))?;
522 self.json.close_object()?;
523 self.context.in_text_block = false;
524 return Ok(());
525 }
526 if self.blockquote_depth.is_positive()
527 && self.context.in_text_block
528 && self
529 .blockquote_content_states
530 .last()
531 .is_some_and(|state| *state == BlockquoteContentState::InChildren)
532 {
533 return close_text_block!(self);
534 }
535 if self.in_list_item_content() {
536 if let Some(entry) = self.list_stack.last_mut() {
537 entry.first_paragraph_consumed = true;
538 }
539 return Ok(());
540 }
541 if self.blockquote_depth.is_positive()
542 || !self.context.in_text_block
543 || self.context.in_table_cell
544 {
545 return Ok(());
546 }
547 close_text_block!(self)
548 }
549
550 fn handle_end_table(&mut self) -> Result<()> {
551 if self.table_depth.is_zero() {
552 return Ok(());
553 }
554 self.json.close_array()?;
555 self.json.close_object()?;
556 self.json.key("children").array(|_| Ok(()))?;
557 self.json.close_object()?;
558 self.pop_ancestor(BlockKind::Table);
559 self.table_depth.dec();
560 Ok(())
561 }
562
563 fn handle_end_table_cell(&mut self) -> Result<()> {
564 self.close_open_link_if_any()?;
565 self.json.close_array()?;
566 self.json.close_object()?;
567 self.context.in_table_cell = false;
568 Ok(())
569 }
570
571 fn handle_end_table_row(&mut self) -> Result<()> {
572 self.json.close_array()?;
573 self.json.close_object()
574 }
575
576 fn handle_end_blockquote(&mut self) -> Result<()> {
577 if self.blockquote_depth.is_zero() {
578 return Ok(());
579 }
580 let state = self.blockquote_content_states.pop().unwrap_or_default();
581 match state {
582 BlockquoteContentState::Pending => {
583 self.json.key("content").open_array()?;
584 self.json.close_array()?;
585 self.json.key("children").open_array()?;
586 self.json.close_array()?;
587 }
588 BlockquoteContentState::InContent => {
589 self.close_open_link_if_any()?;
590 self.json.close_array()?;
591 self.context.in_text_block = false;
592 self.json.key("children").open_array()?;
593 self.json.close_array()?;
594 }
595 BlockquoteContentState::InChildren => {
596 self.json.close_array()?;
597 }
598 }
599 self.pop_ancestor(BlockKind::Blockquote);
600 self.blockquote_depth.dec();
601 self.context.in_text_block = self
602 .blockquote_content_states
603 .last()
604 .is_some_and(|s| *s == BlockquoteContentState::InContent);
605 self.json.close_object()?;
606 Ok(())
607 }
608
609 fn handle_heading(&mut self, level: u8, id: Option<&str>) -> Result<()> {
610 self.json.open_object()?;
611 self.json.key("type").value("heading")?;
612 self.write_id(id)?;
613 self.json
614 .key("props")
615 .object(|j| j.key("level").value(level))?;
616 self.json.key("content").open_array()?;
617 self.context.in_text_block = true;
618 Ok(())
619 }
620
621 fn handle_image(
622 &mut self,
623 source: ImageSource,
624 alt: Option<String>,
625 id: Option<&str>,
626 ) -> Result<()> {
627 self.prepare_for_child_block()?;
628 let caption = alt.unwrap_or_default();
629
630 match source {
631 ImageSource::Uri { uri } => self.json.object(|j| {
632 if let Some(id_val) = id {
633 j.key("id").value(id_val)?;
634 }
635 j.key("type").value("image")?;
636 j.key("props").object(|p| {
637 p.key("url").value(uri.as_str())?;
638 p.key("caption").value(caption.as_str())
639 })?;
640 j.key("content").value(Null)?;
641 j.key("children").array(|_| Ok(()))
642 }),
643 ImageSource::Asset(handle) => {
644 let content_type = handle
645 .content_type()
646 .ok_or_else(|| Error::Other {
647 message: format!("asset not found: {}", handle.asset_id()),
648 })?
649 .into_owned();
650
651 self.json.object(|j| {
652 if let Some(id_val) = id {
653 j.key("id").value(id_val)?;
654 }
655 j.key("type").value("image")?;
656 j.key("props").object(|p| {
657 p.key("url").string_value_streaming(|w| {
658 write!(w, "data:{content_type};base64,")?;
659 let mut enc = base64::write::EncoderWriter::new(
660 w,
661 &base64::engine::general_purpose::STANDARD,
662 );
663 handle.stream_to(&mut enc)?;
664 enc.finish()?;
665 Ok(())
666 })?;
667 p.key("caption").value(caption.as_str())
668 })?;
669 j.key("content").value(Null)?;
670 j.key("children").array(|_| Ok(()))
671 })
672 }
673 _ => Ok(()),
674 }
675 }
676
677 fn handle_line_break(&mut self) -> Result<()> {
678 if self.context.in_text_block || self.context.in_table_cell || self.in_list_item_content() {
679 self.handle_text("\n")
680 } else {
681 Ok(())
682 }
683 }
684
685 fn handle_paragraph(
686 &mut self,
687 id: Option<&str>,
688 alignment: Option<&TextAlignment>,
689 ) -> Result<()> {
690 if self.context.in_table_cell {
692 return Ok(());
693 }
694 let in_current_blockquote_list_item =
695 self.blockquote_depth.is_positive() && self.current_blockquote_contains_list_item();
696 if !self.list_stack.is_empty()
700 && (self.blockquote_depth.is_zero() || in_current_blockquote_list_item)
701 && self
702 .list_stack
703 .last()
704 .is_some_and(|e| e.first_paragraph_consumed)
705 {
706 if self
707 .list_stack
708 .last()
709 .is_some_and(|e| e.content_state == ListContentState::Open)
710 {
711 self.json.close_array()?;
712 if let Some(e) = self.list_stack.last_mut() {
713 e.content_state = ListContentState::Closed;
714 }
715 }
716 if !self
717 .list_stack
718 .last()
719 .is_some_and(|e| e.children_array_open)
720 {
721 self.json.key("children").open_array()?;
722 if let Some(e) = self.list_stack.last_mut() {
723 e.children_array_open = true;
724 }
725 }
726 self.json.open_object()?;
727 self.json.key("type").value("paragraph")?;
728 self.write_paragraph_props(alignment)?;
729 self.json.key("content").open_array()?;
730 self.context.in_text_block = true;
731 return Ok(());
732 }
733 if !self.list_stack.is_empty()
734 && (self.blockquote_depth.is_zero() || in_current_blockquote_list_item)
735 {
736 self.initialize_current_list_item_content(alignment)?;
737 return Ok(());
738 }
739 if self.blockquote_depth.is_positive() {
740 if self
741 .blockquote_content_states
742 .last()
743 .is_some_and(|state| *state == BlockquoteContentState::InChildren)
744 {
745 self.json.open_object()?;
746 self.write_id(id)?;
747 self.json.key("type").value("paragraph")?;
748 self.write_paragraph_props(alignment)?;
749 self.json.key("content").open_array()?;
750 self.context.in_text_block = true;
751 return Ok(());
752 }
753 if self.context.blockquote_has_content {
754 self.handle_text("\n\n")?;
755 }
756 self.prepare_blockquote_inline_content()?;
757 return Ok(());
758 }
759 self.json.open_object()?;
760 self.write_id(id)?;
761 self.json.key("type").value("paragraph")?;
762 self.write_paragraph_props(alignment)?;
763 self.json.key("content").open_array()?;
764 self.context.in_text_block = true;
765 Ok(())
766 }
767
768 fn write_paragraph_props(&mut self, alignment: Option<&TextAlignment>) -> Result<()> {
769 if let Some(value) = non_default_alignment_value(alignment) {
770 self.json
771 .key("props")
772 .object(|j| j.key("textAlignment").value(value))?;
773 }
774 Ok(())
775 }
776
777 fn handle_preformatted(&mut self, id: Option<&str>, syntax: Option<&str>) -> Result<()> {
778 self.json.open_object()?;
779 self.json.key("type").value("codeBlock")?;
780 self.write_id(id)?;
781 if let Some(lang) = syntax {
782 self.json
783 .key("props")
784 .object(|j| j.key("language").value(lang))?;
785 }
786 self.json.key("content").open_array()?;
787 self.context.in_text_block = true;
788 Ok(())
789 }
790
791 fn handle_start_link(&mut self, href: &str) -> Result<()> {
795 if self.list_stack.last().is_some_and(|entry| {
796 entry.content_state == ListContentState::Pending && !entry.first_paragraph_consumed
797 }) {
798 self.initialize_current_list_item_content(None)?;
799 }
800 if !self.context.in_text_block
801 && !self.context.in_table_cell
802 && !self.in_list_item_content()
803 {
804 return Ok(());
805 }
806 if self.in_link {
807 return Ok(());
808 }
809 if self.blockquote_depth.is_positive() {
810 self.context.blockquote_has_content = true;
811 }
812 self.json.open_object()?;
813 self.json.key("type").value("link")?;
814 self.json.key("href").value(href)?;
815 self.json.key("content").open_array()?;
816 self.in_link = true;
817 self.link_emitted_styled_text = false;
818 Ok(())
819 }
820
821 fn handle_start_text_style(&mut self, kind: TextStyleKind) -> Result<()> {
822 self.open_styles.push(kind);
823 Ok(())
824 }
825
826 fn handle_end_text_style(&mut self) -> Result<()> {
827 if self.open_styles.pop().is_none() {
828 return Err(Error::InvalidSequence {
829 expected: "StartTextStyle".to_string(),
830 found: "EndTextStyle".to_string(),
831 message: "cannot close text style because no text style is open".to_string(),
832 });
833 }
834 Ok(())
835 }
836
837 fn current_blockquote_contains_list_item(&self) -> bool {
838 let Some(blockquote_index) = self
839 .open_block_ancestors
840 .iter()
841 .rposition(|kind| *kind == BlockKind::Blockquote)
842 else {
843 return false;
844 };
845 self.open_block_ancestors
846 .iter()
847 .skip(blockquote_index.saturating_add(1))
848 .any(|kind| {
849 matches!(
850 kind,
851 BlockKind::OrderedListItem | BlockKind::UnorderedListItem
852 )
853 })
854 }
855
856 fn handle_start_list_item(
857 &mut self,
858 kind: ListKind,
859 level: u32,
860 start: Option<u64>,
861 ) -> Result<()> {
862 if self.blockquote_depth.is_positive() {
863 self.prepare_blockquote_children()?;
864 if !self.current_blockquote_contains_list_item() {
865 self.open_list_item_object(kind, level, start)?;
866 return Ok(());
867 }
868 }
869 if self.list_stack.is_empty() {
870 self.close_for_block_sibling()?;
871 self.open_list_item_object(kind, level, start)?;
872 return Ok(());
873 }
874
875 let stack_top_level = self.list_stack.last().map_or(0, |entry| entry.level);
876
877 let effective_level = if level > stack_top_level.saturating_add(1) {
880 stack_top_level.saturating_add(1)
881 } else {
882 level
883 };
884
885 if effective_level > stack_top_level {
886 self.open_current_list_item_children()?;
887 self.open_list_item_object(kind, effective_level, start)?;
888 return Ok(());
889 }
890
891 if effective_level == stack_top_level {
892 self.close_current_list_item_object()?;
893 if self.list_stack.is_empty() {
894 self.close_for_block_sibling()?;
895 }
896 self.open_list_item_object(kind, effective_level, start)?;
897 return Ok(());
898 }
899
900 while let Some(top) = self.list_stack.last() {
903 if top.level < effective_level {
904 break;
905 }
906 self.close_current_list_item_object()?;
907 }
908 if self.list_stack.is_empty() {
909 self.close_for_block_sibling()?;
910 }
911 self.open_list_item_object(kind, effective_level, start)?;
912 Ok(())
913 }
914
915 fn handle_start_table(&mut self, id: Option<&str>) -> Result<()> {
916 self.prepare_for_child_block()?;
917 self.json.open_object()?;
918 self.json.key("type").value("table")?;
919 self.write_id(id)?;
920 self.json.key("content").open_object()?;
921 self.json.key("type").value("tableContent")?;
922 self.json.key("columnWidths").array(|_| Ok(()))?;
923 self.json.key("rows").open_array()?;
924 self.table_depth.inc();
925 self.open_block_ancestors.push(BlockKind::Table);
926 self.context.in_text_block = false;
927 Ok(())
928 }
929
930 fn handle_start_table_row(&mut self, id: Option<&str>) -> Result<()> {
931 self.json.open_object()?;
932 self.write_id(id)?;
933 self.json.key("cells").open_array()
934 }
935
936 fn handle_start_table_cell_event(&mut self, event: Event) -> Result<()> {
937 match event {
938 Event::StartTableCell {
939 colspan,
940 id,
941 rowspan,
942 ..
943 }
944 | Event::StartTableHeader {
945 colspan,
946 id,
947 rowspan,
948 ..
949 } => self.handle_table_cell(id.as_deref(), colspan, rowspan),
950 _ => Ok(()),
951 }
952 }
953
954 fn handle_table_cell(
955 &mut self,
956 id: Option<&str>,
957 colspan: Option<u32>,
958 rowspan: Option<u32>,
959 ) -> Result<()> {
960 self.json.open_object()?;
961 self.json.key("type").value("tableCell")?;
962 self.write_id(id)?;
963 if colspan.is_some() || rowspan.is_some() {
964 self.json.key("props").object(|j| {
965 if let Some(n) = colspan {
966 j.key("colspan").value(n)?;
967 }
968 if let Some(n) = rowspan {
969 j.key("rowspan").value(n)?;
970 }
971 Ok(())
972 })?;
973 }
974 self.json.key("content").open_array()?;
975 self.context.in_table_cell = true;
976 self.context.in_text_block = false;
977 Ok(())
978 }
979
980 fn handle_text(&mut self, content: &str) -> Result<()> {
981 if !self.context.in_text_block
982 && !self.context.in_table_cell
983 && !self.in_list_item_content()
984 {
985 return Ok(());
986 }
987 if self.blockquote_depth.is_positive() {
988 self.context.blockquote_has_content = true;
989 }
990 let mut bold = false;
991 let mut italic = false;
992 let mut code = false;
993 let mut strike = false;
994 let mut underline = false;
995 let mut text_color: Option<docspec_core::Color> = None;
996 let mut background_color: Option<docspec_core::Color> = None;
997
998 for kind in &self.open_styles {
999 match kind {
1000 TextStyleKind::Bold => bold = true,
1001 TextStyleKind::Italic => italic = true,
1002 TextStyleKind::Code => code = true,
1003 TextStyleKind::Strikethrough => strike = true,
1004 TextStyleKind::Underline => underline = true,
1005 TextStyleKind::Subscript => {
1006 Self::omit_unsupported_text_style("subscript");
1008 }
1009 TextStyleKind::Superscript => {
1010 Self::omit_unsupported_text_style("superscript");
1012 }
1013 TextStyleKind::TextColor(color) => text_color = Some(*color),
1014 TextStyleKind::Mark(color) => background_color = Some(*color),
1015 future_kind => {
1016 Self::omit_future_text_style(future_kind);
1018 }
1019 }
1020 }
1021
1022 self.json.object(|j| {
1023 j.key("type").value("text")?;
1024 j.key("text").value(content)?;
1025 j.key("styles").object(|s| {
1026 for (key, enabled) in [
1027 ("bold", bold),
1028 ("italic", italic),
1029 ("code", code),
1030 ("strike", strike),
1031 ("underline", underline),
1032 ] {
1033 if enabled {
1034 s.key(key).value(true)?;
1035 }
1036 }
1037 if let Some(c) = text_color {
1038 if let Some(name) = palette::nearest_text_color(&c) {
1039 s.key("textColor").value(name)?;
1040 }
1041 }
1042 if let Some(c) = background_color {
1043 if let Some(name) = palette::nearest_background_color(&c) {
1044 s.key("backgroundColor").value(name)?;
1045 }
1046 }
1047 Ok(())
1048 })
1049 })?;
1050 if self.in_link {
1051 self.link_emitted_styled_text = true;
1052 }
1053 Ok(())
1054 }
1055
1056 fn handle_text_event(&mut self, content: &str) -> Result<()> {
1057 if self.list_stack.last().is_some_and(|entry| {
1059 entry.content_state == ListContentState::Pending && !entry.first_paragraph_consumed
1060 }) {
1061 self.initialize_current_list_item_content(None)?;
1062 }
1063 if self.blockquote_depth.is_positive() && !self.context.in_text_block {
1064 self.prepare_blockquote_inline_content()?;
1065 }
1066 if !self.context.in_text_block
1067 && self.blockquote_depth.is_zero()
1068 && !self.context.in_table_cell
1069 && !self.in_list_item_content()
1070 {
1071 self.handle_paragraph(None, None)?;
1072 }
1073 self.handle_text(content)
1074 }
1075
1076 fn in_list_item_content(&self) -> bool {
1077 self.list_stack
1078 .last()
1079 .is_some_and(|entry| entry.content_state == ListContentState::Open)
1080 }
1081
1082 #[inline]
1088 #[must_use]
1089 pub fn new(writer: W) -> Self {
1090 Self {
1091 blockquote_depth: Depth::default(),
1092 blockquote_content_states: Vec::new(),
1093 context: BlockContext::default(),
1094 in_link: false,
1095 json: JsonEmitter::new(StrusonBackend::new(writer)),
1096 lifted_nested_events: Vec::new(),
1097 lifted_subtree_depth: Depth::default(),
1098 link_emitted_styled_text: false,
1099 list_stack: Vec::new(),
1100 open_block_ancestors: Vec::new(),
1101 open_styles: Vec::new(),
1102 table_depth: Depth::default(),
1103 }
1104 }
1105
1106 fn current_drain_destination(&self) -> DrainDestination {
1107 for (idx, kind) in self.open_block_ancestors.iter().enumerate().rev() {
1108 if blocknote_accepts_child(Some(*kind), ChildKind::Container(BlockKind::Heading)) {
1109 return DrainDestination::Ancestor {
1110 ancestor_index: idx,
1111 kind: *kind,
1112 };
1113 }
1114 }
1115 DrainDestination::DocumentRoot
1116 }
1117
1118 fn compute_rebase_offset(&self, buffered: &[Event]) -> u32 {
1119 let min_buffered_level = buffered
1120 .iter()
1121 .filter_map(|event| match event {
1122 Event::StartOrderedListItem { level, .. }
1123 | Event::StartUnorderedListItem { level, .. } => Some(*level),
1124 _ => None,
1125 })
1126 .min();
1127
1128 let Some(min_level) = min_buffered_level else {
1129 return 0;
1130 };
1131
1132 let required_min_level = self
1133 .list_stack
1134 .last()
1135 .map_or(0, |entry| entry.level.saturating_add(1));
1136 required_min_level.saturating_sub(min_level)
1137 }
1138
1139 fn pop_ancestor(&mut self, kind: BlockKind) {
1140 if let Some(pos) = self
1141 .open_block_ancestors
1142 .iter()
1143 .rposition(|entry| *entry == kind)
1144 {
1145 self.open_block_ancestors.remove(pos);
1146 }
1147 }
1148
1149 fn initialize_current_list_item_content(
1150 &mut self,
1151 alignment: Option<&TextAlignment>,
1152 ) -> Result<()> {
1153 let Some(current_entry) = self.list_stack.last() else {
1154 return Ok(());
1155 };
1156 if current_entry.content_state != ListContentState::Pending {
1157 return Ok(());
1158 }
1159 let kind = current_entry.kind;
1160 let start = current_entry.start;
1161 let alignment_value = non_default_alignment_value(alignment);
1162 if alignment_value.is_some() || (kind == ListKind::Ordered && start.is_some()) {
1163 self.json.key("props").object(|j| {
1164 if let Some(value) = alignment_value {
1165 j.key("textAlignment").value(value)?;
1166 }
1167 if kind == ListKind::Ordered {
1168 if let Some(start_prop) = start {
1169 j.key("start").value(start_prop)?;
1170 }
1171 }
1172 Ok(())
1173 })?;
1174 }
1175 self.json.key("content").open_array()?;
1176 if let Some(entry) = self.list_stack.last_mut() {
1177 entry.content_state = ListContentState::Open;
1178 }
1179 Ok(())
1180 }
1181
1182 fn open_current_list_item_children(&mut self) -> Result<()> {
1183 if self
1184 .list_stack
1185 .last()
1186 .is_some_and(|entry| entry.content_state == ListContentState::Pending)
1187 {
1188 self.initialize_current_list_item_content(None)?;
1189 }
1190 let content_array_open = self
1191 .list_stack
1192 .last()
1193 .is_some_and(|entry| entry.content_state == ListContentState::Open);
1194 if content_array_open {
1195 self.json.close_array()?;
1196 if let Some(entry) = self.list_stack.last_mut() {
1197 entry.content_state = ListContentState::Closed;
1198 entry.first_paragraph_consumed = true;
1199 }
1200 }
1201
1202 let children_array_open = self
1203 .list_stack
1204 .last()
1205 .is_some_and(|entry| entry.children_array_open);
1206 if !children_array_open {
1207 self.json.key("children").open_array()?;
1208 if let Some(entry) = self.list_stack.last_mut() {
1209 entry.children_array_open = true;
1210 }
1211 }
1212
1213 self.close_open_child_text_block()
1214 }
1215
1216 fn prepare_list_item_children(&mut self) -> Result<()> {
1217 self.open_current_list_item_children()
1218 }
1219
1220 fn prepare_for_child_block(&mut self) -> Result<()> {
1221 match self.current_drain_destination() {
1222 DrainDestination::Ancestor {
1223 kind: BlockKind::Blockquote,
1224 ..
1225 } => self.prepare_blockquote_children(),
1226 DrainDestination::Ancestor {
1227 kind: BlockKind::OrderedListItem | BlockKind::UnorderedListItem,
1228 ..
1229 } => self.prepare_list_item_children(),
1230 DrainDestination::Ancestor { .. } | DrainDestination::DocumentRoot => {
1231 self.close_for_block_sibling()
1232 }
1233 }
1234 }
1235
1236 fn open_list_item_object(
1237 &mut self,
1238 kind: ListKind,
1239 level: u32,
1240 start: Option<u64>,
1241 ) -> Result<()> {
1242 self.json.open_object()?;
1243 let type_name = match kind {
1244 ListKind::Ordered => "numberedListItem",
1245 ListKind::Unordered => "bulletListItem",
1246 };
1247 self.json.key("type").value(type_name)?;
1248 let checked_start = start
1249 .map(|start_value| {
1250 u32::try_from(start_value).map_err(|err| Error::Other {
1251 message: format!("ordered list start value out of range: {start_value}: {err}"),
1252 })
1253 })
1254 .transpose()?;
1255 self.list_stack.push(ListStackEntry {
1256 children_array_open: false,
1257 content_state: ListContentState::Pending,
1258 first_paragraph_consumed: false,
1259 kind,
1260 level,
1261 start: checked_start,
1262 });
1263 let ancestor_kind = match kind {
1264 ListKind::Ordered => BlockKind::OrderedListItem,
1265 ListKind::Unordered => BlockKind::UnorderedListItem,
1266 };
1267 self.open_block_ancestors.push(ancestor_kind);
1268 Ok(())
1269 }
1270
1271 fn handle_end_document(&mut self) -> Result<()> {
1272 while !self.list_stack.is_empty() {
1273 self.close_current_list_item_object()?;
1274 }
1275 self.json.close_array()
1276 }
1277
1278 fn write_id(&mut self, id: Option<&str>) -> Result<()> {
1279 if let Some(id_val) = id {
1280 self.json.key("id").value(id_val)?;
1281 }
1282 Ok(())
1283 }
1284
1285 fn omit_unsupported_text_style(_style_name: &str) {}
1286
1287 fn omit_future_text_style(_style: &TextStyleKind) {}
1288
1289 fn should_buffer_for_lift(&self, event: &Event) -> bool {
1290 if self.table_depth.get() >= 2 || self.lifted_subtree_depth.is_positive() {
1291 return true;
1292 }
1293 if self.context.in_table_cell && Self::is_block_level_start(event) {
1294 return true;
1295 }
1296 if matches!(event, Event::Image { .. }) && self.context.in_table_cell {
1297 return true;
1298 }
1299 if matches!(event, Event::StartTable { .. }) && self.table_depth.is_positive() {
1300 return true;
1301 }
1302 false
1303 }
1304
1305 fn update_lift_depth(&mut self, event: &Event) {
1306 match event {
1307 Event::StartTable { .. } => self.table_depth.inc(),
1308 Event::EndTable => self.table_depth.dec(),
1309 _ => {}
1310 }
1311 let non_leaf_block_start =
1312 Self::is_block_level_start(event) && !matches!(event, Event::ThematicBreak { .. });
1313 let non_leaf_block_end =
1314 Self::is_block_level_end(event) && !matches!(event, Event::ThematicBreak { .. });
1315 if non_leaf_block_start
1316 && ((self.context.in_table_cell && self.table_depth.get() == 1)
1317 || self.lifted_subtree_depth.is_positive())
1318 {
1319 self.lifted_subtree_depth.inc();
1320 }
1321 if non_leaf_block_end && self.lifted_subtree_depth.is_positive() {
1322 self.lifted_subtree_depth.dec();
1323 }
1324 }
1325
1326 fn is_outermost_table_close(&self, event: &Event) -> bool {
1327 matches!(event, Event::EndTable) && self.table_depth.get() == 1
1328 }
1329
1330 fn drain_lifted_nested_events(&mut self) -> Result<()> {
1331 let buffered = core::mem::take(&mut self.lifted_nested_events);
1332 if buffered.is_empty() {
1333 return Ok(());
1334 }
1335 match self.current_drain_destination() {
1336 DrainDestination::Ancestor {
1337 ancestor_index,
1338 kind: BlockKind::Blockquote,
1339 } => {
1340 let _ = ancestor_index;
1341 self.prepare_blockquote_children()?;
1342 }
1343 DrainDestination::Ancestor {
1344 ancestor_index,
1345 kind: BlockKind::OrderedListItem | BlockKind::UnorderedListItem,
1346 } => {
1347 let _ = ancestor_index;
1348 self.prepare_list_item_children()?;
1349 }
1350 DrainDestination::Ancestor { ancestor_index, .. } => {
1351 let _ = ancestor_index;
1352 self.close_for_block_sibling()?;
1353 }
1354 DrainDestination::DocumentRoot => {
1355 self.close_for_block_sibling()?;
1356 }
1357 }
1358 let rebase_offset = match self.current_drain_destination() {
1359 DrainDestination::Ancestor {
1360 kind: BlockKind::OrderedListItem | BlockKind::UnorderedListItem,
1361 ..
1362 } => self.compute_rebase_offset(&buffered),
1363 DrainDestination::Ancestor { .. } | DrainDestination::DocumentRoot => 0,
1364 };
1365 for ev in buffered {
1366 let rebased_event = if rebase_offset > 0 {
1367 match ev {
1368 Event::StartOrderedListItem {
1369 id,
1370 level,
1371 start,
1372 style_type,
1373 } => Event::StartOrderedListItem {
1374 id,
1375 level: level.saturating_add(rebase_offset),
1376 start,
1377 style_type,
1378 },
1379 Event::StartUnorderedListItem {
1380 id,
1381 level,
1382 style_type,
1383 } => Event::StartUnorderedListItem {
1384 id,
1385 level: level.saturating_add(rebase_offset),
1386 style_type,
1387 },
1388 other => other,
1389 }
1390 } else {
1391 ev
1392 };
1393 self.handle_event(rebased_event)?;
1394 }
1395 Ok(())
1396 }
1397
1398 fn is_block_level_start(event: &Event) -> bool {
1399 matches!(
1400 event,
1401 Event::StartHeading { .. }
1402 | Event::StartBlockQuote { .. }
1403 | Event::StartPreformatted { .. }
1404 | Event::StartOrderedListItem { .. }
1405 | Event::StartUnorderedListItem { .. }
1406 | Event::ThematicBreak { .. }
1407 )
1408 }
1409
1410 fn is_block_level_end(event: &Event) -> bool {
1411 matches!(
1412 event,
1413 Event::EndHeading
1414 | Event::EndBlockQuote
1415 | Event::EndPreformatted
1416 | Event::EndOrderedListItem
1417 | Event::EndUnorderedListItem
1418 | Event::ThematicBreak { .. }
1419 )
1420 }
1421}
1422
1423impl<W: Write> EventSink for BlockNoteWriter<W> {
1424 #[inline]
1425 fn finish(self) -> Result<()> {
1426 self.json.finish().map(|_| ())
1427 }
1428
1429 #[inline]
1430 fn handle_event(&mut self, event: Event) -> Result<()> {
1431 if self.should_buffer_for_lift(&event) {
1432 self.update_lift_depth(&event);
1433 self.lifted_nested_events.push(event);
1434 return Ok(());
1435 }
1436 let is_outermost_table_close = self.is_outermost_table_close(&event);
1437 let result = match event {
1438 Event::StartDocument { .. } => self.json.open_array(),
1439 Event::EndDocument => self.handle_end_document(),
1440 Event::StartHeading { level, id, .. } => {
1441 self.prepare_for_child_block()?;
1442 self.handle_heading(level, id.as_deref())
1443 }
1444 Event::EndHeading | Event::EndPreformatted => {
1445 if !self.context.in_text_block {
1446 return Ok(());
1447 }
1448 close_text_block!(self)
1449 }
1450 Event::StartParagraph { alignment, id } => {
1451 self.handle_paragraph(id.as_deref(), alignment.as_ref())
1452 }
1453 Event::EndParagraph => self.handle_end_paragraph(),
1454 Event::StartBlockQuote { id, .. } => {
1455 self.prepare_for_child_block()?;
1456 self.handle_blockquote(id.as_deref())
1457 }
1458 Event::EndBlockQuote => self.handle_end_blockquote(),
1459 Event::StartPreformatted { id, syntax, .. } => {
1460 self.prepare_for_child_block()?;
1461 self.handle_preformatted(id.as_deref(), syntax.as_deref())
1462 }
1463 Event::ThematicBreak { id, .. } => {
1464 self.prepare_for_child_block()?;
1465 self.handle_divider(id.as_deref())
1466 }
1467 Event::Text { content } => self.handle_text_event(&content),
1468 Event::StartTextStyle { kind, .. } => self.handle_start_text_style(kind),
1469 Event::EndTextStyle => self.handle_end_text_style(),
1470 Event::Image {
1471 source, alt, id, ..
1472 } => self.handle_image(source, alt, id.as_deref()),
1473 Event::LineBreak | Event::SoftBreak => self.handle_line_break(),
1474 Event::StartOrderedListItem { level, start, .. } => {
1475 self.handle_start_list_item(ListKind::Ordered, level, start)
1476 }
1477 Event::StartUnorderedListItem { level, .. } => {
1478 self.handle_start_list_item(ListKind::Unordered, level, None)
1479 }
1480 Event::EndOrderedListItem | Event::EndUnorderedListItem => self.handle_end_list_item(),
1481 Event::StartTable { id, .. } => self.handle_start_table(id.as_deref()),
1482 Event::EndTable => self.handle_end_table(),
1483 Event::StartTableRow { id, .. } => self.handle_start_table_row(id.as_deref()),
1484 Event::EndTableRow => self.handle_end_table_row(),
1485 event @ (Event::StartTableCell { .. } | Event::StartTableHeader { .. }) => {
1486 self.handle_start_table_cell_event(event)
1487 }
1488 Event::EndTableCell | Event::EndTableHeader => self.handle_end_table_cell(),
1489 Event::StartLink { href, .. } => self.handle_start_link(&href),
1490 Event::EndLink => self.handle_end_link(),
1491 Event::EndCaption
1492 | Event::EndDefinitionDetail
1493 | Event::EndDefinitionList
1494 | Event::EndDefinitionTerm
1495 | Event::EndFootnote
1496 | Event::FootnoteRef { .. }
1497 | Event::StartCaption { .. }
1498 | Event::StartDefinitionDetail { .. }
1499 | Event::StartDefinitionList { .. }
1500 | Event::StartDefinitionTerm { .. }
1501 | Event::StartFootnote { .. }
1502 | _ => Ok(()),
1503 };
1504 if is_outermost_table_close {
1505 result?;
1506 return self.drain_lifted_nested_events();
1507 }
1508 result
1509 }
1510}
1511
1512#[cfg(test)]
1513mod tests {
1514 use super::*;
1515
1516 #[test]
1517 fn list_stack_empty_after_new() {
1518 let mut buf = Vec::new();
1519 let writer = BlockNoteWriter::new(&mut buf);
1520 assert!(writer.list_stack.is_empty());
1521 }
1522
1523 #[test]
1524 fn blockquote_state_stack_empty_after_new() {
1525 let mut buf = Vec::new();
1526 let writer = BlockNoteWriter::new(&mut buf);
1527 assert!(writer.blockquote_content_states.is_empty());
1528 }
1529
1530 #[test]
1531 fn current_drain_destination_unit_walks_stack_top_to_bottom() {
1532 let mut buf = Vec::new();
1533 let mut writer = BlockNoteWriter::new(&mut buf);
1534
1535 writer.open_block_ancestors.push(BlockKind::OrderedListItem);
1536 writer.open_block_ancestors.push(BlockKind::Table);
1537 writer.open_block_ancestors.push(BlockKind::Blockquote);
1538
1539 assert!(matches!(
1540 writer.current_drain_destination(),
1541 DrainDestination::Ancestor {
1542 ancestor_index: 2,
1543 kind: BlockKind::Blockquote,
1544 }
1545 ));
1546 }
1547
1548 #[test]
1549 fn current_drain_destination_skips_table_to_document_root() {
1550 let mut buf = Vec::new();
1551 let mut writer = BlockNoteWriter::new(&mut buf);
1552 writer.open_block_ancestors.push(BlockKind::Table);
1553
1554 assert!(matches!(
1555 writer.current_drain_destination(),
1556 DrainDestination::DocumentRoot
1557 ));
1558 }
1559
1560 #[test]
1561 fn compute_rebase_offset_without_list_items_is_zero() {
1562 let mut buf = Vec::new();
1563 let writer = BlockNoteWriter::new(&mut buf);
1564
1565 assert_eq!(
1566 writer.compute_rebase_offset(&[Event::StartParagraph {
1567 alignment: None,
1568 id: None,
1569 }]),
1570 0
1571 );
1572 }
1573
1574 #[test]
1575 fn compute_rebase_offset_without_destination_list_is_zero_for_level_zero() {
1576 let mut buf = Vec::new();
1577 let writer = BlockNoteWriter::new(&mut buf);
1578
1579 assert_eq!(
1580 writer.compute_rebase_offset(&[Event::StartUnorderedListItem {
1581 id: None,
1582 level: 0,
1583 style_type: docspec_core::ListStyleType::Disc,
1584 }]),
1585 0
1586 );
1587 }
1588
1589 #[test]
1590 fn compute_rebase_offset_uses_current_list_stack_level() {
1591 let mut buf = Vec::new();
1592 let mut writer = BlockNoteWriter::new(&mut buf);
1593 writer.list_stack.push(ListStackEntry {
1594 children_array_open: true,
1595 content_state: ListContentState::Closed,
1596 first_paragraph_consumed: true,
1597 kind: ListKind::Unordered,
1598 level: 2,
1599 start: None,
1600 });
1601
1602 assert_eq!(
1603 writer.compute_rebase_offset(&[
1604 Event::StartOrderedListItem {
1605 id: None,
1606 level: 4,
1607 start: Some(1),
1608 style_type: docspec_core::ListStyleType::Decimal,
1609 },
1610 Event::StartUnorderedListItem {
1611 id: None,
1612 level: 0,
1613 style_type: docspec_core::ListStyleType::Disc,
1614 },
1615 ]),
1616 3
1617 );
1618 }
1619
1620 #[test]
1621 fn compute_rebase_offset_does_not_lower_already_nested_items() {
1622 let mut buf = Vec::new();
1623 let mut writer = BlockNoteWriter::new(&mut buf);
1624 writer.list_stack.push(ListStackEntry {
1625 children_array_open: true,
1626 content_state: ListContentState::Closed,
1627 first_paragraph_consumed: true,
1628 kind: ListKind::Unordered,
1629 level: 1,
1630 start: None,
1631 });
1632
1633 assert_eq!(
1634 writer.compute_rebase_offset(&[Event::StartUnorderedListItem {
1635 id: None,
1636 level: 4,
1637 style_type: docspec_core::ListStyleType::Disc,
1638 }]),
1639 0
1640 );
1641 }
1642
1643 #[test]
1644 fn close_for_block_sibling_with_open_blockquote_prepares_children() {
1645 let mut buf = Vec::new();
1646 let mut writer = BlockNoteWriter::new(&mut buf);
1647 assert!(writer
1648 .handle_event(Event::StartDocument {
1649 id: None,
1650 language: None,
1651 metadata: None,
1652 })
1653 .is_ok());
1654 assert!(writer
1655 .handle_event(Event::StartBlockQuote { id: None })
1656 .is_ok());
1657
1658 assert!(writer.close_for_block_sibling().is_ok());
1659 assert_eq!(
1660 writer.blockquote_content_states.last(),
1661 Some(&BlockquoteContentState::InChildren)
1662 );
1663 }
1664
1665 #[test]
1666 fn close_for_block_sibling_with_nonempty_list_stack_closes_all_items() {
1667 let mut buf = Vec::new();
1671 let mut writer = BlockNoteWriter::new(&mut buf);
1672 assert!(writer
1673 .handle_event(Event::StartDocument {
1674 id: None,
1675 language: None,
1676 metadata: None,
1677 })
1678 .is_ok());
1679 assert!(writer
1680 .handle_event(Event::StartUnorderedListItem {
1681 id: None,
1682 level: 0,
1683 style_type: docspec_core::ListStyleType::Disc,
1684 })
1685 .is_ok());
1686 assert!(
1687 !writer.list_stack.is_empty(),
1688 "list_stack must be non-empty before calling close_for_block_sibling"
1689 );
1690 assert!(writer.close_for_block_sibling().is_ok());
1691 assert!(
1692 writer.list_stack.is_empty(),
1693 "close_for_block_sibling must drain list_stack via close_open_list_items"
1694 );
1695 assert!(writer.handle_event(Event::EndDocument).is_ok());
1696 assert!(writer.finish().is_ok());
1697 }
1698}
1699
1700#[cfg(test)]
1701mod accepts_child_tests {
1702 #![allow(clippy::bool_assert_comparison)]
1703 use super::*;
1704 use docspec_core::BlockKind;
1705
1706 #[test]
1707 fn document_root_accepts_all_block_kinds() {
1708 assert_eq!(
1709 blocknote_accepts_child(None, ChildKind::Container(BlockKind::Blockquote)),
1710 true
1711 );
1712 assert_eq!(
1713 blocknote_accepts_child(None, ChildKind::Container(BlockKind::Caption)),
1714 true
1715 );
1716 assert_eq!(
1717 blocknote_accepts_child(None, ChildKind::Container(BlockKind::DefinitionDetail)),
1718 true
1719 );
1720 assert_eq!(
1721 blocknote_accepts_child(None, ChildKind::Container(BlockKind::DefinitionList)),
1722 true
1723 );
1724 assert_eq!(
1725 blocknote_accepts_child(None, ChildKind::Container(BlockKind::DefinitionTerm)),
1726 true
1727 );
1728 assert_eq!(
1729 blocknote_accepts_child(None, ChildKind::Container(BlockKind::Document)),
1730 true
1731 );
1732 assert_eq!(
1733 blocknote_accepts_child(None, ChildKind::Container(BlockKind::Footnote)),
1734 true
1735 );
1736 assert_eq!(
1737 blocknote_accepts_child(None, ChildKind::Container(BlockKind::Heading)),
1738 true
1739 );
1740 assert_eq!(
1741 blocknote_accepts_child(None, ChildKind::Container(BlockKind::Link)),
1742 true
1743 );
1744 assert_eq!(
1745 blocknote_accepts_child(None, ChildKind::Container(BlockKind::OrderedListItem)),
1746 true
1747 );
1748 assert_eq!(
1749 blocknote_accepts_child(None, ChildKind::Container(BlockKind::Paragraph)),
1750 true
1751 );
1752 assert_eq!(
1753 blocknote_accepts_child(None, ChildKind::Container(BlockKind::Preformatted)),
1754 true
1755 );
1756 assert_eq!(
1757 blocknote_accepts_child(None, ChildKind::Container(BlockKind::Table)),
1758 true
1759 );
1760 assert_eq!(
1761 blocknote_accepts_child(None, ChildKind::Container(BlockKind::TableCell)),
1762 true
1763 );
1764 assert_eq!(
1765 blocknote_accepts_child(None, ChildKind::Container(BlockKind::TableHeader)),
1766 true
1767 );
1768 assert_eq!(
1769 blocknote_accepts_child(None, ChildKind::Container(BlockKind::TableRow)),
1770 true
1771 );
1772 assert_eq!(
1773 blocknote_accepts_child(None, ChildKind::Container(BlockKind::UnorderedListItem)),
1774 true
1775 );
1776 assert_eq!(
1777 blocknote_accepts_child(None, ChildKind::Leaf(BlockLeafKind::Image)),
1778 true
1779 );
1780 assert_eq!(
1781 blocknote_accepts_child(None, ChildKind::Leaf(BlockLeafKind::Divider)),
1782 true
1783 );
1784 }
1785
1786 #[test]
1787 fn heading_accepts_paragraph_child() {
1788 assert_eq!(
1789 blocknote_accepts_child(
1790 Some(BlockKind::Heading),
1791 ChildKind::Container(BlockKind::Paragraph),
1792 ),
1793 true
1794 );
1795 }
1796
1797 #[test]
1798 fn list_item_accepts_heading_child() {
1799 assert_eq!(
1800 blocknote_accepts_child(
1801 Some(BlockKind::OrderedListItem),
1802 ChildKind::Container(BlockKind::Heading),
1803 ),
1804 true
1805 );
1806 }
1807
1808 #[test]
1809 fn blockquote_accepts_list_item_child() {
1810 assert_eq!(
1811 blocknote_accepts_child(
1812 Some(BlockKind::Blockquote),
1813 ChildKind::Container(BlockKind::UnorderedListItem),
1814 ),
1815 true
1816 );
1817 }
1818
1819 #[test]
1820 fn table_cell_rejects_heading() {
1821 assert_eq!(
1822 blocknote_accepts_child(
1823 Some(BlockKind::TableCell),
1824 ChildKind::Container(BlockKind::Heading),
1825 ),
1826 false
1827 );
1828 }
1829
1830 #[test]
1831 fn table_cell_rejects_list_item() {
1832 assert_eq!(
1833 blocknote_accepts_child(
1834 Some(BlockKind::TableCell),
1835 ChildKind::Container(BlockKind::OrderedListItem),
1836 ),
1837 false
1838 );
1839 }
1840
1841 #[test]
1842 fn code_block_rejects_heading() {
1843 assert_eq!(
1844 blocknote_accepts_child(
1845 Some(BlockKind::Preformatted),
1846 ChildKind::Container(BlockKind::Heading),
1847 ),
1848 false
1849 );
1850 }
1851
1852 #[test]
1853 fn paragraph_rejects_blocks() {
1854 assert_eq!(
1855 blocknote_accepts_child(
1856 Some(BlockKind::Paragraph),
1857 ChildKind::Container(BlockKind::Blockquote),
1858 ),
1859 false
1860 );
1861 }
1862
1863 #[test]
1864 fn heading_rejects_table_row_child() {
1865 assert_eq!(
1866 blocknote_accepts_child(
1867 Some(BlockKind::Heading),
1868 ChildKind::Container(BlockKind::TableRow),
1869 ),
1870 false
1871 );
1872 }
1873
1874 #[test]
1875 fn heading_rejects_table_cell_child() {
1876 assert_eq!(
1877 blocknote_accepts_child(
1878 Some(BlockKind::Heading),
1879 ChildKind::Container(BlockKind::TableCell),
1880 ),
1881 false
1882 );
1883 }
1884
1885 #[test]
1886 fn heading_rejects_table_header_child() {
1887 assert_eq!(
1888 blocknote_accepts_child(
1889 Some(BlockKind::Heading),
1890 ChildKind::Container(BlockKind::TableHeader),
1891 ),
1892 false
1893 );
1894 }
1895
1896 #[test]
1897 fn heading_accepts_image_leaf() {
1898 assert_eq!(
1899 blocknote_accepts_child(
1900 Some(BlockKind::Heading),
1901 ChildKind::Leaf(BlockLeafKind::Image)
1902 ),
1903 true
1904 );
1905 }
1906
1907 #[test]
1908 fn heading_accepts_divider_leaf() {
1909 assert_eq!(
1910 blocknote_accepts_child(
1911 Some(BlockKind::Heading),
1912 ChildKind::Leaf(BlockLeafKind::Divider),
1913 ),
1914 true
1915 );
1916 }
1917
1918 #[test]
1919 fn blockquote_accepts_heading_child() {
1920 assert_eq!(
1921 blocknote_accepts_child(
1922 Some(BlockKind::Blockquote),
1923 ChildKind::Container(BlockKind::Heading),
1924 ),
1925 true
1926 );
1927 }
1928
1929 #[test]
1930 fn blockquote_accepts_table_child() {
1931 assert_eq!(
1932 blocknote_accepts_child(
1933 Some(BlockKind::Blockquote),
1934 ChildKind::Container(BlockKind::Table),
1935 ),
1936 true
1937 );
1938 }
1939
1940 #[test]
1941 fn blockquote_rejects_table_row_child() {
1942 assert_eq!(
1943 blocknote_accepts_child(
1944 Some(BlockKind::Blockquote),
1945 ChildKind::Container(BlockKind::TableRow),
1946 ),
1947 false
1948 );
1949 }
1950
1951 #[test]
1952 fn blockquote_rejects_table_cell_child() {
1953 assert_eq!(
1954 blocknote_accepts_child(
1955 Some(BlockKind::Blockquote),
1956 ChildKind::Container(BlockKind::TableCell),
1957 ),
1958 false
1959 );
1960 }
1961
1962 #[test]
1963 fn blockquote_rejects_table_header_child() {
1964 assert_eq!(
1965 blocknote_accepts_child(
1966 Some(BlockKind::Blockquote),
1967 ChildKind::Container(BlockKind::TableHeader),
1968 ),
1969 false
1970 );
1971 }
1972
1973 #[test]
1974 fn ordered_list_item_accepts_image_leaf() {
1975 assert_eq!(
1976 blocknote_accepts_child(
1977 Some(BlockKind::OrderedListItem),
1978 ChildKind::Leaf(BlockLeafKind::Image),
1979 ),
1980 true
1981 );
1982 }
1983
1984 #[test]
1985 fn ordered_list_item_rejects_table_row_child() {
1986 assert_eq!(
1987 blocknote_accepts_child(
1988 Some(BlockKind::OrderedListItem),
1989 ChildKind::Container(BlockKind::TableRow),
1990 ),
1991 false
1992 );
1993 }
1994
1995 #[test]
1996 fn ordered_list_item_rejects_table_cell_child() {
1997 assert_eq!(
1998 blocknote_accepts_child(
1999 Some(BlockKind::OrderedListItem),
2000 ChildKind::Container(BlockKind::TableCell),
2001 ),
2002 false
2003 );
2004 }
2005
2006 #[test]
2007 fn ordered_list_item_rejects_table_header_child() {
2008 assert_eq!(
2009 blocknote_accepts_child(
2010 Some(BlockKind::OrderedListItem),
2011 ChildKind::Container(BlockKind::TableHeader),
2012 ),
2013 false
2014 );
2015 }
2016
2017 #[test]
2018 fn unordered_list_item_accepts_preformatted_child() {
2019 assert_eq!(
2020 blocknote_accepts_child(
2021 Some(BlockKind::UnorderedListItem),
2022 ChildKind::Container(BlockKind::Preformatted),
2023 ),
2024 true
2025 );
2026 }
2027
2028 #[test]
2029 fn unordered_list_item_accepts_divider_leaf() {
2030 assert_eq!(
2031 blocknote_accepts_child(
2032 Some(BlockKind::UnorderedListItem),
2033 ChildKind::Leaf(BlockLeafKind::Divider),
2034 ),
2035 true
2036 );
2037 }
2038
2039 #[test]
2040 fn unordered_list_item_rejects_table_row_child() {
2041 assert_eq!(
2042 blocknote_accepts_child(
2043 Some(BlockKind::UnorderedListItem),
2044 ChildKind::Container(BlockKind::TableRow),
2045 ),
2046 false
2047 );
2048 }
2049
2050 #[test]
2051 fn unordered_list_item_rejects_table_cell_child() {
2052 assert_eq!(
2053 blocknote_accepts_child(
2054 Some(BlockKind::UnorderedListItem),
2055 ChildKind::Container(BlockKind::TableCell),
2056 ),
2057 false
2058 );
2059 }
2060
2061 #[test]
2062 fn unordered_list_item_rejects_table_header_child() {
2063 assert_eq!(
2064 blocknote_accepts_child(
2065 Some(BlockKind::UnorderedListItem),
2066 ChildKind::Container(BlockKind::TableHeader),
2067 ),
2068 false
2069 );
2070 }
2071
2072 #[test]
2073 fn caption_rejects_paragraph_child() {
2074 assert_eq!(
2075 blocknote_accepts_child(
2076 Some(BlockKind::Caption),
2077 ChildKind::Container(BlockKind::Paragraph),
2078 ),
2079 false
2080 );
2081 }
2082
2083 #[test]
2084 fn definition_list_rejects_definition_term_child() {
2085 assert_eq!(
2086 blocknote_accepts_child(
2087 Some(BlockKind::DefinitionList),
2088 ChildKind::Container(BlockKind::DefinitionTerm),
2089 ),
2090 false
2091 );
2092 }
2093
2094 #[test]
2095 fn definition_term_rejects_paragraph_child() {
2096 assert_eq!(
2097 blocknote_accepts_child(
2098 Some(BlockKind::DefinitionTerm),
2099 ChildKind::Container(BlockKind::Paragraph),
2100 ),
2101 false
2102 );
2103 }
2104
2105 #[test]
2106 fn definition_detail_rejects_paragraph_child() {
2107 assert_eq!(
2108 blocknote_accepts_child(
2109 Some(BlockKind::DefinitionDetail),
2110 ChildKind::Container(BlockKind::Paragraph),
2111 ),
2112 false
2113 );
2114 }
2115
2116 #[test]
2117 fn footnote_rejects_paragraph_child() {
2118 assert_eq!(
2119 blocknote_accepts_child(
2120 Some(BlockKind::Footnote),
2121 ChildKind::Container(BlockKind::Paragraph),
2122 ),
2123 false
2124 );
2125 }
2126
2127 #[test]
2128 fn link_rejects_paragraph_child() {
2129 assert_eq!(
2130 blocknote_accepts_child(
2131 Some(BlockKind::Link),
2132 ChildKind::Container(BlockKind::Paragraph),
2133 ),
2134 false
2135 );
2136 }
2137
2138 #[test]
2139 fn document_container_rejects_paragraph_child() {
2140 assert_eq!(
2141 blocknote_accepts_child(
2142 Some(BlockKind::Document),
2143 ChildKind::Container(BlockKind::Paragraph),
2144 ),
2145 false
2146 );
2147 }
2148
2149 #[test]
2150 fn preformatted_rejects_image_leaf() {
2151 assert_eq!(
2152 blocknote_accepts_child(
2153 Some(BlockKind::Preformatted),
2154 ChildKind::Leaf(BlockLeafKind::Image),
2155 ),
2156 false
2157 );
2158 }
2159
2160 #[test]
2161 fn paragraph_rejects_divider_leaf() {
2162 assert_eq!(
2163 blocknote_accepts_child(
2164 Some(BlockKind::Paragraph),
2165 ChildKind::Leaf(BlockLeafKind::Divider),
2166 ),
2167 false
2168 );
2169 }
2170
2171 #[test]
2172 fn table_rejects_table_row_child() {
2173 assert_eq!(
2174 blocknote_accepts_child(
2175 Some(BlockKind::Table),
2176 ChildKind::Container(BlockKind::TableRow),
2177 ),
2178 false
2179 );
2180 }
2181
2182 #[test]
2183 fn table_rejects_image_leaf() {
2184 assert_eq!(
2185 blocknote_accepts_child(
2186 Some(BlockKind::Table),
2187 ChildKind::Leaf(BlockLeafKind::Image)
2188 ),
2189 false
2190 );
2191 }
2192
2193 #[test]
2194 fn table_row_rejects_table_cell_child() {
2195 assert_eq!(
2196 blocknote_accepts_child(
2197 Some(BlockKind::TableRow),
2198 ChildKind::Container(BlockKind::TableCell),
2199 ),
2200 false
2201 );
2202 }
2203
2204 #[test]
2205 fn table_row_rejects_heading_child() {
2206 assert_eq!(
2207 blocknote_accepts_child(
2208 Some(BlockKind::TableRow),
2209 ChildKind::Container(BlockKind::Heading),
2210 ),
2211 false
2212 );
2213 }
2214
2215 #[test]
2216 fn table_header_rejects_heading_child() {
2217 assert_eq!(
2218 blocknote_accepts_child(
2219 Some(BlockKind::TableHeader),
2220 ChildKind::Container(BlockKind::Heading),
2221 ),
2222 false
2223 );
2224 }
2225
2226 #[test]
2227 fn table_header_rejects_divider_leaf() {
2228 assert_eq!(
2229 blocknote_accepts_child(
2230 Some(BlockKind::TableHeader),
2231 ChildKind::Leaf(BlockLeafKind::Divider),
2232 ),
2233 false
2234 );
2235 }
2236}