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}