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