asciidoc_parser/document/
document.rs

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