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