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