1use std::io::Write;
148
149use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
150use base64::write::EncoderWriter as Base64Encoder;
151use docspec_core::{
152 AssetProvider, Depth, Error, Event, EventSink, ImageSource, Result, TextAlignment, TextStyle,
153};
154use docspec_json::{JsonEmitter, Null, StrusonBackend};
155
156macro_rules! close_text_block {
157 ($writer:expr) => {{
158 $writer.close_open_link_if_any()?;
159 $writer.close_content_block()?;
160 $writer.context.in_text_block = false;
161 Ok(())
162 }};
163}
164
165macro_rules! return_if_table_cell {
166 ($writer:expr) => {
167 if $writer.context.in_table_cell {
168 return Ok(());
169 }
170 };
171}
172
173macro_rules! drop_block_in_list_start {
174 ($writer:expr) => {
175 if $writer.in_any_list_item() || $writer.drop_inside_list_depth.is_positive() {
176 $writer.drop_inside_list_depth.inc();
177 return Ok(());
178 }
179 };
180}
181
182macro_rules! drop_block_in_list_end {
183 ($writer:expr) => {
184 if $writer.drop_inside_list_depth.is_positive() {
185 $writer.drop_inside_list_depth.dec();
186 return Ok(());
187 }
188 };
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193enum ListKind {
194 Ordered,
196 Unordered,
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201enum ListContentState {
202 Pending,
203 Open,
204 Closed,
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209struct ListStackEntry {
210 children_array_open: bool,
212 content_state: ListContentState,
214 first_paragraph_consumed: bool,
216 kind: ListKind,
218 level: u32,
220 start: Option<u32>,
222}
223
224#[derive(Default)]
225struct BlockContext {
226 blockquote_has_content: bool,
227 in_table_cell: bool,
228 in_text_block: bool,
229}
230
231fn non_default_alignment_value(alignment: Option<&TextAlignment>) -> Option<&'static str> {
232 match alignment {
233 Some(TextAlignment::Center) => Some("center"),
234 Some(TextAlignment::Right) => Some("right"),
235 Some(TextAlignment::Justify) => Some("justify"),
236 _ => None,
237 }
238}
239
240pub struct BlockNoteWriter<'a, W: Write> {
252 assets: Option<&'a dyn AssetProvider>,
253 blockquote_depth: Depth,
254 blockquote_force_closed_count: Depth,
255 context: BlockContext,
256 drop_inside_list_depth: Depth,
257 dropped_list_depth: Depth,
258 in_link: bool,
260 json: JsonEmitter<StrusonBackend<W>>,
261 link_emitted_styled_text: bool,
263 list_stack: Vec<ListStackEntry>,
264 table_depth: Depth,
265}
266
267impl<'a, W: Write> BlockNoteWriter<'a, W> {
268 fn close_blockquote_for_sibling(&mut self) -> Result<()> {
269 self.close_open_link_if_any()?;
270 self.close_content_block()?;
271 self.blockquote_depth.dec();
272 self.blockquote_force_closed_count.inc();
273 self.context.in_text_block = self.blockquote_depth.is_positive();
274 Ok(())
275 }
276
277 fn close_content_block(&mut self) -> Result<()> {
278 self.json.close_array()?;
279 self.json.key("children").array(|_| Ok(()))?;
280 self.json.close_object()
281 }
282
283 fn close_current_list_item_object(&mut self) -> Result<()> {
284 if self
285 .list_stack
286 .last()
287 .is_some_and(|entry| entry.content_state == ListContentState::Pending)
288 {
289 self.initialize_current_list_item_content(None)?;
290 }
291 let popped_entry = self.list_stack.pop();
292 if let Some(list_entry) = popped_entry {
293 if list_entry.content_state == ListContentState::Open {
294 self.close_open_link_if_any()?;
295 self.json.close_array()?;
296 }
297 if list_entry.children_array_open {
298 self.json.close_array()?;
299 } else {
300 self.json.key("children").array(|_| Ok(()))?;
301 }
302 self.json.close_object()?;
303 }
304 Ok(())
305 }
306
307 fn close_for_block_sibling(&mut self) -> Result<()> {
308 if !self.list_stack.is_empty() {
309 self.close_open_list_items()?;
310 }
311 if self.blockquote_depth.is_positive() {
312 return self.close_blockquote_for_sibling();
313 }
314 if self.context.in_text_block {
315 self.close_open_link_if_any()?;
316 self.close_content_block()?;
317 self.context.in_text_block = false;
318 }
319 Ok(())
320 }
321
322 fn close_open_link_if_any(&mut self) -> Result<()> {
328 if self.in_link {
329 self.handle_end_link()?;
330 }
331 Ok(())
332 }
333
334 fn close_open_list_items(&mut self) -> Result<()> {
335 while !self.list_stack.is_empty() {
336 self.close_current_list_item_object()?;
337 }
338 Ok(())
339 }
340
341 fn encode_asset_as_data_uri(&self, asset_id: &str) -> Result<String> {
348 let provider = self.assets.ok_or_else(|| Error::Other {
349 message: "no AssetProvider configured".to_string(),
350 })?;
351 let content_type = provider
352 .content_type(asset_id)
353 .ok_or_else(|| Error::Other {
354 message: format!("asset not found: {asset_id}"),
355 })?;
356 let prefix = format!("data:{content_type};base64,");
357 let mut data_uri = Vec::with_capacity(prefix.len());
358 data_uri.extend_from_slice(prefix.as_bytes());
359 {
360 let mut enc = Base64Encoder::new(&mut data_uri, &BASE64_STANDARD);
361 provider
362 .stream_to(asset_id, &mut enc)
363 .ok_or_else(|| Error::Other {
364 message: format!("asset not found: {asset_id}"),
365 })?
366 .map_err(Error::from)?;
367 enc.finish().map_err(Error::from)?
368 };
369 String::from_utf8(data_uri).map_err(|e| Error::Other {
370 message: format!("base64 encoding produced invalid UTF-8: {e}"),
371 })
372 }
373
374 fn handle_blockquote(&mut self, id: Option<&String>) -> Result<()> {
375 self.json.open_object()?;
376 self.json.key("type").value("quote")?;
377 self.write_id(id)?;
378 self.json.key("content").open_array()?;
379 self.blockquote_depth.inc();
380 self.context.blockquote_has_content = false;
381 self.context.in_text_block = true;
382 Ok(())
383 }
384
385 fn handle_divider(&mut self, id: Option<&String>) -> Result<()> {
386 self.json.object(|j| {
387 j.key("type").value("divider")?;
388 if let Some(id_val) = id {
389 j.key("id").value(id_val.as_str())?;
390 }
391 Ok(())
392 })
393 }
394
395 fn handle_end_link(&mut self) -> Result<()> {
401 if !self.in_link {
402 return Ok(());
403 }
404 if !self.link_emitted_styled_text {
405 self.json.open_object()?;
406 self.json.key("type").value("text")?;
407 self.json.key("text").value("")?;
408 self.json.key("styles").open_object()?;
409 self.json.close_object()?;
410 self.json.close_object()?;
411 }
412 self.json.close_array()?;
413 self.json.close_object()?;
414 self.in_link = false;
415 self.link_emitted_styled_text = false;
416 Ok(())
417 }
418
419 fn handle_end_list_item(&mut self) -> Result<()> {
420 if self.dropped_list_depth.is_positive() {
421 self.dropped_list_depth.dec();
422 return Ok(());
423 }
424 if self.list_stack.is_empty() {
425 return Ok(());
426 }
427 self.close_current_list_item_object()
428 }
429
430 fn handle_end_paragraph(&mut self) -> Result<()> {
431 if self.drop_inside_list_depth.is_positive() || self.dropped_list_depth.is_positive() {
432 return Ok(());
433 }
434 if !self.list_stack.is_empty()
435 && self
436 .list_stack
437 .last()
438 .is_some_and(|e| e.first_paragraph_consumed)
439 && self.context.in_text_block
440 {
441 self.close_open_link_if_any()?;
442 self.json.close_array()?;
443 self.json.key("children").array(|_| Ok(()))?;
444 self.json.close_object()?;
445 self.context.in_text_block = false;
446 return Ok(());
447 }
448 if self.in_list_item_content() {
449 if let Some(entry) = self.list_stack.last_mut() {
450 entry.first_paragraph_consumed = true;
451 }
452 return Ok(());
453 }
454 if self.blockquote_depth.is_positive()
455 || !self.context.in_text_block
456 || self.context.in_table_cell
457 {
458 return Ok(());
459 }
460 close_text_block!(self)
461 }
462
463 fn handle_end_table(&mut self) -> Result<()> {
464 drop_block_in_list_end!(self);
465 if self.table_depth.is_zero() {
466 return Ok(());
467 }
468 if self.table_depth.get() > 1 {
469 self.table_depth.dec();
470 return Ok(());
471 }
472 self.json.close_array()?;
473 self.json.close_object()?;
474 self.json.key("children").array(|_| Ok(()))?;
475 self.json.close_object()?;
476 self.table_depth.reset();
477 Ok(())
478 }
479
480 fn handle_end_table_cell(&mut self) -> Result<()> {
481 if self.drop_inside_list_depth.is_positive() || self.table_depth.get() > 1 {
482 return Ok(());
483 }
484 self.close_open_link_if_any()?;
485 self.json.close_array()?;
486 self.json.close_object()?;
487 self.context.in_table_cell = false;
488 Ok(())
489 }
490
491 fn handle_end_table_row(&mut self) -> Result<()> {
492 if self.drop_inside_list_depth.is_positive() || self.table_depth.get() > 1 {
493 return Ok(());
494 }
495 self.json.close_array()?;
496 self.json.close_object()
497 }
498
499 fn handle_heading(&mut self, level: u8, id: Option<&String>) -> Result<()> {
500 self.json.open_object()?;
501 self.json.key("type").value("heading")?;
502 self.write_id(id)?;
503 self.json
504 .key("props")
505 .object(|j| j.key("level").value(level))?;
506 self.json.key("content").open_array()?;
507 self.context.in_text_block = true;
508 Ok(())
509 }
510
511 fn handle_image(
512 &mut self,
513 source: ImageSource,
514 alt: Option<String>,
515 id: Option<&String>,
516 ) -> Result<()> {
517 if self.context.in_table_cell || self.drop_inside_list_depth.is_positive() {
518 return Ok(());
519 }
520 if self.in_any_list_item() {
521 return Ok(());
522 }
523 self.close_for_block_sibling()?;
524 let url = match source {
525 ImageSource::Uri { uri } => uri,
526 ImageSource::Asset { asset_id } => self.encode_asset_as_data_uri(&asset_id)?,
527 _ => return Ok(()),
528 };
529 let caption = alt.unwrap_or_default();
530
531 self.json.object(|j| {
532 if let Some(id_val) = id {
533 j.key("id").value(id_val.as_str())?;
534 }
535 j.key("type").value("image")?;
536 j.key("props").object(|p| {
537 p.key("url").value(url.as_str())?;
538 p.key("caption").value(caption.as_str())
539 })?;
540 j.key("content").value(Null)?;
541 j.key("children").array(|_| Ok(()))
542 })
543 }
544
545 fn handle_line_break(&mut self) -> Result<()> {
546 if self.drop_inside_list_depth.is_positive() {
547 return Ok(());
548 }
549 if (self.context.in_text_block || self.context.in_table_cell || self.in_list_item_content())
550 && self.table_depth.get() <= 1
551 {
552 self.handle_text("\n", &TextStyle::default())
553 } else {
554 Ok(())
555 }
556 }
557
558 fn handle_paragraph(
559 &mut self,
560 id: Option<&String>,
561 alignment: Option<&TextAlignment>,
562 ) -> Result<()> {
563 if self.context.in_table_cell {
565 return Ok(());
566 }
567 if self.drop_inside_list_depth.is_positive() || self.dropped_list_depth.is_positive() {
570 return Ok(());
571 }
572 if !self.list_stack.is_empty()
576 && self
577 .list_stack
578 .last()
579 .is_some_and(|e| e.first_paragraph_consumed)
580 {
581 if self
582 .list_stack
583 .last()
584 .is_some_and(|e| e.content_state == ListContentState::Open)
585 {
586 self.json.close_array()?;
587 if let Some(e) = self.list_stack.last_mut() {
588 e.content_state = ListContentState::Closed;
589 }
590 }
591 if !self
592 .list_stack
593 .last()
594 .is_some_and(|e| e.children_array_open)
595 {
596 self.json.key("children").open_array()?;
597 if let Some(e) = self.list_stack.last_mut() {
598 e.children_array_open = true;
599 }
600 }
601 self.json.open_object()?;
602 self.json.key("type").value("paragraph")?;
603 self.write_paragraph_props(alignment)?;
604 self.json.key("content").open_array()?;
605 self.context.in_text_block = true;
606 return Ok(());
607 }
608 if !self.list_stack.is_empty() {
609 self.initialize_current_list_item_content(alignment)?;
610 return Ok(());
611 }
612 if self.blockquote_depth.is_positive() {
613 if self.context.blockquote_has_content {
614 self.handle_text("\n\n", &TextStyle::default())?;
615 }
616 return Ok(());
617 }
618 self.json.open_object()?;
619 self.write_id(id)?;
620 self.json.key("type").value("paragraph")?;
621 self.write_paragraph_props(alignment)?;
622 self.json.key("content").open_array()?;
623 self.context.in_text_block = true;
624 Ok(())
625 }
626
627 fn write_paragraph_props(&mut self, alignment: Option<&TextAlignment>) -> Result<()> {
628 if let Some(value) = non_default_alignment_value(alignment) {
629 self.json
630 .key("props")
631 .object(|j| j.key("textAlignment").value(value))?;
632 }
633 Ok(())
634 }
635
636 fn handle_preformatted(&mut self, id: Option<&String>, syntax: Option<&String>) -> Result<()> {
637 self.json.open_object()?;
638 self.json.key("type").value("codeBlock")?;
639 self.write_id(id)?;
640 if let Some(lang) = syntax {
641 self.json
642 .key("props")
643 .object(|j| j.key("language").value(lang.as_str()))?;
644 }
645 self.json.key("content").open_array()?;
646 self.context.in_text_block = true;
647 Ok(())
648 }
649
650 fn handle_start_link(&mut self, href: &str) -> Result<()> {
654 if self.drop_inside_list_depth.is_positive()
655 || self.dropped_list_depth.is_positive()
656 || self.table_depth.get() > 1
657 {
658 return Ok(());
659 }
660 if self.list_stack.last().is_some_and(|entry| {
661 entry.content_state == ListContentState::Pending && !entry.first_paragraph_consumed
662 }) {
663 self.initialize_current_list_item_content(None)?;
664 }
665 if !self.context.in_text_block
666 && !self.context.in_table_cell
667 && !self.in_list_item_content()
668 {
669 return Ok(());
670 }
671 if self.in_link {
672 return Ok(());
673 }
674 if self.blockquote_depth.is_positive() {
675 self.context.blockquote_has_content = true;
676 }
677 self.json.open_object()?;
678 self.json.key("type").value("link")?;
679 self.json.key("href").value(href)?;
680 self.json.key("content").open_array()?;
681 self.in_link = true;
682 self.link_emitted_styled_text = false;
683 Ok(())
684 }
685
686 fn handle_start_list_item(
687 &mut self,
688 kind: ListKind,
689 id: Option<&String>,
690 level: u32,
691 start: Option<u64>,
692 ) -> Result<()> {
693 if self.context.in_table_cell
694 || self.table_depth.get() > 1
695 || self.drop_inside_list_depth.is_positive()
696 {
697 self.dropped_list_depth.inc();
698 return Ok(());
699 }
700 if self.blockquote_depth.is_positive() {
701 self.close_blockquote_for_sibling()?;
702 }
703 if self.list_stack.is_empty() {
704 self.close_for_block_sibling()?;
705 self.open_list_item_object(kind, id, level, start)?;
706 return Ok(());
707 }
708
709 let stack_top_level = self.list_stack.last().map_or(0, |entry| entry.level);
710
711 let effective_level = if level > stack_top_level.saturating_add(1) {
714 stack_top_level.saturating_add(1)
715 } else {
716 level
717 };
718
719 if effective_level > stack_top_level {
720 self.open_current_list_item_children()?;
721 self.open_list_item_object(kind, id, effective_level, start)?;
722 return Ok(());
723 }
724
725 if effective_level == stack_top_level {
726 self.close_current_list_item_object()?;
727 if self.list_stack.is_empty() {
728 self.close_for_block_sibling()?;
729 }
730 self.open_list_item_object(kind, id, effective_level, start)?;
731 return Ok(());
732 }
733
734 while let Some(top) = self.list_stack.last() {
737 if top.level < effective_level {
738 break;
739 }
740 self.close_current_list_item_object()?;
741 }
742 if self.list_stack.is_empty() {
743 self.close_for_block_sibling()?;
744 }
745 self.open_list_item_object(kind, id, effective_level, start)?;
746 Ok(())
747 }
748
749 fn handle_start_table(&mut self, id: Option<&String>) -> Result<()> {
750 drop_block_in_list_start!(self);
751 if self.table_depth.is_positive() {
752 self.table_depth.inc();
753 return Ok(());
754 }
755 self.close_for_block_sibling()?;
756 self.json.open_object()?;
757 self.json.key("type").value("table")?;
758 self.write_id(id)?;
759 self.json.key("content").open_object()?;
760 self.json.key("type").value("tableContent")?;
761 self.json.key("columnWidths").array(|_| Ok(()))?;
762 self.json.key("rows").open_array()?;
763 self.table_depth.inc();
764 self.context.in_text_block = false;
765 Ok(())
766 }
767
768 fn handle_start_table_row(&mut self, id: Option<&String>) -> Result<()> {
769 if self.drop_inside_list_depth.is_positive() || self.table_depth.get() > 1 {
770 return Ok(());
771 }
772 self.json.open_object()?;
773 self.write_id(id)?;
774 self.json.key("cells").open_array()
775 }
776
777 fn handle_table_cell(&mut self, id: Option<&String>) -> Result<()> {
778 if self.drop_inside_list_depth.is_positive() || self.table_depth.get() > 1 {
779 return Ok(());
780 }
781 self.json.open_object()?;
782 self.json.key("type").value("tableCell")?;
783 self.write_id(id)?;
784 self.json.key("content").open_array()?;
785 self.context.in_table_cell = true;
786 self.context.in_text_block = false;
787 Ok(())
788 }
789
790 fn handle_text(&mut self, content: &str, style: &TextStyle) -> Result<()> {
791 if self.drop_inside_list_depth.is_positive()
792 || self.dropped_list_depth.is_positive()
793 || (!self.context.in_text_block
794 && !self.context.in_table_cell
795 && !self.in_list_item_content())
796 || self.table_depth.get() > 1
797 {
798 return Ok(());
799 }
800 if self.blockquote_depth.is_positive() {
801 self.context.blockquote_has_content = true;
802 }
803 self.json.object(|j| {
804 j.key("type").value("text")?;
805 j.key("text").value(content)?;
806 j.key("styles").object(|s| {
807 for (key, enabled) in [
808 ("bold", style.bold),
809 ("italic", style.italic),
810 ("code", style.code),
811 ("strike", style.strikethrough),
812 ("underline", style.underline),
813 ] {
814 if enabled {
815 s.key(key).value(true)?;
816 }
817 }
818 Ok(())
819 })
820 })?;
821 if self.in_link {
822 self.link_emitted_styled_text = true;
823 }
824 Ok(())
825 }
826
827 fn handle_text_event(&mut self, content: &str, style: &TextStyle) -> Result<()> {
828 if self.drop_inside_list_depth.is_positive()
829 || self.dropped_list_depth.is_positive()
830 || self.table_depth.get() > 1
831 {
832 return Ok(());
833 }
834 if self.list_stack.last().is_some_and(|entry| {
836 entry.content_state == ListContentState::Pending && !entry.first_paragraph_consumed
837 }) {
838 self.initialize_current_list_item_content(None)?;
839 }
840 if !self.context.in_text_block
841 && self.blockquote_depth.is_zero()
842 && !self.context.in_table_cell
843 && !self.in_list_item_content()
844 {
845 self.handle_paragraph(None, None)?;
846 }
847 self.handle_text(content, style)
848 }
849
850 fn in_any_list_item(&self) -> bool {
858 !self.list_stack.is_empty()
859 }
860
861 fn in_list_item_content(&self) -> bool {
862 self.list_stack
863 .last()
864 .is_some_and(|entry| entry.content_state == ListContentState::Open)
865 }
866
867 #[inline]
873 #[must_use]
874 pub fn new(writer: W) -> Self {
875 Self {
876 assets: None,
877 blockquote_depth: Depth::default(),
878 blockquote_force_closed_count: Depth::default(),
879 context: BlockContext::default(),
880 drop_inside_list_depth: Depth::default(),
881 dropped_list_depth: Depth::default(),
882 in_link: false,
883 json: JsonEmitter::new(StrusonBackend::new(writer)),
884 link_emitted_styled_text: false,
885 list_stack: Vec::new(),
886 table_depth: Depth::default(),
887 }
888 }
889
890 fn initialize_current_list_item_content(
891 &mut self,
892 alignment: Option<&TextAlignment>,
893 ) -> Result<()> {
894 let Some(current_entry) = self.list_stack.last() else {
895 return Ok(());
896 };
897 if current_entry.content_state != ListContentState::Pending {
898 return Ok(());
899 }
900 let kind = current_entry.kind;
901 let start = current_entry.start;
902 let alignment_value = non_default_alignment_value(alignment);
903 if alignment_value.is_some() || (kind == ListKind::Ordered && start.is_some()) {
904 self.json.key("props").object(|j| {
905 if let Some(value) = alignment_value {
906 j.key("textAlignment").value(value)?;
907 }
908 if kind == ListKind::Ordered {
909 if let Some(start_prop) = start {
910 j.key("start").value(start_prop)?;
911 }
912 }
913 Ok(())
914 })?;
915 }
916 self.json.key("content").open_array()?;
917 if let Some(entry) = self.list_stack.last_mut() {
918 entry.content_state = ListContentState::Open;
919 }
920 Ok(())
921 }
922
923 fn open_current_list_item_children(&mut self) -> Result<()> {
924 if self
925 .list_stack
926 .last()
927 .is_some_and(|entry| entry.content_state == ListContentState::Pending)
928 {
929 self.initialize_current_list_item_content(None)?;
930 }
931 let content_array_open = self
932 .list_stack
933 .last()
934 .is_some_and(|entry| entry.content_state == ListContentState::Open);
935 if content_array_open {
936 self.json.close_array()?;
937 if let Some(entry) = self.list_stack.last_mut() {
938 entry.content_state = ListContentState::Closed;
939 entry.first_paragraph_consumed = true;
940 }
941 }
942
943 let children_array_open = self
944 .list_stack
945 .last()
946 .is_some_and(|entry| entry.children_array_open);
947 if !children_array_open {
948 self.json.key("children").open_array()?;
949 if let Some(entry) = self.list_stack.last_mut() {
950 entry.children_array_open = true;
951 }
952 }
953 Ok(())
954 }
955
956 fn open_list_item_object(
957 &mut self,
958 kind: ListKind,
959 id: Option<&String>,
960 level: u32,
961 start: Option<u64>,
962 ) -> Result<()> {
963 self.json.open_object()?;
964 self.write_id(id)?;
965 let type_name = match kind {
966 ListKind::Ordered => "numberedListItem",
967 ListKind::Unordered => "bulletListItem",
968 };
969 self.json.key("type").value(type_name)?;
970 let checked_start = start
971 .map(|start_value| {
972 u32::try_from(start_value).map_err(|err| Error::Other {
973 message: format!("ordered list start value out of range: {start_value}: {err}"),
974 })
975 })
976 .transpose()?;
977 self.list_stack.push(ListStackEntry {
978 children_array_open: false,
979 content_state: ListContentState::Pending,
980 first_paragraph_consumed: false,
981 kind,
982 level,
983 start: checked_start,
984 });
985 Ok(())
986 }
987
988 #[inline]
999 #[must_use]
1000 pub fn with_assets(writer: W, assets: &'a dyn AssetProvider) -> Self {
1001 Self {
1002 assets: Some(assets),
1003 blockquote_depth: Depth::default(),
1004 blockquote_force_closed_count: Depth::default(),
1005 context: BlockContext::default(),
1006 drop_inside_list_depth: Depth::default(),
1007 dropped_list_depth: Depth::default(),
1008 in_link: false,
1009 json: JsonEmitter::new(StrusonBackend::new(writer)),
1010 link_emitted_styled_text: false,
1011 list_stack: Vec::new(),
1012 table_depth: Depth::default(),
1013 }
1014 }
1015
1016 fn handle_end_document(&mut self) -> Result<()> {
1017 while !self.list_stack.is_empty() {
1018 self.close_current_list_item_object()?;
1019 }
1020 self.json.close_array()
1021 }
1022
1023 fn write_id(&mut self, id: Option<&String>) -> Result<()> {
1024 if let Some(id_val) = id {
1025 self.json.key("id").value(id_val.as_str())?;
1026 }
1027 Ok(())
1028 }
1029}
1030
1031impl<W: Write> EventSink for BlockNoteWriter<'_, W> {
1032 #[inline]
1033 fn finish(self) -> Result<()> {
1034 self.json.finish().map(|_| ())
1035 }
1036
1037 #[inline]
1038 fn handle_event(&mut self, event: Event) -> Result<()> {
1039 match event {
1040 Event::StartDocument { .. } => self.json.open_array(),
1041 Event::EndDocument => self.handle_end_document(),
1042 Event::StartHeading { level, id, .. } => {
1043 return_if_table_cell!(self);
1044 drop_block_in_list_start!(self);
1045 self.close_for_block_sibling()?;
1046 self.handle_heading(level, id.as_ref())
1047 }
1048 Event::EndHeading => {
1049 drop_block_in_list_end!(self);
1050 if !self.context.in_text_block {
1051 return Ok(());
1052 }
1053 close_text_block!(self)
1054 }
1055 Event::EndPreformatted => {
1056 drop_block_in_list_end!(self);
1057 return_if_table_cell!(self);
1058 if !self.context.in_text_block {
1059 return Ok(());
1060 }
1061 close_text_block!(self)
1062 }
1063 Event::StartParagraph { alignment, id } => {
1064 self.handle_paragraph(id.as_ref(), alignment.as_ref())
1065 }
1066 Event::EndParagraph => self.handle_end_paragraph(),
1067 Event::StartBlockQuote { id, .. } => {
1068 return_if_table_cell!(self);
1069 drop_block_in_list_start!(self);
1070 self.close_for_block_sibling()?;
1071 self.handle_blockquote(id.as_ref())
1072 }
1073 Event::EndBlockQuote => {
1074 drop_block_in_list_end!(self);
1075 return_if_table_cell!(self);
1076 if self.blockquote_force_closed_count.is_positive() {
1077 self.blockquote_force_closed_count.dec();
1078 return Ok(());
1079 }
1080 self.close_open_link_if_any()?;
1081 self.close_content_block()?;
1082 self.blockquote_depth.dec();
1083 self.context.in_text_block = self.blockquote_depth.is_positive();
1084 Ok(())
1085 }
1086 Event::StartPreformatted { id, syntax, .. } => {
1087 return_if_table_cell!(self);
1088 drop_block_in_list_start!(self);
1089 self.close_for_block_sibling()?;
1090 self.handle_preformatted(id.as_ref(), syntax.as_ref())
1091 }
1092 Event::ThematicBreak { id, .. } => {
1093 return_if_table_cell!(self);
1094 if self.in_any_list_item() || self.drop_inside_list_depth.is_positive() {
1095 return Ok(());
1096 }
1097 self.close_for_block_sibling()?;
1098 self.handle_divider(id.as_ref())
1099 }
1100 Event::Text { content, style, .. } => self.handle_text_event(&content, &style),
1101 Event::Image {
1102 source, alt, id, ..
1103 } => self.handle_image(source, alt, id.as_ref()),
1104 Event::LineBreak | Event::SoftBreak => self.handle_line_break(),
1105 Event::StartOrderedListItem {
1106 id, level, start, ..
1107 } => self.handle_start_list_item(ListKind::Ordered, id.as_ref(), level, start),
1108 Event::StartUnorderedListItem { id, level, .. } => {
1109 self.handle_start_list_item(ListKind::Unordered, id.as_ref(), level, None)
1110 }
1111 Event::EndOrderedListItem | Event::EndUnorderedListItem => self.handle_end_list_item(),
1112 Event::StartTable { id, .. } => self.handle_start_table(id.as_ref()),
1113 Event::EndTable => self.handle_end_table(),
1114 Event::StartTableRow { id, .. } => self.handle_start_table_row(id.as_ref()),
1115 Event::EndTableRow => self.handle_end_table_row(),
1116 Event::StartTableCell { id, .. } | Event::StartTableHeader { id, .. } => {
1117 self.handle_table_cell(id.as_ref())
1118 }
1119 Event::EndTableCell | Event::EndTableHeader => self.handle_end_table_cell(),
1120 Event::StartLink { href, .. } => self.handle_start_link(&href),
1121 Event::EndLink => self.handle_end_link(),
1122 Event::EndCaption
1123 | Event::EndDefinitionDetail
1124 | Event::EndDefinitionList
1125 | Event::EndDefinitionTerm
1126 | Event::EndFootnote
1127 | Event::FootnoteRef { .. }
1128 | Event::StartCaption { .. }
1129 | Event::StartDefinitionDetail { .. }
1130 | Event::StartDefinitionList { .. }
1131 | Event::StartDefinitionTerm { .. }
1132 | Event::StartFootnote { .. }
1133 | _ => Ok(()),
1134 }
1135 }
1136}
1137
1138#[cfg(test)]
1139mod tests {
1140 use super::*;
1141
1142 #[test]
1143 fn list_stack_empty_after_new() {
1144 let mut buf = Vec::new();
1145 let writer = BlockNoteWriter::new(&mut buf);
1146 assert!(writer.list_stack.is_empty());
1147 }
1148
1149 #[test]
1150 fn close_for_block_sibling_with_nonempty_list_stack_closes_all_items() {
1151 let mut buf = Vec::new();
1155 let mut writer = BlockNoteWriter::new(&mut buf);
1156 assert!(writer
1157 .handle_event(Event::StartDocument {
1158 id: None,
1159 language: None,
1160 metadata: None,
1161 })
1162 .is_ok());
1163 assert!(writer
1164 .handle_event(Event::StartUnorderedListItem {
1165 id: None,
1166 level: 0,
1167 style_type: docspec_core::ListStyleType::Disc,
1168 })
1169 .is_ok());
1170 assert!(
1171 !writer.list_stack.is_empty(),
1172 "list_stack must be non-empty before calling close_for_block_sibling"
1173 );
1174 assert!(writer.close_for_block_sibling().is_ok());
1175 assert!(
1176 writer.list_stack.is_empty(),
1177 "close_for_block_sibling must drain list_stack via close_open_list_items"
1178 );
1179 assert!(writer.handle_event(Event::EndDocument).is_ok());
1180 assert!(writer.finish().is_ok());
1181 }
1182}