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