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