asciidoc_parser/attributes/
attrlist.rs

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