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                caption: None,
690                number: None,
691                anchor: None,
692                anchor_reftext: None,
693                attrlist: None,
694            })
695        );
696
697        let _ = blocks.next().unwrap();
698
699        let block3 = blocks.next().unwrap();
700
701        assert_eq!(
702            block3,
703            &Block::Simple(SimpleBlock {
704                content: Content {
705                    original: Span {
706                        data: "Are we still agreed? {agreed}",
707                        line: 5,
708                        col: 1,
709                        offset: 38,
710                    },
711                    rendered: "Are we still agreed? no",
712                },
713                source: Span {
714                    data: "Are we still agreed? {agreed}",
715                    line: 5,
716                    col: 1,
717                    offset: 38,
718                },
719                style: SimpleBlockStyle::Paragraph,
720                title_source: None,
721                title: None,
722                caption: None,
723                number: None,
724                anchor: None,
725                anchor_reftext: None,
726                attrlist: None,
727            })
728        );
729
730        let mut warnings = doc.warnings();
731        assert!(warnings.next().is_none());
732    }
733
734    #[test]
735    fn block_enforces_permission() {
736        let mut parser = Parser::default().with_intrinsic_attribute(
737            "agreed",
738            "yes",
739            ModificationContext::ApiOnly,
740        );
741
742        let doc = parser.parse("Hello\n\n:agreed: no\n\nAre we agreed? {agreed}");
743
744        let mut blocks = doc.nested_blocks();
745        let _ = blocks.next().unwrap();
746        let _ = blocks.next().unwrap();
747        let block3 = blocks.next().unwrap();
748
749        assert_eq!(
750            block3,
751            &Block::Simple(SimpleBlock {
752                content: Content {
753                    original: Span {
754                        data: "Are we agreed? {agreed}",
755                        line: 5,
756                        col: 1,
757                        offset: 20,
758                    },
759                    rendered: "Are we agreed? yes",
760                },
761                source: Span {
762                    data: "Are we agreed? {agreed}",
763                    line: 5,
764                    col: 1,
765                    offset: 20,
766                },
767                style: SimpleBlockStyle::Paragraph,
768                title_source: None,
769                title: None,
770                caption: None,
771                number: None,
772                anchor: None,
773                anchor_reftext: None,
774                attrlist: None,
775            })
776        );
777
778        let mut warnings = doc.warnings();
779        let warning1 = warnings.next().unwrap();
780
781        assert_eq!(
782            &warning1.source,
783            Span {
784                data: ":agreed: no",
785                line: 3,
786                col: 1,
787                offset: 7,
788            }
789        );
790
791        assert_eq!(
792            warning1.warning,
793            WarningType::AttributeValueIsLocked("agreed".to_owned(),)
794        );
795
796        assert!(warnings.next().is_none());
797    }
798
799    mod interpreted_value {
800        mod impl_debug {
801            use crate::document::InterpretedValue;
802
803            #[test]
804            fn value_empty_string() {
805                let interpreted_value = InterpretedValue::Value("".to_string());
806                let debug_output = format!("{:?}", interpreted_value);
807                assert_eq!(debug_output, "InterpretedValue::Value(\"\")");
808            }
809
810            #[test]
811            fn value_simple_string() {
812                let interpreted_value = InterpretedValue::Value("hello".to_string());
813                let debug_output = format!("{:?}", interpreted_value);
814                assert_eq!(debug_output, "InterpretedValue::Value(\"hello\")");
815            }
816
817            #[test]
818            fn value_string_with_spaces() {
819                let interpreted_value = InterpretedValue::Value("hello world".to_string());
820                let debug_output = format!("{:?}", interpreted_value);
821                assert_eq!(debug_output, "InterpretedValue::Value(\"hello world\")");
822            }
823
824            #[test]
825            fn value_string_with_special_chars() {
826                let interpreted_value = InterpretedValue::Value("test!@#$%^&*()".to_string());
827                let debug_output = format!("{:?}", interpreted_value);
828                assert_eq!(debug_output, "InterpretedValue::Value(\"test!@#$%^&*()\")");
829            }
830
831            #[test]
832            fn value_string_with_quotes() {
833                let interpreted_value = InterpretedValue::Value("value\"with'quotes".to_string());
834                let debug_output = format!("{:?}", interpreted_value);
835                assert_eq!(
836                    debug_output,
837                    "InterpretedValue::Value(\"value\\\"with'quotes\")"
838                );
839            }
840
841            #[test]
842            fn value_string_with_newlines() {
843                let interpreted_value = InterpretedValue::Value("line1\nline2\nline3".to_string());
844                let debug_output = format!("{:?}", interpreted_value);
845                assert_eq!(
846                    debug_output,
847                    "InterpretedValue::Value(\"line1\\nline2\\nline3\")"
848                );
849            }
850
851            #[test]
852            fn value_string_with_backslashes() {
853                let interpreted_value = InterpretedValue::Value("path\\to\\file".to_string());
854                let debug_output = format!("{:?}", interpreted_value);
855                assert_eq!(
856                    debug_output,
857                    "InterpretedValue::Value(\"path\\\\to\\\\file\")"
858                );
859            }
860
861            #[test]
862            fn value_string_with_unicode() {
863                let interpreted_value = InterpretedValue::Value("café 🚀 ñoño".to_string());
864                let debug_output = format!("{:?}", interpreted_value);
865                assert_eq!(debug_output, "InterpretedValue::Value(\"café 🚀 ñoño\")");
866            }
867
868            #[test]
869            fn set() {
870                let interpreted_value = InterpretedValue::Set;
871                let debug_output = format!("{:?}", interpreted_value);
872                assert_eq!(debug_output, "InterpretedValue::Set");
873            }
874
875            #[test]
876            fn unset() {
877                let interpreted_value = InterpretedValue::Unset;
878                let debug_output = format!("{:?}", interpreted_value);
879                assert_eq!(debug_output, "InterpretedValue::Unset");
880            }
881        }
882    }
883}