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, 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/// 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 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    /// Return the document header.
128    pub fn header(&self) -> &Header<'_> {
129        &self.internal.borrow_dependent().header
130    }
131
132    /// Return an iterator over any warnings found during parsing.
133    pub fn warnings(&self) -> Iter<'_, Warning<'_>> {
134        self.internal.borrow_dependent().warnings.iter()
135    }
136
137    /// Return a [`Span`] describing the entire document source.
138    pub fn span(&self) -> Span<'_> {
139        self.internal.borrow_dependent().source
140    }
141
142    /// Return the source map that tracks original file locations.
143    pub fn source_map(&self) -> &SourceMap {
144        &self.internal.borrow_dependent().source_map
145    }
146
147    /// Return the document catalog for accessing referenceable elements.
148    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        // Document title is reflected in the Header.
168        None
169    }
170
171    fn title(&self) -> Option<&str> {
172        // Document title is reflected in the Header.
173        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        // Document attributes are reflected in the Header.
186        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, SimpleBlockStyle},
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                    style: SimpleBlockStyle::Paragraph,
345                    title_source: None,
346                    title: None,
347                    anchor: None,
348                    anchor_reftext: None,
349                    attrlist: None,
350                })],
351                warnings: &[],
352                source_map: SourceMap(&[]),
353                catalog: Catalog::default(),
354            }
355        );
356
357        assert!(doc.anchor().is_none());
358        assert!(doc.anchor_reftext().is_none());
359    }
360
361    #[test]
362    fn two_simple_blocks() {
363        assert_eq!(
364            Parser::default().parse("abc\n\ndef"),
365            Document {
366                header: Header {
367                    title_source: None,
368                    title: None,
369                    attributes: &[],
370                    author_line: None,
371                    revision_line: None,
372                    comments: &[],
373                    source: Span {
374                        data: "",
375                        line: 1,
376                        col: 1,
377                        offset: 0
378                    },
379                },
380                source: Span {
381                    data: "abc\n\ndef",
382                    line: 1,
383                    col: 1,
384                    offset: 0
385                },
386                blocks: &[
387                    Block::Simple(SimpleBlock {
388                        content: Content {
389                            original: Span {
390                                data: "abc",
391                                line: 1,
392                                col: 1,
393                                offset: 0,
394                            },
395                            rendered: "abc",
396                        },
397                        source: Span {
398                            data: "abc",
399                            line: 1,
400                            col: 1,
401                            offset: 0,
402                        },
403                        style: SimpleBlockStyle::Paragraph,
404                        title_source: None,
405                        title: None,
406                        anchor: None,
407                        anchor_reftext: None,
408                        attrlist: None,
409                    }),
410                    Block::Simple(SimpleBlock {
411                        content: Content {
412                            original: Span {
413                                data: "def",
414                                line: 3,
415                                col: 1,
416                                offset: 5,
417                            },
418                            rendered: "def",
419                        },
420                        source: Span {
421                            data: "def",
422                            line: 3,
423                            col: 1,
424                            offset: 5,
425                        },
426                        style: SimpleBlockStyle::Paragraph,
427                        title_source: None,
428                        title: None,
429                        anchor: None,
430                        anchor_reftext: None,
431                        attrlist: None,
432                    })
433                ],
434                warnings: &[],
435                source_map: SourceMap(&[]),
436                catalog: Catalog::default(),
437            }
438        );
439    }
440
441    #[test]
442    fn two_blocks_and_title() {
443        assert_eq!(
444            Parser::default().parse("= Example Title\n\nabc\n\ndef"),
445            Document {
446                header: Header {
447                    title_source: Some(Span {
448                        data: "Example Title",
449                        line: 1,
450                        col: 3,
451                        offset: 2,
452                    }),
453                    title: Some("Example Title"),
454                    attributes: &[],
455                    author_line: None,
456                    revision_line: None,
457                    comments: &[],
458                    source: Span {
459                        data: "= Example Title",
460                        line: 1,
461                        col: 1,
462                        offset: 0,
463                    }
464                },
465                blocks: &[
466                    Block::Simple(SimpleBlock {
467                        content: Content {
468                            original: Span {
469                                data: "abc",
470                                line: 3,
471                                col: 1,
472                                offset: 17,
473                            },
474                            rendered: "abc",
475                        },
476                        source: Span {
477                            data: "abc",
478                            line: 3,
479                            col: 1,
480                            offset: 17,
481                        },
482                        style: SimpleBlockStyle::Paragraph,
483                        title_source: None,
484                        title: None,
485                        anchor: None,
486                        anchor_reftext: None,
487                        attrlist: None,
488                    }),
489                    Block::Simple(SimpleBlock {
490                        content: Content {
491                            original: Span {
492                                data: "def",
493                                line: 5,
494                                col: 1,
495                                offset: 22,
496                            },
497                            rendered: "def",
498                        },
499                        source: Span {
500                            data: "def",
501                            line: 5,
502                            col: 1,
503                            offset: 22,
504                        },
505                        style: SimpleBlockStyle::Paragraph,
506                        title_source: None,
507                        title: None,
508                        anchor: None,
509                        anchor_reftext: None,
510                        attrlist: None,
511                    })
512                ],
513                source: Span {
514                    data: "= Example Title\n\nabc\n\ndef",
515                    line: 1,
516                    col: 1,
517                    offset: 0
518                },
519                warnings: &[],
520                source_map: SourceMap(&[]),
521                catalog: Catalog::default(),
522            }
523        );
524    }
525
526    #[test]
527    fn blank_lines_before_header() {
528        let doc = Parser::default().parse("\n\n= Example Title\n\nabc\n\ndef");
529
530        assert_eq!(
531            doc,
532            Document {
533                header: Header {
534                    title_source: Some(Span {
535                        data: "Example Title",
536                        line: 3,
537                        col: 3,
538                        offset: 4,
539                    },),
540                    title: Some("Example Title",),
541                    attributes: &[],
542                    author_line: None,
543                    revision_line: None,
544                    comments: &[],
545                    source: Span {
546                        data: "= Example Title",
547                        line: 3,
548                        col: 1,
549                        offset: 2,
550                    },
551                },
552                blocks: &[
553                    Block::Simple(SimpleBlock {
554                        content: Content {
555                            original: Span {
556                                data: "abc",
557                                line: 5,
558                                col: 1,
559                                offset: 19,
560                            },
561                            rendered: "abc",
562                        },
563                        source: Span {
564                            data: "abc",
565                            line: 5,
566                            col: 1,
567                            offset: 19,
568                        },
569                        style: SimpleBlockStyle::Paragraph,
570                        title_source: None,
571                        title: None,
572                        anchor: None,
573                        anchor_reftext: None,
574                        attrlist: None,
575                    },),
576                    Block::Simple(SimpleBlock {
577                        content: Content {
578                            original: Span {
579                                data: "def",
580                                line: 7,
581                                col: 1,
582                                offset: 24,
583                            },
584                            rendered: "def",
585                        },
586                        source: Span {
587                            data: "def",
588                            line: 7,
589                            col: 1,
590                            offset: 24,
591                        },
592                        style: SimpleBlockStyle::Paragraph,
593                        title_source: None,
594                        title: None,
595                        anchor: None,
596                        anchor_reftext: None,
597                        attrlist: None,
598                    },),
599                ],
600                source: Span {
601                    data: "\n\n= Example Title\n\nabc\n\ndef",
602                    line: 1,
603                    col: 1,
604                    offset: 0,
605                },
606                warnings: &[],
607                source_map: SourceMap(&[]),
608                catalog: Catalog::default(),
609            }
610        );
611    }
612
613    #[test]
614    fn blank_lines_and_comment_before_header() {
615        let doc =
616            Parser::default().parse("\n// ignore this comment\n= Example Title\n\nabc\n\ndef");
617
618        assert_eq!(
619            doc,
620            Document {
621                header: Header {
622                    title_source: Some(Span {
623                        data: "Example Title",
624                        line: 3,
625                        col: 3,
626                        offset: 26,
627                    },),
628                    title: Some("Example Title",),
629                    attributes: &[],
630                    author_line: None,
631                    revision_line: None,
632                    comments: &[Span {
633                        data: "// ignore this comment",
634                        line: 2,
635                        col: 1,
636                        offset: 1,
637                    },],
638                    source: Span {
639                        data: "// ignore this comment\n= Example Title",
640                        line: 2,
641                        col: 1,
642                        offset: 1,
643                    },
644                },
645                blocks: &[
646                    Block::Simple(SimpleBlock {
647                        content: Content {
648                            original: Span {
649                                data: "abc",
650                                line: 5,
651                                col: 1,
652                                offset: 41,
653                            },
654                            rendered: "abc",
655                        },
656                        source: Span {
657                            data: "abc",
658                            line: 5,
659                            col: 1,
660                            offset: 41,
661                        },
662                        style: SimpleBlockStyle::Paragraph,
663                        title_source: None,
664                        title: None,
665                        anchor: None,
666                        anchor_reftext: None,
667                        attrlist: None,
668                    },),
669                    Block::Simple(SimpleBlock {
670                        content: Content {
671                            original: Span {
672                                data: "def",
673                                line: 7,
674                                col: 1,
675                                offset: 46,
676                            },
677                            rendered: "def",
678                        },
679                        source: Span {
680                            data: "def",
681                            line: 7,
682                            col: 1,
683                            offset: 46,
684                        },
685                        style: SimpleBlockStyle::Paragraph,
686                        title_source: None,
687                        title: None,
688                        anchor: None,
689                        anchor_reftext: None,
690                        attrlist: None,
691                    },),
692                ],
693                source: Span {
694                    data: "\n// ignore this comment\n= Example Title\n\nabc\n\ndef",
695                    line: 1,
696                    col: 1,
697                    offset: 0,
698                },
699                warnings: &[],
700                source_map: SourceMap(&[]),
701                catalog: Catalog::default(),
702            }
703        );
704    }
705
706    #[test]
707    fn extra_space_before_title() {
708        assert_eq!(
709            Parser::default().parse("=   Example Title\n\nabc"),
710            Document {
711                header: Header {
712                    title_source: Some(Span {
713                        data: "Example Title",
714                        line: 1,
715                        col: 5,
716                        offset: 4,
717                    }),
718                    title: Some("Example Title"),
719                    attributes: &[],
720                    author_line: None,
721                    revision_line: None,
722                    comments: &[],
723                    source: Span {
724                        data: "=   Example Title",
725                        line: 1,
726                        col: 1,
727                        offset: 0,
728                    }
729                },
730                blocks: &[Block::Simple(SimpleBlock {
731                    content: Content {
732                        original: Span {
733                            data: "abc",
734                            line: 3,
735                            col: 1,
736                            offset: 19,
737                        },
738                        rendered: "abc",
739                    },
740                    source: Span {
741                        data: "abc",
742                        line: 3,
743                        col: 1,
744                        offset: 19,
745                    },
746                    style: SimpleBlockStyle::Paragraph,
747                    title_source: None,
748                    title: None,
749                    anchor: None,
750                    anchor_reftext: None,
751                    attrlist: None,
752                })],
753                source: Span {
754                    data: "=   Example Title\n\nabc",
755                    line: 1,
756                    col: 1,
757                    offset: 0
758                },
759                warnings: &[],
760                source_map: SourceMap(&[]),
761                catalog: Catalog::default(),
762            }
763        );
764    }
765
766    #[test]
767    fn err_bad_header() {
768        assert_eq!(
769            Parser::default().parse(
770                "= Title\nJane Smith <jane@example.com>\nv1, 2025-09-28\nnot an attribute\n"
771            ),
772            Document {
773                header: Header {
774                    title_source: Some(Span {
775                        data: "Title",
776                        line: 1,
777                        col: 3,
778                        offset: 2,
779                    }),
780                    title: Some("Title"),
781                    attributes: &[],
782                    author_line: Some(AuthorLine {
783                        authors: &[Author {
784                            name: "Jane Smith",
785                            firstname: "Jane",
786                            middlename: None,
787                            lastname: Some("Smith"),
788                            email: Some("jane@example.com"),
789                        }],
790                        source: Span {
791                            data: "Jane Smith <jane@example.com>",
792                            line: 2,
793                            col: 1,
794                            offset: 8,
795                        },
796                    }),
797                    revision_line: Some(RevisionLine {
798                        revnumber: Some("1",),
799                        revdate: "2025-09-28",
800                        revremark: None,
801                        source: Span {
802                            data: "v1, 2025-09-28",
803                            line: 3,
804                            col: 1,
805                            offset: 38,
806                        },
807                    },),
808                    comments: &[],
809                    source: Span {
810                        data: "= Title\nJane Smith <jane@example.com>\nv1, 2025-09-28",
811                        line: 1,
812                        col: 1,
813                        offset: 0,
814                    }
815                },
816                blocks: &[Block::Simple(SimpleBlock {
817                    content: Content {
818                        original: Span {
819                            data: "not an attribute",
820                            line: 4,
821                            col: 1,
822                            offset: 53,
823                        },
824                        rendered: "not an attribute",
825                    },
826                    source: Span {
827                        data: "not an attribute",
828                        line: 4,
829                        col: 1,
830                        offset: 53,
831                    },
832                    style: SimpleBlockStyle::Paragraph,
833                    title_source: None,
834                    title: None,
835                    anchor: None,
836                    anchor_reftext: None,
837                    attrlist: None,
838                })],
839                source: Span {
840                    data: "= Title\nJane Smith <jane@example.com>\nv1, 2025-09-28\nnot an attribute",
841                    line: 1,
842                    col: 1,
843                    offset: 0
844                },
845                warnings: &[Warning {
846                    source: Span {
847                        data: "not an attribute",
848                        line: 4,
849                        col: 1,
850                        offset: 53,
851                    },
852                    warning: WarningType::DocumentHeaderNotTerminated,
853                },],
854                source_map: SourceMap(&[]),
855                catalog: Catalog::default(),
856            }
857        );
858    }
859
860    #[test]
861    fn err_bad_header_and_bad_macro() {
862        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]");
863
864        assert_eq!(
865            Document {
866                header: Header {
867                    title_source: Some(Span {
868                        data: "Title",
869                        line: 1,
870                        col: 3,
871                        offset: 2,
872                    }),
873                    title: Some("Title"),
874                    attributes: &[],
875                    author_line: Some(AuthorLine {
876                        authors: &[Author {
877                            name: "Jane Smith",
878                            firstname: "Jane",
879                            middlename: None,
880                            lastname: Some("Smith"),
881                            email: Some("jane@example.com"),
882                        }],
883                        source: Span {
884                            data: "Jane Smith <jane@example.com>",
885                            line: 2,
886                            col: 1,
887                            offset: 8,
888                        },
889                    }),
890                    revision_line: Some(RevisionLine {
891                        revnumber: Some("1"),
892                        revdate: "2025-09-28",
893                        revremark: None,
894                        source: Span {
895                            data: "v1, 2025-09-28",
896                            line: 3,
897                            col: 1,
898                            offset: 38,
899                        },
900                    },),
901                    comments: &[],
902                    source: Span {
903                        data: "= Title\nJane Smith <jane@example.com>\nv1, 2025-09-28",
904                        line: 1,
905                        col: 1,
906                        offset: 0,
907                    }
908                },
909                blocks: &[
910                    Block::Preamble(Preamble {
911                        blocks: &[Block::Simple(SimpleBlock {
912                            content: Content {
913                                original: Span {
914                                    data: "not an attribute",
915                                    line: 4,
916                                    col: 1,
917                                    offset: 53,
918                                },
919                                rendered: "not an attribute",
920                            },
921                            source: Span {
922                                data: "not an attribute",
923                                line: 4,
924                                col: 1,
925                                offset: 53,
926                            },
927                            style: SimpleBlockStyle::Paragraph,
928                            title_source: None,
929                            title: None,
930                            anchor: None,
931                            anchor_reftext: None,
932                            attrlist: None,
933                        },),],
934                        source: Span {
935                            data: "not an attribute",
936                            line: 4,
937                            col: 1,
938                            offset: 53,
939                        },
940                    },),
941                    Block::Section(SectionBlock {
942                        level: 1,
943                        section_title: Content {
944                            original: Span {
945                                data: "Section Title",
946                                line: 6,
947                                col: 4,
948                                offset: 74,
949                            },
950                            rendered: "Section Title",
951                        },
952                        blocks: &[Block::Media(MediaBlock {
953                            type_: MediaType::Image,
954                            target: Span {
955                                data: "bar",
956                                line: 8,
957                                col: 8,
958                                offset: 96,
959                            },
960                            macro_attrlist: Attrlist {
961                                attributes: &[
962                                    ElementAttribute {
963                                        name: Some("alt"),
964                                        shorthand_items: &[],
965                                        value: "Sunset"
966                                    },
967                                    ElementAttribute {
968                                        name: Some("width"),
969                                        shorthand_items: &[],
970                                        value: "300"
971                                    },
972                                    ElementAttribute {
973                                        name: Some("height"),
974                                        shorthand_items: &[],
975                                        value: "400"
976                                    },
977                                ],
978                                anchor: None,
979                                source: Span {
980                                    data: "alt=Sunset,width=300,,height=400",
981                                    line: 8,
982                                    col: 12,
983                                    offset: 100,
984                                },
985                            },
986                            source: Span {
987                                data: "image::bar[alt=Sunset,width=300,,height=400]",
988                                line: 8,
989                                col: 1,
990                                offset: 89,
991                            },
992                            title_source: None,
993                            title: None,
994                            anchor: None,
995                            anchor_reftext: None,
996                            attrlist: None,
997                        },),],
998                        source: Span {
999                            data: "== Section Title\n\nimage::bar[alt=Sunset,width=300,,height=400]",
1000                            line: 6,
1001                            col: 1,
1002                            offset: 71,
1003                        },
1004                        title_source: None,
1005                        title: None,
1006                        anchor: None,
1007                        anchor_reftext: None,
1008                        attrlist: None,
1009                        section_type: SectionType::Normal,
1010                        section_id: Some("_section_title"),
1011                        section_number: None,
1012                    },)
1013                ],
1014                source: Span {
1015                    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]",
1016                    line: 1,
1017                    col: 1,
1018                    offset: 0
1019                },
1020                warnings: &[
1021                    Warning {
1022                        source: Span {
1023                            data: "not an attribute",
1024                            line: 4,
1025                            col: 1,
1026                            offset: 53,
1027                        },
1028                        warning: WarningType::DocumentHeaderNotTerminated,
1029                    },
1030                    Warning {
1031                        source: Span {
1032                            data: "alt=Sunset,width=300,,height=400",
1033                            line: 8,
1034                            col: 12,
1035                            offset: 100,
1036                        },
1037                        warning: WarningType::EmptyAttributeValue,
1038                    },
1039                ],
1040                source_map: SourceMap(&[]),
1041                catalog: Catalog {
1042                    refs: HashMap::from([(
1043                        "_section_title",
1044                        RefEntry {
1045                            id: "_section_title",
1046                            reftext: Some("Section Title",),
1047                            ref_type: RefType::Section,
1048                        }
1049                    ),]),
1050                    reftext_to_id: HashMap::from([("Section Title", "_section_title"),]),
1051                }
1052            },
1053            doc
1054        );
1055    }
1056
1057    #[test]
1058    fn impl_debug() {
1059        let doc = Parser::default().parse("= Example Title\n\nabc\n\ndef");
1060
1061        assert_eq!(
1062            format!("{doc:#?}"),
1063            r#"Document {
1064    header: Header {
1065        title_source: Some(
1066            Span {
1067                data: "Example Title",
1068                line: 1,
1069                col: 3,
1070                offset: 2,
1071            },
1072        ),
1073        title: Some(
1074            "Example Title",
1075        ),
1076        attributes: &[],
1077        author_line: None,
1078        revision_line: None,
1079        comments: &[],
1080        source: Span {
1081            data: "= Example Title",
1082            line: 1,
1083            col: 1,
1084            offset: 0,
1085        },
1086    },
1087    blocks: &[
1088        Block::Simple(
1089            SimpleBlock {
1090                content: Content {
1091                    original: Span {
1092                        data: "abc",
1093                        line: 3,
1094                        col: 1,
1095                        offset: 17,
1096                    },
1097                    rendered: "abc",
1098                },
1099                source: Span {
1100                    data: "abc",
1101                    line: 3,
1102                    col: 1,
1103                    offset: 17,
1104                },
1105                style: SimpleBlockStyle::Paragraph,
1106                title_source: None,
1107                title: None,
1108                anchor: None,
1109                anchor_reftext: None,
1110                attrlist: None,
1111            },
1112        ),
1113        Block::Simple(
1114            SimpleBlock {
1115                content: Content {
1116                    original: Span {
1117                        data: "def",
1118                        line: 5,
1119                        col: 1,
1120                        offset: 22,
1121                    },
1122                    rendered: "def",
1123                },
1124                source: Span {
1125                    data: "def",
1126                    line: 5,
1127                    col: 1,
1128                    offset: 22,
1129                },
1130                style: SimpleBlockStyle::Paragraph,
1131                title_source: None,
1132                title: None,
1133                anchor: None,
1134                anchor_reftext: None,
1135                attrlist: None,
1136            },
1137        ),
1138    ],
1139    source: Span {
1140        data: "= Example Title\n\nabc\n\ndef",
1141        line: 1,
1142        col: 1,
1143        offset: 0,
1144    },
1145    warnings: &[],
1146    source_map: SourceMap(&[]),
1147    catalog: Catalog {
1148        refs: HashMap::from([]),
1149        reftext_to_id: HashMap::from([]),
1150    },
1151}"#
1152        );
1153    }
1154}