Skip to main content

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