asciidoc_parser/attributes/
attrlist.rs

1use std::slice::Iter;
2
3use crate::{
4    HasSpan, Parser, Span,
5    attributes::{ElementAttribute, element_attribute::ParseShorthand},
6    content::{Content, SubstitutionStep},
7    span::MatchedItem,
8    strings::CowStr,
9    warnings::{MatchAndWarnings, Warning, WarningType},
10};
11
12/// The source text that’s used to define attributes for an element is referred
13/// to as an **attrlist.** An attrlist is always enclosed in a pair of square
14/// brackets. This applies for block attributes as well as attributes on a block
15/// or inline macro. The processor splits the attrlist into individual attribute
16/// entries, determines whether each entry is a positional or named attribute,
17/// parses the entry accordingly, and assigns the result as an attribute on the
18/// node.
19#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct Attrlist<'src> {
21    attributes: Vec<ElementAttribute<'src>>,
22    source: Span<'src>,
23}
24
25impl<'src> Attrlist<'src> {
26    /// **IMPORTANT:** This `source` span passed to this function should NOT
27    /// include the opening or closing square brackets for the attrlist.
28    /// This is because the rules for closing brackets differ when parsing
29    /// inline, macro, and block elements.
30    pub(crate) fn parse(
31        source: Span<'src>,
32        parser: &Parser,
33    ) -> MatchAndWarnings<'src, MatchedItem<'src, Self>> {
34        let mut attributes: Vec<ElementAttribute> = vec![];
35        let mut parse_shorthand_items = true;
36        let mut warnings: Vec<Warning<'src>> = vec![];
37
38        // Apply attribute value substitutions before parsing attrlist content.
39        let source_cow = if source.contains('{') && source.contains('}') {
40            let mut content = Content::from(source);
41            SubstitutionStep::AttributeReferences.apply(&mut content, parser, None);
42            CowStr::from(content.rendered.to_string())
43        } else {
44            CowStr::from(source.data())
45        };
46
47        if source_cow.starts_with('[') && source_cow.ends_with(']') {
48            todo!("Parse block anchor syntax (issue #122)");
49        }
50
51        let mut index = 0;
52
53        let after_index = loop {
54            let (maybe_attr, warning_types) = ElementAttribute::parse(
55                &source_cow,
56                index,
57                parser,
58                ParseShorthand(parse_shorthand_items),
59            );
60
61            // Because we do attribute value substitution early on in parsing, we can't
62            // pinpoint the exact location of warnings in an attribute list. For that
63            // reason, individual attribute parsing only returns the warning type and we
64            // then map it back to the entire attrlist source.
65            for warning_type in warning_types {
66                warnings.push(Warning {
67                    source,
68                    warning: warning_type,
69                });
70            }
71
72            let Some((attr, new_index)) = maybe_attr else {
73                break index;
74            };
75
76            if attr.name().is_none() {
77                parse_shorthand_items = false;
78            }
79
80            let mut after = Span::new(source_cow.as_ref()).discard(new_index);
81
82            if attr.name().is_none()
83                && attr.value().is_empty()
84                && after.is_empty()
85                && attributes.is_empty()
86            {
87                break index;
88            }
89
90            attributes.push(attr);
91
92            after = after.take_whitespace().after;
93
94            match after.take_prefix(",") {
95                Some(comma) => {
96                    after = comma.after.take_whitespace().after;
97
98                    if after.starts_with(',') {
99                        warnings.push(Warning {
100                            source,
101                            warning: WarningType::EmptyAttributeValue,
102                        });
103                        after = after.discard(1);
104                        index = after.byte_offset();
105                        continue;
106                    }
107
108                    index = after.byte_offset();
109                }
110                None => {
111                    break after.byte_offset();
112                }
113            }
114        };
115
116        if after_index < source_cow.len() {
117            warnings.push(Warning {
118                source,
119                warning: WarningType::MissingCommaAfterQuotedAttributeValue,
120            });
121        }
122
123        MatchAndWarnings {
124            item: MatchedItem {
125                item: Self { attributes, source },
126                after: source.discard_all(),
127            },
128            warnings,
129        }
130    }
131
132    /// Returns an iterator over the attributes contained within
133    /// this attrlist.
134    pub fn attributes(&'src self) -> Iter<'src, ElementAttribute<'src>> {
135        self.attributes.iter()
136    }
137
138    /// Returns the first attribute with the given name.
139    pub fn named_attribute(&'src self, name: &str) -> Option<&'src ElementAttribute<'src>> {
140        self.attributes.iter().find(|attr| {
141            if let Some(attr_name) = attr.name() {
142                attr_name == name
143            } else {
144                false
145            }
146        })
147    }
148
149    /// Returns the given (1-based) positional attribute.
150    ///
151    /// **IMPORTANT:** Named attributes with names are disregarded when
152    /// counting.
153    pub fn nth_attribute(&'src self, n: usize) -> Option<&'src ElementAttribute<'src>> {
154        if n == 0 {
155            None
156        } else {
157            self.attributes
158                .iter()
159                .filter(|attr| attr.name().is_none())
160                .nth(n - 1)
161        }
162    }
163
164    /// Returns the first attribute with the given name or (1-based) index.
165    ///
166    /// Some block and macro types provide implicit mappings between attribute
167    /// names and positions to permit a shorthand syntax.
168    ///
169    /// This method will search by name first, and fall back to positional
170    /// indexing if the name doesn't yield a match.
171    pub fn named_or_positional_attribute(
172        &'src self,
173        name: &str,
174        index: usize,
175    ) -> Option<&'src ElementAttribute<'src>> {
176        self.named_attribute(name)
177            .or_else(|| self.nth_attribute(index))
178    }
179
180    /// Returns the ID attribute (if any).
181    ///
182    /// You can assign an ID to a block using the shorthand syntax, the longhand
183    /// syntax, or a legacy block anchor.
184    ///
185    /// In the shorthand syntax, you prefix the name with a hash (`#`) in the
186    /// first position attribute:
187    ///
188    /// ```asciidoc
189    /// [#goals]
190    /// * Goal 1
191    /// * Goal 2
192    /// ```
193    ///
194    /// In the longhand syntax, you use a standard named attribute:
195    ///
196    /// ```asciidoc
197    /// [id=goals]
198    /// * Goal 1
199    /// * Goal 2
200    /// ```
201    ///
202    /// In the legacy block anchor syntax, you surround the name with double
203    /// square brackets:
204    ///
205    /// ```asciidoc
206    /// [[goals]]
207    /// * Goal 1
208    /// * Goal 2
209    /// ```
210    pub fn id(&'src self) -> Option<&'src str> {
211        self.nth_attribute(1)
212            .and_then(|attr1| attr1.id())
213            .or_else(|| self.named_attribute("id").map(|attr| attr.value()))
214    }
215
216    /// Returns any role attributes that were found.
217    ///
218    /// You can assign one or more roles to blocks and most inline elements
219    /// using the `role` attribute. The `role` attribute is a [named attribute].
220    /// Even though the attribute name is singular, it may contain multiple
221    /// (space-separated) roles. Roles may also be defined using a shorthand
222    /// (dot-prefixed) syntax.
223    ///
224    /// A role:
225    /// 1. adds additional semantics to an element
226    /// 2. can be used to apply additional styling to a group of elements (e.g.,
227    ///    via a CSS class selector)
228    /// 3. may activate additional behavior if recognized by the converter
229    ///
230    /// **TIP:** The `role` attribute in AsciiDoc always get mapped to the
231    /// `class` attribute in the HTML output. In other words, role names are
232    /// synonymous with HTML class names, thus allowing output elements to be
233    /// identified and styled in CSS using class selectors (e.g.,
234    /// `sidebarblock.role1`).
235    ///
236    /// [named attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/#named
237    pub fn roles(&'src self) -> Vec<&'src str> {
238        let mut roles = self
239            .nth_attribute(1)
240            .map(|attr1| attr1.roles())
241            .unwrap_or_default();
242
243        if let Some(role_attr) = self.named_attribute("role") {
244            let mut role_span = Span::new(role_attr.value());
245            let mut formal_roles: Vec<&'src str> = vec![];
246            role_span = role_span.take_while(|c| c == ' ').after;
247
248            while !role_span.is_empty() {
249                let mi = role_span.take_while(|c| c != ' ');
250                if !mi.item.is_empty() {
251                    formal_roles.push(mi.item.data());
252                }
253                role_span = mi.after.take_while(|c| c == ' ').after;
254            }
255
256            roles.append(&mut formal_roles);
257        }
258
259        roles
260    }
261
262    /// Returns any option attributes that were found.
263    ///
264    /// The `options` attribute (often abbreviated as `opts`) is a versatile
265    /// [named attribute] that can be assigned one or more values. It can be
266    /// defined globally as document attribute as well as a block attribute on
267    /// an individual block.
268    ///
269    /// There is no strict schema for options. Any options which are not
270    /// recognized are ignored.
271    ///
272    /// You can assign one or more options to a block using the shorthand or
273    /// formal syntax for the options attribute.
274    ///
275    /// # Shorthand options syntax for blocks
276    ///
277    /// To assign an option to a block, prefix the value with a percent sign
278    /// (`%`) in an attribute list. The percent sign implicitly sets the
279    /// `options` attribute.
280    ///
281    /// ## Example 1: Sidebar block with an option assigned using the shorthand dot
282    ///
283    /// ```asciidoc
284    /// [%option]
285    /// ****
286    /// This is a sidebar with an option assigned to it, named option.
287    /// ****
288    /// ```
289    ///
290    /// You can assign multiple options to a block by prefixing each value with
291    /// a percent sign (`%`).
292    ///
293    /// ## Example 2: Sidebar with two options assigned using the shorthand dot
294    /// ```asciidoc
295    /// [%option1%option2]
296    /// ****
297    /// This is a sidebar with two options assigned to it, named option1 and option2.
298    /// ****
299    /// ```
300    ///
301    /// # Formal options syntax for blocks
302    ///
303    /// Explicitly set `options` or `opts`, followed by the equals sign (`=`),
304    /// and then the value in an attribute list.
305    ///
306    /// ## Example 3. Sidebar block with an option assigned using the formal syntax
307    /// ```asciidoc
308    /// [opts=option]
309    /// ****
310    /// This is a sidebar with an option assigned to it, named option.
311    /// ****
312    /// ```
313    ///
314    /// Separate multiple option values with commas (`,`).
315    ///
316    /// ## Example 4. Sidebar with three options assigned using the formal syntax
317    /// ```asciidoc
318    /// [opts="option1,option2"]
319    /// ****
320    /// This is a sidebar with two options assigned to it, option1 and option2.
321    /// ****
322    /// ```
323    ///
324    /// [named attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/#named
325    pub fn options(&'src self) -> Vec<&'src str> {
326        let mut options = self
327            .nth_attribute(1)
328            .map(|attr1| attr1.options())
329            .unwrap_or_default();
330
331        if let Some(option_attr) = self.named_attribute("opts") {
332            let mut option_span = Span::new(option_attr.value());
333            let mut formal_options: Vec<&'src str> = vec![];
334            option_span = option_span.take_while(|c| c == ',').after;
335
336            while !option_span.is_empty() {
337                let mi = option_span.take_while(|c| c != ',');
338                if !mi.item.is_empty() {
339                    formal_options.push(mi.item.data());
340                }
341                option_span = mi.after.take_while(|c| c == ',').after;
342            }
343
344            options.append(&mut formal_options);
345        }
346
347        if let Some(option_attr) = self.named_attribute("options") {
348            let mut option_span = Span::new(option_attr.value());
349            let mut formal_options: Vec<&'_ str> = vec![];
350            option_span = option_span.take_while(|c| c == ',').after;
351
352            while !option_span.is_empty() {
353                let mi = option_span.take_while(|c| c != ',');
354                if !mi.item.is_empty() {
355                    formal_options.push(mi.item.data());
356                }
357                option_span = mi.after.take_while(|c| c == ',').after;
358            }
359
360            options.append(&mut formal_options);
361        }
362
363        options
364    }
365
366    /// Returns `true` if this attribute list has the named option.
367    ///
368    /// See [`options()`] for a description of option syntax.
369    ///
370    /// [`options()`]: Self::options
371    pub fn has_option<N: AsRef<str>>(&'src self, name: N) -> bool {
372        // PERF: Might help to optimize away the construction of the options Vec.
373        let options = self.options();
374        let name = name.as_ref();
375        options.contains(&name)
376    }
377}
378
379impl<'src> HasSpan<'src> for Attrlist<'src> {
380    fn span(&self) -> Span<'src> {
381        self.source
382    }
383}