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