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