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 attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
132        None
133    }
134}
135
136/// The interpreted value of an [`Attribute`].
137///
138/// If the value contains a textual value, this value will
139/// have any continuation markers resolved, but will no longer
140/// contain a reference to the [`Span`] that contains the value.
141#[derive(Clone, Eq, PartialEq)]
142pub enum InterpretedValue {
143    /// A custom value with all necessary interpolations applied.
144    Value(String),
145
146    /// No explicit value. This is typically interpreted as either
147    /// boolean `true` or a default value for a built-in attribute.
148    Set,
149
150    /// Explicitly unset. This is typically interpreted as boolean `false`.
151    Unset,
152}
153
154impl std::fmt::Debug for InterpretedValue {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        match self {
157            InterpretedValue::Value(value) => f
158                .debug_tuple("InterpretedValue::Value")
159                .field(value)
160                .finish(),
161
162            InterpretedValue::Set => write!(f, "InterpretedValue::Set"),
163            InterpretedValue::Unset => write!(f, "InterpretedValue::Unset"),
164        }
165    }
166}
167
168impl InterpretedValue {
169    fn from_raw_value(raw_value: &Span<'_>, parser: &Parser) -> Self {
170        let data = raw_value.data();
171        let mut content = Content::from(*raw_value);
172
173        if data.contains('\n') {
174            let lines: Vec<&str> = data.lines().collect();
175            let last_count = lines.len() - 1;
176
177            let value: Vec<String> = lines
178                .iter()
179                .enumerate()
180                .map(|(count, line)| {
181                    let line = if count > 0 {
182                        line.trim_start_matches(' ')
183                    } else {
184                        line
185                    };
186
187                    let line = line
188                        .trim_start_matches('\r')
189                        .trim_end_matches(' ')
190                        .trim_end_matches('\\')
191                        .trim_end_matches(' ');
192
193                    if line.ends_with('+') {
194                        format!("{}\n", line.trim_end_matches('+').trim_end_matches(' '))
195                    } else if count < last_count {
196                        format!("{line} ")
197                    } else {
198                        line.to_string()
199                    }
200                })
201                .collect();
202
203            content.rendered = CowStr::Boxed(value.join("").into_boxed_str());
204        }
205
206        SubstitutionGroup::Header.apply(&mut content, parser, None);
207
208        InterpretedValue::Value(content.rendered.into_string())
209    }
210
211    pub(crate) fn as_maybe_str(&self) -> Option<&str> {
212        match self {
213            InterpretedValue::Value(value) => Some(value.as_ref()),
214            _ => None,
215        }
216    }
217}