asciidoc_parser/document/
attribute.rs

1use crate::{
2    HasSpan, Parser, Span,
3    content::{Content, SubstitutionGroup},
4    span::MatchedItem,
5    strings::CowStr,
6};
7
8/// Document attributes are effectively document-scoped variables for the
9/// AsciiDoc language. The AsciiDoc language defines a set of built-in
10/// attributes, and also allows the author (or extensions) to define additional
11/// document attributes, which may replace built-in attributes when permitted.
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct Attribute<'src> {
14    name: Span<'src>,
15    value_source: Option<Span<'src>>,
16    value: InterpretedValue,
17    source: Span<'src>,
18}
19
20impl<'src> Attribute<'src> {
21    pub(crate) fn parse(
22        source: Span<'src>,
23        parser: &Parser<'_>,
24    ) -> Option<MatchedItem<'src, Self>> {
25        let attr_line = source.take_line_with_continuation()?;
26        let colon = attr_line.item.take_prefix(":")?;
27
28        let mut unset = false;
29        let line = if colon.after.starts_with('!') {
30            unset = true;
31            colon.after.slice_from(1..)
32        } else {
33            colon.after
34        };
35
36        let name = line.take_user_attr_name()?;
37
38        let line = if name.after.starts_with('!') && !unset {
39            unset = true;
40            name.after.slice_from(1..)
41        } else {
42            name.after
43        };
44
45        let line = line.take_prefix(":")?;
46
47        let (value, value_source) = if unset {
48            // Ensure line is now empty except for comment.
49            (InterpretedValue::Unset, None)
50        } else if line.after.is_empty() {
51            (InterpretedValue::Set, None)
52        } else {
53            let raw_value = line.after.take_whitespace();
54            (
55                InterpretedValue::from_raw_value(&raw_value.after, parser),
56                Some(raw_value.after),
57            )
58        };
59
60        let source = source.trim_remainder(attr_line.after);
61        Some(MatchedItem {
62            item: Self {
63                name: name.item,
64                value_source,
65                value,
66                source: source.trim_trailing_whitespace(),
67            },
68            after: attr_line.after,
69        })
70    }
71
72    /// Return a [`Span`] describing the attribute name.
73    pub fn name(&'src self) -> &'src Span<'src> {
74        &self.name
75    }
76
77    /// Return a [`Span`] containing the attribute's raw value (if present).
78    pub fn raw_value(&'src self) -> Option<Span<'src>> {
79        self.value_source
80    }
81
82    /// Return the attribute's interpolated value.
83    pub fn value(&'src self) -> &'src InterpretedValue {
84        &self.value
85    }
86}
87
88impl<'src> HasSpan<'src> for Attribute<'src> {
89    fn span(&self) -> Span<'src> {
90        self.source
91    }
92}
93
94/// The interpreted value of an [`Attribute`].
95///
96/// If the value contains a textual value, this value will
97/// have any continuation markers resolved, but will no longer
98/// contain a reference to the [`Span`] that contains the value.
99#[derive(Clone, Debug, Eq, PartialEq)]
100pub enum InterpretedValue {
101    /// A custom value with all necessary interpolations applied.
102    Value(String),
103
104    /// No explicit value. This is typically interpreted as either
105    /// boolean `true` or a default value for a built-in attribute.
106    Set,
107
108    /// Explicitly unset. This is typically interpreted as boolean `false`.
109    Unset,
110}
111
112impl InterpretedValue {
113    fn from_raw_value(raw_value: &Span<'_>, parser: &Parser) -> Self {
114        let data = raw_value.data();
115        let mut content = Content::from(*raw_value);
116
117        if data.contains('\n') {
118            let lines: Vec<&str> = data.lines().collect();
119            let last_count = lines.len() - 1;
120
121            let value: Vec<String> = lines
122                .iter()
123                .enumerate()
124                .map(|(count, line)| {
125                    let line = if count > 0 {
126                        line.trim_start_matches(' ')
127                    } else {
128                        line
129                    };
130
131                    let line = line
132                        .trim_start_matches('\r')
133                        .trim_end_matches(' ')
134                        .trim_end_matches('\\')
135                        .trim_end_matches(' ');
136
137                    if line.ends_with('+') {
138                        format!("{}\n", line.trim_end_matches('+').trim_end_matches(' '))
139                    } else if count < last_count {
140                        format!("{line} ")
141                    } else {
142                        line.to_string()
143                    }
144                })
145                .collect();
146
147            content.rendered = CowStr::Boxed(value.join("").into_boxed_str());
148        }
149
150        SubstitutionGroup::Header.apply(&mut content, parser, None);
151
152        InterpretedValue::Value(content.rendered.into_string())
153    }
154
155    pub(crate) fn as_maybe_str(&self) -> Option<&str> {
156        match self {
157            InterpretedValue::Value(value) => Some(value.as_ref()),
158            _ => None,
159        }
160    }
161}