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