1use std::{marker::PhantomData, slice::Iter};
4
5use self_cell::self_cell;
6
7use crate::{
8 Parser, Span,
9 attributes::Attrlist,
10 blocks::{Block, ContentModel, IsBlock, Preamble, parse_utils::parse_blocks_until},
11 document::{Catalog, Header},
12 internal::debug::DebugSliceReference,
13 parser::SourceMap,
14 strings::CowStr,
15 warnings::Warning,
16};
17
18#[derive(Eq, PartialEq)]
31pub struct Document<'src> {
32 internal: Internal,
33 _phantom: PhantomData<&'src ()>,
34}
35
36#[derive(Debug, Eq, PartialEq)]
39struct InternalDependent<'src> {
40 header: Header<'src>,
41 blocks: Vec<Block<'src>>,
42 source: Span<'src>,
43 warnings: Vec<Warning<'src>>,
44 source_map: SourceMap,
45 catalog: Catalog,
46}
47
48self_cell! {
49 struct Internal {
51 owner: String,
52 #[covariant]
53 dependent: InternalDependent,
54 }
55 impl {Debug, Eq, PartialEq}
56}
57
58impl<'src> Document<'src> {
59 pub(crate) fn parse(source: &str, source_map: SourceMap, parser: &mut Parser) -> Self {
60 let owned_source = source.to_string();
61
62 let internal = Internal::new(owned_source, |owned_src| {
63 let source = Span::new(owned_src);
64
65 let mi = Header::parse(source, parser);
66 let after_header = mi.item.after;
67
68 parser.sectnumlevels = parser
69 .attribute_value("sectnumlevels")
70 .as_maybe_str()
71 .and_then(|s| s.parse::<usize>().ok())
72 .unwrap_or(3);
73
74 let header = mi.item.item;
75 let mut warnings = mi.warnings;
76
77 let mut maw_blocks = parse_blocks_until(after_header, |_| false, parser);
78
79 if !maw_blocks.warnings.is_empty() {
80 warnings.append(&mut maw_blocks.warnings);
81 }
82
83 let mut blocks = maw_blocks.item.item;
84 let mut has_content_blocks = false;
85 let mut preamble_split_index: Option<usize> = None;
86
87 for (index, block) in blocks.iter().enumerate() {
88 match block {
89 Block::DocumentAttribute(_) => (),
90 Block::Section(_) => {
91 if has_content_blocks {
92 preamble_split_index = Some(index);
93 }
94 break;
95 }
96 _ => {
97 has_content_blocks = true;
98 }
99 }
100 }
101
102 if let Some(index) = preamble_split_index {
103 let mut section_blocks = blocks.split_off(index);
104
105 let preamble = Preamble::from_blocks(blocks, after_header);
106
107 section_blocks.insert(0, Block::Preamble(preamble));
108 blocks = section_blocks;
109 }
110
111 InternalDependent {
112 header,
113 blocks,
114 source: source.trim_trailing_whitespace(),
115 warnings,
116 source_map,
117 catalog: parser.take_catalog(),
118 }
119 });
120
121 Self {
122 internal,
123 _phantom: PhantomData,
124 }
125 }
126
127 pub fn header(&self) -> &Header<'_> {
129 &self.internal.borrow_dependent().header
130 }
131
132 pub fn warnings(&self) -> Iter<'_, Warning<'_>> {
134 self.internal.borrow_dependent().warnings.iter()
135 }
136
137 pub fn span(&self) -> Span<'_> {
139 self.internal.borrow_dependent().source
140 }
141
142 pub fn source_map(&self) -> &SourceMap {
144 &self.internal.borrow_dependent().source_map
145 }
146
147 pub fn catalog(&self) -> &Catalog {
149 &self.internal.borrow_dependent().catalog
150 }
151}
152
153impl<'src> IsBlock<'src> for Document<'src> {
154 fn content_model(&self) -> ContentModel {
155 ContentModel::Compound
156 }
157
158 fn raw_context(&self) -> CowStr<'src> {
159 "document".into()
160 }
161
162 fn nested_blocks(&'src self) -> Iter<'src, Block<'src>> {
163 self.internal.borrow_dependent().blocks.iter()
164 }
165
166 fn title_source(&'src self) -> Option<Span<'src>> {
167 None
169 }
170
171 fn title(&self) -> Option<&str> {
172 None
174 }
175
176 fn anchor(&'src self) -> Option<Span<'src>> {
177 None
178 }
179
180 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
181 None
182 }
183
184 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
185 None
187 }
188}
189
190impl std::fmt::Debug for Document<'_> {
191 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 let dependent = self.internal.borrow_dependent();
193 f.debug_struct("Document")
194 .field("header", &dependent.header)
195 .field("blocks", &DebugSliceReference(&dependent.blocks))
196 .field("source", &dependent.source)
197 .field("warnings", &DebugSliceReference(&dependent.warnings))
198 .field("source_map", &dependent.source_map)
199 .field("catalog", &dependent.catalog)
200 .finish()
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 #![allow(clippy::unwrap_used)]
207
208 use std::{collections::HashMap, ops::Deref};
209
210 use pretty_assertions_sorted::assert_eq;
211
212 use crate::{
213 Parser,
214 blocks::{ContentModel, IsBlock, MediaType},
215 content::SubstitutionGroup,
216 document::RefType,
217 tests::prelude::*,
218 warnings::WarningType,
219 };
220
221 #[test]
222 fn empty_source() {
223 let doc = Parser::default().parse("");
224
225 assert_eq!(doc.content_model(), ContentModel::Compound);
226 assert_eq!(doc.raw_context().deref(), "document");
227 assert_eq!(doc.resolved_context().deref(), "document");
228 assert!(doc.declared_style().is_none());
229 assert!(doc.id().is_none());
230 assert!(doc.roles().is_empty());
231 assert!(doc.title_source().is_none());
232 assert!(doc.title().is_none());
233 assert!(doc.anchor().is_none());
234 assert!(doc.anchor_reftext().is_none());
235 assert!(doc.attrlist().is_none());
236 assert_eq!(doc.substitution_group(), SubstitutionGroup::Normal);
237
238 assert_eq!(
239 doc,
240 Document {
241 header: Header {
242 title_source: None,
243 title: None,
244 attributes: &[],
245 author_line: None,
246 revision_line: None,
247 comments: &[],
248 source: Span {
249 data: "",
250 line: 1,
251 col: 1,
252 offset: 0
253 },
254 },
255 source: Span {
256 data: "",
257 line: 1,
258 col: 1,
259 offset: 0
260 },
261 blocks: &[],
262 warnings: &[],
263 source_map: SourceMap(&[]),
264 catalog: Catalog::default(),
265 }
266 );
267 }
268
269 #[test]
270 fn only_spaces() {
271 assert_eq!(
272 Parser::default().parse(" "),
273 Document {
274 header: Header {
275 title_source: None,
276 title: None,
277 attributes: &[],
278 author_line: None,
279 revision_line: None,
280 comments: &[],
281 source: Span {
282 data: "",
283 line: 1,
284 col: 5,
285 offset: 4
286 },
287 },
288 source: Span {
289 data: "",
290 line: 1,
291 col: 1,
292 offset: 0
293 },
294 blocks: &[],
295 warnings: &[],
296 source_map: SourceMap(&[]),
297 catalog: Catalog::default(),
298 }
299 );
300 }
301
302 #[test]
303 fn one_simple_block() {
304 let doc = Parser::default().parse("abc");
305 assert_eq!(
306 doc,
307 Document {
308 header: Header {
309 title_source: None,
310 title: None,
311 attributes: &[],
312 author_line: None,
313 revision_line: None,
314 comments: &[],
315 source: Span {
316 data: "",
317 line: 1,
318 col: 1,
319 offset: 0
320 },
321 },
322 source: Span {
323 data: "abc",
324 line: 1,
325 col: 1,
326 offset: 0
327 },
328 blocks: &[Block::Simple(SimpleBlock {
329 content: Content {
330 original: Span {
331 data: "abc",
332 line: 1,
333 col: 1,
334 offset: 0,
335 },
336 rendered: "abc",
337 },
338 source: Span {
339 data: "abc",
340 line: 1,
341 col: 1,
342 offset: 0,
343 },
344 title_source: None,
345 title: None,
346 anchor: None,
347 anchor_reftext: None,
348 attrlist: None,
349 })],
350 warnings: &[],
351 source_map: SourceMap(&[]),
352 catalog: Catalog::default(),
353 }
354 );
355
356 assert!(doc.anchor().is_none());
357 assert!(doc.anchor_reftext().is_none());
358 }
359
360 #[test]
361 fn two_simple_blocks() {
362 assert_eq!(
363 Parser::default().parse("abc\n\ndef"),
364 Document {
365 header: Header {
366 title_source: None,
367 title: None,
368 attributes: &[],
369 author_line: None,
370 revision_line: None,
371 comments: &[],
372 source: Span {
373 data: "",
374 line: 1,
375 col: 1,
376 offset: 0
377 },
378 },
379 source: Span {
380 data: "abc\n\ndef",
381 line: 1,
382 col: 1,
383 offset: 0
384 },
385 blocks: &[
386 Block::Simple(SimpleBlock {
387 content: Content {
388 original: Span {
389 data: "abc",
390 line: 1,
391 col: 1,
392 offset: 0,
393 },
394 rendered: "abc",
395 },
396 source: Span {
397 data: "abc",
398 line: 1,
399 col: 1,
400 offset: 0,
401 },
402 title_source: None,
403 title: None,
404 anchor: None,
405 anchor_reftext: None,
406 attrlist: None,
407 }),
408 Block::Simple(SimpleBlock {
409 content: Content {
410 original: Span {
411 data: "def",
412 line: 3,
413 col: 1,
414 offset: 5,
415 },
416 rendered: "def",
417 },
418 source: Span {
419 data: "def",
420 line: 3,
421 col: 1,
422 offset: 5,
423 },
424 title_source: None,
425 title: None,
426 anchor: None,
427 anchor_reftext: None,
428 attrlist: None,
429 })
430 ],
431 warnings: &[],
432 source_map: SourceMap(&[]),
433 catalog: Catalog::default(),
434 }
435 );
436 }
437
438 #[test]
439 fn two_blocks_and_title() {
440 assert_eq!(
441 Parser::default().parse("= Example Title\n\nabc\n\ndef"),
442 Document {
443 header: Header {
444 title_source: Some(Span {
445 data: "Example Title",
446 line: 1,
447 col: 3,
448 offset: 2,
449 }),
450 title: Some("Example Title"),
451 attributes: &[],
452 author_line: None,
453 revision_line: None,
454 comments: &[],
455 source: Span {
456 data: "= Example Title",
457 line: 1,
458 col: 1,
459 offset: 0,
460 }
461 },
462 blocks: &[
463 Block::Simple(SimpleBlock {
464 content: Content {
465 original: Span {
466 data: "abc",
467 line: 3,
468 col: 1,
469 offset: 17,
470 },
471 rendered: "abc",
472 },
473 source: Span {
474 data: "abc",
475 line: 3,
476 col: 1,
477 offset: 17,
478 },
479 title_source: None,
480 title: None,
481 anchor: None,
482 anchor_reftext: None,
483 attrlist: None,
484 }),
485 Block::Simple(SimpleBlock {
486 content: Content {
487 original: Span {
488 data: "def",
489 line: 5,
490 col: 1,
491 offset: 22,
492 },
493 rendered: "def",
494 },
495 source: Span {
496 data: "def",
497 line: 5,
498 col: 1,
499 offset: 22,
500 },
501 title_source: None,
502 title: None,
503 anchor: None,
504 anchor_reftext: None,
505 attrlist: None,
506 })
507 ],
508 source: Span {
509 data: "= Example Title\n\nabc\n\ndef",
510 line: 1,
511 col: 1,
512 offset: 0
513 },
514 warnings: &[],
515 source_map: SourceMap(&[]),
516 catalog: Catalog::default(),
517 }
518 );
519 }
520
521 #[test]
522 fn blank_lines_before_header() {
523 let doc = Parser::default().parse("\n\n= Example Title\n\nabc\n\ndef");
524
525 assert_eq!(
526 doc,
527 Document {
528 header: Header {
529 title_source: Some(Span {
530 data: "Example Title",
531 line: 3,
532 col: 3,
533 offset: 4,
534 },),
535 title: Some("Example Title",),
536 attributes: &[],
537 author_line: None,
538 revision_line: None,
539 comments: &[],
540 source: Span {
541 data: "= Example Title",
542 line: 3,
543 col: 1,
544 offset: 2,
545 },
546 },
547 blocks: &[
548 Block::Simple(SimpleBlock {
549 content: Content {
550 original: Span {
551 data: "abc",
552 line: 5,
553 col: 1,
554 offset: 19,
555 },
556 rendered: "abc",
557 },
558 source: Span {
559 data: "abc",
560 line: 5,
561 col: 1,
562 offset: 19,
563 },
564 title_source: None,
565 title: None,
566 anchor: None,
567 anchor_reftext: None,
568 attrlist: None,
569 },),
570 Block::Simple(SimpleBlock {
571 content: Content {
572 original: Span {
573 data: "def",
574 line: 7,
575 col: 1,
576 offset: 24,
577 },
578 rendered: "def",
579 },
580 source: Span {
581 data: "def",
582 line: 7,
583 col: 1,
584 offset: 24,
585 },
586 title_source: None,
587 title: None,
588 anchor: None,
589 anchor_reftext: None,
590 attrlist: None,
591 },),
592 ],
593 source: Span {
594 data: "\n\n= Example Title\n\nabc\n\ndef",
595 line: 1,
596 col: 1,
597 offset: 0,
598 },
599 warnings: &[],
600 source_map: SourceMap(&[]),
601 catalog: Catalog::default(),
602 }
603 );
604 }
605
606 #[test]
607 fn blank_lines_and_comment_before_header() {
608 let doc =
609 Parser::default().parse("\n// ignore this comment\n= Example Title\n\nabc\n\ndef");
610
611 assert_eq!(
612 doc,
613 Document {
614 header: Header {
615 title_source: Some(Span {
616 data: "Example Title",
617 line: 3,
618 col: 3,
619 offset: 26,
620 },),
621 title: Some("Example Title",),
622 attributes: &[],
623 author_line: None,
624 revision_line: None,
625 comments: &[Span {
626 data: "// ignore this comment",
627 line: 2,
628 col: 1,
629 offset: 1,
630 },],
631 source: Span {
632 data: "// ignore this comment\n= Example Title",
633 line: 2,
634 col: 1,
635 offset: 1,
636 },
637 },
638 blocks: &[
639 Block::Simple(SimpleBlock {
640 content: Content {
641 original: Span {
642 data: "abc",
643 line: 5,
644 col: 1,
645 offset: 41,
646 },
647 rendered: "abc",
648 },
649 source: Span {
650 data: "abc",
651 line: 5,
652 col: 1,
653 offset: 41,
654 },
655 title_source: None,
656 title: None,
657 anchor: None,
658 anchor_reftext: None,
659 attrlist: None,
660 },),
661 Block::Simple(SimpleBlock {
662 content: Content {
663 original: Span {
664 data: "def",
665 line: 7,
666 col: 1,
667 offset: 46,
668 },
669 rendered: "def",
670 },
671 source: Span {
672 data: "def",
673 line: 7,
674 col: 1,
675 offset: 46,
676 },
677 title_source: None,
678 title: None,
679 anchor: None,
680 anchor_reftext: None,
681 attrlist: None,
682 },),
683 ],
684 source: Span {
685 data: "\n// ignore this comment\n= Example Title\n\nabc\n\ndef",
686 line: 1,
687 col: 1,
688 offset: 0,
689 },
690 warnings: &[],
691 source_map: SourceMap(&[]),
692 catalog: Catalog::default(),
693 }
694 );
695 }
696
697 #[test]
698 fn extra_space_before_title() {
699 assert_eq!(
700 Parser::default().parse("= Example Title\n\nabc"),
701 Document {
702 header: Header {
703 title_source: Some(Span {
704 data: "Example Title",
705 line: 1,
706 col: 5,
707 offset: 4,
708 }),
709 title: Some("Example Title"),
710 attributes: &[],
711 author_line: None,
712 revision_line: None,
713 comments: &[],
714 source: Span {
715 data: "= Example Title",
716 line: 1,
717 col: 1,
718 offset: 0,
719 }
720 },
721 blocks: &[Block::Simple(SimpleBlock {
722 content: Content {
723 original: Span {
724 data: "abc",
725 line: 3,
726 col: 1,
727 offset: 19,
728 },
729 rendered: "abc",
730 },
731 source: Span {
732 data: "abc",
733 line: 3,
734 col: 1,
735 offset: 19,
736 },
737 title_source: None,
738 title: None,
739 anchor: None,
740 anchor_reftext: None,
741 attrlist: None,
742 })],
743 source: Span {
744 data: "= Example Title\n\nabc",
745 line: 1,
746 col: 1,
747 offset: 0
748 },
749 warnings: &[],
750 source_map: SourceMap(&[]),
751 catalog: Catalog::default(),
752 }
753 );
754 }
755
756 #[test]
757 fn err_bad_header() {
758 assert_eq!(
759 Parser::default().parse(
760 "= Title\nJane Smith <jane@example.com>\nv1, 2025-09-28\nnot an attribute\n"
761 ),
762 Document {
763 header: Header {
764 title_source: Some(Span {
765 data: "Title",
766 line: 1,
767 col: 3,
768 offset: 2,
769 }),
770 title: Some("Title"),
771 attributes: &[],
772 author_line: Some(AuthorLine {
773 authors: &[Author {
774 name: "Jane Smith",
775 firstname: "Jane",
776 middlename: None,
777 lastname: Some("Smith"),
778 email: Some("jane@example.com"),
779 }],
780 source: Span {
781 data: "Jane Smith <jane@example.com>",
782 line: 2,
783 col: 1,
784 offset: 8,
785 },
786 }),
787 revision_line: Some(RevisionLine {
788 revnumber: Some("1",),
789 revdate: "2025-09-28",
790 revremark: None,
791 source: Span {
792 data: "v1, 2025-09-28",
793 line: 3,
794 col: 1,
795 offset: 38,
796 },
797 },),
798 comments: &[],
799 source: Span {
800 data: "= Title\nJane Smith <jane@example.com>\nv1, 2025-09-28",
801 line: 1,
802 col: 1,
803 offset: 0,
804 }
805 },
806 blocks: &[Block::Simple(SimpleBlock {
807 content: Content {
808 original: Span {
809 data: "not an attribute",
810 line: 4,
811 col: 1,
812 offset: 53,
813 },
814 rendered: "not an attribute",
815 },
816 source: Span {
817 data: "not an attribute",
818 line: 4,
819 col: 1,
820 offset: 53,
821 },
822 title_source: None,
823 title: None,
824 anchor: None,
825 anchor_reftext: None,
826 attrlist: None,
827 })],
828 source: Span {
829 data: "= Title\nJane Smith <jane@example.com>\nv1, 2025-09-28\nnot an attribute",
830 line: 1,
831 col: 1,
832 offset: 0
833 },
834 warnings: &[Warning {
835 source: Span {
836 data: "not an attribute",
837 line: 4,
838 col: 1,
839 offset: 53,
840 },
841 warning: WarningType::DocumentHeaderNotTerminated,
842 },],
843 source_map: SourceMap(&[]),
844 catalog: Catalog::default(),
845 }
846 );
847 }
848
849 #[test]
850 fn err_bad_header_and_bad_macro() {
851 let doc = Parser::default().parse("= Title\nJane Smith <jane@example.com>\nv1, 2025-09-28\nnot an attribute\n\n== Section Title\n\nimage::bar[alt=Sunset,width=300,,height=400]");
852
853 assert_eq!(
854 Document {
855 header: Header {
856 title_source: Some(Span {
857 data: "Title",
858 line: 1,
859 col: 3,
860 offset: 2,
861 }),
862 title: Some("Title"),
863 attributes: &[],
864 author_line: Some(AuthorLine {
865 authors: &[Author {
866 name: "Jane Smith",
867 firstname: "Jane",
868 middlename: None,
869 lastname: Some("Smith"),
870 email: Some("jane@example.com"),
871 }],
872 source: Span {
873 data: "Jane Smith <jane@example.com>",
874 line: 2,
875 col: 1,
876 offset: 8,
877 },
878 }),
879 revision_line: Some(RevisionLine {
880 revnumber: Some("1"),
881 revdate: "2025-09-28",
882 revremark: None,
883 source: Span {
884 data: "v1, 2025-09-28",
885 line: 3,
886 col: 1,
887 offset: 38,
888 },
889 },),
890 comments: &[],
891 source: Span {
892 data: "= Title\nJane Smith <jane@example.com>\nv1, 2025-09-28",
893 line: 1,
894 col: 1,
895 offset: 0,
896 }
897 },
898 blocks: &[
899 Block::Preamble(Preamble {
900 blocks: &[Block::Simple(SimpleBlock {
901 content: Content {
902 original: Span {
903 data: "not an attribute",
904 line: 4,
905 col: 1,
906 offset: 53,
907 },
908 rendered: "not an attribute",
909 },
910 source: Span {
911 data: "not an attribute",
912 line: 4,
913 col: 1,
914 offset: 53,
915 },
916 title_source: None,
917 title: None,
918 anchor: None,
919 anchor_reftext: None,
920 attrlist: None,
921 },),],
922 source: Span {
923 data: "not an attribute",
924 line: 4,
925 col: 1,
926 offset: 53,
927 },
928 },),
929 Block::Section(SectionBlock {
930 level: 1,
931 section_title: Content {
932 original: Span {
933 data: "Section Title",
934 line: 6,
935 col: 4,
936 offset: 74,
937 },
938 rendered: "Section Title",
939 },
940 blocks: &[Block::Media(MediaBlock {
941 type_: MediaType::Image,
942 target: Span {
943 data: "bar",
944 line: 8,
945 col: 8,
946 offset: 96,
947 },
948 macro_attrlist: Attrlist {
949 attributes: &[
950 ElementAttribute {
951 name: Some("alt"),
952 shorthand_items: &[],
953 value: "Sunset"
954 },
955 ElementAttribute {
956 name: Some("width"),
957 shorthand_items: &[],
958 value: "300"
959 },
960 ElementAttribute {
961 name: Some("height"),
962 shorthand_items: &[],
963 value: "400"
964 },
965 ],
966 anchor: None,
967 source: Span {
968 data: "alt=Sunset,width=300,,height=400",
969 line: 8,
970 col: 12,
971 offset: 100,
972 },
973 },
974 source: Span {
975 data: "image::bar[alt=Sunset,width=300,,height=400]",
976 line: 8,
977 col: 1,
978 offset: 89,
979 },
980 title_source: None,
981 title: None,
982 anchor: None,
983 anchor_reftext: None,
984 attrlist: None,
985 },),],
986 source: Span {
987 data: "== Section Title\n\nimage::bar[alt=Sunset,width=300,,height=400]",
988 line: 6,
989 col: 1,
990 offset: 71,
991 },
992 title_source: None,
993 title: None,
994 anchor: None,
995 anchor_reftext: None,
996 attrlist: None,
997 section_type: SectionType::Normal,
998 section_id: Some("_section_title"),
999 section_number: None,
1000 },)
1001 ],
1002 source: Span {
1003 data: "= Title\nJane Smith <jane@example.com>\nv1, 2025-09-28\nnot an attribute\n\n== Section Title\n\nimage::bar[alt=Sunset,width=300,,height=400]",
1004 line: 1,
1005 col: 1,
1006 offset: 0
1007 },
1008 warnings: &[
1009 Warning {
1010 source: Span {
1011 data: "not an attribute",
1012 line: 4,
1013 col: 1,
1014 offset: 53,
1015 },
1016 warning: WarningType::DocumentHeaderNotTerminated,
1017 },
1018 Warning {
1019 source: Span {
1020 data: "alt=Sunset,width=300,,height=400",
1021 line: 8,
1022 col: 12,
1023 offset: 100,
1024 },
1025 warning: WarningType::EmptyAttributeValue,
1026 },
1027 ],
1028 source_map: SourceMap(&[]),
1029 catalog: Catalog {
1030 refs: HashMap::from([(
1031 "_section_title",
1032 RefEntry {
1033 id: "_section_title",
1034 reftext: Some("Section Title",),
1035 ref_type: RefType::Section,
1036 }
1037 ),]),
1038 reftext_to_id: HashMap::from([("Section Title", "_section_title"),]),
1039 }
1040 },
1041 doc
1042 );
1043 }
1044
1045 #[test]
1046 fn impl_debug() {
1047 let doc = Parser::default().parse("= Example Title\n\nabc\n\ndef");
1048
1049 assert_eq!(
1050 format!("{doc:#?}"),
1051 r#"Document {
1052 header: Header {
1053 title_source: Some(
1054 Span {
1055 data: "Example Title",
1056 line: 1,
1057 col: 3,
1058 offset: 2,
1059 },
1060 ),
1061 title: Some(
1062 "Example Title",
1063 ),
1064 attributes: &[],
1065 author_line: None,
1066 revision_line: None,
1067 comments: &[],
1068 source: Span {
1069 data: "= Example Title",
1070 line: 1,
1071 col: 1,
1072 offset: 0,
1073 },
1074 },
1075 blocks: &[
1076 Block::Simple(
1077 SimpleBlock {
1078 content: Content {
1079 original: Span {
1080 data: "abc",
1081 line: 3,
1082 col: 1,
1083 offset: 17,
1084 },
1085 rendered: "abc",
1086 },
1087 source: Span {
1088 data: "abc",
1089 line: 3,
1090 col: 1,
1091 offset: 17,
1092 },
1093 title_source: None,
1094 title: None,
1095 anchor: None,
1096 anchor_reftext: None,
1097 attrlist: None,
1098 },
1099 ),
1100 Block::Simple(
1101 SimpleBlock {
1102 content: Content {
1103 original: Span {
1104 data: "def",
1105 line: 5,
1106 col: 1,
1107 offset: 22,
1108 },
1109 rendered: "def",
1110 },
1111 source: Span {
1112 data: "def",
1113 line: 5,
1114 col: 1,
1115 offset: 22,
1116 },
1117 title_source: None,
1118 title: None,
1119 anchor: None,
1120 anchor_reftext: None,
1121 attrlist: None,
1122 },
1123 ),
1124 ],
1125 source: Span {
1126 data: "= Example Title\n\nabc\n\ndef",
1127 line: 1,
1128 col: 1,
1129 offset: 0,
1130 },
1131 warnings: &[],
1132 source_map: SourceMap(&[]),
1133 catalog: Catalog {
1134 refs: HashMap::from([]),
1135 reftext_to_id: HashMap::from([]),
1136 },
1137}"#
1138 );
1139 }
1140}