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