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