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(
41        source: Span<'src>,
42        parser: &Parser<'_>,
43    ) -> Option<MatchedItem<'src, Self>> {
44        let attr_line = source.take_line_with_continuation()?;
45        let colon = attr_line.item.take_prefix(":")?;
46
47        let mut unset = false;
48        let line = if colon.after.starts_with('!') {
49            unset = true;
50            colon.after.slice_from(1..)
51        } else {
52            colon.after
53        };
54
55        let name = line.take_user_attr_name()?;
56
57        let line = if name.after.starts_with('!') && !unset {
58            unset = true;
59            name.after.slice_from(1..)
60        } else {
61            name.after
62        };
63
64        let line = line.take_prefix(":")?;
65
66        let (value, value_source) = if unset {
67            // Ensure line is now empty except for comment.
68            (InterpretedValue::Unset, None)
69        } else if line.after.is_empty() {
70            (InterpretedValue::Set, None)
71        } else {
72            let raw_value = line.after.take_whitespace();
73            (
74                InterpretedValue::from_raw_value(&raw_value.after, parser),
75                Some(raw_value.after),
76            )
77        };
78
79        let source = source.trim_remainder(attr_line.after);
80        Some(MatchedItem {
81            item: Self {
82                name: name.item,
83                value_source,
84                value,
85                source: source.trim_trailing_whitespace(),
86            },
87            after: attr_line.after,
88        })
89    }
90
91    /// Return a [`Span`] describing the attribute name.
92    pub fn name(&'src self) -> &'src Span<'src> {
93        &self.name
94    }
95
96    /// Return a [`Span`] containing the attribute's raw value (if present).
97    pub fn raw_value(&'src self) -> Option<Span<'src>> {
98        self.value_source
99    }
100
101    /// Return the attribute's interpolated value.
102    pub fn value(&'src self) -> &'src InterpretedValue {
103        &self.value
104    }
105}
106
107impl<'src> HasSpan<'src> for Attribute<'src> {
108    fn span(&self) -> Span<'src> {
109        self.source
110    }
111}
112
113impl<'src> IsBlock<'src> for Attribute<'src> {
114    fn content_model(&self) -> ContentModel {
115        ContentModel::Empty
116    }
117
118    fn raw_context(&self) -> CowStr<'src> {
119        "attribute".into()
120    }
121
122    fn title_source(&'src self) -> Option<Span<'src>> {
123        None
124    }
125
126    fn title(&self) -> Option<&str> {
127        None
128    }
129
130    fn anchor(&'src self) -> Option<Span<'src>> {
131        None
132    }
133
134    fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
135        None
136    }
137}
138
139/// The interpreted value of an [`Attribute`].
140///
141/// If the value contains a textual value, this value will
142/// have any continuation markers resolved, but will no longer
143/// contain a reference to the [`Span`] that contains the value.
144#[derive(Clone, Eq, PartialEq)]
145pub enum InterpretedValue {
146    /// A custom value with all necessary interpolations applied.
147    Value(String),
148
149    /// No explicit value. This is typically interpreted as either
150    /// boolean `true` or a default value for a built-in attribute.
151    Set,
152
153    /// Explicitly unset. This is typically interpreted as boolean `false`.
154    Unset,
155}
156
157impl std::fmt::Debug for InterpretedValue {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        match self {
160            InterpretedValue::Value(value) => f
161                .debug_tuple("InterpretedValue::Value")
162                .field(value)
163                .finish(),
164
165            InterpretedValue::Set => write!(f, "InterpretedValue::Set"),
166            InterpretedValue::Unset => write!(f, "InterpretedValue::Unset"),
167        }
168    }
169}
170
171impl InterpretedValue {
172    fn from_raw_value(raw_value: &Span<'_>, parser: &Parser) -> Self {
173        let data = raw_value.data();
174        let mut content = Content::from(*raw_value);
175
176        if data.contains('\n') {
177            let lines: Vec<&str> = data.lines().collect();
178            let last_count = lines.len() - 1;
179
180            let value: Vec<String> = lines
181                .iter()
182                .enumerate()
183                .map(|(count, line)| {
184                    let line = if count > 0 {
185                        line.trim_start_matches(' ')
186                    } else {
187                        line
188                    };
189
190                    let line = line
191                        .trim_start_matches('\r')
192                        .trim_end_matches(' ')
193                        .trim_end_matches('\\')
194                        .trim_end_matches(' ');
195
196                    if line.ends_with('+') {
197                        format!("{}\n", line.trim_end_matches('+').trim_end_matches(' '))
198                    } else if count < last_count {
199                        format!("{line} ")
200                    } else {
201                        line.to_string()
202                    }
203                })
204                .collect();
205
206            content.rendered = CowStr::Boxed(value.join("").into_boxed_str());
207        }
208
209        SubstitutionGroup::Header.apply(&mut content, parser, None);
210
211        InterpretedValue::Value(content.rendered.into_string())
212    }
213
214    pub(crate) fn as_maybe_str(&self) -> Option<&str> {
215        match self {
216            InterpretedValue::Value(value) => Some(value.as_ref()),
217            _ => None,
218        }
219    }
220}