Skip to main content

asciidoc_parser/document/
attribute.rs

1use crate::{
2    HasSpan, Parser, Span,
3    attributes::Attrlist,
4    blocks::{ContentModel, IsBlock},
5    content::{Content, SubstitutionGroup},
6    span::MatchedItem,
7    strings::CowStr,
8};
9
10/// Document attributes are effectively document-scoped variables for the
11/// AsciiDoc language. The AsciiDoc language defines a set of built-in
12/// attributes, and also allows the author (or extensions) to define additional
13/// document attributes, which may replace built-in attributes when permitted.
14///
15/// An attribute entry is most often declared in the document header. For
16/// attributes that allow it (which includes general purpose attributes), the
17/// attribute entry can alternately be declared between blocks in the document
18/// body (i.e., the portion of the document below the header).
19///
20/// When an attribute is defined in the document body using an attribute entry,
21/// that’s simply referred to as a document attribute. For any attribute defined
22/// in the body, the attribute is available from the point it is set until it is
23/// unset. Attributes defined in the body are not available via the document
24/// metadata.
25///
26/// An attribute declared between blocks (i.e. in the document body) is
27/// represented in this using the same structure (`Attribute`) as a header
28/// attribute. Since it lives between blocks, we treat it as though it was a
29/// block (and thus implement [`IsBlock`] on this type) even though is not
30/// technically a block.
31#[derive(Clone, Debug, Eq, PartialEq)]
32pub struct Attribute<'src> {
33    name: Span<'src>,
34    value_source: Option<Span<'src>>,
35    value: InterpretedValue,
36    source: Span<'src>,
37}
38
39impl<'src> Attribute<'src> {
40    pub(crate) fn parse(source: Span<'src>, parser: &Parser) -> Option<MatchedItem<'src, Self>> {
41        let attr_line = source.take_line_with_continuation()?;
42        let colon = attr_line.item.take_prefix(":")?;
43
44        let mut unset = false;
45        let line = if colon.after.starts_with('!') {
46            unset = true;
47            colon.after.slice_from(1..)
48        } else {
49            colon.after
50        };
51
52        let name = line.take_user_attr_name()?;
53
54        let line = if name.after.starts_with('!') && !unset {
55            unset = true;
56            name.after.slice_from(1..)
57        } else {
58            name.after
59        };
60
61        let line = line.take_prefix(":")?;
62
63        let (value, value_source) = if unset {
64            // Ensure line is now empty except for comment.
65            (InterpretedValue::Unset, None)
66        } else if line.after.is_empty() {
67            (InterpretedValue::Set, None)
68        } else {
69            let raw_value = line.after.take_whitespace();
70            (
71                InterpretedValue::from_raw_value(&raw_value.after, parser),
72                Some(raw_value.after),
73            )
74        };
75
76        let source = source.trim_remainder(attr_line.after);
77        Some(MatchedItem {
78            item: Self {
79                name: name.item,
80                value_source,
81                value,
82                source: source.trim_trailing_whitespace(),
83            },
84            after: attr_line.after,
85        })
86    }
87
88    /// Return a [`Span`] describing the attribute name.
89    pub fn name(&'src self) -> &'src Span<'src> {
90        &self.name
91    }
92
93    /// Return a [`Span`] containing the attribute's raw value (if present).
94    pub fn raw_value(&'src self) -> Option<Span<'src>> {
95        self.value_source
96    }
97
98    /// Return the attribute's interpolated value.
99    pub fn value(&'src self) -> &'src InterpretedValue {
100        &self.value
101    }
102}
103
104impl<'src> HasSpan<'src> for Attribute<'src> {
105    fn span(&self) -> Span<'src> {
106        self.source
107    }
108}
109
110impl<'src> IsBlock<'src> for Attribute<'src> {
111    fn content_model(&self) -> ContentModel {
112        ContentModel::Empty
113    }
114
115    fn raw_context(&self) -> CowStr<'src> {
116        "attribute".into()
117    }
118
119    fn title_source(&'src self) -> Option<Span<'src>> {
120        None
121    }
122
123    fn title(&self) -> Option<&str> {
124        None
125    }
126
127    fn anchor(&'src self) -> Option<Span<'src>> {
128        None
129    }
130
131    fn anchor_reftext(&'src self) -> Option<Span<'src>> {
132        None
133    }
134
135    fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
136        None
137    }
138}
139
140/// The interpreted value of an [`Attribute`].
141///
142/// If the value contains a textual value, this value will
143/// have any continuation markers resolved, but will no longer
144/// contain a reference to the [`Span`] that contains the value.
145#[derive(Clone, Eq, PartialEq)]
146pub enum InterpretedValue {
147    /// A custom value with all necessary interpolations applied.
148    Value(String),
149
150    /// No explicit value. This is typically interpreted as either
151    /// boolean `true` or a default value for a built-in attribute.
152    Set,
153
154    /// Explicitly unset. This is typically interpreted as boolean `false`.
155    Unset,
156}
157
158impl std::fmt::Debug for InterpretedValue {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        match self {
161            InterpretedValue::Value(value) => f
162                .debug_tuple("InterpretedValue::Value")
163                .field(value)
164                .finish(),
165
166            InterpretedValue::Set => write!(f, "InterpretedValue::Set"),
167            InterpretedValue::Unset => write!(f, "InterpretedValue::Unset"),
168        }
169    }
170}
171
172impl InterpretedValue {
173    fn from_raw_value(raw_value: &Span<'_>, parser: &Parser) -> Self {
174        let data = raw_value.data();
175        let mut content = Content::from(*raw_value);
176
177        if data.contains('\n') {
178            let lines: Vec<&str> = data.lines().collect();
179            let last_count = lines.len() - 1;
180
181            let value: Vec<String> = lines
182                .iter()
183                .enumerate()
184                .map(|(count, line)| {
185                    let line = if count > 0 {
186                        line.trim_start_matches(' ')
187                    } else {
188                        line
189                    };
190
191                    let line = line.trim_start_matches('\r').trim_end_matches(' ');
192
193                    // Strip the soft-wrap line continuation marker (a space
194                    // followed by a backslash). Only non-final lines carry a
195                    // continuation; a trailing backslash on the final line is a
196                    // literal character and is preserved.
197                    let line = if count < last_count {
198                        line.trim_end_matches('\\').trim_end_matches(' ')
199                    } else {
200                        line
201                    };
202
203                    // A hard line break marker (a space followed by `+`) before
204                    // a line continuation preserves the newline instead of
205                    // folding it into a space. The `+` itself is left in the
206                    // value verbatim: the post_replacements substitution step is
207                    // not applied to attribute entry values, so the `+` is only
208                    // interpreted as a line break later, when the value is used
209                    // in a block whose substitutions include post_replacements.
210                    if line.ends_with(" +") {
211                        format!("{line}\n")
212                    } else if count < last_count {
213                        format!("{line} ")
214                    } else {
215                        line.to_string()
216                    }
217                })
218                .collect();
219
220            content.rendered = CowStr::Boxed(value.join("").into_boxed_str());
221        } else if let Some(stripped) = data.strip_suffix(" +") {
222            // A single-line value ending in a bare hard line break marker (a
223            // space followed by a single `+`) has that marker stripped. This
224            // mirrors Asciidoctor, where ` +` at the end of an attribute value
225            // is treated as a legacy line-continuation marker: it is removed and
226            // the remaining value is right-trimmed. Contrast with a multi-line
227            // (continued) value, where the marker is preserved so it can drive
228            // post_replacements when the value is later used.
229            //
230            // Trim only ASCII whitespace (matching Ruby's `rstrip`, which
231            // Asciidoctor applies here); Unicode whitespace such as a
232            // non-breaking space is significant and must be preserved.
233            content.rendered = stripped
234                .trim_end_matches([' ', '\t', '\n', '\r', '\x0C', '\x0B'])
235                .into();
236        }
237
238        SubstitutionGroup::Header.apply(&mut content, parser, None);
239
240        InterpretedValue::Value(content.rendered.into_string())
241    }
242
243    pub(crate) fn as_maybe_str(&self) -> Option<&str> {
244        match self {
245            InterpretedValue::Value(value) => Some(value.as_ref()),
246            _ => None,
247        }
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    #![allow(clippy::panic)]
254    #![allow(clippy::unwrap_used)]
255
256    use std::ops::Deref;
257
258    use crate::{blocks::ContentModel, tests::prelude::*};
259
260    #[test]
261    fn impl_clone() {
262        // Silly test to mark the #[derive(...)] line as covered.
263        let h1 =
264            crate::document::Attribute::parse(crate::Span::new(":foo: bar"), &Parser::default())
265                .unwrap();
266        let h2 = h1.clone();
267        assert_eq!(h1, h2);
268    }
269
270    #[test]
271    fn simple_value() {
272        let mi = crate::document::Attribute::parse(
273            crate::Span::new(":foo: bar\nblah"),
274            &Parser::default(),
275        )
276        .unwrap();
277
278        assert_eq!(
279            mi.item,
280            Attribute {
281                name: Span {
282                    data: "foo",
283                    line: 1,
284                    col: 2,
285                    offset: 1,
286                },
287                value_source: Some(Span {
288                    data: "bar",
289                    line: 1,
290                    col: 7,
291                    offset: 6,
292                }),
293                value: InterpretedValue::Value("bar"),
294                source: Span {
295                    data: ":foo: bar",
296                    line: 1,
297                    col: 1,
298                    offset: 0,
299                }
300            }
301        );
302
303        assert_eq!(mi.item.value(), InterpretedValue::Value("bar"));
304
305        assert_eq!(
306            mi.after,
307            Span {
308                data: "blah",
309                line: 2,
310                col: 1,
311                offset: 10
312            }
313        );
314    }
315
316    #[test]
317    fn no_value() {
318        let mi =
319            crate::document::Attribute::parse(crate::Span::new(":foo:\nblah"), &Parser::default())
320                .unwrap();
321
322        assert_eq!(
323            mi.item,
324            Attribute {
325                name: Span {
326                    data: "foo",
327                    line: 1,
328                    col: 2,
329                    offset: 1,
330                },
331                value_source: None,
332                value: InterpretedValue::Set,
333                source: Span {
334                    data: ":foo:",
335                    line: 1,
336                    col: 1,
337                    offset: 0,
338                }
339            }
340        );
341
342        assert_eq!(mi.item.value(), InterpretedValue::Set);
343
344        assert_eq!(
345            mi.after,
346            Span {
347                data: "blah",
348                line: 2,
349                col: 1,
350                offset: 6
351            }
352        );
353    }
354
355    #[test]
356    fn name_with_hyphens() {
357        let mi = crate::document::Attribute::parse(
358            crate::Span::new(":name-with-hyphen:"),
359            &Parser::default(),
360        )
361        .unwrap();
362
363        assert_eq!(
364            mi.item,
365            Attribute {
366                name: Span {
367                    data: "name-with-hyphen",
368                    line: 1,
369                    col: 2,
370                    offset: 1,
371                },
372                value_source: None,
373                value: InterpretedValue::Set,
374                source: Span {
375                    data: ":name-with-hyphen:",
376                    line: 1,
377                    col: 1,
378                    offset: 0,
379                }
380            }
381        );
382
383        assert_eq!(mi.item.value(), InterpretedValue::Set);
384
385        assert_eq!(
386            mi.after,
387            Span {
388                data: "",
389                line: 1,
390                col: 19,
391                offset: 18
392            }
393        );
394    }
395
396    #[test]
397    fn unset_prefix() {
398        let mi =
399            crate::document::Attribute::parse(crate::Span::new(":!foo:\nblah"), &Parser::default())
400                .unwrap();
401
402        assert_eq!(
403            mi.item,
404            Attribute {
405                name: Span {
406                    data: "foo",
407                    line: 1,
408                    col: 3,
409                    offset: 2,
410                },
411                value_source: None,
412                value: InterpretedValue::Unset,
413                source: Span {
414                    data: ":!foo:",
415                    line: 1,
416                    col: 1,
417                    offset: 0,
418                }
419            }
420        );
421
422        assert_eq!(mi.item.value(), InterpretedValue::Unset);
423
424        assert_eq!(
425            mi.after,
426            Span {
427                data: "blah",
428                line: 2,
429                col: 1,
430                offset: 7
431            }
432        );
433    }
434
435    #[test]
436    fn unset_postfix() {
437        let mi =
438            crate::document::Attribute::parse(crate::Span::new(":foo!:\nblah"), &Parser::default())
439                .unwrap();
440
441        assert_eq!(
442            mi.item,
443            Attribute {
444                name: Span {
445                    data: "foo",
446                    line: 1,
447                    col: 2,
448                    offset: 1,
449                },
450                value_source: None,
451                value: InterpretedValue::Unset,
452                source: Span {
453                    data: ":foo!:",
454                    line: 1,
455                    col: 1,
456                    offset: 0,
457                }
458            }
459        );
460
461        assert_eq!(mi.item.value(), InterpretedValue::Unset);
462
463        assert_eq!(
464            mi.after,
465            Span {
466                data: "blah",
467                line: 2,
468                col: 1,
469                offset: 7
470            }
471        );
472    }
473
474    #[test]
475    fn err_unset_prefix_and_postfix() {
476        assert!(
477            crate::document::Attribute::parse(
478                crate::Span::new(":!foo!:\nblah"),
479                &Parser::default()
480            )
481            .is_none()
482        );
483    }
484
485    #[test]
486    fn err_invalid_ident1() {
487        assert!(
488            crate::document::Attribute::parse(
489                crate::Span::new(":@invalid:\nblah"),
490                &Parser::default()
491            )
492            .is_none()
493        );
494    }
495
496    #[test]
497    fn err_invalid_ident2() {
498        assert!(
499            crate::document::Attribute::parse(
500                crate::Span::new(":invalid@:\nblah"),
501                &Parser::default()
502            )
503            .is_none()
504        );
505    }
506
507    #[test]
508    fn err_invalid_ident3() {
509        assert!(
510            crate::document::Attribute::parse(
511                crate::Span::new(":-invalid:\nblah"),
512                &Parser::default()
513            )
514            .is_none()
515        );
516    }
517
518    #[test]
519    fn value_with_soft_wrap() {
520        let mi = crate::document::Attribute::parse(
521            crate::Span::new(":foo: bar \\\n blah"),
522            &Parser::default(),
523        )
524        .unwrap();
525
526        assert_eq!(
527            mi.item,
528            Attribute {
529                name: Span {
530                    data: "foo",
531                    line: 1,
532                    col: 2,
533                    offset: 1,
534                },
535                value_source: Some(Span {
536                    data: "bar \\\n blah",
537                    line: 1,
538                    col: 7,
539                    offset: 6,
540                }),
541                value: InterpretedValue::Value("bar blah"),
542                source: Span {
543                    data: ":foo: bar \\\n blah",
544                    line: 1,
545                    col: 1,
546                    offset: 0,
547                }
548            }
549        );
550
551        assert_eq!(mi.item.value(), InterpretedValue::Value("bar blah"));
552
553        assert_eq!(
554            mi.after,
555            Span {
556                data: "",
557                line: 2,
558                col: 6,
559                offset: 17
560            }
561        );
562    }
563
564    #[test]
565    fn bare_trailing_backslash_is_literal() {
566        // A bare trailing backslash (no preceding space) is not a soft-wrap line
567        // continuation; it is a literal character and the value ends at that line.
568        // See https://github.com/asciidoc-rs/asciidoc-parser/issues/666.
569        let mi = crate::document::Attribute::parse(
570            crate::Span::new(":longpath: very/long/path/to/some/\\\nsubdirectory"),
571            &Parser::default(),
572        )
573        .unwrap();
574
575        assert_eq!(
576            mi.item,
577            Attribute {
578                name: Span {
579                    data: "longpath",
580                    line: 1,
581                    col: 2,
582                    offset: 1,
583                },
584                value_source: Some(Span {
585                    data: "very/long/path/to/some/\\",
586                    line: 1,
587                    col: 12,
588                    offset: 11,
589                }),
590                value: InterpretedValue::Value("very/long/path/to/some/\\"),
591                source: Span {
592                    data: ":longpath: very/long/path/to/some/\\",
593                    line: 1,
594                    col: 1,
595                    offset: 0,
596                }
597            }
598        );
599
600        assert_eq!(
601            mi.item.value(),
602            InterpretedValue::Value("very/long/path/to/some/\\")
603        );
604
605        // `subdirectory` is left as a separate line, not folded into the value.
606        assert_eq!(
607            mi.after,
608            Span {
609                data: "subdirectory",
610                line: 2,
611                col: 1,
612                offset: 36
613            }
614        );
615    }
616
617    #[test]
618    fn literal_trailing_backslash_on_final_line() {
619        // The soft-wrap continuation on the first line is folded, but the bare
620        // trailing backslash on the final line is kept as a literal character.
621        let mi = crate::document::Attribute::parse(
622            crate::Span::new(":foo: bar \\\nbaz\\"),
623            &Parser::default(),
624        )
625        .unwrap();
626
627        assert_eq!(mi.item.value(), InterpretedValue::Value("bar baz\\"));
628    }
629
630    #[test]
631    fn value_with_hard_wrap() {
632        let mi = crate::document::Attribute::parse(
633            crate::Span::new(":foo: bar + \\\n blah"),
634            &Parser::default(),
635        )
636        .unwrap();
637
638        assert_eq!(
639            mi.item,
640            Attribute {
641                name: Span {
642                    data: "foo",
643                    line: 1,
644                    col: 2,
645                    offset: 1,
646                },
647                value_source: Some(Span {
648                    data: "bar + \\\n blah",
649                    line: 1,
650                    col: 7,
651                    offset: 6,
652                }),
653                value: InterpretedValue::Value("bar +\nblah"),
654                source: Span {
655                    data: ":foo: bar + \\\n blah",
656                    line: 1,
657                    col: 1,
658                    offset: 0,
659                }
660            }
661        );
662
663        assert_eq!(mi.item.value(), InterpretedValue::Value("bar +\nblah"));
664
665        assert_eq!(
666            mi.after,
667            Span {
668                data: "",
669                line: 2,
670                col: 6,
671                offset: 19
672            }
673        );
674    }
675
676    #[test]
677    fn single_line_trailing_hard_break_marker_is_stripped() {
678        // A single-line value ending in a bare hard line break marker (a space
679        // followed by a single `+`) has that marker stripped from the
680        // interpreted value, matching Asciidoctor. The raw `value_source` still
681        // contains the literal ` +`. Contrast with `value_with_hard_wrap`, where
682        // a ` +` on a *continued* (multi-line) value is preserved as a newline.
683        let mi = crate::document::Attribute::parse(
684            crate::Span::new(":foo: bar +\nblah"),
685            &Parser::default(),
686        )
687        .unwrap();
688
689        assert_eq!(
690            mi.item,
691            Attribute {
692                name: Span {
693                    data: "foo",
694                    line: 1,
695                    col: 2,
696                    offset: 1,
697                },
698                value_source: Some(Span {
699                    data: "bar +",
700                    line: 1,
701                    col: 7,
702                    offset: 6,
703                }),
704                value: InterpretedValue::Value("bar"),
705                source: Span {
706                    data: ":foo: bar +",
707                    line: 1,
708                    col: 1,
709                    offset: 0,
710                }
711            }
712        );
713
714        assert_eq!(mi.item.value(), InterpretedValue::Value("bar"));
715
716        // `blah` is a separate line, not folded into the value (there is no line
717        // continuation).
718        assert_eq!(
719            mi.after,
720            Span {
721                data: "blah",
722                line: 2,
723                col: 1,
724                offset: 12
725            }
726        );
727    }
728
729    #[test]
730    fn single_line_hard_break_marker_edge_cases() {
731        // The marker is exactly a space followed by a single `+`. Any leading
732        // whitespace before the `+` (including tabs) is trimmed from the result,
733        // but only the single trailing ` +` is removed (not a repeated marker).
734        let value = |src| {
735            crate::document::Attribute::parse(crate::Span::new(src), &Parser::default())
736                .unwrap()
737                .item
738                .value()
739                .clone()
740        };
741
742        // Extra space(s) before the `+` are trimmed away with the marker.
743        assert_eq!(value(":foo: bar  +\nx"), InterpretedValue::Value("bar"));
744
745        // A tab preceding the marker's space is also trimmed.
746        assert_eq!(value(":foo: bar\t +\nx"), InterpretedValue::Value("bar"));
747
748        // `++` is not a hard line break marker; it is preserved verbatim.
749        assert_eq!(value(":foo: bar ++\nx"), InterpretedValue::Value("bar ++"));
750
751        // A `+` with no preceding space is a literal character.
752        assert_eq!(value(":foo: bar+\nx"), InterpretedValue::Value("bar+"));
753
754        // A tab (rather than a space) before the `+` does not form a marker.
755        assert_eq!(value(":foo: bar\t+\nx"), InterpretedValue::Value("bar\t+"));
756
757        // A lone `+` (nothing before the space) is preserved.
758        assert_eq!(value(":foo: +\nx"), InterpretedValue::Value("+"));
759
760        // Only the final ` +` is stripped; an earlier ` +` remains literal.
761        assert_eq!(value(":foo: bar + +\nx"), InterpretedValue::Value("bar +"));
762
763        // Trimming after the marker uses ASCII whitespace rules (Ruby `rstrip`);
764        // a preceding non-breaking space is significant and is preserved.
765        assert_eq!(
766            value(":foo: bar\u{00a0} +\nx"),
767            InterpretedValue::Value("bar\u{00a0}")
768        );
769    }
770
771    #[test]
772    fn is_block() {
773        let mut parser = Parser::default();
774        let maw = crate::blocks::Block::parse(crate::Span::new(":foo: bar\nblah"), &mut parser);
775
776        let mi = maw.item.unwrap();
777        let block = mi.item;
778
779        assert_eq!(
780            block,
781            Block::DocumentAttribute(Attribute {
782                name: Span {
783                    data: "foo",
784                    line: 1,
785                    col: 2,
786                    offset: 1,
787                },
788                value_source: Some(Span {
789                    data: "bar",
790                    line: 1,
791                    col: 7,
792                    offset: 6,
793                }),
794                value: InterpretedValue::Value("bar"),
795                source: Span {
796                    data: ":foo: bar",
797                    line: 1,
798                    col: 1,
799                    offset: 0,
800                }
801            })
802        );
803
804        assert_eq!(block.content_model(), ContentModel::Empty);
805        assert!(block.rendered_content().is_none());
806        assert_eq!(block.raw_context().deref(), "attribute");
807        assert!(block.nested_blocks().next().is_none());
808        assert!(block.title_source().is_none());
809        assert!(block.title().is_none());
810        assert!(block.anchor().is_none());
811        assert!(block.anchor_reftext().is_none());
812        assert!(block.attrlist().is_none());
813        assert_eq!(block.substitution_group(), SubstitutionGroup::Normal);
814
815        assert_eq!(
816            block.span(),
817            Span {
818                data: ":foo: bar",
819                line: 1,
820                col: 1,
821                offset: 0,
822            }
823        );
824
825        let crate::blocks::Block::DocumentAttribute(attr) = block else {
826            panic!("Wrong type");
827        };
828
829        assert_eq!(attr.value(), InterpretedValue::Value("bar"));
830
831        assert_eq!(
832            mi.after,
833            Span {
834                data: "blah",
835                line: 2,
836                col: 1,
837                offset: 10
838            }
839        );
840    }
841
842    #[test]
843    fn affects_document_state() {
844        let mut parser = Parser::default().with_intrinsic_attribute(
845            "agreed",
846            "yes",
847            ModificationContext::Anywhere,
848        );
849
850        let doc =
851            parser.parse("We are agreed? {agreed}\n\n:agreed: no\n\nAre we still agreed? {agreed}");
852
853        let mut blocks = doc.nested_blocks();
854
855        let block1 = blocks.next().unwrap();
856
857        assert_eq!(
858            block1,
859            &Block::Simple(SimpleBlock {
860                content: Content {
861                    original: Span {
862                        data: "We are agreed? {agreed}",
863                        line: 1,
864                        col: 1,
865                        offset: 0,
866                    },
867                    rendered: "We are agreed? yes",
868                },
869                source: Span {
870                    data: "We are agreed? {agreed}",
871                    line: 1,
872                    col: 1,
873                    offset: 0,
874                },
875                style: SimpleBlockStyle::Paragraph,
876                title_source: None,
877                title: None,
878                caption: None,
879                number: None,
880                anchor: None,
881                anchor_reftext: None,
882                attrlist: None,
883            })
884        );
885
886        let _ = blocks.next().unwrap();
887
888        let block3 = blocks.next().unwrap();
889
890        assert_eq!(
891            block3,
892            &Block::Simple(SimpleBlock {
893                content: Content {
894                    original: Span {
895                        data: "Are we still agreed? {agreed}",
896                        line: 5,
897                        col: 1,
898                        offset: 38,
899                    },
900                    rendered: "Are we still agreed? no",
901                },
902                source: Span {
903                    data: "Are we still agreed? {agreed}",
904                    line: 5,
905                    col: 1,
906                    offset: 38,
907                },
908                style: SimpleBlockStyle::Paragraph,
909                title_source: None,
910                title: None,
911                caption: None,
912                number: None,
913                anchor: None,
914                anchor_reftext: None,
915                attrlist: None,
916            })
917        );
918
919        let mut warnings = doc.warnings();
920        assert!(warnings.next().is_none());
921    }
922
923    #[test]
924    fn block_enforces_permission() {
925        let mut parser = Parser::default().with_intrinsic_attribute(
926            "agreed",
927            "yes",
928            ModificationContext::ApiOnly,
929        );
930
931        let doc = parser.parse("Hello\n\n:agreed: no\n\nAre we agreed? {agreed}");
932
933        let mut blocks = doc.nested_blocks();
934        let _ = blocks.next().unwrap();
935        let _ = blocks.next().unwrap();
936        let block3 = blocks.next().unwrap();
937
938        assert_eq!(
939            block3,
940            &Block::Simple(SimpleBlock {
941                content: Content {
942                    original: Span {
943                        data: "Are we agreed? {agreed}",
944                        line: 5,
945                        col: 1,
946                        offset: 20,
947                    },
948                    rendered: "Are we agreed? yes",
949                },
950                source: Span {
951                    data: "Are we agreed? {agreed}",
952                    line: 5,
953                    col: 1,
954                    offset: 20,
955                },
956                style: SimpleBlockStyle::Paragraph,
957                title_source: None,
958                title: None,
959                caption: None,
960                number: None,
961                anchor: None,
962                anchor_reftext: None,
963                attrlist: None,
964            })
965        );
966
967        let mut warnings = doc.warnings();
968        let warning1 = warnings.next().unwrap();
969
970        assert_eq!(
971            &warning1.source,
972            Span {
973                data: ":agreed: no",
974                line: 3,
975                col: 1,
976                offset: 7,
977            }
978        );
979
980        assert_eq!(
981            warning1.warning,
982            WarningType::AttributeValueIsLocked("agreed".to_owned(),)
983        );
984
985        assert!(warnings.next().is_none());
986    }
987
988    mod interpreted_value {
989        mod impl_debug {
990            use crate::document::InterpretedValue;
991
992            #[test]
993            fn value_empty_string() {
994                let interpreted_value = InterpretedValue::Value("".to_string());
995                let debug_output = format!("{:?}", interpreted_value);
996                assert_eq!(debug_output, "InterpretedValue::Value(\"\")");
997            }
998
999            #[test]
1000            fn value_simple_string() {
1001                let interpreted_value = InterpretedValue::Value("hello".to_string());
1002                let debug_output = format!("{:?}", interpreted_value);
1003                assert_eq!(debug_output, "InterpretedValue::Value(\"hello\")");
1004            }
1005
1006            #[test]
1007            fn value_string_with_spaces() {
1008                let interpreted_value = InterpretedValue::Value("hello world".to_string());
1009                let debug_output = format!("{:?}", interpreted_value);
1010                assert_eq!(debug_output, "InterpretedValue::Value(\"hello world\")");
1011            }
1012
1013            #[test]
1014            fn value_string_with_special_chars() {
1015                let interpreted_value = InterpretedValue::Value("test!@#$%^&*()".to_string());
1016                let debug_output = format!("{:?}", interpreted_value);
1017                assert_eq!(debug_output, "InterpretedValue::Value(\"test!@#$%^&*()\")");
1018            }
1019
1020            #[test]
1021            fn value_string_with_quotes() {
1022                let interpreted_value = InterpretedValue::Value("value\"with'quotes".to_string());
1023                let debug_output = format!("{:?}", interpreted_value);
1024                assert_eq!(
1025                    debug_output,
1026                    "InterpretedValue::Value(\"value\\\"with'quotes\")"
1027                );
1028            }
1029
1030            #[test]
1031            fn value_string_with_newlines() {
1032                let interpreted_value = InterpretedValue::Value("line1\nline2\nline3".to_string());
1033                let debug_output = format!("{:?}", interpreted_value);
1034                assert_eq!(
1035                    debug_output,
1036                    "InterpretedValue::Value(\"line1\\nline2\\nline3\")"
1037                );
1038            }
1039
1040            #[test]
1041            fn value_string_with_backslashes() {
1042                let interpreted_value = InterpretedValue::Value("path\\to\\file".to_string());
1043                let debug_output = format!("{:?}", interpreted_value);
1044                assert_eq!(
1045                    debug_output,
1046                    "InterpretedValue::Value(\"path\\\\to\\\\file\")"
1047                );
1048            }
1049
1050            #[test]
1051            fn value_string_with_unicode() {
1052                let interpreted_value = InterpretedValue::Value("café 🚀 ñoño".to_string());
1053                let debug_output = format!("{:?}", interpreted_value);
1054                assert_eq!(debug_output, "InterpretedValue::Value(\"café 🚀 ñoño\")");
1055            }
1056
1057            #[test]
1058            fn set() {
1059                let interpreted_value = InterpretedValue::Set;
1060                let debug_output = format!("{:?}", interpreted_value);
1061                assert_eq!(debug_output, "InterpretedValue::Set");
1062            }
1063
1064            #[test]
1065            fn unset() {
1066                let interpreted_value = InterpretedValue::Unset;
1067                let debug_output = format!("{:?}", interpreted_value);
1068                assert_eq!(debug_output, "InterpretedValue::Unset");
1069            }
1070        }
1071    }
1072}