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                    if line.ends_with('+') {
204                        format!("{}\n", line.trim_end_matches('+').trim_end_matches(' '))
205                    } else if count < last_count {
206                        format!("{line} ")
207                    } else {
208                        line.to_string()
209                    }
210                })
211                .collect();
212
213            content.rendered = CowStr::Boxed(value.join("").into_boxed_str());
214        }
215
216        SubstitutionGroup::Header.apply(&mut content, parser, None);
217
218        InterpretedValue::Value(content.rendered.into_string())
219    }
220
221    pub(crate) fn as_maybe_str(&self) -> Option<&str> {
222        match self {
223            InterpretedValue::Value(value) => Some(value.as_ref()),
224            _ => None,
225        }
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    #![allow(clippy::panic)]
232    #![allow(clippy::unwrap_used)]
233
234    use std::ops::Deref;
235
236    use crate::{blocks::ContentModel, tests::prelude::*};
237
238    #[test]
239    fn impl_clone() {
240        // Silly test to mark the #[derive(...)] line as covered.
241        let h1 =
242            crate::document::Attribute::parse(crate::Span::new(":foo: bar"), &Parser::default())
243                .unwrap();
244        let h2 = h1.clone();
245        assert_eq!(h1, h2);
246    }
247
248    #[test]
249    fn simple_value() {
250        let mi = crate::document::Attribute::parse(
251            crate::Span::new(":foo: bar\nblah"),
252            &Parser::default(),
253        )
254        .unwrap();
255
256        assert_eq!(
257            mi.item,
258            Attribute {
259                name: Span {
260                    data: "foo",
261                    line: 1,
262                    col: 2,
263                    offset: 1,
264                },
265                value_source: Some(Span {
266                    data: "bar",
267                    line: 1,
268                    col: 7,
269                    offset: 6,
270                }),
271                value: InterpretedValue::Value("bar"),
272                source: Span {
273                    data: ":foo: bar",
274                    line: 1,
275                    col: 1,
276                    offset: 0,
277                }
278            }
279        );
280
281        assert_eq!(mi.item.value(), InterpretedValue::Value("bar"));
282
283        assert_eq!(
284            mi.after,
285            Span {
286                data: "blah",
287                line: 2,
288                col: 1,
289                offset: 10
290            }
291        );
292    }
293
294    #[test]
295    fn no_value() {
296        let mi =
297            crate::document::Attribute::parse(crate::Span::new(":foo:\nblah"), &Parser::default())
298                .unwrap();
299
300        assert_eq!(
301            mi.item,
302            Attribute {
303                name: Span {
304                    data: "foo",
305                    line: 1,
306                    col: 2,
307                    offset: 1,
308                },
309                value_source: None,
310                value: InterpretedValue::Set,
311                source: Span {
312                    data: ":foo:",
313                    line: 1,
314                    col: 1,
315                    offset: 0,
316                }
317            }
318        );
319
320        assert_eq!(mi.item.value(), InterpretedValue::Set);
321
322        assert_eq!(
323            mi.after,
324            Span {
325                data: "blah",
326                line: 2,
327                col: 1,
328                offset: 6
329            }
330        );
331    }
332
333    #[test]
334    fn name_with_hyphens() {
335        let mi = crate::document::Attribute::parse(
336            crate::Span::new(":name-with-hyphen:"),
337            &Parser::default(),
338        )
339        .unwrap();
340
341        assert_eq!(
342            mi.item,
343            Attribute {
344                name: Span {
345                    data: "name-with-hyphen",
346                    line: 1,
347                    col: 2,
348                    offset: 1,
349                },
350                value_source: None,
351                value: InterpretedValue::Set,
352                source: Span {
353                    data: ":name-with-hyphen:",
354                    line: 1,
355                    col: 1,
356                    offset: 0,
357                }
358            }
359        );
360
361        assert_eq!(mi.item.value(), InterpretedValue::Set);
362
363        assert_eq!(
364            mi.after,
365            Span {
366                data: "",
367                line: 1,
368                col: 19,
369                offset: 18
370            }
371        );
372    }
373
374    #[test]
375    fn unset_prefix() {
376        let mi =
377            crate::document::Attribute::parse(crate::Span::new(":!foo:\nblah"), &Parser::default())
378                .unwrap();
379
380        assert_eq!(
381            mi.item,
382            Attribute {
383                name: Span {
384                    data: "foo",
385                    line: 1,
386                    col: 3,
387                    offset: 2,
388                },
389                value_source: None,
390                value: InterpretedValue::Unset,
391                source: Span {
392                    data: ":!foo:",
393                    line: 1,
394                    col: 1,
395                    offset: 0,
396                }
397            }
398        );
399
400        assert_eq!(mi.item.value(), InterpretedValue::Unset);
401
402        assert_eq!(
403            mi.after,
404            Span {
405                data: "blah",
406                line: 2,
407                col: 1,
408                offset: 7
409            }
410        );
411    }
412
413    #[test]
414    fn unset_postfix() {
415        let mi =
416            crate::document::Attribute::parse(crate::Span::new(":foo!:\nblah"), &Parser::default())
417                .unwrap();
418
419        assert_eq!(
420            mi.item,
421            Attribute {
422                name: Span {
423                    data: "foo",
424                    line: 1,
425                    col: 2,
426                    offset: 1,
427                },
428                value_source: None,
429                value: InterpretedValue::Unset,
430                source: Span {
431                    data: ":foo!:",
432                    line: 1,
433                    col: 1,
434                    offset: 0,
435                }
436            }
437        );
438
439        assert_eq!(mi.item.value(), InterpretedValue::Unset);
440
441        assert_eq!(
442            mi.after,
443            Span {
444                data: "blah",
445                line: 2,
446                col: 1,
447                offset: 7
448            }
449        );
450    }
451
452    #[test]
453    fn err_unset_prefix_and_postfix() {
454        assert!(
455            crate::document::Attribute::parse(
456                crate::Span::new(":!foo!:\nblah"),
457                &Parser::default()
458            )
459            .is_none()
460        );
461    }
462
463    #[test]
464    fn err_invalid_ident1() {
465        assert!(
466            crate::document::Attribute::parse(
467                crate::Span::new(":@invalid:\nblah"),
468                &Parser::default()
469            )
470            .is_none()
471        );
472    }
473
474    #[test]
475    fn err_invalid_ident2() {
476        assert!(
477            crate::document::Attribute::parse(
478                crate::Span::new(":invalid@:\nblah"),
479                &Parser::default()
480            )
481            .is_none()
482        );
483    }
484
485    #[test]
486    fn err_invalid_ident3() {
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 value_with_soft_wrap() {
498        let mi = crate::document::Attribute::parse(
499            crate::Span::new(":foo: bar \\\n blah"),
500            &Parser::default(),
501        )
502        .unwrap();
503
504        assert_eq!(
505            mi.item,
506            Attribute {
507                name: Span {
508                    data: "foo",
509                    line: 1,
510                    col: 2,
511                    offset: 1,
512                },
513                value_source: Some(Span {
514                    data: "bar \\\n blah",
515                    line: 1,
516                    col: 7,
517                    offset: 6,
518                }),
519                value: InterpretedValue::Value("bar blah"),
520                source: Span {
521                    data: ":foo: bar \\\n blah",
522                    line: 1,
523                    col: 1,
524                    offset: 0,
525                }
526            }
527        );
528
529        assert_eq!(mi.item.value(), InterpretedValue::Value("bar blah"));
530
531        assert_eq!(
532            mi.after,
533            Span {
534                data: "",
535                line: 2,
536                col: 6,
537                offset: 17
538            }
539        );
540    }
541
542    #[test]
543    fn bare_trailing_backslash_is_literal() {
544        // A bare trailing backslash (no preceding space) is not a soft-wrap line
545        // continuation; it is a literal character and the value ends at that line.
546        // See https://github.com/asciidoc-rs/asciidoc-parser/issues/666.
547        let mi = crate::document::Attribute::parse(
548            crate::Span::new(":longpath: very/long/path/to/some/\\\nsubdirectory"),
549            &Parser::default(),
550        )
551        .unwrap();
552
553        assert_eq!(
554            mi.item,
555            Attribute {
556                name: Span {
557                    data: "longpath",
558                    line: 1,
559                    col: 2,
560                    offset: 1,
561                },
562                value_source: Some(Span {
563                    data: "very/long/path/to/some/\\",
564                    line: 1,
565                    col: 12,
566                    offset: 11,
567                }),
568                value: InterpretedValue::Value("very/long/path/to/some/\\"),
569                source: Span {
570                    data: ":longpath: very/long/path/to/some/\\",
571                    line: 1,
572                    col: 1,
573                    offset: 0,
574                }
575            }
576        );
577
578        assert_eq!(
579            mi.item.value(),
580            InterpretedValue::Value("very/long/path/to/some/\\")
581        );
582
583        // `subdirectory` is left as a separate line, not folded into the value.
584        assert_eq!(
585            mi.after,
586            Span {
587                data: "subdirectory",
588                line: 2,
589                col: 1,
590                offset: 36
591            }
592        );
593    }
594
595    #[test]
596    fn literal_trailing_backslash_on_final_line() {
597        // The soft-wrap continuation on the first line is folded, but the bare
598        // trailing backslash on the final line is kept as a literal character.
599        let mi = crate::document::Attribute::parse(
600            crate::Span::new(":foo: bar \\\nbaz\\"),
601            &Parser::default(),
602        )
603        .unwrap();
604
605        assert_eq!(mi.item.value(), InterpretedValue::Value("bar baz\\"));
606    }
607
608    #[test]
609    fn value_with_hard_wrap() {
610        let mi = crate::document::Attribute::parse(
611            crate::Span::new(":foo: bar + \\\n blah"),
612            &Parser::default(),
613        )
614        .unwrap();
615
616        assert_eq!(
617            mi.item,
618            Attribute {
619                name: Span {
620                    data: "foo",
621                    line: 1,
622                    col: 2,
623                    offset: 1,
624                },
625                value_source: Some(Span {
626                    data: "bar + \\\n blah",
627                    line: 1,
628                    col: 7,
629                    offset: 6,
630                }),
631                value: InterpretedValue::Value("bar\nblah"),
632                source: Span {
633                    data: ":foo: bar + \\\n blah",
634                    line: 1,
635                    col: 1,
636                    offset: 0,
637                }
638            }
639        );
640
641        assert_eq!(mi.item.value(), InterpretedValue::Value("bar\nblah"));
642
643        assert_eq!(
644            mi.after,
645            Span {
646                data: "",
647                line: 2,
648                col: 6,
649                offset: 19
650            }
651        );
652    }
653
654    #[test]
655    fn is_block() {
656        let mut parser = Parser::default();
657        let maw = crate::blocks::Block::parse(crate::Span::new(":foo: bar\nblah"), &mut parser);
658
659        let mi = maw.item.unwrap();
660        let block = mi.item;
661
662        assert_eq!(
663            block,
664            Block::DocumentAttribute(Attribute {
665                name: Span {
666                    data: "foo",
667                    line: 1,
668                    col: 2,
669                    offset: 1,
670                },
671                value_source: Some(Span {
672                    data: "bar",
673                    line: 1,
674                    col: 7,
675                    offset: 6,
676                }),
677                value: InterpretedValue::Value("bar"),
678                source: Span {
679                    data: ":foo: bar",
680                    line: 1,
681                    col: 1,
682                    offset: 0,
683                }
684            })
685        );
686
687        assert_eq!(block.content_model(), ContentModel::Empty);
688        assert!(block.rendered_content().is_none());
689        assert_eq!(block.raw_context().deref(), "attribute");
690        assert!(block.nested_blocks().next().is_none());
691        assert!(block.title_source().is_none());
692        assert!(block.title().is_none());
693        assert!(block.anchor().is_none());
694        assert!(block.anchor_reftext().is_none());
695        assert!(block.attrlist().is_none());
696        assert_eq!(block.substitution_group(), SubstitutionGroup::Normal);
697
698        assert_eq!(
699            block.span(),
700            Span {
701                data: ":foo: bar",
702                line: 1,
703                col: 1,
704                offset: 0,
705            }
706        );
707
708        let crate::blocks::Block::DocumentAttribute(attr) = block else {
709            panic!("Wrong type");
710        };
711
712        assert_eq!(attr.value(), InterpretedValue::Value("bar"));
713
714        assert_eq!(
715            mi.after,
716            Span {
717                data: "blah",
718                line: 2,
719                col: 1,
720                offset: 10
721            }
722        );
723    }
724
725    #[test]
726    fn affects_document_state() {
727        let mut parser = Parser::default().with_intrinsic_attribute(
728            "agreed",
729            "yes",
730            ModificationContext::Anywhere,
731        );
732
733        let doc =
734            parser.parse("We are agreed? {agreed}\n\n:agreed: no\n\nAre we still agreed? {agreed}");
735
736        let mut blocks = doc.nested_blocks();
737
738        let block1 = blocks.next().unwrap();
739
740        assert_eq!(
741            block1,
742            &Block::Simple(SimpleBlock {
743                content: Content {
744                    original: Span {
745                        data: "We are agreed? {agreed}",
746                        line: 1,
747                        col: 1,
748                        offset: 0,
749                    },
750                    rendered: "We are agreed? yes",
751                },
752                source: Span {
753                    data: "We are agreed? {agreed}",
754                    line: 1,
755                    col: 1,
756                    offset: 0,
757                },
758                style: SimpleBlockStyle::Paragraph,
759                title_source: None,
760                title: None,
761                caption: None,
762                number: None,
763                anchor: None,
764                anchor_reftext: None,
765                attrlist: None,
766            })
767        );
768
769        let _ = blocks.next().unwrap();
770
771        let block3 = blocks.next().unwrap();
772
773        assert_eq!(
774            block3,
775            &Block::Simple(SimpleBlock {
776                content: Content {
777                    original: Span {
778                        data: "Are we still agreed? {agreed}",
779                        line: 5,
780                        col: 1,
781                        offset: 38,
782                    },
783                    rendered: "Are we still agreed? no",
784                },
785                source: Span {
786                    data: "Are we still agreed? {agreed}",
787                    line: 5,
788                    col: 1,
789                    offset: 38,
790                },
791                style: SimpleBlockStyle::Paragraph,
792                title_source: None,
793                title: None,
794                caption: None,
795                number: None,
796                anchor: None,
797                anchor_reftext: None,
798                attrlist: None,
799            })
800        );
801
802        let mut warnings = doc.warnings();
803        assert!(warnings.next().is_none());
804    }
805
806    #[test]
807    fn block_enforces_permission() {
808        let mut parser = Parser::default().with_intrinsic_attribute(
809            "agreed",
810            "yes",
811            ModificationContext::ApiOnly,
812        );
813
814        let doc = parser.parse("Hello\n\n:agreed: no\n\nAre we agreed? {agreed}");
815
816        let mut blocks = doc.nested_blocks();
817        let _ = blocks.next().unwrap();
818        let _ = blocks.next().unwrap();
819        let block3 = blocks.next().unwrap();
820
821        assert_eq!(
822            block3,
823            &Block::Simple(SimpleBlock {
824                content: Content {
825                    original: Span {
826                        data: "Are we agreed? {agreed}",
827                        line: 5,
828                        col: 1,
829                        offset: 20,
830                    },
831                    rendered: "Are we agreed? yes",
832                },
833                source: Span {
834                    data: "Are we agreed? {agreed}",
835                    line: 5,
836                    col: 1,
837                    offset: 20,
838                },
839                style: SimpleBlockStyle::Paragraph,
840                title_source: None,
841                title: None,
842                caption: None,
843                number: None,
844                anchor: None,
845                anchor_reftext: None,
846                attrlist: None,
847            })
848        );
849
850        let mut warnings = doc.warnings();
851        let warning1 = warnings.next().unwrap();
852
853        assert_eq!(
854            &warning1.source,
855            Span {
856                data: ":agreed: no",
857                line: 3,
858                col: 1,
859                offset: 7,
860            }
861        );
862
863        assert_eq!(
864            warning1.warning,
865            WarningType::AttributeValueIsLocked("agreed".to_owned(),)
866        );
867
868        assert!(warnings.next().is_none());
869    }
870
871    mod interpreted_value {
872        mod impl_debug {
873            use crate::document::InterpretedValue;
874
875            #[test]
876            fn value_empty_string() {
877                let interpreted_value = InterpretedValue::Value("".to_string());
878                let debug_output = format!("{:?}", interpreted_value);
879                assert_eq!(debug_output, "InterpretedValue::Value(\"\")");
880            }
881
882            #[test]
883            fn value_simple_string() {
884                let interpreted_value = InterpretedValue::Value("hello".to_string());
885                let debug_output = format!("{:?}", interpreted_value);
886                assert_eq!(debug_output, "InterpretedValue::Value(\"hello\")");
887            }
888
889            #[test]
890            fn value_string_with_spaces() {
891                let interpreted_value = InterpretedValue::Value("hello world".to_string());
892                let debug_output = format!("{:?}", interpreted_value);
893                assert_eq!(debug_output, "InterpretedValue::Value(\"hello world\")");
894            }
895
896            #[test]
897            fn value_string_with_special_chars() {
898                let interpreted_value = InterpretedValue::Value("test!@#$%^&*()".to_string());
899                let debug_output = format!("{:?}", interpreted_value);
900                assert_eq!(debug_output, "InterpretedValue::Value(\"test!@#$%^&*()\")");
901            }
902
903            #[test]
904            fn value_string_with_quotes() {
905                let interpreted_value = InterpretedValue::Value("value\"with'quotes".to_string());
906                let debug_output = format!("{:?}", interpreted_value);
907                assert_eq!(
908                    debug_output,
909                    "InterpretedValue::Value(\"value\\\"with'quotes\")"
910                );
911            }
912
913            #[test]
914            fn value_string_with_newlines() {
915                let interpreted_value = InterpretedValue::Value("line1\nline2\nline3".to_string());
916                let debug_output = format!("{:?}", interpreted_value);
917                assert_eq!(
918                    debug_output,
919                    "InterpretedValue::Value(\"line1\\nline2\\nline3\")"
920                );
921            }
922
923            #[test]
924            fn value_string_with_backslashes() {
925                let interpreted_value = InterpretedValue::Value("path\\to\\file".to_string());
926                let debug_output = format!("{:?}", interpreted_value);
927                assert_eq!(
928                    debug_output,
929                    "InterpretedValue::Value(\"path\\\\to\\\\file\")"
930                );
931            }
932
933            #[test]
934            fn value_string_with_unicode() {
935                let interpreted_value = InterpretedValue::Value("café 🚀 ñoño".to_string());
936                let debug_output = format!("{:?}", interpreted_value);
937                assert_eq!(debug_output, "InterpretedValue::Value(\"café 🚀 ñoño\")");
938            }
939
940            #[test]
941            fn set() {
942                let interpreted_value = InterpretedValue::Set;
943                let debug_output = format!("{:?}", interpreted_value);
944                assert_eq!(debug_output, "InterpretedValue::Set");
945            }
946
947            #[test]
948            fn unset() {
949                let interpreted_value = InterpretedValue::Unset;
950                let debug_output = format!("{:?}", interpreted_value);
951                assert_eq!(debug_output, "InterpretedValue::Unset");
952            }
953        }
954    }
955}