asciidoc_parser/attributes/
element_attribute.rs

1use crate::{Parser, Span, span::MatchedItem, strings::CowStr, warnings::WarningType};
2
3/// This struct represents a single element attribute.
4///
5/// Element attributes define the built-in and user-defined settings and
6/// metadata that can be applied to an individual block element or inline
7/// element in a document (including macros). Although the include directive is
8/// not technically an element, element attributes can also be defined on an
9/// include directive.
10#[derive(Clone, Debug, Eq, PartialEq)]
11pub struct ElementAttribute<'src> {
12    name: Option<CowStr<'src>>,
13    value: CowStr<'src>,
14    shorthand_item_indices: Vec<usize>,
15}
16
17impl<'src> ElementAttribute<'src> {
18    pub(crate) fn parse(
19        source_text: &CowStr<'src>,
20        start_index: usize,
21        _parser: &Parser,
22        mut parse_shorthand: ParseShorthand,
23    ) -> (Option<(Self, usize)>, Vec<WarningType>) {
24        let mut warnings: Vec<WarningType> = vec![];
25
26        let (name, value, shorthand_item_indices, offset) = {
27            let mut source = Span::new(source_text.as_ref());
28            source = source.discard(start_index);
29
30            let (name, after): (Option<Span<'_>>, Span) = match source.take_attr_name() {
31                Some(name) => {
32                    let space = name.after.take_whitespace_with_newline();
33                    match space.after.take_prefix("=") {
34                        Some(equals) => {
35                            let space = equals.after.take_whitespace_with_newline();
36                            if space.after.is_empty() || space.after.starts_with(',') {
37                                // TO DO: Is this a warning? Possible spec ambiguity.
38                                (None, source)
39                            } else {
40                                (Some(name.item), space.after)
41                            }
42                        }
43                        None => (None, source),
44                    }
45                }
46                None => (None, source),
47            };
48
49            let after = after.take_whitespace_with_newline().after;
50
51            let value = match after.data().chars().next() {
52                Some('\'') | Some('"') => match after.take_quoted_string() {
53                    Some(v) => {
54                        parse_shorthand = ParseShorthand(false);
55                        v
56                    }
57                    None => {
58                        warnings.push(WarningType::AttributeValueMissingTerminatingQuote);
59                        return (None, warnings);
60                    }
61                },
62                _ => after.take_while(|c| c != ','),
63            };
64
65            let after = value.after;
66            let value = cowstr_from_source_and_span(source_text, &value.item);
67
68            // TO DO: Redo this to support substitutions but only in correct circumstances.
69            // It doesn't apply in all cases.
70            // let value: CowStr<'_> = if value.item.data().contains(['<', '>', '&', '{']) {
71            //     let mut content = Content::from(value.item);
72            //     SubstitutionGroup::AttributeEntryValue.apply(&mut content, parser, None);
73            //     CowStr::from(content.rendered().to_string())
74            // } else {
75            //     cowstr_from_source_and_span(source_text, &value.item)
76            // };
77
78            let shorthand_item_indices = if name.is_none() && parse_shorthand.0 {
79                parse_shorthand_items(&value, &mut warnings)
80            } else {
81                vec![]
82            };
83
84            let name = name.map(|name| cowstr_from_source_and_span(source_text, &name));
85
86            (name, value, shorthand_item_indices, after.byte_offset())
87        };
88
89        (
90            Some((
91                Self {
92                    name,
93                    value,
94                    shorthand_item_indices,
95                },
96                offset,
97            )),
98            warnings,
99        )
100    }
101
102    /// Return the attribute name, if one was found`.
103    pub fn name(&'src self) -> Option<&'src str> {
104        if let Some(ref name) = self.name {
105            Some(name.as_ref())
106        } else {
107            None
108        }
109    }
110
111    /// Return the shorthand items, if applicable.
112    ///
113    /// Shorthand items are only parsed for certain element attributes. If this
114    /// attribute is not of the appropriate kind, this will return an empty
115    /// list.
116    pub fn shorthand_items(&'src self) -> Vec<&'src str> {
117        let mut result = vec![];
118        let value = self.value.as_ref();
119
120        let mut iter = self.shorthand_item_indices.iter().peekable();
121
122        loop {
123            let Some(curr) = iter.next() else { break };
124            let mut next_item = if let Some(next) = iter.peek() {
125                &value[*curr..**next]
126            } else {
127                &value[*curr..]
128            };
129
130            if next_item == "#" || next_item == "." || next_item == "%" {
131                continue;
132            }
133
134            next_item = next_item.trim_end();
135
136            if !next_item.is_empty() {
137                result.push(next_item);
138            }
139        }
140
141        result
142    }
143
144    /// Return the block style name from shorthand syntax.
145    pub fn block_style(&'src self) -> Option<&'src str> {
146        self.shorthand_items()
147            .first()
148            .filter(|v| !v.chars().any(is_shorthand_delimiter))
149            .cloned()
150    }
151
152    /// Return the ID attribute from shorthand syntax.
153    ///
154    /// If multiple ID attributes were specified, only the first
155    /// match is returned. (Multiple IDs are not supported.)
156    ///
157    /// You can assign an ID to a block using the shorthand syntax, the longhand
158    /// syntax, or a legacy block anchor.
159    ///
160    /// In the shorthand syntax, you prefix the name with a hash (`#`) in the
161    /// first position attribute:
162    ///
163    /// ```asciidoc
164    /// [#goals]
165    /// * Goal 1
166    /// * Goal 2
167    /// ```
168    ///
169    /// In the longhand syntax, you use a standard named attribute:
170    ///
171    /// ```asciidoc
172    /// [id=goals]
173    /// * Goal 1
174    /// * Goal 2
175    /// ```
176    ///
177    /// In the legacy block anchor syntax, you surround the name with double
178    /// square brackets:
179    ///
180    /// ```asciidoc
181    /// [[goals]]
182    /// * Goal 1
183    /// * Goal 2
184    /// ```
185    pub fn id(&'src self) -> Option<&'src str> {
186        self.shorthand_items()
187            .iter()
188            .find(|v| v.starts_with('#'))
189            .map(|v| &v[1..])
190    }
191
192    /// Return any role attributes that were found in shorthand syntax.
193    ///     
194    /// You can assign one or more roles to blocks and most inline elements
195    /// using the `role` attribute. The `role` attribute is a [named attribute].
196    /// Even though the attribute name is singular, it may contain multiple
197    /// (space-separated) roles. Roles may also be defined using a shorthand
198    /// (dot-prefixed) syntax.
199    ///
200    /// A role:
201    /// 1. adds additional semantics to an element
202    /// 2. can be used to apply additional styling to a group of elements (e.g.,
203    ///    via a CSS class selector)
204    /// 3. may activate additional behavior if recognized by the converter
205    ///
206    /// **TIP:** The `role` attribute in AsciiDoc always get mapped to the
207    /// `class` attribute in the HTML output. In other words, role names are
208    /// synonymous with HTML class names, thus allowing output elements to be
209    /// identified and styled in CSS using class selectors (e.g.,
210    /// `sidebarblock.role1`).
211    ///
212    /// [named attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/#named
213    pub fn roles(&'src self) -> Vec<&'src str> {
214        self.shorthand_items()
215            .iter()
216            .filter(|span| span.starts_with('.'))
217            .map(|span| &span[1..])
218            .collect()
219    }
220
221    /// Return any option attributes that were found in shorthand syntax.
222    ///     
223    /// The `options` attribute (often abbreviated as `opts`) is a versatile
224    /// [named attribute] that can be assigned one or more values. It can be
225    /// defined globally as document attribute as well as a block attribute on
226    /// an individual block.
227    ///
228    /// There is no strict schema for options. Any options which are not
229    /// recognized are ignored.
230    ///
231    /// You can assign one or more options to a block using the shorthand or
232    /// formal syntax for the options attribute.
233    ///
234    /// # Shorthand options syntax for blocks
235    ///
236    /// To assign an option to a block, prefix the value with a percent sign
237    /// (`%`) in an attribute list. The percent sign implicitly sets the
238    /// `options` attribute.
239    ///
240    /// ## Example 1: Sidebar block with an option assigned using the shorthand dot
241    ///
242    /// ```asciidoc
243    /// [%option]
244    /// ****
245    /// This is a sidebar with an option assigned to it, named option.
246    /// ****
247    /// ```
248    ///
249    /// You can assign multiple options to a block by prest
250    /// fixing each value with
251    /// a percent sign (`%`).
252    ///
253    /// ## Example 2: Sidebar with two options assigned using the shorthand dot
254    /// ```asciidoc
255    /// [%option1%option2]
256    /// ****
257    /// This is a sidebar with two options assigned to it, named option1 and option2.
258    /// ****
259    /// ```
260    ///
261    /// # Formal options syntax for blocks
262    ///
263    /// Explicitly set `options` or `opts`, followed by the equals sign (`=`),
264    /// and then the value in an attribute list.
265    ///
266    /// ## Example 3. Sidebar block with an option assigned using the formal syntax
267    /// ```asciidoc
268    /// [opts=option]
269    /// ****
270    /// This is a sidebar with an option assigned to it, named option.
271    /// ****
272    /// ```
273    ///
274    /// Separate multiple option values with commas (`,`).
275    ///
276    /// ## Example 4. Sidebar with three options assigned using the formal syntax
277    /// ```asciidoc
278    /// [opts="option1,option2"]
279    /// ****
280    /// This is a sidebar with two options assigned to it, option1 and option2.
281    /// ****
282    /// ```
283    ///
284    /// [named attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/#named
285    pub fn options(&'src self) -> Vec<&'src str> {
286        self.shorthand_items()
287            .iter()
288            .filter(|v| v.starts_with('%'))
289            .map(|v| &v[1..])
290            .collect()
291    }
292
293    /// Return the attribute's value.
294    ///
295    /// Note that this value will have had special characters and attribute
296    /// value replacements applied to it.
297    pub fn value(&'src self) -> &'src str {
298        self.value.as_ref()
299    }
300}
301
302fn parse_shorthand_items(source: &str, warnings: &mut Vec<WarningType>) -> Vec<usize> {
303    let mut shorthand_item_indices: Vec<usize> = vec![];
304    let mut span = Span::new(source);
305
306    // Look for block style selector.
307    if let Some(block_style_pr) = span.split_at_match_non_empty(is_shorthand_delimiter) {
308        shorthand_item_indices.push(block_style_pr.item.discard_whitespace().byte_offset());
309
310        span = block_style_pr.after;
311    }
312
313    while !span.is_empty() {
314        // Assumption: First character is a delimiter.
315        let after_delimiter = span.discard(1);
316
317        match after_delimiter.position(is_shorthand_delimiter) {
318            None => {
319                if after_delimiter.is_empty() {
320                    warnings.push(WarningType::EmptyShorthandItem);
321                    shorthand_item_indices.push(span.byte_offset());
322                    span = after_delimiter;
323                } else {
324                    shorthand_item_indices.push(span.byte_offset());
325                    span = span.discard_all();
326                }
327            }
328
329            Some(0) => {
330                shorthand_item_indices.push(span.byte_offset());
331                warnings.push(WarningType::EmptyShorthandItem);
332                span = after_delimiter;
333            }
334
335            Some(index) => {
336                let mi: MatchedItem<Span> = span.into_parse_result(index + 1);
337                shorthand_item_indices.push(span.byte_offset());
338                span = mi.after;
339            }
340        }
341    }
342
343    shorthand_item_indices
344}
345
346fn is_shorthand_delimiter(c: char) -> bool {
347    c == '#' || c == '%' || c == '.'
348}
349
350#[derive(Clone, Debug)]
351pub(crate) struct ParseShorthand(pub bool);
352
353fn cowstr_from_source_and_span<'src>(source: &CowStr<'src>, span: &Span<'_>) -> CowStr<'src> {
354    if let CowStr::Borrowed(source) = source {
355        let borrowed: Span<'src> = Span::new(source)
356            .discard(span.byte_offset())
357            .slice_to(..span.len());
358
359        CowStr::Borrowed(borrowed.data())
360    } else {
361        CowStr::from(span.data().to_string())
362    }
363}