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