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