asciidoc_parser/document/document.rs
1//! Describes the top-level document structure.
2
3use std::{marker::PhantomData, rc::Rc, slice::Iter};
4
5use self_cell::self_cell;
6
7use crate::{
8 HasSpan, Parser, Span,
9 attributes::Attrlist,
10 blocks::{Block, ContentModel, IsBlock, Preamble, parse_utils::parse_blocks_until},
11 document::{
12 Author, Catalog, Docinfo, DocinfoLocation, Header, InterpretedValue, TocConfig, TocMode,
13 },
14 internal::debug::DebugSliceReference,
15 parser::{
16 CatalogResolver, DeferredWarning, InlineSubstitutionRenderer, ReferenceResolver,
17 ReferenceWarning, ResolvedAttributes, SourceMap,
18 },
19 strings::CowStr,
20 warnings::{Warning, WarningType},
21};
22
23/// A document represents the top-level block element in AsciiDoc. It consists
24/// of an optional document header and either a) one or more sections preceded
25/// by an optional preamble or b) a sequence of top-level blocks only.
26///
27/// The document can be configured using a document header. The header is not a
28/// block itself, but contributes metadata to the document, such as the document
29/// title and document attributes.
30///
31/// The `Document` structure is a self-contained package of the original content
32/// that was parsed and the data structures that describe that parsed content.
33/// The API functions on this struct can be used to understand the parse
34/// results.
35#[derive(Eq, PartialEq)]
36pub struct Document<'src> {
37 internal: Internal,
38 _phantom: PhantomData<&'src ()>,
39}
40
41/// Internal dependent struct containing the actual data members that reference
42/// the owned source.
43#[derive(Debug, Eq, PartialEq)]
44struct InternalDependent<'src> {
45 header: Header<'src>,
46 blocks: Vec<Block<'src>>,
47 source: Span<'src>,
48 warnings: Vec<Warning<'src>>,
49 source_map: SourceMap,
50 catalog: Catalog,
51 attributes: ResolvedAttributes,
52 toc: TocConfig,
53 docinfo: Docinfo,
54}
55
56self_cell! {
57 /// Internal implementation struct containing the actual data members.
58 struct Internal {
59 owner: String,
60 #[covariant]
61 dependent: InternalDependent,
62 }
63 impl {Debug, Eq, PartialEq}
64}
65
66impl<'src> Document<'src> {
67 pub(crate) fn parse(
68 source: &str,
69 source_map: SourceMap,
70 preprocessor_warnings: Vec<DeferredWarning>,
71 parser: &mut Parser,
72 ) -> Self {
73 let owned_source = source.to_string();
74
75 // Publish the source map on the parser for the duration of the parse so
76 // an AsciiDoc table cell can map a position in this (preprocessed)
77 // source back to the file and line it originally came from — needed to
78 // report an unresolved `include::` directive inside such a cell against
79 // the correct cursor. The document keeps its own copy of the map, so
80 // clear the parser's reference once parsing completes.
81 let source_map = Rc::new(source_map);
82 parser.source_map = Some(Rc::clone(&source_map));
83
84 let internal = Internal::new(owned_source, |owned_src| {
85 let source = Span::new(owned_src);
86
87 let mi = Header::parse(source, parser);
88 let after_header = mi.item.after;
89
90 parser.sectnumlevels = parser
91 .attribute_value("sectnumlevels")
92 .as_maybe_str()
93 .and_then(|s| s.parse::<usize>().ok())
94 .unwrap_or(3);
95
96 let header = mi.item.item;
97 let mut warnings = mi.warnings;
98
99 // Derive the `iconsdir` default from `imagesdir` (`{imagesdir}/icons`)
100 // now that the header is fully parsed, unless the author set
101 // `iconsdir` explicitly in the header (in which case it wins).
102 let iconsdir_set_in_header = header.attributes().any(|a| a.name().data() == "iconsdir");
103 parser.apply_iconsdir_default(iconsdir_set_in_header);
104
105 let mut maw_blocks = parse_blocks_until(after_header, |_, _| false, parser);
106
107 if !maw_blocks.warnings.is_empty() {
108 warnings.append(&mut maw_blocks.warnings);
109 }
110
111 // Warnings recorded while replacing attribute references (e.g. a
112 // reference to a missing attribute under `attribute-missing=warn`)
113 // are collected on the parser, where only owned offsets — not
114 // borrowed spans — can live. Now that the document's owned source is
115 // available, turn each one back into a spanned `Warning`.
116 let root = Span::new(owned_src);
117
118 // Warnings raised during preprocessing (e.g. an unresolved include
119 // directive) are carried the same way and reconstituted here.
120 for pw in preprocessor_warnings {
121 warnings.push(Warning {
122 source: root.slice(pw.offset..pw.offset + pw.len),
123 warning: pw.warning,
124 origin: None,
125 });
126 }
127
128 for sw in parser.take_substitution_warnings() {
129 warnings.push(Warning {
130 source: root.slice(sw.offset..sw.offset + sw.len),
131 warning: sw.warning,
132 origin: None,
133 });
134 }
135
136 let mut blocks = maw_blocks.item.item;
137 let mut has_content_blocks = false;
138 let mut preamble_split_index: Option<usize> = None;
139
140 // Only look for preamble content if document has a title.
141 // Asciidoctor only creates a preamble when there's a document title.
142 if header.title().is_some() {
143 for (index, block) in blocks.iter().enumerate() {
144 match block {
145 Block::DocumentAttribute(_) => (),
146 Block::Section(_) => {
147 if has_content_blocks {
148 preamble_split_index = Some(index);
149 }
150 break;
151 }
152 _ => {
153 has_content_blocks = true;
154 }
155 }
156 }
157 }
158
159 if let Some(index) = preamble_split_index {
160 let mut section_blocks = blocks.split_off(index);
161
162 let preamble = Preamble::from_blocks(blocks, after_header);
163
164 section_blocks.insert(0, Block::Preamble(preamble));
165 blocks = section_blocks;
166 }
167
168 // Under `doctype: inline`, only the first eligible block is converted,
169 // as bare inline content, and everything after it is dropped (the
170 // rendering lives on the embed path). A compound or empty candidate
171 // has no inline content to emit, so warn here — matching
172 // Asciidoctor's `Document#convert` — and let the embed path render
173 // nothing. This runs on the final block list (after any preamble
174 // split) and uses the same candidate selection as the renderer, so
175 // the two always agree on which block is the candidate.
176 if matches!(
177 parser.attribute_value("doctype"),
178 InterpretedValue::Value(ref v) if v == "inline"
179 ) && let Some(first) = first_inline_candidate(blocks.iter())
180 && matches!(
181 first.content_model(),
182 ContentModel::Compound | ContentModel::Empty
183 )
184 {
185 warnings.push(Warning {
186 source: first.span(),
187 warning: WarningType::NoInlineDoctypeCandidate,
188 origin: None,
189 });
190 }
191
192 // Capture the parser's fully-resolved attribute state so it can be
193 // read back through the `Document` (via `attribute_value`,
194 // `has_attribute`, and `is_attribute_set`) without a `Parser` in
195 // hand — the embed path a renderer uses for `convert_document`.
196 let attributes = parser.snapshot_attributes();
197
198 // The `toc` family of attributes is header-only, so the resolved
199 // placement, depth, title, and class are fixed once the header (and
200 // body) have been processed. Capture them here, while the parser
201 // still holds the document's resolved attribute state.
202 let toc = TocConfig::from_parser(parser);
203
204 // Resolve docinfo from the final attribute state and the parser's
205 // configured docinfo file handler (empty when no handler is set).
206 let docinfo = Docinfo::resolve(parser);
207
208 InternalDependent {
209 header,
210 blocks,
211 source: source.trim_trailing_whitespace(),
212 warnings,
213 source_map: (*source_map).clone(),
214 catalog: parser.take_catalog(),
215 attributes,
216 toc,
217 docinfo,
218 }
219 });
220
221 // The parse is complete; the document now owns its source map.
222 parser.source_map = None;
223
224 Self {
225 internal,
226 _phantom: PhantomData,
227 }
228 }
229
230 /// Return the document header.
231 pub fn header(&self) -> &Header<'_> {
232 &self.internal.borrow_dependent().header
233 }
234
235 /// Return the document's authors.
236 ///
237 /// Authors may be declared on the [author line] or via the `author` /
238 /// `author_N` document attributes; this returns the resolved list
239 /// regardless of which mechanism was used. See [`Header::authors`].
240 ///
241 /// [author line]: https://docs.asciidoctor.org/asciidoc/latest/document/author-line/
242 pub fn authors(&self) -> &[Author] {
243 self.header().authors()
244 }
245
246 /// Return the document title (the level-0 `= Title`), if there was one.
247 ///
248 /// If the title contains a subtitle, this returns the full, combined title.
249 /// Use [`Header::main_title`] and [`Header::subtitle`] (via [`header`]) to
250 /// access the partitioned title.
251 ///
252 /// [`header`]: Self::header
253 pub fn doctitle(&self) -> Option<&str> {
254 self.header().title()
255 }
256
257 /// Return the document subtitle, if the document title contained one.
258 ///
259 /// A subtitle is the text following the final subtitle separator (a colon
260 /// followed by a space, by default) in the document title. See
261 /// [`Header::subtitle`].
262 pub fn subtitle(&self) -> Option<&str> {
263 self.header().subtitle()
264 }
265
266 /// Returns the resolved interpreted value of the named [document
267 /// attribute], as of the end of parsing.
268 ///
269 /// This mirrors [`Parser::attribute_value`] and is the accessor to use on
270 /// the *embed* path — rendering a [`Document`] you already hold, without a
271 /// [`Parser`] in hand. The value reflects the document's final attribute
272 /// state: built-in defaults, values set in the header or body, and the
273 /// current value of any counter of the same name. An attribute that is not
274 /// present, or is present but explicitly [unset], resolves to
275 /// [`InterpretedValue::Unset`].
276 ///
277 /// [document attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/document-attributes/
278 /// [unset]: https://docs.asciidoctor.org/asciidoc/latest/attributes/unset-attributes/
279 /// [`Parser::attribute_value`]: crate::Parser::attribute_value
280 pub fn attribute_value<N: AsRef<str>>(&self, name: N) -> InterpretedValue {
281 self.internal
282 .borrow_dependent()
283 .attributes
284 .attribute_value(name)
285 }
286
287 /// Returns `true` if the document has a [document attribute] by this name
288 /// (whether or not it is set), as of the end of parsing.
289 ///
290 /// This mirrors [`Parser::has_attribute`].
291 ///
292 /// [document attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/document-attributes/
293 /// [`Parser::has_attribute`]: crate::Parser::has_attribute
294 pub fn has_attribute<N: AsRef<str>>(&self, name: N) -> bool {
295 self.internal
296 .borrow_dependent()
297 .attributes
298 .has_attribute(name)
299 }
300
301 /// Returns `true` if the document has a [document attribute] by this name
302 /// which has been set (i.e. is present and not [unset]), as of the end of
303 /// parsing.
304 ///
305 /// This mirrors [`Parser::is_attribute_set`].
306 ///
307 /// [document attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/document-attributes/
308 /// [unset]: https://docs.asciidoctor.org/asciidoc/latest/attributes/unset-attributes/
309 /// [`Parser::is_attribute_set`]: crate::Parser::is_attribute_set
310 pub fn is_attribute_set<N: AsRef<str>>(&self, name: N) -> bool {
311 self.internal
312 .borrow_dependent()
313 .attributes
314 .is_attribute_set(name)
315 }
316
317 /// Return where (and whether) this document's table of contents is
318 /// generated, resolved from the [`toc` attribute].
319 ///
320 /// [`toc` attribute]: https://docs.asciidoctor.org/asciidoc/latest/toc/
321 pub fn toc_mode(&self) -> TocMode {
322 self.internal.borrow_dependent().toc.mode
323 }
324
325 /// Return the depth of section levels included in this document's table of
326 /// contents, resolved from the [`toclevels` attribute] (default `2`).
327 ///
328 /// [`toclevels` attribute]: https://docs.asciidoctor.org/asciidoc/latest/toc/levels/
329 pub fn toc_levels(&self) -> usize {
330 self.internal.borrow_dependent().toc.levels
331 }
332
333 /// Return the title of this document's table of contents, resolved from the
334 /// [`toc-title` attribute] (default _Table of Contents_).
335 ///
336 /// [`toc-title` attribute]: https://docs.asciidoctor.org/asciidoc/latest/toc/title/
337 pub fn toc_title(&self) -> &str {
338 &self.internal.borrow_dependent().toc.title
339 }
340
341 /// Return the CSS class applied to this document's table of contents
342 /// container, resolved from the [`toc-class` attribute] (default `toc`).
343 ///
344 /// [`toc-class` attribute]: https://docs.asciidoctor.org/asciidoc/latest/toc/
345 pub fn toc_class(&self) -> &str {
346 &self.internal.borrow_dependent().toc.class
347 }
348
349 /// Return this document's resolved [docinfo] content for `location`.
350 ///
351 /// [Docinfo] is custom content read from external *docinfo files* and
352 /// injected into the head, header, or footer of the converted output. The
353 /// returned string is the concatenation of the applicable shared and
354 /// private docinfo files (shared first, matching Asciidoctor), with
355 /// `docinfosubs` substitutions already applied.
356 ///
357 /// An empty string is returned when no docinfo applies to the location —
358 /// for example when no [`DocinfoFileHandler`] was configured on the parser,
359 /// the `docinfo` attribute did not enable that scope/location, or no
360 /// matching file was found. Docinfo files are resolved through a
361 /// caller-supplied [`DocinfoFileHandler`], since this crate does not read
362 /// from the filesystem itself.
363 ///
364 /// [docinfo]: https://docs.asciidoctor.org/asciidoc/latest/docinfo/
365 /// [Docinfo]: https://docs.asciidoctor.org/asciidoc/latest/docinfo/
366 /// [`DocinfoFileHandler`]: crate::parser::DocinfoFileHandler
367 pub fn docinfo(&self, location: DocinfoLocation) -> &str {
368 self.internal.borrow_dependent().docinfo.content(location)
369 }
370
371 /// Return an iterator over any warnings found during parsing.
372 pub fn warnings(&self) -> Iter<'_, Warning<'_>> {
373 self.internal.borrow_dependent().warnings.iter()
374 }
375
376 /// Return a [`Span`] describing the entire document source.
377 pub fn span(&self) -> Span<'_> {
378 self.internal.borrow_dependent().source
379 }
380
381 /// Return the source map that tracks original file locations.
382 pub fn source_map(&self) -> &SourceMap {
383 &self.internal.borrow_dependent().source_map
384 }
385
386 /// Return the document catalog for accessing referenceable elements.
387 pub fn catalog(&self) -> &Catalog {
388 &self.internal.borrow_dependent().catalog
389 }
390
391 /// Resolve the document's deferred cross-references using a caller-supplied
392 /// [`ReferenceResolver`] and [`InlineSubstitutionRenderer`].
393 ///
394 /// This is the entry point for multi-document workflows: parse each
395 /// document with [`Parser::parse_deferred`], then call this with a
396 /// resolver that resolves targets against whatever combined index the
397 /// caller has built (this crate does not merge catalogs). The resolver
398 /// binds the "from" document, so a single shared resolver can be
399 /// parametrized per call site.
400 ///
401 /// Resolution is non-destructive and may be repeated (e.g. for incremental
402 /// builds or multiple output targets): the original target text is
403 /// retained, so re-resolving is always possible.
404 ///
405 /// Each call is a **full, independent resolution sweep**. Every
406 /// cross-reference is re-resolved against `resolver`, overwriting any
407 /// result from a previous pass, and the returned [`ReferenceWarning`]s
408 /// reflect only what *this* `resolver` could not resolve — a prior pass
409 /// having resolved a target does not suppress a warning here.
410 /// Consequently, resolving with a resolver that knows fewer targets
411 /// than an earlier pass (for example, calling this after
412 /// [`Parser::parse`] has already auto-resolved against the document's
413 /// own catalog) will re-report those now-unknown targets as unresolved.
414 /// Multi-document pipelines should therefore start from
415 /// [`Parser::parse_deferred`], which does not auto-resolve.
416 pub fn resolve_references(
417 &mut self,
418 resolver: &dyn ReferenceResolver,
419 renderer: &dyn InlineSubstitutionRenderer,
420 ) -> Vec<ReferenceWarning> {
421 let mut warnings = Vec::new();
422
423 self.internal.with_dependent_mut(|_owner, dependent| {
424 for block in dependent.blocks.iter_mut() {
425 block.resolve_references(resolver, renderer, &mut warnings);
426 }
427
428 // Footnote text is extracted out of block content, so its
429 // cross-references are resolved here rather than by the block pass
430 // above. The host resolver does not alias the catalog, so the
431 // footnotes can be borrowed mutably in place.
432 for footnote in dependent.catalog.footnotes.iter_mut() {
433 footnote.resolve_references(resolver, renderer, &mut warnings);
434 }
435 });
436
437 warnings
438 }
439
440 /// Resolve the document's deferred cross-references against its own
441 /// catalog.
442 ///
443 /// This is the single-document convenience path used by [`Parser::parse`].
444 pub(crate) fn resolve_against_own_catalog(
445 &mut self,
446 renderer: &dyn InlineSubstitutionRenderer,
447 ) -> Vec<ReferenceWarning> {
448 let mut warnings = Vec::new();
449
450 self.internal.with_dependent_mut(|_owner, dependent| {
451 // The footnotes are moved out of the catalog so they can be resolved
452 // mutably while the `CatalogResolver` borrows the (footnote-free)
453 // catalog. Footnotes are never cross-reference *targets*, so their
454 // absence does not affect resolution.
455 let mut footnotes = dependent.catalog.take_footnotes();
456
457 let resolver = CatalogResolver::new(&dependent.catalog);
458 for block in dependent.blocks.iter_mut() {
459 block.resolve_references(&resolver, renderer, &mut warnings);
460 }
461
462 // Footnote text is extracted out of block content, so its
463 // cross-references are resolved here rather than by the block pass
464 // above.
465 for footnote in footnotes.iter_mut() {
466 footnote.resolve_references(&resolver, renderer, &mut warnings);
467 }
468
469 dependent.catalog.restore_footnotes(footnotes);
470 });
471
472 warnings
473 }
474}
475
476impl<'src> IsBlock<'src> for Document<'src> {
477 fn content_model(&self) -> ContentModel {
478 ContentModel::Compound
479 }
480
481 fn raw_context(&self) -> CowStr<'src> {
482 "document".into()
483 }
484
485 fn nested_blocks(&'src self) -> Iter<'src, Block<'src>> {
486 self.internal.borrow_dependent().blocks.iter()
487 }
488
489 fn title_source(&'src self) -> Option<Span<'src>> {
490 // Document title is reflected in the Header.
491 None
492 }
493
494 fn title(&self) -> Option<&str> {
495 // Document title is reflected in the Header.
496 None
497 }
498
499 fn anchor(&'src self) -> Option<Span<'src>> {
500 None
501 }
502
503 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
504 None
505 }
506
507 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
508 // Document attributes are reflected in the Header.
509 None
510 }
511}
512
513impl std::fmt::Debug for Document<'_> {
514 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
515 let dependent = self.internal.borrow_dependent();
516 f.debug_struct("Document")
517 .field("header", &dependent.header)
518 .field("blocks", &DebugSliceReference(&dependent.blocks))
519 .field("source", &dependent.source)
520 .field("warnings", &DebugSliceReference(&dependent.warnings))
521 .field("source_map", &dependent.source_map)
522 .field("catalog", &dependent.catalog)
523 .finish()
524 }
525}
526
527/// Returns the first block eligible to be the sole rendered block of an
528/// `inline` document.
529///
530/// A document-attribute entry and a comment (either a `[comment]`-styled block
531/// or a `////` comment block) produce no output, so they are transparent here
532/// and skipped, mirroring how Asciidoctor drops them before taking `blocks[0]`.
533/// The returned block is the one an `inline` document renders (when it holds
534/// inline content) or reports as having *no inline candidate* (when it is
535/// compound or empty).
536///
537/// Both the parse-time `no inline candidate` check and the embed-path renderer
538/// select the candidate through this function so the two never disagree about
539/// which block is the candidate.
540pub(crate) fn first_inline_candidate<'a, 'src>(
541 blocks: impl Iterator<Item = &'a Block<'src>>,
542) -> Option<&'a Block<'src>>
543where
544 'src: 'a,
545{
546 blocks.into_iter().find(|b| {
547 !matches!(b, Block::DocumentAttribute(_))
548 && b.resolved_context().as_ref() != "comment"
549 && b.declared_style() != Some("comment")
550 })
551}
552
553#[cfg(test)]
554mod tests {
555 #![allow(clippy::unwrap_used)]
556
557 use std::{collections::HashMap, ops::Deref};
558
559 use crate::{
560 blocks::{ContentModel, MediaType},
561 document::RefType,
562 tests::prelude::*,
563 };
564
565 #[test]
566 fn empty_source() {
567 let doc = Parser::default().parse("");
568
569 assert_eq!(doc.content_model(), ContentModel::Compound);
570 assert_eq!(doc.raw_context().deref(), "document");
571 assert_eq!(doc.resolved_context().deref(), "document");
572 assert!(doc.declared_style().is_none());
573 assert!(doc.id().is_none());
574 assert!(doc.roles().is_empty());
575 assert!(doc.title_source().is_none());
576 assert!(doc.title().is_none());
577 assert!(doc.anchor().is_none());
578 assert!(doc.anchor_reftext().is_none());
579 assert!(doc.attrlist().is_none());
580 assert_eq!(doc.substitution_group(), SubstitutionGroup::Normal);
581
582 assert_eq!(
583 doc,
584 Document {
585 header: Header {
586 title_source: None,
587 title: None,
588 attributes: &[],
589 author_line: None,
590 revision_line: None,
591 comments: &[],
592 source: Span {
593 data: "",
594 line: 1,
595 col: 1,
596 offset: 0
597 },
598 },
599 source: Span {
600 data: "",
601 line: 1,
602 col: 1,
603 offset: 0
604 },
605 blocks: &[],
606 warnings: &[],
607 source_map: SourceMap(&[]),
608 catalog: Catalog::default(),
609 }
610 );
611 }
612
613 #[test]
614 fn only_spaces() {
615 assert_eq!(
616 Parser::default().parse(" "),
617 Document {
618 header: Header {
619 title_source: None,
620 title: None,
621 attributes: &[],
622 author_line: None,
623 revision_line: None,
624 comments: &[],
625 source: Span {
626 data: "",
627 line: 1,
628 col: 5,
629 offset: 4
630 },
631 },
632 source: Span {
633 data: "",
634 line: 1,
635 col: 1,
636 offset: 0
637 },
638 blocks: &[],
639 warnings: &[],
640 source_map: SourceMap(&[]),
641 catalog: Catalog::default(),
642 }
643 );
644 }
645
646 #[test]
647 fn one_simple_block() {
648 let doc = Parser::default().parse("abc");
649 assert_eq!(
650 doc,
651 Document {
652 header: Header {
653 title_source: None,
654 title: None,
655 attributes: &[],
656 author_line: None,
657 revision_line: None,
658 comments: &[],
659 source: Span {
660 data: "",
661 line: 1,
662 col: 1,
663 offset: 0
664 },
665 },
666 source: Span {
667 data: "abc",
668 line: 1,
669 col: 1,
670 offset: 0
671 },
672 blocks: &[Block::Simple(SimpleBlock {
673 content: Content {
674 original: Span {
675 data: "abc",
676 line: 1,
677 col: 1,
678 offset: 0,
679 },
680 rendered: "abc",
681 },
682 source: Span {
683 data: "abc",
684 line: 1,
685 col: 1,
686 offset: 0,
687 },
688 style: SimpleBlockStyle::Paragraph,
689 title_source: None,
690 title: None,
691 caption: None,
692 number: None,
693 anchor: None,
694 anchor_reftext: None,
695 attrlist: None,
696 })],
697 warnings: &[],
698 source_map: SourceMap(&[]),
699 catalog: Catalog::default(),
700 }
701 );
702
703 assert!(doc.anchor().is_none());
704 assert!(doc.anchor_reftext().is_none());
705 }
706
707 #[test]
708 fn two_simple_blocks() {
709 assert_eq!(
710 Parser::default().parse("abc\n\ndef"),
711 Document {
712 header: Header {
713 title_source: None,
714 title: None,
715 attributes: &[],
716 author_line: None,
717 revision_line: None,
718 comments: &[],
719 source: Span {
720 data: "",
721 line: 1,
722 col: 1,
723 offset: 0
724 },
725 },
726 source: Span {
727 data: "abc\n\ndef",
728 line: 1,
729 col: 1,
730 offset: 0
731 },
732 blocks: &[
733 Block::Simple(SimpleBlock {
734 content: Content {
735 original: Span {
736 data: "abc",
737 line: 1,
738 col: 1,
739 offset: 0,
740 },
741 rendered: "abc",
742 },
743 source: Span {
744 data: "abc",
745 line: 1,
746 col: 1,
747 offset: 0,
748 },
749 style: SimpleBlockStyle::Paragraph,
750 title_source: None,
751 title: None,
752 caption: None,
753 number: None,
754 anchor: None,
755 anchor_reftext: None,
756 attrlist: None,
757 }),
758 Block::Simple(SimpleBlock {
759 content: Content {
760 original: Span {
761 data: "def",
762 line: 3,
763 col: 1,
764 offset: 5,
765 },
766 rendered: "def",
767 },
768 source: Span {
769 data: "def",
770 line: 3,
771 col: 1,
772 offset: 5,
773 },
774 style: SimpleBlockStyle::Paragraph,
775 title_source: None,
776 title: None,
777 caption: None,
778 number: None,
779 anchor: None,
780 anchor_reftext: None,
781 attrlist: None,
782 })
783 ],
784 warnings: &[],
785 source_map: SourceMap(&[]),
786 catalog: Catalog::default(),
787 }
788 );
789 }
790
791 #[test]
792 fn two_blocks_and_title() {
793 assert_eq!(
794 Parser::default().parse("= Example Title\n\nabc\n\ndef"),
795 Document {
796 header: Header {
797 title_source: Some(Span {
798 data: "Example Title",
799 line: 1,
800 col: 3,
801 offset: 2,
802 }),
803 title: Some("Example Title"),
804 attributes: &[],
805 author_line: None,
806 revision_line: None,
807 comments: &[],
808 source: Span {
809 data: "= Example Title",
810 line: 1,
811 col: 1,
812 offset: 0,
813 }
814 },
815 blocks: &[
816 Block::Simple(SimpleBlock {
817 content: Content {
818 original: Span {
819 data: "abc",
820 line: 3,
821 col: 1,
822 offset: 17,
823 },
824 rendered: "abc",
825 },
826 source: Span {
827 data: "abc",
828 line: 3,
829 col: 1,
830 offset: 17,
831 },
832 style: SimpleBlockStyle::Paragraph,
833 title_source: None,
834 title: None,
835 caption: None,
836 number: None,
837 anchor: None,
838 anchor_reftext: None,
839 attrlist: None,
840 }),
841 Block::Simple(SimpleBlock {
842 content: Content {
843 original: Span {
844 data: "def",
845 line: 5,
846 col: 1,
847 offset: 22,
848 },
849 rendered: "def",
850 },
851 source: Span {
852 data: "def",
853 line: 5,
854 col: 1,
855 offset: 22,
856 },
857 style: SimpleBlockStyle::Paragraph,
858 title_source: None,
859 title: None,
860 caption: None,
861 number: None,
862 anchor: None,
863 anchor_reftext: None,
864 attrlist: None,
865 })
866 ],
867 source: Span {
868 data: "= Example Title\n\nabc\n\ndef",
869 line: 1,
870 col: 1,
871 offset: 0
872 },
873 warnings: &[],
874 source_map: SourceMap(&[]),
875 catalog: Catalog::default(),
876 }
877 );
878 }
879
880 #[test]
881 fn blank_lines_before_header() {
882 let doc = Parser::default().parse("\n\n= Example Title\n\nabc\n\ndef");
883
884 assert_eq!(
885 doc,
886 Document {
887 header: Header {
888 title_source: Some(Span {
889 data: "Example Title",
890 line: 3,
891 col: 3,
892 offset: 4,
893 },),
894 title: Some("Example Title",),
895 attributes: &[],
896 author_line: None,
897 revision_line: None,
898 comments: &[],
899 source: Span {
900 data: "= Example Title",
901 line: 3,
902 col: 1,
903 offset: 2,
904 },
905 },
906 blocks: &[
907 Block::Simple(SimpleBlock {
908 content: Content {
909 original: Span {
910 data: "abc",
911 line: 5,
912 col: 1,
913 offset: 19,
914 },
915 rendered: "abc",
916 },
917 source: Span {
918 data: "abc",
919 line: 5,
920 col: 1,
921 offset: 19,
922 },
923 style: SimpleBlockStyle::Paragraph,
924 title_source: None,
925 title: None,
926 caption: None,
927 number: None,
928 anchor: None,
929 anchor_reftext: None,
930 attrlist: None,
931 },),
932 Block::Simple(SimpleBlock {
933 content: Content {
934 original: Span {
935 data: "def",
936 line: 7,
937 col: 1,
938 offset: 24,
939 },
940 rendered: "def",
941 },
942 source: Span {
943 data: "def",
944 line: 7,
945 col: 1,
946 offset: 24,
947 },
948 style: SimpleBlockStyle::Paragraph,
949 title_source: None,
950 title: None,
951 caption: None,
952 number: None,
953 anchor: None,
954 anchor_reftext: None,
955 attrlist: None,
956 },),
957 ],
958 source: Span {
959 data: "\n\n= Example Title\n\nabc\n\ndef",
960 line: 1,
961 col: 1,
962 offset: 0,
963 },
964 warnings: &[],
965 source_map: SourceMap(&[]),
966 catalog: Catalog::default(),
967 }
968 );
969 }
970
971 #[test]
972 fn blank_lines_and_comment_before_header() {
973 let doc =
974 Parser::default().parse("\n// ignore this comment\n= Example Title\n\nabc\n\ndef");
975
976 assert_eq!(
977 doc,
978 Document {
979 header: Header {
980 title_source: Some(Span {
981 data: "Example Title",
982 line: 3,
983 col: 3,
984 offset: 26,
985 },),
986 title: Some("Example Title",),
987 attributes: &[],
988 author_line: None,
989 revision_line: None,
990 comments: &[Span {
991 data: "// ignore this comment",
992 line: 2,
993 col: 1,
994 offset: 1,
995 },],
996 source: Span {
997 data: "// ignore this comment\n= Example Title",
998 line: 2,
999 col: 1,
1000 offset: 1,
1001 },
1002 },
1003 blocks: &[
1004 Block::Simple(SimpleBlock {
1005 content: Content {
1006 original: Span {
1007 data: "abc",
1008 line: 5,
1009 col: 1,
1010 offset: 41,
1011 },
1012 rendered: "abc",
1013 },
1014 source: Span {
1015 data: "abc",
1016 line: 5,
1017 col: 1,
1018 offset: 41,
1019 },
1020 style: SimpleBlockStyle::Paragraph,
1021 title_source: None,
1022 title: None,
1023 caption: None,
1024 number: None,
1025 anchor: None,
1026 anchor_reftext: None,
1027 attrlist: None,
1028 },),
1029 Block::Simple(SimpleBlock {
1030 content: Content {
1031 original: Span {
1032 data: "def",
1033 line: 7,
1034 col: 1,
1035 offset: 46,
1036 },
1037 rendered: "def",
1038 },
1039 source: Span {
1040 data: "def",
1041 line: 7,
1042 col: 1,
1043 offset: 46,
1044 },
1045 style: SimpleBlockStyle::Paragraph,
1046 title_source: None,
1047 title: None,
1048 caption: None,
1049 number: None,
1050 anchor: None,
1051 anchor_reftext: None,
1052 attrlist: None,
1053 },),
1054 ],
1055 source: Span {
1056 data: "\n// ignore this comment\n= Example Title\n\nabc\n\ndef",
1057 line: 1,
1058 col: 1,
1059 offset: 0,
1060 },
1061 warnings: &[],
1062 source_map: SourceMap(&[]),
1063 catalog: Catalog::default(),
1064 }
1065 );
1066 }
1067
1068 #[test]
1069 fn extra_space_before_title() {
1070 assert_eq!(
1071 Parser::default().parse("= Example Title\n\nabc"),
1072 Document {
1073 header: Header {
1074 title_source: Some(Span {
1075 data: "Example Title",
1076 line: 1,
1077 col: 5,
1078 offset: 4,
1079 }),
1080 title: Some("Example Title"),
1081 attributes: &[],
1082 author_line: None,
1083 revision_line: None,
1084 comments: &[],
1085 source: Span {
1086 data: "= Example Title",
1087 line: 1,
1088 col: 1,
1089 offset: 0,
1090 }
1091 },
1092 blocks: &[Block::Simple(SimpleBlock {
1093 content: Content {
1094 original: Span {
1095 data: "abc",
1096 line: 3,
1097 col: 1,
1098 offset: 19,
1099 },
1100 rendered: "abc",
1101 },
1102 source: Span {
1103 data: "abc",
1104 line: 3,
1105 col: 1,
1106 offset: 19,
1107 },
1108 style: SimpleBlockStyle::Paragraph,
1109 title_source: None,
1110 title: None,
1111 caption: None,
1112 number: None,
1113 anchor: None,
1114 anchor_reftext: None,
1115 attrlist: None,
1116 })],
1117 source: Span {
1118 data: "= Example Title\n\nabc",
1119 line: 1,
1120 col: 1,
1121 offset: 0
1122 },
1123 warnings: &[],
1124 source_map: SourceMap(&[]),
1125 catalog: Catalog::default(),
1126 }
1127 );
1128 }
1129
1130 #[test]
1131 fn err_bad_header() {
1132 assert_eq!(
1133 Parser::default().parse(
1134 "= Title\nJane Smith <jane@example.com>\nv1, 2025-09-28\nnot an attribute\n"
1135 ),
1136 Document {
1137 header: Header {
1138 title_source: Some(Span {
1139 data: "Title",
1140 line: 1,
1141 col: 3,
1142 offset: 2,
1143 }),
1144 title: Some("Title"),
1145 attributes: &[],
1146 author_line: Some(AuthorLine {
1147 authors: &[Author {
1148 name: "Jane Smith",
1149 firstname: "Jane",
1150 middlename: None,
1151 lastname: Some("Smith"),
1152 email: Some("jane@example.com"),
1153 }],
1154 source: Span {
1155 data: "Jane Smith <jane@example.com>",
1156 line: 2,
1157 col: 1,
1158 offset: 8,
1159 },
1160 }),
1161 revision_line: Some(RevisionLine {
1162 revnumber: Some("1",),
1163 revdate: "2025-09-28",
1164 revremark: None,
1165 source: Span {
1166 data: "v1, 2025-09-28",
1167 line: 3,
1168 col: 1,
1169 offset: 38,
1170 },
1171 },),
1172 comments: &[],
1173 source: Span {
1174 data: "= Title\nJane Smith <jane@example.com>\nv1, 2025-09-28",
1175 line: 1,
1176 col: 1,
1177 offset: 0,
1178 }
1179 },
1180 blocks: &[Block::Simple(SimpleBlock {
1181 content: Content {
1182 original: Span {
1183 data: "not an attribute",
1184 line: 4,
1185 col: 1,
1186 offset: 53,
1187 },
1188 rendered: "not an attribute",
1189 },
1190 source: Span {
1191 data: "not an attribute",
1192 line: 4,
1193 col: 1,
1194 offset: 53,
1195 },
1196 style: SimpleBlockStyle::Paragraph,
1197 title_source: None,
1198 title: None,
1199 caption: None,
1200 number: None,
1201 anchor: None,
1202 anchor_reftext: None,
1203 attrlist: None,
1204 })],
1205 source: Span {
1206 data: "= Title\nJane Smith <jane@example.com>\nv1, 2025-09-28\nnot an attribute",
1207 line: 1,
1208 col: 1,
1209 offset: 0
1210 },
1211 warnings: &[Warning {
1212 source: Span {
1213 data: "not an attribute",
1214 line: 4,
1215 col: 1,
1216 offset: 53,
1217 },
1218 warning: WarningType::DocumentHeaderNotTerminated,
1219 },],
1220 source_map: SourceMap(&[]),
1221 catalog: Catalog::default(),
1222 }
1223 );
1224 }
1225
1226 #[test]
1227 fn err_bad_header_and_bad_macro() {
1228 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]");
1229
1230 assert_eq!(
1231 Document {
1232 header: Header {
1233 title_source: Some(Span {
1234 data: "Title",
1235 line: 1,
1236 col: 3,
1237 offset: 2,
1238 }),
1239 title: Some("Title"),
1240 attributes: &[],
1241 author_line: Some(AuthorLine {
1242 authors: &[Author {
1243 name: "Jane Smith",
1244 firstname: "Jane",
1245 middlename: None,
1246 lastname: Some("Smith"),
1247 email: Some("jane@example.com"),
1248 }],
1249 source: Span {
1250 data: "Jane Smith <jane@example.com>",
1251 line: 2,
1252 col: 1,
1253 offset: 8,
1254 },
1255 }),
1256 revision_line: Some(RevisionLine {
1257 revnumber: Some("1"),
1258 revdate: "2025-09-28",
1259 revremark: None,
1260 source: Span {
1261 data: "v1, 2025-09-28",
1262 line: 3,
1263 col: 1,
1264 offset: 38,
1265 },
1266 },),
1267 comments: &[],
1268 source: Span {
1269 data: "= Title\nJane Smith <jane@example.com>\nv1, 2025-09-28",
1270 line: 1,
1271 col: 1,
1272 offset: 0,
1273 }
1274 },
1275 blocks: &[
1276 Block::Preamble(Preamble {
1277 blocks: &[Block::Simple(SimpleBlock {
1278 content: Content {
1279 original: Span {
1280 data: "not an attribute",
1281 line: 4,
1282 col: 1,
1283 offset: 53,
1284 },
1285 rendered: "not an attribute",
1286 },
1287 source: Span {
1288 data: "not an attribute",
1289 line: 4,
1290 col: 1,
1291 offset: 53,
1292 },
1293 style: SimpleBlockStyle::Paragraph,
1294 title_source: None,
1295 title: None,
1296 caption: None,
1297 number: None,
1298 anchor: None,
1299 anchor_reftext: None,
1300 attrlist: None,
1301 },),],
1302 source: Span {
1303 data: "not an attribute",
1304 line: 4,
1305 col: 1,
1306 offset: 53,
1307 },
1308 },),
1309 Block::Section(SectionBlock {
1310 level: 1,
1311 section_title: Content {
1312 original: Span {
1313 data: "Section Title",
1314 line: 6,
1315 col: 4,
1316 offset: 74,
1317 },
1318 rendered: "Section Title",
1319 },
1320 blocks: &[Block::Media(MediaBlock {
1321 type_: MediaType::Image,
1322 target: Span {
1323 data: "bar",
1324 line: 8,
1325 col: 8,
1326 offset: 96,
1327 },
1328 macro_attrlist: Attrlist {
1329 attributes: &[
1330 ElementAttribute {
1331 name: Some("alt"),
1332 shorthand_items: &[],
1333 value: "Sunset"
1334 },
1335 ElementAttribute {
1336 name: Some("width"),
1337 shorthand_items: &[],
1338 value: "300"
1339 },
1340 ElementAttribute {
1341 name: Some("height"),
1342 shorthand_items: &[],
1343 value: "400"
1344 },
1345 ],
1346 anchor: None,
1347 source: Span {
1348 data: "alt=Sunset,width=300,,height=400",
1349 line: 8,
1350 col: 12,
1351 offset: 100,
1352 },
1353 },
1354 source: Span {
1355 data: "image::bar[alt=Sunset,width=300,,height=400]",
1356 line: 8,
1357 col: 1,
1358 offset: 89,
1359 },
1360 title_source: None,
1361 title: None,
1362 caption: None,
1363 number: None,
1364 anchor: None,
1365 anchor_reftext: None,
1366 attrlist: None,
1367 },),],
1368 source: Span {
1369 data: "== Section Title\n\nimage::bar[alt=Sunset,width=300,,height=400]",
1370 line: 6,
1371 col: 1,
1372 offset: 71,
1373 },
1374 title_source: None,
1375 title: None,
1376 anchor: None,
1377 anchor_reftext: None,
1378 attrlist: None,
1379 section_type: SectionType::Normal,
1380 section_id: Some("_section_title"),
1381 caption: None,
1382 section_number: None,
1383 },)
1384 ],
1385 source: Span {
1386 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]",
1387 line: 1,
1388 col: 1,
1389 offset: 0
1390 },
1391 warnings: &[
1392 Warning {
1393 source: Span {
1394 data: "not an attribute",
1395 line: 4,
1396 col: 1,
1397 offset: 53,
1398 },
1399 warning: WarningType::DocumentHeaderNotTerminated,
1400 },
1401 Warning {
1402 source: Span {
1403 data: "alt=Sunset,width=300,,height=400",
1404 line: 8,
1405 col: 12,
1406 offset: 100,
1407 },
1408 warning: WarningType::EmptyAttributeValue,
1409 },
1410 ],
1411 source_map: SourceMap(&[]),
1412 catalog: Catalog {
1413 refs: HashMap::from([(
1414 "_section_title",
1415 RefEntry {
1416 id: "_section_title",
1417 reftext: Some("Section Title",),
1418 ref_type: RefType::Section,
1419 }
1420 ),]),
1421 reftext_to_id: HashMap::from([("Section Title", "_section_title"),]),
1422 }
1423 },
1424 doc
1425 );
1426 }
1427
1428 #[test]
1429 fn impl_debug() {
1430 let doc = Parser::default().parse("= Example Title\n\nabc\n\ndef");
1431
1432 assert_eq!(
1433 format!("{doc:#?}"),
1434 r#"Document {
1435 header: Header {
1436 title_source: Some(
1437 Span {
1438 data: "Example Title",
1439 line: 1,
1440 col: 3,
1441 offset: 2,
1442 },
1443 ),
1444 title: Some(
1445 "Example Title",
1446 ),
1447 main_title: Some(
1448 "Example Title",
1449 ),
1450 subtitle: None,
1451 attributes: &[],
1452 author_line: None,
1453 authors: [],
1454 revision_line: None,
1455 comments: &[],
1456 source: Span {
1457 data: "= Example Title",
1458 line: 1,
1459 col: 1,
1460 offset: 0,
1461 },
1462 },
1463 blocks: &[
1464 Block::Simple(
1465 SimpleBlock {
1466 content: Content {
1467 original: Span {
1468 data: "abc",
1469 line: 3,
1470 col: 1,
1471 offset: 17,
1472 },
1473 rendered: "abc",
1474 },
1475 source: Span {
1476 data: "abc",
1477 line: 3,
1478 col: 1,
1479 offset: 17,
1480 },
1481 style: SimpleBlockStyle::Paragraph,
1482 title_source: None,
1483 title: None,
1484 caption: None,
1485 number: None,
1486 anchor: None,
1487 anchor_reftext: None,
1488 attrlist: None,
1489 },
1490 ),
1491 Block::Simple(
1492 SimpleBlock {
1493 content: Content {
1494 original: Span {
1495 data: "def",
1496 line: 5,
1497 col: 1,
1498 offset: 22,
1499 },
1500 rendered: "def",
1501 },
1502 source: Span {
1503 data: "def",
1504 line: 5,
1505 col: 1,
1506 offset: 22,
1507 },
1508 style: SimpleBlockStyle::Paragraph,
1509 title_source: None,
1510 title: None,
1511 caption: None,
1512 number: None,
1513 anchor: None,
1514 anchor_reftext: None,
1515 attrlist: None,
1516 },
1517 ),
1518 ],
1519 source: Span {
1520 data: "= Example Title\n\nabc\n\ndef",
1521 line: 1,
1522 col: 1,
1523 offset: 0,
1524 },
1525 warnings: &[],
1526 source_map: SourceMap(&[]),
1527 catalog: Catalog {
1528 refs: HashMap::from([]),
1529 reftext_to_id: HashMap::from([]),
1530 footnotes: [],
1531 },
1532}"#
1533 );
1534 }
1535
1536 mod attribute_access {
1537 use crate::{document::InterpretedValue, tests::prelude::*};
1538
1539 #[test]
1540 fn built_in_default() {
1541 // `doctype` is a built-in attribute with a default of `article`; it
1542 // should read back through the `Document` even though the source
1543 // never sets it.
1544 let doc = Parser::default().parse("Hello.");
1545
1546 assert!(doc.has_attribute("doctype"));
1547 assert!(doc.is_attribute_set("doctype"));
1548 assert_eq!(
1549 doc.attribute_value("doctype"),
1550 InterpretedValue::Value("article".to_string())
1551 );
1552 }
1553
1554 #[test]
1555 fn header_set_attribute() {
1556 let doc = Parser::default().parse("= Title\n:lang: fr\n\nBonjour.");
1557
1558 assert!(doc.has_attribute("lang"));
1559 assert!(doc.is_attribute_set("lang"));
1560 assert_eq!(
1561 doc.attribute_value("lang"),
1562 InterpretedValue::Value("fr".to_string())
1563 );
1564 }
1565
1566 #[test]
1567 fn body_set_attribute() {
1568 // An attribute set in the document body (not the header) is part of
1569 // the final resolved state and must be visible on the `Document`.
1570 let doc = Parser::default().parse("First paragraph.\n\n:foo: bar\n\nSecond paragraph.");
1571
1572 assert!(doc.has_attribute("foo"));
1573 assert!(doc.is_attribute_set("foo"));
1574 assert_eq!(
1575 doc.attribute_value("foo"),
1576 InterpretedValue::Value("bar".to_string())
1577 );
1578 }
1579
1580 #[test]
1581 fn set_flag_attribute() {
1582 // A bare `:sectnums:` turns the attribute on; its resolved value is
1583 // the built-in default `all`.
1584 let doc = Parser::default().parse("= Title\n:sectnums:\n\nBody.");
1585
1586 assert!(doc.has_attribute("sectnums"));
1587 assert!(doc.is_attribute_set("sectnums"));
1588 assert_eq!(
1589 doc.attribute_value("sectnums"),
1590 InterpretedValue::Value("all".to_string())
1591 );
1592 }
1593
1594 #[test]
1595 fn unset_attribute() {
1596 // `sectnums` exists in the built-in table but is unset by default.
1597 let doc = Parser::default().parse("Hello.");
1598
1599 assert!(doc.has_attribute("sectnums"));
1600 assert!(!doc.is_attribute_set("sectnums"));
1601 assert_eq!(doc.attribute_value("sectnums"), InterpretedValue::Unset);
1602 }
1603
1604 #[test]
1605 fn explicitly_unset_attribute() {
1606 // `:!sectnums:` explicitly unsets an otherwise-set attribute: it is
1607 // present but not set.
1608 let doc = Parser::default().parse("= Title\n:sectnums:\n:!sectnums:\n\nBody.");
1609
1610 assert!(doc.has_attribute("sectnums"));
1611 assert!(!doc.is_attribute_set("sectnums"));
1612 assert_eq!(doc.attribute_value("sectnums"), InterpretedValue::Unset);
1613 }
1614
1615 #[test]
1616 fn absent_attribute() {
1617 let doc = Parser::default().parse("Hello.");
1618
1619 assert!(!doc.has_attribute("no-such-attribute"));
1620 assert!(!doc.is_attribute_set("no-such-attribute"));
1621 assert_eq!(
1622 doc.attribute_value("no-such-attribute"),
1623 InterpretedValue::Unset
1624 );
1625 }
1626
1627 #[test]
1628 fn matches_parser_state() {
1629 // The values read back through the `Document` must equal what the
1630 // `Parser` itself reports after `parse`.
1631 let mut parser = Parser::default();
1632 let doc = parser.parse("= Title\n:lang: de\n:sectnums:\n\nBody.");
1633
1634 for name in [
1635 "lang",
1636 "sectnums",
1637 "doctype",
1638 "notitle",
1639 "no-such-attribute",
1640 ] {
1641 assert_eq!(doc.attribute_value(name), parser.attribute_value(name));
1642 assert_eq!(doc.has_attribute(name), parser.has_attribute(name));
1643 assert_eq!(doc.is_attribute_set(name), parser.is_attribute_set(name));
1644 }
1645 }
1646
1647 #[test]
1648 fn counter_value() {
1649 // A counter's current value is part of the resolved attribute state
1650 // and supersedes any like-named attribute.
1651 let doc = Parser::default().parse("{counter:my-counter}\n\n{counter:my-counter}");
1652
1653 assert!(doc.has_attribute("my-counter"));
1654 assert!(doc.is_attribute_set("my-counter"));
1655 assert_eq!(
1656 doc.attribute_value("my-counter"),
1657 InterpretedValue::Value("2".to_string())
1658 );
1659 }
1660 }
1661}