asciidoc_parser/attributes/
element_attribute.rs

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