Skip to main content

asciidoc_parser/attributes/
attrlist.rs

1use crate::{
2    HasSpan, Parser, Span,
3    attributes::{ElementAttribute, element_attribute::ParseShorthand},
4    content::{Content, SubstitutionStep},
5    internal::{debug::DebugSliceReference, opaque_iter::opaque_slice_iter},
6    span::MatchedItem,
7    strings::CowStr,
8    warnings::{MatchAndWarnings, Warning, WarningType},
9};
10
11opaque_slice_iter! {
12    /// An iterator over the [`ElementAttribute`]s in an [`Attrlist`], returned
13    /// by [`Attrlist::attributes`].
14    pub struct ElementAttributes<'a> yielding ElementAttribute<'a>;
15}
16
17/// The source text that’s used to define attributes for an element is referred
18/// to as an **attrlist.** An attrlist is always enclosed in a pair of square
19/// brackets. This applies for block attributes as well as attributes on a block
20/// or inline macro. The processor splits the attrlist into individual attribute
21/// entries, determines whether each entry is a positional or named attribute,
22/// parses the entry accordingly, and assigns the result as an attribute on the
23/// node.
24#[derive(Clone, Eq, Hash, PartialEq, Default)]
25pub struct Attrlist<'src> {
26    attributes: Vec<ElementAttribute<'src>>,
27    anchor: Option<CowStr<'src>>,
28    source: Span<'src>,
29}
30
31impl<'src> Attrlist<'src> {
32    /// **IMPORTANT:** This `source` span passed to this function should NOT
33    /// include the opening or closing square brackets for the attrlist.
34    /// This is because the rules for closing brackets differ when parsing
35    /// inline, macro, and block elements.
36    pub(crate) fn parse(
37        source: Span<'src>,
38        parser: &Parser,
39        attrlist_context: AttrlistContext,
40    ) -> MatchAndWarnings<'src, MatchedItem<'src, Self>> {
41        let mut attributes: Vec<ElementAttribute> = vec![];
42        let mut parse_shorthand_items = true;
43        let mut warnings: Vec<Warning<'src>> = vec![];
44
45        // Apply attribute value substitutions before parsing attrlist content.
46        let source_cow = if source.contains('{') && source.contains('}') {
47            let mut content = Content::from(source);
48            SubstitutionStep::AttributeReferences.apply(&mut content, parser, None);
49            CowStr::from(content.rendered.to_string())
50        } else {
51            CowStr::from(source.data())
52        };
53
54        if source_cow.starts_with('[') && source_cow.ends_with(']') {
55            let anchor = source_cow[1..source_cow.len() - 1].to_owned();
56
57            return MatchAndWarnings {
58                item: MatchedItem {
59                    item: Self {
60                        attributes,
61                        anchor: Some(CowStr::from(anchor)),
62                        source,
63                    },
64                    after: source.discard_all(),
65                },
66                warnings,
67            };
68        }
69
70        let mut index = 0;
71
72        // 1-based counter over every comma-delimited entry, incremented per
73        // entry – named attributes and blank (`nil`) slots included – so that
74        // positional attributes are numbered the way Asciidoctor numbers them
75        // (see `nth_attribute`).
76        let mut entry_number = 0usize;
77
78        let after_index = loop {
79            entry_number += 1;
80
81            let (mut attr, new_index, warning_types) = ElementAttribute::parse(
82                &source_cow,
83                index,
84                parser,
85                ParseShorthand(parse_shorthand_items),
86                attrlist_context,
87            );
88
89            // Because we do attribute value substitution early on in parsing, we can't
90            // pinpoint the exact location of warnings in an attribute list. For that
91            // reason, individual attribute parsing only returns the warning type and we
92            // then map it back to the entire attrlist source.
93            for warning_type in warning_types {
94                warnings.push(Warning {
95                    source,
96                    warning: warning_type,
97                    origin: None,
98                });
99            }
100
101            // Shorthand items (the `#id`, `.role`, and `%option` entries) are
102            // only recognized in the first attribute position. Once the first
103            // attribute has been parsed – whether it was positional or named –
104            // disable shorthand parsing so that, for example, a `%header`
105            // entered after a named `cols` attribute is not mistaken for an
106            // option (the processor ignores it).
107            parse_shorthand_items = false;
108
109            let mut after = Span::new(source_cow.as_ref()).discard(new_index);
110
111            // A completely empty (or whitespace-only) attribute list: the first
112            // entry is an empty, *unquoted* positional with nothing after it.
113            // Yield no attributes. An explicit empty *quoted* positional
114            // (`""` / `''`) carries a value and is kept below, so it is excluded
115            // here by `!attr.value_is_quoted()`.
116            if attr.name().is_none()
117                && attr.value().is_empty()
118                && !attr.value_is_quoted()
119                && after.is_empty()
120                && attributes.is_empty()
121            {
122                break index;
123            }
124
125            if attr.name().is_some() {
126                // A named attribute whose value is the literal `None` unsets the
127                // attribute (Asciidoctor semantics); it still consumes a
128                // position but is not stored.
129                if attr.value() != "None" {
130                    attributes.push(attr);
131                }
132            } else if !attr.value().is_empty() || attr.value_is_quoted() {
133                // A positional attribute – including an explicit empty quoted
134                // value (`""` / `''`). Record its position so later positionals
135                // stay aligned across named and blank entries.
136                attr.set_positional_index(entry_number);
137                attributes.push(attr);
138            }
139
140            // Otherwise this is an empty, unquoted positional: a blank (`nil`)
141            // slot. It consumes `entry_number` (already incremented) but is not
142            // stored, so a later positional keeps its Asciidoctor position.
143
144            after = after.take_whitespace().after;
145
146            match after.take_prefix(",") {
147                Some(comma) => {
148                    after = comma.after.take_whitespace().after;
149
150                    if after.starts_with(',') {
151                        warnings.push(Warning {
152                            source,
153                            warning: WarningType::EmptyAttributeValue,
154                            origin: None,
155                        });
156
157                        // Consume the blank slot between consecutive commas here,
158                        // advancing the position counter past it.
159                        entry_number += 1;
160                        after = after.discard(1);
161                        index = after.byte_offset();
162                        continue;
163                    }
164
165                    index = after.byte_offset();
166                }
167                None => {
168                    break after.byte_offset();
169                }
170            }
171        };
172
173        if after_index < source_cow.len() {
174            warnings.push(Warning {
175                source,
176                warning: WarningType::MissingCommaAfterQuotedAttributeValue,
177                origin: None,
178            });
179        }
180
181        MatchAndWarnings {
182            item: MatchedItem {
183                item: Self {
184                    attributes,
185                    anchor: None,
186                    source,
187                },
188                after: source.discard_all(),
189            },
190            warnings,
191        }
192    }
193
194    /// Build the attribute list implied by a language-aware fenced code block
195    /// (`` ```lang ``).
196    ///
197    /// A fenced code block whose opening fence carries a language is shorthand
198    /// for a source block: it is equivalent to a `[source,<language>]`
199    /// attribute list applied to a listing block. The synthesized list
200    /// therefore carries the `source` block style in the first position and
201    /// the language in the second, so that downstream consumers resolve the
202    /// block to a source (highlighted listing) block and can read the
203    /// language via [`nth_attribute(2)`](Self::nth_attribute) – without
204    /// this parser performing any syntax highlighting itself.
205    ///
206    /// The `source` span is set to the language span, since that is the only
207    /// portion of the synthesized list that appears in the document source.
208    pub(crate) fn source_with_language(language: Span<'src>) -> Self {
209        Self {
210            attributes: vec![
211                ElementAttribute::synthesized_source_style(),
212                ElementAttribute::positional_from_span(language),
213            ],
214            anchor: None,
215            source: language,
216        }
217    }
218
219    /// Merge a subsequent block attribute line into this one.
220    ///
221    /// A block can be preceded by more than one attribute list line (optionally
222    /// straddling the block title). Asciidoctor merges every such line into a
223    /// single set of attributes, where a later line wins on a name (or
224    /// position) conflict and otherwise accumulates. This method folds `later`
225    /// into `self` using those semantics:
226    ///
227    /// * **Named attributes** accumulate; a later attribute with the same name
228    ///   replaces the earlier one (in place, preserving order).
229    /// * **Positional attributes** are matched by position. A later positional
230    ///   replaces the earlier one at the same position. The first positional
231    ///   additionally carries the block style and shorthand items (`#id`,
232    ///   `.role`, `%option`), which are merged via
233    ///   [`ElementAttribute::merge_block_style_shorthand`].
234    /// * **Roles** follow Asciidoctor's running model once a formal `role=`
235    ///   entry is in play: a formal `role=` *replaces* every role accumulated
236    ///   so far, while shorthand `.role` entries *append*. Because a replacing
237    ///   `role=` on one line can sit between shorthand roles on earlier and
238    ///   later lines, the resolved (ordered) role list is folded into the
239    ///   formal `role` attribute and the first positional is left free of role
240    ///   shorthand, so [`roles`](Self::roles) reports the resolved list. When
241    ///   no formal `role=` is involved, shorthand roles simply accumulate as
242    ///   above.
243    /// * The **anchor** is taken from the later line if it specifies one.
244    ///
245    /// The `source` span is left pointing at the first line, since the merged
246    /// attributes no longer correspond to a single contiguous span.
247    pub(crate) fn merge_block_attribute_line(&mut self, later: Attrlist<'src>) {
248        // Roles only need the running-model treatment once a formal `role=`
249        // entry appears (on this line or an earlier, already-folded one).
250        // Otherwise shorthand roles accumulate through the ordinary first-
251        // positional merge below, unchanged.
252        let fold_roles =
253            self.named_attribute("role").is_some() || later.named_attribute("role").is_some();
254
255        let resolved_roles: Option<Vec<String>> = if fold_roles {
256            // The roles accumulated so far, in resolved order (a folded `role`
257            // attribute holds them; failing that, `self`'s shorthand/formal
258            // roles are read the ordinary way).
259            let current: Vec<String> = self.roles().iter().map(|r| r.to_string()).collect();
260
261            // A formal `role=` on the later line replaces the running list;
262            // otherwise it carries forward. The later line's shorthand roles
263            // then append (matching Asciidoctor, where a line's `role=` is set
264            // before its shorthand roles are appended).
265            let later_formal: Option<Vec<String>> = later
266                .named_attribute("role")
267                .map(|attr| split_role_value(attr.value()).map(str::to_string).collect());
268
269            let later_shorthand: Vec<String> = later
270                .nth_attribute(1)
271                .map(|attr| attr.roles().iter().map(|r| r.to_string()).collect())
272                .unwrap_or_default();
273
274            let mut resolved = later_formal.unwrap_or(current);
275            resolved.extend(later_shorthand);
276
277            // Strip role shorthand from our own first positional so the formal
278            // `role` attribute set below is the only remaining source of roles.
279            if let Some(existing) = self.nth_positional_mut(1) {
280                *existing = existing.without_shorthand_roles();
281            }
282
283            Some(resolved)
284        } else {
285            None
286        };
287
288        let Attrlist {
289            attributes: later_attributes,
290            anchor: later_anchor,
291            source: _,
292        } = later;
293
294        if later_anchor.is_some() {
295            self.anchor = later_anchor;
296        }
297
298        for attr in later_attributes {
299            // When folding roles, the resolved role list is applied afterward,
300            // so skip the later line's formal `role` attribute here (it is
301            // accounted for in `resolved_roles`).
302            if fold_roles && attr.name_str() == Some("role") {
303                continue;
304            }
305
306            // An attribute carries a positional index exactly when it is a
307            // positional (unnamed) attribute; a named attribute has `None`.
308            // Dispatching on the index keeps a positional at its Asciidoctor
309            // position – the same 1-based entry count `nth_attribute` uses,
310            // which includes named entries and blank slots – so positions stay
311            // aligned across lines even when a later line interleaves named
312            // attributes before a positional.
313            match attr.positional_index() {
314                // Named: accumulate, with a later attribute replacing the
315                // earlier one of the same name in place.
316                None => {
317                    if let Some(existing) = self
318                        .attributes
319                        .iter_mut()
320                        .find(|a| a.name_str() == attr.name_str())
321                    {
322                        *existing = attr;
323                    } else {
324                        self.attributes.push(attr);
325                    }
326                }
327
328                // The first positional additionally carries the block style and
329                // shorthand items (`#id`, `.role`, `%option`), which are merged.
330                // While folding roles, its role shorthand is dropped so roles
331                // flow solely through the formal `role` attribute.
332                Some(1) => {
333                    let attr = if fold_roles {
334                        attr.without_shorthand_roles()
335                    } else {
336                        attr
337                    };
338
339                    if let Some(existing) = self.nth_positional_mut(1) {
340                        *existing = ElementAttribute::merge_block_style_shorthand(existing, &attr);
341                    } else {
342                        self.attributes.push(attr);
343                    }
344                }
345
346                // A later positional replaces the earlier one at the same
347                // position, otherwise it extends the list.
348                Some(position) => {
349                    if let Some(existing) = self.nth_positional_mut(position) {
350                        *existing = attr;
351                    } else {
352                        self.attributes.push(attr);
353                    }
354                }
355            }
356        }
357
358        // Record the resolved roles in the formal `role` attribute.
359        if let Some(resolved) = resolved_roles {
360            self.set_role_attribute(resolved);
361        }
362    }
363
364    /// Replace (or clear) the formal `role` attribute with the given resolved
365    /// role list. Called by
366    /// [`merge_block_attribute_line`](Self::merge_block_attribute_line) once
367    /// roles have been resolved under Asciidoctor's running model. An empty
368    /// list removes any existing `role` attribute.
369    fn set_role_attribute(&mut self, roles: Vec<String>) {
370        if roles.is_empty() {
371            self.attributes.retain(|a| a.name_str() != Some("role"));
372            return;
373        }
374
375        let attr = ElementAttribute::synthesized_role(roles.join(" "));
376        if let Some(existing) = self
377            .attributes
378            .iter_mut()
379            .find(|a| a.name_str() == Some("role"))
380        {
381            *existing = attr;
382        } else {
383            self.attributes.push(attr);
384        }
385    }
386
387    /// Return a mutable reference to the positional attribute at (1-based)
388    /// Asciidoctor position `n` – the position recorded on each attribute, not
389    /// its ordinal among stored positionals (the two differ once named entries
390    /// or blank slots consume positions). See
391    /// [`nth_attribute`](Self::nth_attribute).
392    ///
393    /// `n` must be 1 or greater; the only caller is
394    /// [`merge_block_attribute_line`](Self::merge_block_attribute_line), which
395    /// always passes a positive position.
396    fn nth_positional_mut(&mut self, n: usize) -> Option<&mut ElementAttribute<'src>> {
397        debug_assert!(n >= 1, "nth_positional_mut requires a 1-based position");
398
399        self.attributes
400            .iter_mut()
401            .find(|attr| attr.positional_index() == Some(n))
402    }
403
404    /// Returns an iterator over the attributes contained within
405    /// this attrlist.
406    pub fn attributes(&'src self) -> ElementAttributes<'src> {
407        ElementAttributes::new(&self.attributes)
408    }
409
410    /// Returns the anchor found in this attribute list, if any.
411    pub fn anchor(&'src self) -> Option<&'src str> {
412        self.anchor.as_deref()
413    }
414
415    /// Returns the `title=` attribute's value and whether the normal
416    /// substitution group has already been applied to it, if the attribute is
417    /// present.
418    ///
419    /// Unlike [`named_attribute`](Self::named_attribute), this borrows for the
420    /// duration of the call only, so it can be read from an `Attrlist` that is
421    /// about to be moved (as when block metadata derives a block title from a
422    /// `title=` attribute before storing the attribute list on the block). The
423    /// returned flag (see [`ElementAttribute::value_is_substituted`]) lets the
424    /// caller avoid substituting an already-substituted (single-quoted) value a
425    /// second time.
426    pub(crate) fn title_attribute(&self) -> Option<(&str, bool)> {
427        self.attributes
428            .iter()
429            .find(|attr| attr.name_str() == Some("title"))
430            .map(|attr| (attr.value_str(), attr.value_is_substituted()))
431    }
432
433    /// Returns the first attribute with the given name.
434    pub fn named_attribute(&'src self, name: &str) -> Option<&'src ElementAttribute<'src>> {
435        self.attributes.iter().find(|attr| {
436            if let Some(attr_name) = attr.name() {
437                attr_name == name
438            } else {
439                false
440            }
441        })
442    }
443
444    /// Returns the given (1-based) positional attribute.
445    ///
446    /// **IMPORTANT:** Positions are numbered the way Asciidoctor numbers them:
447    /// every comma-delimited entry consumes a position, including named entries
448    /// and blank (`nil`) slots. A later positional therefore keeps its position
449    /// even when an earlier entry is named or left blank (e.g.
450    /// `image::x[Alt,,3]` has `Alt` at position 1 and `3` at position 3,
451    /// with position 2 empty). A position that is empty, or that is
452    /// occupied by a named attribute, yields `None`.
453    pub fn nth_attribute(&'src self, n: usize) -> Option<&'src ElementAttribute<'src>> {
454        if n == 0 {
455            None
456        } else {
457            self.attributes
458                .iter()
459                .find(|attr| attr.positional_index() == Some(n))
460        }
461    }
462
463    /// Returns the first attribute with the given name or (1-based) index.
464    ///
465    /// Some block and macro types provide implicit mappings between attribute
466    /// names and positions to permit a shorthand syntax.
467    ///
468    /// This method will search by name first, and fall back to positional
469    /// indexing if the name doesn't yield a match.
470    pub fn named_or_positional_attribute(
471        &'src self,
472        name: &str,
473        index: usize,
474    ) -> Option<&'src ElementAttribute<'src>> {
475        self.named_attribute(name)
476            .or_else(|| self.nth_attribute(index))
477    }
478
479    /// Returns the ID attribute (if any).
480    ///
481    /// You can assign an ID to a block using the shorthand syntax, the longhand
482    /// syntax, or a legacy block anchor.
483    ///
484    /// In the shorthand syntax, you prefix the name with a hash (`#`) in the
485    /// first position attribute:
486    ///
487    /// ```asciidoc
488    /// [#goals]
489    /// * Goal 1
490    /// * Goal 2
491    /// ```
492    ///
493    /// In the longhand syntax, you use a standard named attribute:
494    ///
495    /// ```asciidoc
496    /// [id=goals]
497    /// * Goal 1
498    /// * Goal 2
499    /// ```
500    ///
501    /// In the legacy block anchor syntax, you surround the name with double
502    /// square brackets:
503    ///
504    /// ```asciidoc
505    /// [[goals]]
506    /// * Goal 1
507    /// * Goal 2
508    /// ```
509    pub fn id(&'src self) -> Option<&'src str> {
510        self.anchor().or_else(|| {
511            self.nth_attribute(1)
512                .and_then(|attr1| attr1.id())
513                .or_else(|| self.named_attribute("id").map(|attr| attr.value()))
514        })
515    }
516
517    /// Returns any role attributes that were found.
518    ///
519    /// You can assign one or more roles to blocks and most inline elements
520    /// using the `role` attribute. The `role` attribute is a [named attribute].
521    /// Even though the attribute name is singular, it may contain multiple
522    /// (space-separated) roles. Roles may also be defined using a shorthand
523    /// (dot-prefixed) syntax.
524    ///
525    /// A role:
526    /// 1. adds additional semantics to an element
527    /// 2. can be used to apply additional styling to a group of elements (e.g.,
528    ///    via a CSS class selector)
529    /// 3. may activate additional behavior if recognized by the converter
530    ///
531    /// **TIP:** The `role` attribute in AsciiDoc always get mapped to the
532    /// `class` attribute in the HTML output. In other words, role names are
533    /// synonymous with HTML class names, thus allowing output elements to be
534    /// identified and styled in CSS using class selectors (e.g.,
535    /// `sidebarblock.role1`).
536    ///
537    /// [named attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/#named
538    pub fn roles(&'src self) -> Vec<&'src str> {
539        let mut roles = self
540            .nth_attribute(1)
541            .map(|attr1| attr1.roles())
542            .unwrap_or_default();
543
544        if let Some(role_attr) = self.named_attribute("role") {
545            roles.extend(split_role_value(role_attr.value()));
546        }
547
548        roles
549    }
550
551    /// Returns any option attributes that were found.
552    ///
553    /// The `options` attribute (often abbreviated as `opts`) is a versatile
554    /// [named attribute] that can be assigned one or more values. It can be
555    /// defined globally as document attribute as well as a block attribute on
556    /// an individual block.
557    ///
558    /// There is no strict schema for options. Any options which are not
559    /// recognized are ignored.
560    ///
561    /// You can assign one or more options to a block using the shorthand or
562    /// formal syntax for the options attribute.
563    ///
564    /// # Shorthand options syntax for blocks
565    ///
566    /// To assign an option to a block, prefix the value with a percent sign
567    /// (`%`) in an attribute list. The percent sign implicitly sets the
568    /// `options` attribute.
569    ///
570    /// ## Example 1: Sidebar block with an option assigned using the shorthand dot
571    ///
572    /// ```asciidoc
573    /// [%option]
574    /// ****
575    /// This is a sidebar with an option assigned to it, named option.
576    /// ****
577    /// ```
578    ///
579    /// You can assign multiple options to a block by prefixing each value with
580    /// a percent sign (`%`).
581    ///
582    /// ## Example 2: Sidebar with two options assigned using the shorthand dot
583    /// ```asciidoc
584    /// [%option1%option2]
585    /// ****
586    /// This is a sidebar with two options assigned to it, named option1 and option2.
587    /// ****
588    /// ```
589    ///
590    /// # Formal options syntax for blocks
591    ///
592    /// Explicitly set `options` or `opts`, followed by the equals sign (`=`),
593    /// and then the value in an attribute list.
594    ///
595    /// ## Example 3. Sidebar block with an option assigned using the formal syntax
596    /// ```asciidoc
597    /// [opts=option]
598    /// ****
599    /// This is a sidebar with an option assigned to it, named option.
600    /// ****
601    /// ```
602    ///
603    /// Separate multiple option values with commas (`,`).
604    ///
605    /// ## Example 4. Sidebar with three options assigned using the formal syntax
606    /// ```asciidoc
607    /// [opts="option1,option2"]
608    /// ****
609    /// This is a sidebar with two options assigned to it, option1 and option2.
610    /// ****
611    /// ```
612    ///
613    /// [named attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/#named
614    pub fn options(&'src self) -> Vec<&'src str> {
615        let mut options = self
616            .nth_attribute(1)
617            .map(|attr1| attr1.options())
618            .unwrap_or_default();
619
620        if let Some(option_attr) = self.named_attribute("opts") {
621            options.append(&mut split_options(option_attr.value()));
622        }
623
624        if let Some(option_attr) = self.named_attribute("options") {
625            options.append(&mut split_options(option_attr.value()));
626        }
627
628        options
629    }
630
631    /// Returns `true` if this attribute list has the named option.
632    ///
633    /// See [`options()`] for a description of option syntax.
634    ///
635    /// [`options()`]: Self::options
636    pub fn has_option<N: AsRef<str>>(&'src self, name: N) -> bool {
637        // PERF: Might help to optimize away the construction of the options Vec.
638        let options = self.options();
639        let name = name.as_ref();
640        options.contains(&name)
641    }
642
643    /// Return the block style name from shorthand syntax.
644    pub fn block_style(&'src self) -> Option<&'src str> {
645        self.nth_attribute(1).and_then(|a| a.block_style())
646    }
647}
648
649impl<'src> HasSpan<'src> for Attrlist<'src> {
650    fn span(&self) -> Span<'src> {
651        self.source
652    }
653}
654
655impl std::fmt::Debug for Attrlist<'_> {
656    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
657        f.debug_struct("Attrlist")
658            .field("attributes", &DebugSliceReference(&self.attributes))
659            .field("anchor", &self.anchor)
660            .field("source", &self.source)
661            .finish()
662    }
663}
664
665/// Split an `opts`/`options` attribute value into individual option tokens,
666/// matching Asciidoctor: split on commas, trim surrounding whitespace from each
667/// token, and drop empty tokens. So `'opt1,,opt2 , opt3'` yields `opt1`,
668/// `opt2`, `opt3`.
669fn split_options(value: &str) -> Vec<&str> {
670    value
671        .split(',')
672        .map(str::trim)
673        .filter(|opt| !opt.is_empty())
674        .collect()
675}
676
677/// Split a formal `role` attribute value into its individual role names: split
678/// on ASCII spaces and drop empty tokens. So `'role1  role2'` yields `role1`,
679/// `role2`.
680///
681/// This is the single source of truth for how a `role=` value is tokenized,
682/// shared by [`Attrlist::roles`] (which borrows the names) and the
683/// block-attribute merge (which owns them), so the two can never diverge.
684fn split_role_value(value: &str) -> impl Iterator<Item = &str> {
685    value.split(' ').filter(|role| !role.is_empty())
686}
687
688/// Context for attribute list parsing.
689#[derive(Clone, Copy, Debug, Eq, PartialEq)]
690pub(crate) enum AttrlistContext {
691    Block,
692    Inline,
693}
694
695#[cfg(test)]
696mod tests {
697    #![allow(clippy::unwrap_used)]
698
699    use crate::{attributes::AttrlistContext, tests::prelude::*};
700
701    #[test]
702    fn impl_clone() {
703        // Silly test to mark the #[derive(...)] line as covered.
704        let p = Parser::default();
705        let b1 = crate::attributes::Attrlist::parse(
706            crate::Span::new("abc"),
707            &p,
708            AttrlistContext::Inline,
709        )
710        .unwrap_if_no_warnings();
711
712        let b2 = b1.item.clone();
713        assert_eq!(b1.item, b2);
714    }
715
716    #[test]
717    fn impl_default() {
718        let attrlist = crate::attributes::Attrlist::default();
719
720        assert_eq!(
721            attrlist,
722            Attrlist {
723                attributes: &[],
724                anchor: None,
725                source: Span {
726                    data: "",
727                    line: 1,
728                    col: 1,
729                    offset: 0
730                }
731            }
732        );
733
734        assert!(attrlist.named_attribute("foo").is_none());
735
736        assert!(attrlist.nth_attribute(0).is_none());
737        assert!(attrlist.nth_attribute(1).is_none());
738        assert!(attrlist.nth_attribute(42).is_none());
739
740        assert!(attrlist.named_or_positional_attribute("foo", 0).is_none());
741        assert!(attrlist.named_or_positional_attribute("foo", 1).is_none());
742        assert!(attrlist.named_or_positional_attribute("foo", 42).is_none());
743
744        assert!(attrlist.id().is_none());
745        assert!(attrlist.roles().is_empty());
746        assert!(attrlist.block_style().is_none());
747
748        assert_eq!(
749            attrlist.span(),
750            Span {
751                data: "",
752                line: 1,
753                col: 1,
754                offset: 0,
755            }
756        );
757    }
758
759    #[test]
760    fn empty_source() {
761        let p = Parser::default();
762
763        let mi =
764            crate::attributes::Attrlist::parse(crate::Span::default(), &p, AttrlistContext::Inline)
765                .unwrap_if_no_warnings();
766
767        assert_eq!(
768            mi.item,
769            Attrlist {
770                attributes: &[],
771                anchor: None,
772                source: Span {
773                    data: "",
774                    line: 1,
775                    col: 1,
776                    offset: 0
777                }
778            }
779        );
780
781        assert!(mi.item.named_attribute("foo").is_none());
782
783        assert!(mi.item.nth_attribute(0).is_none());
784        assert!(mi.item.nth_attribute(1).is_none());
785        assert!(mi.item.nth_attribute(42).is_none());
786
787        assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
788        assert!(mi.item.named_or_positional_attribute("foo", 1).is_none());
789        assert!(mi.item.named_or_positional_attribute("foo", 42).is_none());
790
791        assert!(mi.item.id().is_none());
792        assert!(mi.item.roles().is_empty());
793        assert!(mi.item.block_style().is_none());
794
795        assert_eq!(
796            mi.item.span(),
797            Span {
798                data: "",
799                line: 1,
800                col: 1,
801                offset: 0,
802            }
803        );
804
805        assert_eq!(
806            mi.after,
807            Span {
808                data: "",
809                line: 1,
810                col: 1,
811                offset: 0
812            }
813        );
814    }
815
816    #[test]
817    fn empty_positional_attributes() {
818        let p = Parser::default();
819
820        let mi = crate::attributes::Attrlist::parse(
821            crate::Span::new(",300,400"),
822            &p,
823            AttrlistContext::Inline,
824        )
825        .unwrap_if_no_warnings();
826
827        // A leading comma leaves position 1 blank (a `nil` slot, as in
828        // Asciidoctor): it consumes the position but stores no attribute, so
829        // `300` and `400` remain at positions 2 and 3.
830        assert_eq!(
831            mi.item,
832            Attrlist {
833                attributes: &[
834                    ElementAttribute {
835                        name: None,
836                        shorthand_items: &[],
837                        value: "300"
838                    },
839                    ElementAttribute {
840                        name: None,
841                        shorthand_items: &[],
842                        value: "400"
843                    }
844                ],
845                anchor: None,
846                source: Span {
847                    data: ",300,400",
848                    line: 1,
849                    col: 1,
850                    offset: 0
851                }
852            }
853        );
854
855        assert!(mi.item.named_attribute("foo").is_none());
856        assert!(mi.item.nth_attribute(0).is_none());
857        assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
858
859        assert!(mi.item.id().is_none());
860        assert!(mi.item.roles().is_empty());
861        assert!(mi.item.block_style().is_none());
862
863        // Position 1 is the blank slot: no attribute there.
864        assert!(mi.item.nth_attribute(1).is_none());
865        assert!(mi.item.named_or_positional_attribute("alt", 1).is_none());
866
867        assert_eq!(
868            mi.item.nth_attribute(2).unwrap(),
869            ElementAttribute {
870                name: None,
871                shorthand_items: &[],
872                value: "300"
873            }
874        );
875
876        assert_eq!(
877            mi.item.named_or_positional_attribute("width", 2).unwrap(),
878            ElementAttribute {
879                name: None,
880                shorthand_items: &[],
881                value: "300"
882            }
883        );
884
885        assert_eq!(
886            mi.item.nth_attribute(3).unwrap(),
887            ElementAttribute {
888                name: None,
889                shorthand_items: &[],
890                value: "400"
891            }
892        );
893
894        assert_eq!(
895            mi.item.named_or_positional_attribute("height", 3).unwrap(),
896            ElementAttribute {
897                name: None,
898                shorthand_items: &[],
899                value: "400"
900            }
901        );
902
903        assert!(mi.item.nth_attribute(4).is_none());
904        assert!(mi.item.named_or_positional_attribute("height", 4).is_none());
905        assert!(mi.item.nth_attribute(42).is_none());
906
907        assert_eq!(
908            mi.item.span(),
909            Span {
910                data: ",300,400",
911                line: 1,
912                col: 1,
913                offset: 0,
914            }
915        );
916
917        assert_eq!(
918            mi.after,
919            Span {
920                data: "",
921                line: 1,
922                col: 9,
923                offset: 8
924            }
925        );
926    }
927
928    #[test]
929    fn only_positional_attributes() {
930        let p = Parser::default();
931
932        let mi = crate::attributes::Attrlist::parse(
933            crate::Span::new("Sunset,300,400"),
934            &p,
935            AttrlistContext::Inline,
936        )
937        .unwrap_if_no_warnings();
938
939        assert_eq!(
940            mi.item,
941            Attrlist {
942                attributes: &[
943                    ElementAttribute {
944                        name: None,
945                        shorthand_items: &["Sunset"],
946                        value: "Sunset"
947                    },
948                    ElementAttribute {
949                        name: None,
950                        shorthand_items: &[],
951                        value: "300"
952                    },
953                    ElementAttribute {
954                        name: None,
955                        shorthand_items: &[],
956                        value: "400"
957                    }
958                ],
959                anchor: None,
960                source: Span {
961                    data: "Sunset,300,400",
962                    line: 1,
963                    col: 1,
964                    offset: 0
965                }
966            }
967        );
968
969        assert!(mi.item.named_attribute("foo").is_none());
970        assert!(mi.item.nth_attribute(0).is_none());
971        assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
972
973        assert!(mi.item.id().is_none());
974        assert!(mi.item.roles().is_empty());
975        assert_eq!(mi.item.block_style().unwrap(), "Sunset");
976
977        assert_eq!(
978            mi.item.nth_attribute(1).unwrap(),
979            ElementAttribute {
980                name: None,
981                shorthand_items: &["Sunset"],
982                value: "Sunset"
983            }
984        );
985
986        assert_eq!(
987            mi.item.named_or_positional_attribute("alt", 1).unwrap(),
988            ElementAttribute {
989                name: None,
990                shorthand_items: &["Sunset"],
991                value: "Sunset"
992            }
993        );
994
995        assert_eq!(
996            mi.item.nth_attribute(2).unwrap(),
997            ElementAttribute {
998                name: None,
999                shorthand_items: &[],
1000                value: "300"
1001            }
1002        );
1003
1004        assert_eq!(
1005            mi.item.named_or_positional_attribute("width", 2).unwrap(),
1006            ElementAttribute {
1007                name: None,
1008                shorthand_items: &[],
1009                value: "300"
1010            }
1011        );
1012
1013        assert_eq!(
1014            mi.item.nth_attribute(3).unwrap(),
1015            ElementAttribute {
1016                name: None,
1017                shorthand_items: &[],
1018                value: "400"
1019            }
1020        );
1021
1022        assert_eq!(
1023            mi.item.named_or_positional_attribute("height", 3).unwrap(),
1024            ElementAttribute {
1025                name: None,
1026                shorthand_items: &[],
1027                value: "400"
1028            }
1029        );
1030
1031        assert!(mi.item.nth_attribute(4).is_none());
1032        assert!(mi.item.named_or_positional_attribute("height", 4).is_none());
1033        assert!(mi.item.nth_attribute(42).is_none());
1034
1035        assert_eq!(
1036            mi.item.span(),
1037            Span {
1038                data: "Sunset,300,400",
1039                line: 1,
1040                col: 1,
1041                offset: 0,
1042            }
1043        );
1044
1045        assert_eq!(
1046            mi.after,
1047            Span {
1048                data: "",
1049                line: 1,
1050                col: 15,
1051                offset: 14
1052            }
1053        );
1054    }
1055
1056    #[test]
1057    fn trim_trailing_space() {
1058        let p = Parser::default();
1059
1060        let mi = crate::attributes::Attrlist::parse(
1061            crate::Span::new("Sunset ,300 , 400"),
1062            &p,
1063            AttrlistContext::Inline,
1064        )
1065        .unwrap_if_no_warnings();
1066
1067        assert_eq!(
1068            mi.item,
1069            Attrlist {
1070                attributes: &[
1071                    ElementAttribute {
1072                        name: None,
1073                        shorthand_items: &["Sunset"],
1074                        value: "Sunset"
1075                    },
1076                    ElementAttribute {
1077                        name: None,
1078                        shorthand_items: &[],
1079                        value: "300"
1080                    },
1081                    ElementAttribute {
1082                        name: None,
1083                        shorthand_items: &[],
1084                        value: "400"
1085                    }
1086                ],
1087                anchor: None,
1088                source: Span {
1089                    data: "Sunset ,300 , 400",
1090                    line: 1,
1091                    col: 1,
1092                    offset: 0
1093                }
1094            }
1095        );
1096
1097        assert!(mi.item.named_attribute("foo").is_none());
1098        assert!(mi.item.nth_attribute(0).is_none());
1099        assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
1100
1101        assert!(mi.item.id().is_none());
1102        assert!(mi.item.roles().is_empty());
1103        assert_eq!(mi.item.block_style().unwrap(), "Sunset");
1104
1105        assert_eq!(
1106            mi.item.nth_attribute(1).unwrap(),
1107            ElementAttribute {
1108                name: None,
1109                shorthand_items: &["Sunset"],
1110                value: "Sunset"
1111            }
1112        );
1113
1114        assert_eq!(
1115            mi.item.named_or_positional_attribute("alt", 1).unwrap(),
1116            ElementAttribute {
1117                name: None,
1118                shorthand_items: &["Sunset"],
1119                value: "Sunset"
1120            }
1121        );
1122
1123        assert_eq!(
1124            mi.item.nth_attribute(2).unwrap(),
1125            ElementAttribute {
1126                name: None,
1127                shorthand_items: &[],
1128                value: "300"
1129            }
1130        );
1131
1132        assert_eq!(
1133            mi.item.named_or_positional_attribute("width", 2).unwrap(),
1134            ElementAttribute {
1135                name: None,
1136                shorthand_items: &[],
1137                value: "300"
1138            }
1139        );
1140
1141        assert_eq!(
1142            mi.item.nth_attribute(3).unwrap(),
1143            ElementAttribute {
1144                name: None,
1145                shorthand_items: &[],
1146                value: "400"
1147            }
1148        );
1149
1150        assert_eq!(
1151            mi.item.named_or_positional_attribute("height", 3).unwrap(),
1152            ElementAttribute {
1153                name: None,
1154                shorthand_items: &[],
1155                value: "400"
1156            }
1157        );
1158
1159        assert!(mi.item.nth_attribute(4).is_none());
1160        assert!(mi.item.named_or_positional_attribute("height", 4).is_none());
1161        assert!(mi.item.nth_attribute(42).is_none());
1162
1163        assert_eq!(
1164            mi.item.span(),
1165            Span {
1166                data: "Sunset ,300 , 400",
1167                line: 1,
1168                col: 1,
1169                offset: 0,
1170            }
1171        );
1172
1173        assert_eq!(
1174            mi.after,
1175            Span {
1176                data: "",
1177                line: 1,
1178                col: 18,
1179                offset: 17
1180            }
1181        );
1182    }
1183
1184    #[test]
1185    fn only_named_attributes() {
1186        let p = Parser::default();
1187
1188        let mi = crate::attributes::Attrlist::parse(
1189            crate::Span::new("alt=Sunset,width=300,height=400"),
1190            &p,
1191            AttrlistContext::Inline,
1192        )
1193        .unwrap_if_no_warnings();
1194
1195        assert_eq!(
1196            mi.item,
1197            Attrlist {
1198                attributes: &[
1199                    ElementAttribute {
1200                        name: Some("alt"),
1201                        shorthand_items: &[],
1202                        value: "Sunset"
1203                    },
1204                    ElementAttribute {
1205                        name: Some("width"),
1206                        shorthand_items: &[],
1207                        value: "300"
1208                    },
1209                    ElementAttribute {
1210                        name: Some("height"),
1211                        shorthand_items: &[],
1212                        value: "400"
1213                    }
1214                ],
1215                anchor: None,
1216                source: Span {
1217                    data: "alt=Sunset,width=300,height=400",
1218                    line: 1,
1219                    col: 1,
1220                    offset: 0
1221                }
1222            }
1223        );
1224
1225        assert!(mi.item.named_attribute("foo").is_none());
1226        assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
1227
1228        assert_eq!(
1229            mi.item.named_attribute("alt").unwrap(),
1230            ElementAttribute {
1231                name: Some("alt"),
1232                shorthand_items: &[],
1233                value: "Sunset"
1234            }
1235        );
1236
1237        assert_eq!(
1238            mi.item.named_or_positional_attribute("alt", 1).unwrap(),
1239            ElementAttribute {
1240                name: Some("alt"),
1241                shorthand_items: &[],
1242                value: "Sunset"
1243            }
1244        );
1245
1246        assert_eq!(
1247            mi.item.named_attribute("width").unwrap(),
1248            ElementAttribute {
1249                name: Some("width"),
1250                shorthand_items: &[],
1251                value: "300"
1252            }
1253        );
1254
1255        assert_eq!(
1256            mi.item.named_or_positional_attribute("width", 2).unwrap(),
1257            ElementAttribute {
1258                name: Some("width"),
1259                shorthand_items: &[],
1260                value: "300"
1261            }
1262        );
1263
1264        assert_eq!(
1265            mi.item.named_attribute("height").unwrap(),
1266            ElementAttribute {
1267                name: Some("height"),
1268                shorthand_items: &[],
1269                value: "400"
1270            }
1271        );
1272
1273        assert_eq!(
1274            mi.item.named_or_positional_attribute("height", 3).unwrap(),
1275            ElementAttribute {
1276                name: Some("height"),
1277                shorthand_items: &[],
1278                value: "400"
1279            }
1280        );
1281
1282        assert!(mi.item.nth_attribute(0).is_none());
1283        assert!(mi.item.nth_attribute(1).is_none());
1284        assert!(mi.item.nth_attribute(2).is_none());
1285        assert!(mi.item.nth_attribute(3).is_none());
1286        assert!(mi.item.nth_attribute(4).is_none());
1287        assert!(mi.item.nth_attribute(42).is_none());
1288
1289        assert!(mi.item.id().is_none());
1290        assert!(mi.item.roles().is_empty());
1291        assert!(mi.item.block_style().is_none());
1292
1293        assert_eq!(
1294            mi.item.span(),
1295            Span {
1296                data: "alt=Sunset,width=300,height=400",
1297                line: 1,
1298                col: 1,
1299                offset: 0
1300            }
1301        );
1302
1303        assert_eq!(
1304            mi.after,
1305            Span {
1306                data: "",
1307                line: 1,
1308                col: 32,
1309                offset: 31
1310            }
1311        );
1312    }
1313
1314    #[test]
1315    fn ignore_named_attribute_with_none_value() {
1316        let p = Parser::default();
1317        let mi = crate::attributes::Attrlist::parse(
1318            crate::Span::new("alt=Sunset,width=None,height=400"),
1319            &p,
1320            AttrlistContext::Inline,
1321        )
1322        .unwrap_if_no_warnings();
1323
1324        assert_eq!(
1325            mi.item,
1326            Attrlist {
1327                attributes: &[
1328                    ElementAttribute {
1329                        name: Some("alt"),
1330                        shorthand_items: &[],
1331                        value: "Sunset"
1332                    },
1333                    ElementAttribute {
1334                        name: Some("height"),
1335                        shorthand_items: &[],
1336                        value: "400"
1337                    }
1338                ],
1339                anchor: None,
1340                source: Span {
1341                    data: "alt=Sunset,width=None,height=400",
1342                    line: 1,
1343                    col: 1,
1344                    offset: 0
1345                }
1346            }
1347        );
1348
1349        assert!(mi.item.named_attribute("foo").is_none());
1350        assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
1351
1352        assert_eq!(
1353            mi.item.named_attribute("alt").unwrap(),
1354            ElementAttribute {
1355                name: Some("alt"),
1356                shorthand_items: &[],
1357                value: "Sunset"
1358            }
1359        );
1360
1361        assert_eq!(
1362            mi.item.named_or_positional_attribute("alt", 1).unwrap(),
1363            ElementAttribute {
1364                name: Some("alt"),
1365                shorthand_items: &[],
1366                value: "Sunset"
1367            }
1368        );
1369
1370        assert!(mi.item.named_attribute("width").is_none());
1371        assert!(mi.item.named_or_positional_attribute("width", 2).is_none());
1372
1373        assert_eq!(
1374            mi.item.named_attribute("height").unwrap(),
1375            ElementAttribute {
1376                name: Some("height"),
1377                shorthand_items: &[],
1378                value: "400"
1379            }
1380        );
1381
1382        assert_eq!(
1383            mi.item.named_or_positional_attribute("height", 2).unwrap(),
1384            ElementAttribute {
1385                name: Some("height"),
1386                shorthand_items: &[],
1387                value: "400"
1388            }
1389        );
1390
1391        assert!(mi.item.nth_attribute(0).is_none());
1392        assert!(mi.item.nth_attribute(1).is_none());
1393        assert!(mi.item.nth_attribute(2).is_none());
1394        assert!(mi.item.nth_attribute(3).is_none());
1395        assert!(mi.item.nth_attribute(4).is_none());
1396        assert!(mi.item.nth_attribute(42).is_none());
1397
1398        assert!(mi.item.id().is_none());
1399        assert!(mi.item.roles().is_empty());
1400        assert!(mi.item.block_style().is_none());
1401
1402        assert_eq!(
1403            mi.item.span(),
1404            Span {
1405                data: "alt=Sunset,width=None,height=400",
1406                line: 1,
1407                col: 1,
1408                offset: 0
1409            }
1410        );
1411
1412        assert_eq!(
1413            mi.after,
1414            Span {
1415                data: "",
1416                line: 1,
1417                col: 33,
1418                offset: 32
1419            }
1420        );
1421    }
1422
1423    #[test]
1424    fn err_unparsed_remainder_after_value() {
1425        let p = Parser::default();
1426
1427        let maw = crate::attributes::Attrlist::parse(
1428            crate::Span::new("alt=\"Sunset\"width=300"),
1429            &p,
1430            AttrlistContext::Inline,
1431        );
1432
1433        let mi = maw.item.clone();
1434
1435        assert_eq!(
1436            mi.item,
1437            Attrlist {
1438                attributes: &[ElementAttribute {
1439                    name: Some("alt"),
1440                    shorthand_items: &[],
1441                    value: "Sunset"
1442                }],
1443                anchor: None,
1444                source: Span {
1445                    data: "alt=\"Sunset\"width=300",
1446                    line: 1,
1447                    col: 1,
1448                    offset: 0
1449                }
1450            }
1451        );
1452
1453        assert_eq!(
1454            mi.after,
1455            Span {
1456                data: "",
1457                line: 1,
1458                col: 22,
1459                offset: 21
1460            }
1461        );
1462
1463        assert_eq!(
1464            maw.warnings,
1465            vec![Warning {
1466                source: Span {
1467                    data: "alt=\"Sunset\"width=300",
1468                    line: 1,
1469                    col: 1,
1470                    offset: 0,
1471                },
1472                warning: WarningType::MissingCommaAfterQuotedAttributeValue,
1473            }]
1474        );
1475    }
1476
1477    #[test]
1478    fn propagates_error_from_element_attribute() {
1479        let p = Parser::default();
1480
1481        let maw = crate::attributes::Attrlist::parse(
1482            crate::Span::new("foo%#id"),
1483            &p,
1484            AttrlistContext::Inline,
1485        );
1486
1487        let mi = maw.item.clone();
1488
1489        assert_eq!(
1490            mi.item,
1491            Attrlist {
1492                attributes: &[ElementAttribute {
1493                    name: None,
1494                    shorthand_items: &["foo", "#id"],
1495                    value: "foo%#id"
1496                }],
1497                anchor: None,
1498                source: Span {
1499                    data: "foo%#id",
1500                    line: 1,
1501                    col: 1,
1502                    offset: 0
1503                }
1504            }
1505        );
1506
1507        assert_eq!(
1508            mi.after,
1509            Span {
1510                data: "",
1511                line: 1,
1512                col: 8,
1513                offset: 7
1514            }
1515        );
1516
1517        assert_eq!(
1518            maw.warnings,
1519            vec![Warning {
1520                source: Span {
1521                    data: "foo%#id",
1522                    line: 1,
1523                    col: 1,
1524                    offset: 0,
1525                },
1526                warning: WarningType::EmptyShorthandName,
1527            }]
1528        );
1529    }
1530
1531    #[test]
1532    fn merge_block_attribute_line_anchor_later_wins() {
1533        let p = Parser::default();
1534
1535        let mut first = crate::attributes::Attrlist::parse(
1536            crate::Span::new("[id1]"),
1537            &p,
1538            AttrlistContext::Block,
1539        )
1540        .unwrap_if_no_warnings()
1541        .item;
1542
1543        let later = crate::attributes::Attrlist::parse(
1544            crate::Span::new("[id2]"),
1545            &p,
1546            AttrlistContext::Block,
1547        )
1548        .unwrap_if_no_warnings()
1549        .item;
1550
1551        assert_eq!(first.anchor(), Some("id1"));
1552        first.merge_block_attribute_line(later);
1553        assert_eq!(first.anchor(), Some("id2"));
1554    }
1555
1556    #[test]
1557    fn merge_block_attribute_line_anchor_retained_when_later_has_none() {
1558        let p = Parser::default();
1559
1560        let mut first = crate::attributes::Attrlist::parse(
1561            crate::Span::new("[id1]"),
1562            &p,
1563            AttrlistContext::Block,
1564        )
1565        .unwrap_if_no_warnings()
1566        .item;
1567
1568        let later = crate::attributes::Attrlist::parse(
1569            crate::Span::new("foo=bar"),
1570            &p,
1571            AttrlistContext::Block,
1572        )
1573        .unwrap_if_no_warnings()
1574        .item;
1575
1576        first.merge_block_attribute_line(later);
1577        assert_eq!(first.anchor(), Some("id1"));
1578        assert_eq!(first.named_attribute("foo").unwrap().value(), "bar");
1579    }
1580
1581    #[test]
1582    fn merge_block_attribute_line_positions_account_for_named_entries() {
1583        // A later line whose positional is preceded by a named attribute must
1584        // merge at its Asciidoctor position (which counts the named entry), not
1585        // at its ordinal among unnamed entries. Here `Author2` is the later
1586        // line's *second* entry, so it replaces position 2 (`Author1`) rather
1587        // than merging into position 1 (`quote`); `Extra` is position 3.
1588        let p = Parser::default();
1589
1590        let mut first = crate::attributes::Attrlist::parse(
1591            crate::Span::new("quote,Author1"),
1592            &p,
1593            AttrlistContext::Block,
1594        )
1595        .unwrap_if_no_warnings()
1596        .item;
1597
1598        let later = crate::attributes::Attrlist::parse(
1599            crate::Span::new("width=300,Author2,Extra"),
1600            &p,
1601            AttrlistContext::Block,
1602        )
1603        .unwrap_if_no_warnings()
1604        .item;
1605
1606        first.merge_block_attribute_line(later);
1607
1608        assert_eq!(first.nth_attribute(1).unwrap().value(), "quote");
1609        assert_eq!(first.nth_attribute(2).unwrap().value(), "Author2");
1610        assert_eq!(first.nth_attribute(3).unwrap().value(), "Extra");
1611        assert_eq!(first.named_attribute("width").unwrap().value(), "300");
1612    }
1613
1614    #[test]
1615    fn anchor_syntax() {
1616        let p = Parser::default();
1617
1618        let maw = crate::attributes::Attrlist::parse(
1619            crate::Span::new("[notice]"),
1620            &p,
1621            AttrlistContext::Inline,
1622        );
1623
1624        let mi = maw.item.clone();
1625
1626        assert_eq!(
1627            mi.item,
1628            Attrlist {
1629                attributes: &[],
1630                anchor: Some("notice"),
1631                source: Span {
1632                    data: "[notice]",
1633                    line: 1,
1634                    col: 1,
1635                    offset: 0
1636                }
1637            }
1638        );
1639
1640        assert_eq!(
1641            mi.after,
1642            Span {
1643                data: "",
1644                line: 1,
1645                col: 9,
1646                offset: 8
1647            }
1648        );
1649
1650        assert!(maw.warnings.is_empty());
1651    }
1652
1653    mod id {
1654        use crate::{attributes::AttrlistContext, tests::prelude::*};
1655
1656        #[test]
1657        fn via_shorthand_syntax() {
1658            let p = Parser::default();
1659
1660            let mi = crate::attributes::Attrlist::parse(
1661                crate::Span::new("#goals"),
1662                &p,
1663                AttrlistContext::Inline,
1664            )
1665            .unwrap_if_no_warnings();
1666
1667            assert_eq!(
1668                mi.item,
1669                Attrlist {
1670                    attributes: &[ElementAttribute {
1671                        name: None,
1672                        shorthand_items: &["#goals"],
1673                        value: "#goals"
1674                    }],
1675                    anchor: None,
1676                    source: Span {
1677                        data: "#goals",
1678                        line: 1,
1679                        col: 1,
1680                        offset: 0
1681                    }
1682                }
1683            );
1684
1685            assert!(mi.item.named_attribute("foo").is_none());
1686            assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
1687            assert_eq!(mi.item.id().unwrap(), "goals");
1688            assert!(mi.item.roles().is_empty());
1689            assert!(mi.item.block_style().is_none());
1690
1691            assert_eq!(
1692                mi.item.span(),
1693                Span {
1694                    data: "#goals",
1695                    line: 1,
1696                    col: 1,
1697                    offset: 0
1698                }
1699            );
1700
1701            assert_eq!(
1702                mi.after,
1703                Span {
1704                    data: "",
1705                    line: 1,
1706                    col: 7,
1707                    offset: 6
1708                }
1709            );
1710        }
1711
1712        #[test]
1713        fn via_named_attribute() {
1714            let p = Parser::default();
1715
1716            let mi = crate::attributes::Attrlist::parse(
1717                crate::Span::new("foo=bar,id=goals"),
1718                &p,
1719                AttrlistContext::Inline,
1720            )
1721            .unwrap_if_no_warnings();
1722
1723            assert_eq!(
1724                mi.item,
1725                Attrlist {
1726                    attributes: &[
1727                        ElementAttribute {
1728                            name: Some("foo"),
1729                            shorthand_items: &[],
1730                            value: "bar"
1731                        },
1732                        ElementAttribute {
1733                            name: Some("id"),
1734                            shorthand_items: &[],
1735                            value: "goals"
1736                        },
1737                    ],
1738                    anchor: None,
1739                    source: Span {
1740                        data: "foo=bar,id=goals",
1741                        line: 1,
1742                        col: 1,
1743                        offset: 0
1744                    }
1745                }
1746            );
1747
1748            assert_eq!(
1749                mi.item.named_attribute("foo").unwrap(),
1750                ElementAttribute {
1751                    name: Some("foo"),
1752                    shorthand_items: &[],
1753                    value: "bar"
1754                }
1755            );
1756
1757            assert_eq!(
1758                mi.item.named_attribute("id").unwrap(),
1759                ElementAttribute {
1760                    name: Some("id"),
1761                    shorthand_items: &[],
1762                    value: "goals"
1763                }
1764            );
1765
1766            assert_eq!(mi.item.id().unwrap(), "goals");
1767            assert!(mi.item.roles().is_empty());
1768            assert!(mi.item.block_style().is_none());
1769
1770            assert_eq!(
1771                mi.after,
1772                Span {
1773                    data: "",
1774                    line: 1,
1775                    col: 17,
1776                    offset: 16
1777                }
1778            );
1779        }
1780
1781        #[test]
1782        fn via_block_anchor_syntax() {
1783            let p = Parser::default();
1784
1785            let mi = crate::attributes::Attrlist::parse(
1786                crate::Span::new("[goals]"),
1787                &p,
1788                AttrlistContext::Inline,
1789            )
1790            .unwrap_if_no_warnings();
1791
1792            assert_eq!(
1793                mi.item,
1794                Attrlist {
1795                    attributes: &[],
1796                    anchor: Some("goals"),
1797                    source: Span {
1798                        data: "[goals]",
1799                        line: 1,
1800                        col: 1,
1801                        offset: 0
1802                    }
1803                }
1804            );
1805
1806            assert_eq!(mi.item.id().unwrap(), "goals");
1807
1808            assert_eq!(
1809                mi.after,
1810                Span {
1811                    data: "",
1812                    line: 1,
1813                    col: 8,
1814                    offset: 7
1815                }
1816            );
1817        }
1818
1819        #[test]
1820        fn shorthand_only_first_attribute() {
1821            let p = Parser::default();
1822
1823            let mi = crate::attributes::Attrlist::parse(
1824                crate::Span::new("foo,blah#goals"),
1825                &p,
1826                AttrlistContext::Inline,
1827            )
1828            .unwrap_if_no_warnings();
1829
1830            assert_eq!(
1831                mi.item,
1832                Attrlist {
1833                    attributes: &[
1834                        ElementAttribute {
1835                            name: None,
1836                            shorthand_items: &["foo"],
1837                            value: "foo"
1838                        },
1839                        ElementAttribute {
1840                            name: None,
1841                            shorthand_items: &[],
1842                            value: "blah#goals"
1843                        },
1844                    ],
1845                    anchor: None,
1846                    source: Span {
1847                        data: "foo,blah#goals",
1848                        line: 1,
1849                        col: 1,
1850                        offset: 0
1851                    }
1852                }
1853            );
1854
1855            assert!(mi.item.id().is_none());
1856            assert!(mi.item.roles().is_empty());
1857            assert_eq!(mi.item.block_style().unwrap(), "foo");
1858
1859            assert_eq!(
1860                mi.after,
1861                Span {
1862                    data: "",
1863                    line: 1,
1864                    col: 15,
1865                    offset: 14
1866                }
1867            );
1868        }
1869    }
1870
1871    mod roles {
1872        use crate::{attributes::AttrlistContext, tests::prelude::*};
1873
1874        #[test]
1875        fn via_shorthand_syntax() {
1876            let p = Parser::default();
1877
1878            let mi = crate::attributes::Attrlist::parse(
1879                crate::Span::new(".rolename"),
1880                &p,
1881                AttrlistContext::Inline,
1882            )
1883            .unwrap_if_no_warnings();
1884
1885            assert_eq!(
1886                mi.item,
1887                Attrlist {
1888                    attributes: &[ElementAttribute {
1889                        name: None,
1890                        shorthand_items: &[".rolename"],
1891                        value: ".rolename"
1892                    }],
1893                    anchor: None,
1894                    source: Span {
1895                        data: ".rolename",
1896                        line: 1,
1897                        col: 1,
1898                        offset: 0
1899                    }
1900                }
1901            );
1902
1903            assert!(mi.item.named_attribute("foo").is_none());
1904            assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
1905
1906            let roles = mi.item.roles();
1907            let mut roles = roles.iter();
1908            assert_eq!(roles.next().unwrap(), &"rolename");
1909            assert!(roles.next().is_none());
1910
1911            assert!(mi.item.block_style().is_none());
1912
1913            assert_eq!(
1914                mi.item.span(),
1915                Span {
1916                    data: ".rolename",
1917                    line: 1,
1918                    col: 1,
1919                    offset: 0
1920                }
1921            );
1922
1923            assert_eq!(
1924                mi.after,
1925                Span {
1926                    data: "",
1927                    line: 1,
1928                    col: 10,
1929                    offset: 9
1930                }
1931            );
1932        }
1933
1934        #[test]
1935        fn via_shorthand_syntax_trim_trailing_whitespace() {
1936            let p = Parser::default();
1937
1938            let mi = crate::attributes::Attrlist::parse(
1939                crate::Span::new(".rolename "),
1940                &p,
1941                AttrlistContext::Inline,
1942            )
1943            .unwrap_if_no_warnings();
1944
1945            assert_eq!(
1946                mi.item,
1947                Attrlist {
1948                    attributes: &[ElementAttribute {
1949                        name: None,
1950                        shorthand_items: &[".rolename"],
1951                        value: ".rolename"
1952                    }],
1953                    anchor: None,
1954                    source: Span {
1955                        data: ".rolename ",
1956                        line: 1,
1957                        col: 1,
1958                        offset: 0
1959                    }
1960                }
1961            );
1962
1963            assert!(mi.item.named_attribute("foo").is_none());
1964            assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
1965
1966            let roles = mi.item.roles();
1967            let mut roles = roles.iter();
1968
1969            assert_eq!(roles.next().unwrap(), &"rolename");
1970            assert!(roles.next().is_none());
1971
1972            assert!(mi.item.block_style().is_none());
1973
1974            assert_eq!(
1975                mi.item.span(),
1976                Span {
1977                    data: ".rolename ",
1978                    line: 1,
1979                    col: 1,
1980                    offset: 0
1981                }
1982            );
1983
1984            assert_eq!(
1985                mi.after,
1986                Span {
1987                    data: "",
1988                    line: 1,
1989                    col: 11,
1990                    offset: 10
1991                }
1992            );
1993        }
1994
1995        #[test]
1996        fn multiple_roles_via_shorthand_syntax() {
1997            let p = Parser::default();
1998
1999            let mi = crate::attributes::Attrlist::parse(
2000                crate::Span::new(".role1.role2.role3"),
2001                &p,
2002                AttrlistContext::Inline,
2003            )
2004            .unwrap_if_no_warnings();
2005
2006            assert_eq!(
2007                mi.item,
2008                Attrlist {
2009                    attributes: &[ElementAttribute {
2010                        name: None,
2011                        shorthand_items: &[".role1", ".role2", ".role3"],
2012                        value: ".role1.role2.role3"
2013                    }],
2014                    anchor: None,
2015                    source: Span {
2016                        data: ".role1.role2.role3",
2017                        line: 1,
2018                        col: 1,
2019                        offset: 0
2020                    }
2021                }
2022            );
2023
2024            assert!(mi.item.named_attribute("foo").is_none());
2025            assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
2026
2027            let roles = mi.item.roles();
2028            let mut roles = roles.iter();
2029            assert_eq!(roles.next().unwrap(), &"role1");
2030            assert_eq!(roles.next().unwrap(), &"role2");
2031            assert_eq!(roles.next().unwrap(), &"role3");
2032            assert!(roles.next().is_none());
2033
2034            assert!(mi.item.block_style().is_none());
2035
2036            assert_eq!(
2037                mi.item.span(),
2038                Span {
2039                    data: ".role1.role2.role3",
2040                    line: 1,
2041                    col: 1,
2042                    offset: 0
2043                }
2044            );
2045
2046            assert_eq!(
2047                mi.after,
2048                Span {
2049                    data: "",
2050                    line: 1,
2051                    col: 19,
2052                    offset: 18
2053                }
2054            );
2055        }
2056
2057        #[test]
2058        fn multiple_roles_via_shorthand_syntax_trim_whitespace() {
2059            let p = Parser::default();
2060
2061            let mi = crate::attributes::Attrlist::parse(
2062                crate::Span::new(".role1 .role2 .role3 "),
2063                &p,
2064                AttrlistContext::Inline,
2065            )
2066            .unwrap_if_no_warnings();
2067
2068            assert_eq!(
2069                mi.item,
2070                Attrlist {
2071                    attributes: &[ElementAttribute {
2072                        name: None,
2073                        shorthand_items: &[".role1", ".role2", ".role3"],
2074                        value: ".role1 .role2 .role3"
2075                    }],
2076                    anchor: None,
2077                    source: Span {
2078                        data: ".role1 .role2 .role3 ",
2079                        line: 1,
2080                        col: 1,
2081                        offset: 0
2082                    }
2083                }
2084            );
2085
2086            assert!(mi.item.named_attribute("foo").is_none());
2087            assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
2088
2089            let roles = mi.item.roles();
2090            let mut roles = roles.iter();
2091            assert_eq!(roles.next().unwrap(), &"role1");
2092            assert_eq!(roles.next().unwrap(), &"role2");
2093            assert_eq!(roles.next().unwrap(), &"role3");
2094            assert!(roles.next().is_none());
2095
2096            assert!(mi.item.block_style().is_none());
2097
2098            assert_eq!(
2099                mi.item.span(),
2100                Span {
2101                    data: ".role1 .role2 .role3 ",
2102                    line: 1,
2103                    col: 1,
2104                    offset: 0
2105                }
2106            );
2107
2108            assert_eq!(
2109                mi.after,
2110                Span {
2111                    data: "",
2112                    line: 1,
2113                    col: 22,
2114                    offset: 21
2115                }
2116            );
2117        }
2118
2119        #[test]
2120        fn via_named_attribute() {
2121            let p = Parser::default();
2122
2123            let mi = crate::attributes::Attrlist::parse(
2124                crate::Span::new("foo=bar,role=role1"),
2125                &p,
2126                AttrlistContext::Inline,
2127            )
2128            .unwrap_if_no_warnings();
2129
2130            assert_eq!(
2131                mi.item,
2132                Attrlist {
2133                    attributes: &[
2134                        ElementAttribute {
2135                            name: Some("foo"),
2136                            shorthand_items: &[],
2137                            value: "bar"
2138                        },
2139                        ElementAttribute {
2140                            name: Some("role"),
2141                            shorthand_items: &[],
2142                            value: "role1"
2143                        },
2144                    ],
2145                    anchor: None,
2146                    source: Span {
2147                        data: "foo=bar,role=role1",
2148                        line: 1,
2149                        col: 1,
2150                        offset: 0
2151                    }
2152                }
2153            );
2154
2155            assert_eq!(
2156                mi.item.named_attribute("foo").unwrap(),
2157                ElementAttribute {
2158                    name: Some("foo"),
2159                    shorthand_items: &[],
2160                    value: "bar"
2161                }
2162            );
2163
2164            assert_eq!(
2165                mi.item.named_attribute("role").unwrap(),
2166                ElementAttribute {
2167                    name: Some("role"),
2168                    shorthand_items: &[],
2169                    value: "role1"
2170                }
2171            );
2172
2173            let roles = mi.item.roles();
2174            let mut roles = roles.iter();
2175            assert_eq!(roles.next().unwrap(), &"role1");
2176            assert!(roles.next().is_none());
2177
2178            assert!(mi.item.block_style().is_none());
2179
2180            assert_eq!(
2181                mi.after,
2182                Span {
2183                    data: "",
2184                    line: 1,
2185                    col: 19,
2186                    offset: 18
2187                }
2188            );
2189        }
2190
2191        #[test]
2192        fn multiple_roles_via_named_attribute() {
2193            let p = Parser::default();
2194
2195            let mi = crate::attributes::Attrlist::parse(
2196                crate::Span::new("foo=bar,role=role1 role2   role3 "),
2197                &p,
2198                AttrlistContext::Inline,
2199            )
2200            .unwrap_if_no_warnings();
2201
2202            assert_eq!(
2203                mi.item,
2204                Attrlist {
2205                    attributes: &[
2206                        ElementAttribute {
2207                            name: Some("foo"),
2208                            shorthand_items: &[],
2209                            value: "bar"
2210                        },
2211                        ElementAttribute {
2212                            name: Some("role"),
2213                            shorthand_items: &[],
2214                            value: "role1 role2   role3"
2215                        },
2216                    ],
2217                    anchor: None,
2218                    source: Span {
2219                        data: "foo=bar,role=role1 role2   role3 ",
2220                        line: 1,
2221                        col: 1,
2222                        offset: 0
2223                    }
2224                }
2225            );
2226
2227            assert_eq!(
2228                mi.item.named_attribute("foo").unwrap(),
2229                ElementAttribute {
2230                    name: Some("foo"),
2231                    shorthand_items: &[],
2232                    value: "bar"
2233                }
2234            );
2235
2236            assert_eq!(
2237                mi.item.named_attribute("role").unwrap(),
2238                ElementAttribute {
2239                    name: Some("role"),
2240                    shorthand_items: &[],
2241                    value: "role1 role2   role3"
2242                }
2243            );
2244
2245            let roles = mi.item.roles();
2246            let mut roles = roles.iter();
2247            assert_eq!(roles.next().unwrap(), &"role1");
2248            assert_eq!(roles.next().unwrap(), &"role2");
2249            assert_eq!(roles.next().unwrap(), &"role3");
2250            assert!(roles.next().is_none());
2251
2252            assert!(mi.item.block_style().is_none());
2253
2254            assert_eq!(
2255                mi.after,
2256                Span {
2257                    data: "",
2258                    line: 1,
2259                    col: 34,
2260                    offset: 33
2261                }
2262            );
2263        }
2264
2265        #[test]
2266        fn shorthand_role_and_named_attribute_role() {
2267            let p = Parser::default();
2268
2269            let mi = crate::attributes::Attrlist::parse(
2270                crate::Span::new("#foo.sh1.sh2,role=na1 na2   na3 "),
2271                &p,
2272                AttrlistContext::Inline,
2273            )
2274            .unwrap_if_no_warnings();
2275
2276            assert_eq!(
2277                mi.item,
2278                Attrlist {
2279                    attributes: &[
2280                        ElementAttribute {
2281                            name: None,
2282                            shorthand_items: &["#foo", ".sh1", ".sh2"],
2283                            value: "#foo.sh1.sh2"
2284                        },
2285                        ElementAttribute {
2286                            name: Some("role"),
2287                            shorthand_items: &[],
2288                            value: "na1 na2   na3"
2289                        },
2290                    ],
2291                    anchor: None,
2292                    source: Span {
2293                        data: "#foo.sh1.sh2,role=na1 na2   na3 ",
2294                        line: 1,
2295                        col: 1,
2296                        offset: 0
2297                    }
2298                }
2299            );
2300
2301            assert!(mi.item.named_attribute("foo").is_none());
2302
2303            assert_eq!(
2304                mi.item.named_attribute("role").unwrap(),
2305                ElementAttribute {
2306                    name: Some("role"),
2307                    shorthand_items: &[],
2308                    value: "na1 na2   na3"
2309                }
2310            );
2311
2312            let roles = mi.item.roles();
2313            let mut roles = roles.iter();
2314            assert_eq!(roles.next().unwrap(), &"sh1");
2315            assert_eq!(roles.next().unwrap(), &"sh2");
2316            assert_eq!(roles.next().unwrap(), &"na1");
2317            assert_eq!(roles.next().unwrap(), &"na2");
2318            assert_eq!(roles.next().unwrap(), &"na3");
2319            assert!(roles.next().is_none());
2320
2321            assert!(mi.item.block_style().is_none());
2322
2323            assert_eq!(
2324                mi.after,
2325                Span {
2326                    data: "",
2327                    line: 1,
2328                    col: 33,
2329                    offset: 32
2330                }
2331            );
2332        }
2333
2334        #[test]
2335        fn shorthand_only_first_attribute() {
2336            let p = Parser::default();
2337
2338            let mi = crate::attributes::Attrlist::parse(
2339                crate::Span::new("foo,blah.rolename"),
2340                &p,
2341                AttrlistContext::Inline,
2342            )
2343            .unwrap_if_no_warnings();
2344
2345            assert_eq!(
2346                mi.item,
2347                Attrlist {
2348                    attributes: &[
2349                        ElementAttribute {
2350                            name: None,
2351                            shorthand_items: &["foo"],
2352                            value: "foo"
2353                        },
2354                        ElementAttribute {
2355                            name: None,
2356                            shorthand_items: &[],
2357                            value: "blah.rolename"
2358                        },
2359                    ],
2360                    anchor: None,
2361                    source: Span {
2362                        data: "foo,blah.rolename",
2363                        line: 1,
2364                        col: 1,
2365                        offset: 0
2366                    }
2367                }
2368            );
2369
2370            let roles = mi.item.roles();
2371            assert_eq!(roles.iter().len(), 0);
2372
2373            assert_eq!(mi.item.block_style().unwrap(), "foo");
2374
2375            assert_eq!(
2376                mi.after,
2377                Span {
2378                    data: "",
2379                    line: 1,
2380                    col: 18,
2381                    offset: 17
2382                }
2383            );
2384        }
2385    }
2386
2387    mod options {
2388        use crate::{attributes::AttrlistContext, tests::prelude::*};
2389
2390        #[test]
2391        fn via_shorthand_syntax() {
2392            let p = Parser::default();
2393
2394            let mi = crate::attributes::Attrlist::parse(
2395                crate::Span::new("%option"),
2396                &p,
2397                AttrlistContext::Inline,
2398            )
2399            .unwrap_if_no_warnings();
2400
2401            assert_eq!(
2402                mi.item,
2403                Attrlist {
2404                    attributes: &[ElementAttribute {
2405                        name: None,
2406                        shorthand_items: &["%option"],
2407                        value: "%option"
2408                    }],
2409                    anchor: None,
2410                    source: Span {
2411                        data: "%option",
2412                        line: 1,
2413                        col: 1,
2414                        offset: 0
2415                    }
2416                }
2417            );
2418
2419            assert!(mi.item.named_attribute("foo").is_none());
2420            assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
2421
2422            let options = mi.item.options();
2423            let mut options = options.iter();
2424            assert_eq!(options.next().unwrap(), &"option",);
2425            assert!(options.next().is_none());
2426
2427            assert!(mi.item.has_option("option"));
2428            assert!(!mi.item.has_option("option1"));
2429
2430            assert_eq!(
2431                mi.item.span(),
2432                Span {
2433                    data: "%option",
2434                    line: 1,
2435                    col: 1,
2436                    offset: 0
2437                }
2438            );
2439
2440            assert_eq!(
2441                mi.after,
2442                Span {
2443                    data: "",
2444                    line: 1,
2445                    col: 8,
2446                    offset: 7
2447                }
2448            );
2449        }
2450
2451        #[test]
2452        fn multiple_options_via_shorthand_syntax() {
2453            let p = Parser::default();
2454
2455            let mi = crate::attributes::Attrlist::parse(
2456                crate::Span::new("%option1%option2%option3"),
2457                &p,
2458                AttrlistContext::Inline,
2459            )
2460            .unwrap_if_no_warnings();
2461
2462            assert_eq!(
2463                mi.item,
2464                Attrlist {
2465                    attributes: &[ElementAttribute {
2466                        name: None,
2467                        shorthand_items: &["%option1", "%option2", "%option3",],
2468                        value: "%option1%option2%option3"
2469                    }],
2470                    anchor: None,
2471                    source: Span {
2472                        data: "%option1%option2%option3",
2473                        line: 1,
2474                        col: 1,
2475                        offset: 0
2476                    }
2477                }
2478            );
2479
2480            assert!(mi.item.named_attribute("foo").is_none());
2481            assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
2482
2483            let options = mi.item.options();
2484            let mut options = options.iter();
2485            assert_eq!(options.next().unwrap(), &"option1");
2486            assert_eq!(options.next().unwrap(), &"option2");
2487            assert_eq!(options.next().unwrap(), &"option3");
2488            assert!(options.next().is_none());
2489
2490            assert!(mi.item.has_option("option1"));
2491            assert!(mi.item.has_option("option2"));
2492            assert!(mi.item.has_option("option3"));
2493            assert!(!mi.item.has_option("option4"));
2494
2495            assert_eq!(
2496                mi.item.span(),
2497                Span {
2498                    data: "%option1%option2%option3",
2499                    line: 1,
2500                    col: 1,
2501                    offset: 0
2502                }
2503            );
2504
2505            assert_eq!(
2506                mi.after,
2507                Span {
2508                    data: "",
2509                    line: 1,
2510                    col: 25,
2511                    offset: 24
2512                }
2513            );
2514        }
2515
2516        #[test]
2517        fn via_options_attribute() {
2518            let p = Parser::default();
2519
2520            let mi = crate::attributes::Attrlist::parse(
2521                crate::Span::new("foo=bar,options=option1"),
2522                &p,
2523                AttrlistContext::Inline,
2524            )
2525            .unwrap_if_no_warnings();
2526
2527            assert_eq!(
2528                mi.item,
2529                Attrlist {
2530                    attributes: &[
2531                        ElementAttribute {
2532                            name: Some("foo"),
2533                            shorthand_items: &[],
2534                            value: "bar"
2535                        },
2536                        ElementAttribute {
2537                            name: Some("options"),
2538                            shorthand_items: &[],
2539                            value: "option1"
2540                        },
2541                    ],
2542                    anchor: None,
2543                    source: Span {
2544                        data: "foo=bar,options=option1",
2545                        line: 1,
2546                        col: 1,
2547                        offset: 0
2548                    }
2549                }
2550            );
2551
2552            assert_eq!(
2553                mi.item.named_attribute("foo").unwrap(),
2554                ElementAttribute {
2555                    name: Some("foo"),
2556                    shorthand_items: &[],
2557                    value: "bar"
2558                }
2559            );
2560
2561            assert_eq!(
2562                mi.item.named_attribute("options").unwrap(),
2563                ElementAttribute {
2564                    name: Some("options"),
2565                    shorthand_items: &[],
2566                    value: "option1"
2567                }
2568            );
2569
2570            let options = mi.item.options();
2571            let mut options = options.iter();
2572            assert_eq!(options.next().unwrap(), &"option1");
2573            assert!(options.next().is_none());
2574
2575            assert!(mi.item.has_option("option1"));
2576            assert!(!mi.item.has_option("option2"));
2577
2578            assert_eq!(
2579                mi.after,
2580                Span {
2581                    data: "",
2582                    line: 1,
2583                    col: 24,
2584                    offset: 23
2585                }
2586            );
2587        }
2588
2589        #[test]
2590        fn via_opts_attribute() {
2591            let p = Parser::default();
2592
2593            let mi = crate::attributes::Attrlist::parse(
2594                crate::Span::new("foo=bar,opts=option1"),
2595                &p,
2596                AttrlistContext::Inline,
2597            )
2598            .unwrap_if_no_warnings();
2599
2600            assert_eq!(
2601                mi.item,
2602                Attrlist {
2603                    attributes: &[
2604                        ElementAttribute {
2605                            name: Some("foo"),
2606                            shorthand_items: &[],
2607                            value: "bar"
2608                        },
2609                        ElementAttribute {
2610                            name: Some("opts"),
2611                            shorthand_items: &[],
2612                            value: "option1"
2613                        },
2614                    ],
2615                    anchor: None,
2616                    source: Span {
2617                        data: "foo=bar,opts=option1",
2618                        line: 1,
2619                        col: 1,
2620                        offset: 0
2621                    }
2622                }
2623            );
2624
2625            assert_eq!(
2626                mi.item.named_attribute("foo").unwrap(),
2627                ElementAttribute {
2628                    name: Some("foo"),
2629                    shorthand_items: &[],
2630                    value: "bar"
2631                }
2632            );
2633
2634            assert_eq!(
2635                mi.item.named_attribute("opts").unwrap(),
2636                ElementAttribute {
2637                    name: Some("opts"),
2638                    shorthand_items: &[],
2639                    value: "option1"
2640                }
2641            );
2642
2643            let options = mi.item.options();
2644            let mut options = options.iter();
2645            assert_eq!(options.next().unwrap(), &"option1");
2646            assert!(options.next().is_none());
2647
2648            assert!(!mi.item.has_option("option"));
2649            assert!(mi.item.has_option("option1"));
2650            assert!(!mi.item.has_option("option2"));
2651
2652            assert_eq!(
2653                mi.after,
2654                Span {
2655                    data: "",
2656                    line: 1,
2657                    col: 21,
2658                    offset: 20
2659                }
2660            );
2661        }
2662
2663        #[test]
2664        fn multiple_options_via_named_attribute() {
2665            let p = Parser::default();
2666
2667            let mi = crate::attributes::Attrlist::parse(
2668                crate::Span::new("foo=bar,options=\"option1,option2,option3\""),
2669                &p,
2670                AttrlistContext::Inline,
2671            )
2672            .unwrap_if_no_warnings();
2673
2674            assert_eq!(
2675                mi.item,
2676                Attrlist {
2677                    attributes: &[
2678                        ElementAttribute {
2679                            name: Some("foo"),
2680                            shorthand_items: &[],
2681                            value: "bar"
2682                        },
2683                        ElementAttribute {
2684                            name: Some("options"),
2685                            shorthand_items: &[],
2686                            value: "option1,option2,option3"
2687                        },
2688                    ],
2689                    anchor: None,
2690                    source: Span {
2691                        data: "foo=bar,options=\"option1,option2,option3\"",
2692                        line: 1,
2693                        col: 1,
2694                        offset: 0
2695                    }
2696                }
2697            );
2698
2699            assert_eq!(
2700                mi.item.named_attribute("foo").unwrap(),
2701                ElementAttribute {
2702                    name: Some("foo"),
2703                    shorthand_items: &[],
2704                    value: "bar"
2705                }
2706            );
2707
2708            assert_eq!(
2709                mi.item.named_attribute("options").unwrap(),
2710                ElementAttribute {
2711                    name: Some("options"),
2712                    shorthand_items: &[],
2713                    value: "option1,option2,option3"
2714                }
2715            );
2716
2717            let options = mi.item.options();
2718            let mut options = options.iter();
2719            assert_eq!(options.next().unwrap(), &"option1");
2720            assert_eq!(options.next().unwrap(), &"option2");
2721            assert_eq!(options.next().unwrap(), &"option3");
2722            assert!(options.next().is_none());
2723
2724            assert!(mi.item.has_option("option1"));
2725            assert!(mi.item.has_option("option2"));
2726            assert!(mi.item.has_option("option3"));
2727            assert!(!mi.item.has_option("option4"));
2728
2729            assert_eq!(
2730                mi.after,
2731                Span {
2732                    data: "",
2733                    line: 1,
2734                    col: 42,
2735                    offset: 41
2736                }
2737            );
2738        }
2739
2740        #[test]
2741        fn shorthand_option_and_named_attribute_option() {
2742            let p = Parser::default();
2743
2744            let mi = crate::attributes::Attrlist::parse(
2745                crate::Span::new("#foo%sh1%sh2,options=\"na1,na2,na3\""),
2746                &p,
2747                AttrlistContext::Inline,
2748            )
2749            .unwrap_if_no_warnings();
2750
2751            assert_eq!(
2752                mi.item,
2753                Attrlist {
2754                    attributes: &[
2755                        ElementAttribute {
2756                            name: None,
2757                            shorthand_items: &["#foo", "%sh1", "%sh2"],
2758                            value: "#foo%sh1%sh2"
2759                        },
2760                        ElementAttribute {
2761                            name: Some("options"),
2762                            shorthand_items: &[],
2763                            value: "na1,na2,na3"
2764                        },
2765                    ],
2766                    anchor: None,
2767                    source: Span {
2768                        data: "#foo%sh1%sh2,options=\"na1,na2,na3\"",
2769                        line: 1,
2770                        col: 1,
2771                        offset: 0
2772                    }
2773                }
2774            );
2775
2776            assert!(mi.item.named_attribute("foo").is_none(),);
2777
2778            assert_eq!(
2779                mi.item.named_attribute("options").unwrap(),
2780                ElementAttribute {
2781                    name: Some("options"),
2782                    shorthand_items: &[],
2783                    value: "na1,na2,na3"
2784                }
2785            );
2786
2787            let options = mi.item.options();
2788            let mut options = options.iter();
2789            assert_eq!(options.next().unwrap(), &"sh1");
2790            assert_eq!(options.next().unwrap(), &"sh2");
2791            assert_eq!(options.next().unwrap(), &"na1");
2792            assert_eq!(options.next().unwrap(), &"na2");
2793            assert_eq!(options.next().unwrap(), &"na3");
2794            assert!(options.next().is_none(),);
2795
2796            assert!(mi.item.has_option("sh1"));
2797            assert!(mi.item.has_option("sh2"));
2798            assert!(!mi.item.has_option("sh3"));
2799            assert!(mi.item.has_option("na1"));
2800            assert!(mi.item.has_option("na2"));
2801            assert!(mi.item.has_option("na3"));
2802            assert!(!mi.item.has_option("na4"));
2803
2804            assert_eq!(
2805                mi.after,
2806                Span {
2807                    data: "",
2808                    line: 1,
2809                    col: 35,
2810                    offset: 34
2811                }
2812            );
2813        }
2814
2815        #[test]
2816        fn shorthand_only_first_attribute() {
2817            let p = Parser::default();
2818
2819            let mi = crate::attributes::Attrlist::parse(
2820                crate::Span::new("foo,blah%option"),
2821                &p,
2822                AttrlistContext::Inline,
2823            )
2824            .unwrap_if_no_warnings();
2825
2826            assert_eq!(
2827                mi.item,
2828                Attrlist {
2829                    attributes: &[
2830                        ElementAttribute {
2831                            name: None,
2832                            shorthand_items: &["foo"],
2833                            value: "foo"
2834                        },
2835                        ElementAttribute {
2836                            name: None,
2837                            shorthand_items: &[],
2838                            value: "blah%option"
2839                        },
2840                    ],
2841                    anchor: None,
2842                    source: Span {
2843                        data: "foo,blah%option",
2844                        line: 1,
2845                        col: 1,
2846                        offset: 0
2847                    }
2848                }
2849            );
2850
2851            let options = mi.item.options();
2852            assert_eq!(options.iter().len(), 0);
2853
2854            assert!(!mi.item.has_option("option"));
2855
2856            assert_eq!(
2857                mi.after,
2858                Span {
2859                    data: "",
2860                    line: 1,
2861                    col: 16,
2862                    offset: 15
2863                }
2864            );
2865        }
2866    }
2867
2868    #[test]
2869    fn block_style() {
2870        let p = Parser::default();
2871
2872        let mi = crate::attributes::Attrlist::parse(
2873            crate::Span::new("blah#goals"),
2874            &p,
2875            AttrlistContext::Inline,
2876        )
2877        .unwrap_if_no_warnings();
2878
2879        let attrlist = mi.item;
2880        assert_eq!(attrlist.block_style().unwrap(), "blah");
2881    }
2882
2883    #[test]
2884    fn err_double_comma() {
2885        let p = Parser::default();
2886
2887        let maw = crate::attributes::Attrlist::parse(
2888            crate::Span::new("alt=Sunset,width=300,,height=400"),
2889            &p,
2890            AttrlistContext::Inline,
2891        );
2892
2893        let mi = maw.item.clone();
2894
2895        assert_eq!(
2896            mi.item,
2897            Attrlist {
2898                attributes: &[
2899                    ElementAttribute {
2900                        name: Some("alt"),
2901                        shorthand_items: &[],
2902                        value: "Sunset"
2903                    },
2904                    ElementAttribute {
2905                        name: Some("width"),
2906                        shorthand_items: &[],
2907                        value: "300"
2908                    },
2909                    ElementAttribute {
2910                        name: Some("height"),
2911                        shorthand_items: &[],
2912                        value: "400"
2913                    },
2914                ],
2915                anchor: None,
2916                source: Span {
2917                    data: "alt=Sunset,width=300,,height=400",
2918                    line: 1,
2919                    col: 1,
2920                    offset: 0,
2921                }
2922            }
2923        );
2924
2925        assert_eq!(
2926            mi.after,
2927            Span {
2928                data: "",
2929                line: 1,
2930                col: 33,
2931                offset: 32,
2932            }
2933        );
2934
2935        assert_eq!(
2936            maw.warnings,
2937            vec![Warning {
2938                source: Span {
2939                    data: "alt=Sunset,width=300,,height=400",
2940                    line: 1,
2941                    col: 1,
2942                    offset: 0,
2943                },
2944                warning: WarningType::EmptyAttributeValue,
2945            }]
2946        );
2947    }
2948
2949    #[test]
2950    fn applies_attribute_substitution_before_parsing() {
2951        let p = Parser::default().with_intrinsic_attribute(
2952            "sunset_dimensions",
2953            "300,400",
2954            ModificationContext::Anywhere,
2955        );
2956
2957        let mi = crate::attributes::Attrlist::parse(
2958            crate::Span::new("Sunset,{sunset_dimensions}"),
2959            &p,
2960            AttrlistContext::Inline,
2961        )
2962        .unwrap_if_no_warnings();
2963
2964        assert_eq!(
2965            mi.item,
2966            Attrlist {
2967                attributes: &[
2968                    ElementAttribute {
2969                        name: None,
2970                        shorthand_items: &["Sunset"],
2971                        value: "Sunset"
2972                    },
2973                    ElementAttribute {
2974                        name: None,
2975                        shorthand_items: &[],
2976                        value: "300"
2977                    },
2978                    ElementAttribute {
2979                        name: None,
2980                        shorthand_items: &[],
2981                        value: "400"
2982                    }
2983                ],
2984                anchor: None,
2985                source: Span {
2986                    data: "Sunset,{sunset_dimensions}",
2987                    line: 1,
2988                    col: 1,
2989                    offset: 0
2990                }
2991            }
2992        );
2993
2994        assert!(mi.item.named_attribute("foo").is_none());
2995        assert!(mi.item.nth_attribute(0).is_none());
2996        assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
2997
2998        assert!(mi.item.id().is_none());
2999        assert!(mi.item.roles().is_empty());
3000        assert_eq!(mi.item.block_style().unwrap(), "Sunset");
3001
3002        assert_eq!(
3003            mi.item.nth_attribute(1).unwrap(),
3004            ElementAttribute {
3005                name: None,
3006                shorthand_items: &["Sunset"],
3007                value: "Sunset"
3008            }
3009        );
3010
3011        assert_eq!(
3012            mi.item.named_or_positional_attribute("alt", 1).unwrap(),
3013            ElementAttribute {
3014                name: None,
3015                shorthand_items: &["Sunset"],
3016                value: "Sunset"
3017            }
3018        );
3019
3020        assert_eq!(
3021            mi.item.nth_attribute(2).unwrap(),
3022            ElementAttribute {
3023                name: None,
3024                shorthand_items: &[],
3025                value: "300"
3026            }
3027        );
3028
3029        assert_eq!(
3030            mi.item.named_or_positional_attribute("width", 2).unwrap(),
3031            ElementAttribute {
3032                name: None,
3033                shorthand_items: &[],
3034                value: "300"
3035            }
3036        );
3037
3038        assert_eq!(
3039            mi.item.nth_attribute(3).unwrap(),
3040            ElementAttribute {
3041                name: None,
3042                shorthand_items: &[],
3043                value: "400"
3044            }
3045        );
3046
3047        assert_eq!(
3048            mi.item.named_or_positional_attribute("height", 3).unwrap(),
3049            ElementAttribute {
3050                name: None,
3051                shorthand_items: &[],
3052                value: "400"
3053            }
3054        );
3055
3056        assert!(mi.item.nth_attribute(4).is_none());
3057        assert!(mi.item.named_or_positional_attribute("height", 4).is_none());
3058        assert!(mi.item.nth_attribute(42).is_none());
3059
3060        assert_eq!(
3061            mi.item.span(),
3062            Span {
3063                data: "Sunset,{sunset_dimensions}",
3064                line: 1,
3065                col: 1,
3066                offset: 0,
3067            }
3068        );
3069
3070        assert_eq!(
3071            mi.after,
3072            Span {
3073                data: "",
3074                line: 1,
3075                col: 27,
3076                offset: 26,
3077            }
3078        );
3079    }
3080
3081    #[test]
3082    fn ignores_unknown_attribute_when_applying_attribution_substitution() {
3083        let p = Parser::default().with_intrinsic_attribute(
3084            "sunset_dimensions",
3085            "300,400",
3086            ModificationContext::Anywhere,
3087        );
3088
3089        let mi = crate::attributes::Attrlist::parse(
3090            crate::Span::new("Sunset,{not_sunset_dimensions}"),
3091            &p,
3092            AttrlistContext::Inline,
3093        )
3094        .unwrap_if_no_warnings();
3095
3096        assert_eq!(
3097            mi.item,
3098            Attrlist {
3099                attributes: &[
3100                    ElementAttribute {
3101                        name: None,
3102                        shorthand_items: &["Sunset"],
3103                        value: "Sunset"
3104                    },
3105                    ElementAttribute {
3106                        name: None,
3107                        shorthand_items: &[],
3108                        value: "{not_sunset_dimensions}"
3109                    },
3110                ],
3111                anchor: None,
3112                source: Span {
3113                    data: "Sunset,{not_sunset_dimensions}",
3114                    line: 1,
3115                    col: 1,
3116                    offset: 0
3117                }
3118            }
3119        );
3120
3121        assert!(mi.item.named_attribute("foo").is_none());
3122        assert!(mi.item.nth_attribute(0).is_none());
3123        assert!(mi.item.named_or_positional_attribute("foo", 0).is_none());
3124
3125        assert!(mi.item.id().is_none());
3126        assert!(mi.item.roles().is_empty());
3127        assert_eq!(mi.item.block_style().unwrap(), "Sunset");
3128
3129        assert_eq!(
3130            mi.item.nth_attribute(1).unwrap(),
3131            ElementAttribute {
3132                name: None,
3133                shorthand_items: &["Sunset"],
3134                value: "Sunset"
3135            }
3136        );
3137
3138        assert_eq!(
3139            mi.item.named_or_positional_attribute("alt", 1).unwrap(),
3140            ElementAttribute {
3141                name: None,
3142                shorthand_items: &["Sunset"],
3143                value: "Sunset"
3144            }
3145        );
3146
3147        assert_eq!(
3148            mi.item.nth_attribute(2).unwrap(),
3149            ElementAttribute {
3150                name: None,
3151                shorthand_items: &[],
3152                value: "{not_sunset_dimensions}"
3153            }
3154        );
3155
3156        assert_eq!(
3157            mi.item.named_or_positional_attribute("width", 2).unwrap(),
3158            ElementAttribute {
3159                name: None,
3160                shorthand_items: &[],
3161                value: "{not_sunset_dimensions}"
3162            }
3163        );
3164
3165        assert!(mi.item.nth_attribute(3).is_none());
3166        assert!(mi.item.named_or_positional_attribute("height", 3).is_none());
3167        assert!(mi.item.nth_attribute(42).is_none());
3168
3169        assert_eq!(
3170            mi.item.span(),
3171            Span {
3172                data: "Sunset,{not_sunset_dimensions}",
3173                line: 1,
3174                col: 1,
3175                offset: 0,
3176            }
3177        );
3178
3179        assert_eq!(
3180            mi.after,
3181            Span {
3182                data: "",
3183                line: 1,
3184                col: 31,
3185                offset: 30,
3186            }
3187        );
3188    }
3189
3190    #[test]
3191    fn impl_debug() {
3192        let p = Parser::default();
3193
3194        let mi = crate::attributes::Attrlist::parse(
3195            crate::Span::new("Sunset,300,400"),
3196            &p,
3197            AttrlistContext::Inline,
3198        )
3199        .unwrap_if_no_warnings();
3200
3201        let attrlist = mi.item;
3202
3203        assert_eq!(
3204            format!("{attrlist:#?}"),
3205            r#"Attrlist {
3206    attributes: &[
3207        ElementAttribute {
3208            name: None,
3209            value: "Sunset",
3210            shorthand_item_indices: [
3211                0,
3212            ],
3213        },
3214        ElementAttribute {
3215            name: None,
3216            value: "300",
3217            shorthand_item_indices: [],
3218        },
3219        ElementAttribute {
3220            name: None,
3221            value: "400",
3222            shorthand_item_indices: [],
3223        },
3224    ],
3225    anchor: None,
3226    source: Span {
3227        data: "Sunset,300,400",
3228        line: 1,
3229        col: 1,
3230        offset: 0,
3231    },
3232}"#
3233        );
3234    }
3235}