asciidoc_parser/attributes/
element_attribute.rs

1use crate::{
2    Parser, Span,
3    attributes::AttrlistContext,
4    content::{Content, SubstitutionGroup},
5    span::MatchedItem,
6    strings::CowStr,
7    warnings::WarningType,
8};
9
10/// This struct represents a single element attribute.
11///
12/// Element attributes define the built-in and user-defined settings and
13/// metadata that can be applied to an individual block element or inline
14/// element in a document (including macros). Although the include directive is
15/// not technically an element, element attributes can also be defined on an
16/// include directive.
17#[derive(Clone, Debug, Eq, PartialEq)]
18pub struct ElementAttribute<'src> {
19    name: Option<CowStr<'src>>,
20    value: CowStr<'src>,
21    shorthand_item_indices: Vec<usize>,
22}
23
24impl<'src> ElementAttribute<'src> {
25    pub(crate) fn parse(
26        source_text: &CowStr<'src>,
27        start_index: usize,
28        parser: &Parser,
29        mut parse_shorthand: ParseShorthand,
30        attrlist_context: AttrlistContext,
31    ) -> (Self, usize, Vec<WarningType>) {
32        let mut warnings: Vec<WarningType> = vec![];
33
34        let (name, value, shorthand_item_indices, offset) = {
35            let mut source = Span::new(source_text.as_ref());
36            source = source.discard(start_index);
37
38            let (name, after): (Option<Span<'_>>, Span) = match source.take_attr_name() {
39                Some(name) => {
40                    let space = name.after.take_whitespace_with_newline();
41                    match space.after.take_prefix("=") {
42                        Some(equals) => {
43                            let space = equals.after.take_whitespace_with_newline();
44                            if space.after.is_empty() || space.after.starts_with(',') {
45                                // TO DO: Is this a warning? Possible spec ambiguity.
46                                (None, source)
47                            } else {
48                                (Some(name.item), space.after)
49                            }
50                        }
51                        None => (None, source),
52                    }
53                }
54                None => (None, source),
55            };
56
57            let after = after.take_whitespace_with_newline().after;
58            let first_char = after.data().chars().next();
59
60            let value = match first_char {
61                Some('\'') | Some('"') => match after.take_quoted_string() {
62                    Some(v) => {
63                        parse_shorthand = ParseShorthand(false);
64                        v
65                    }
66                    None => {
67                        warnings.push(WarningType::AttributeValueMissingTerminatingQuote);
68                        after.take_while(|c| c != ',').trim_item_trailing_spaces()
69                    }
70                },
71                _ => after.take_while(|c| c != ',').trim_item_trailing_spaces(),
72            };
73
74            let after = value.after;
75            let mut value = cowstr_from_source_and_span(source_text, &value.item);
76
77            if let Some(first) = first_char
78                && (first == '\'' || first == '\"')
79            {
80                let escaped_quote = format!("\\{first}");
81                let mut new_value = value.replace(&escaped_quote, &first.to_string());
82
83                if first == '\'' && attrlist_context == AttrlistContext::Block {
84                    let span = Span::new(&new_value);
85                    let mut content = Content::from(span);
86                    SubstitutionGroup::Normal.apply(&mut content, parser, None);
87
88                    if content.rendered.as_ref() != new_value {
89                        new_value = content.rendered.to_string();
90                    }
91                }
92
93                if new_value != *value {
94                    value = CowStr::from(new_value);
95                }
96            }
97
98            let shorthand_item_indices = if name.is_none() && parse_shorthand.0 {
99                parse_shorthand_items(&value, &mut warnings)
100            } else {
101                vec![]
102            };
103
104            let name = name.map(|name| cowstr_from_source_and_span(source_text, &name));
105
106            (name, value, shorthand_item_indices, after.byte_offset())
107        };
108
109        (
110            Self {
111                name,
112                value,
113                shorthand_item_indices,
114            },
115            offset,
116            warnings,
117        )
118    }
119
120    /// Return the attribute name, if one was found`.
121    pub fn name(&'src self) -> Option<&'src str> {
122        if let Some(ref name) = self.name {
123            Some(name.as_ref())
124        } else {
125            None
126        }
127    }
128
129    /// Return the shorthand items, if applicable.
130    ///
131    /// Shorthand items are only parsed for certain element attributes. If this
132    /// attribute is not of the appropriate kind, this will return an empty
133    /// list.
134    pub fn shorthand_items(&'src self) -> Vec<&'src str> {
135        let mut result = vec![];
136        let value = self.value.as_ref();
137
138        let mut iter = self.shorthand_item_indices.iter().peekable();
139
140        loop {
141            let Some(curr) = iter.next() else { break };
142            let mut next_item = if let Some(next) = iter.peek() {
143                &value[*curr..**next]
144            } else {
145                &value[*curr..]
146            };
147
148            if next_item == "#" || next_item == "." || next_item == "%" {
149                continue;
150            }
151
152            next_item = next_item.trim_end();
153
154            if !next_item.is_empty() {
155                result.push(next_item);
156            }
157        }
158
159        result
160    }
161
162    /// Return the block style name from shorthand syntax.
163    pub fn block_style(&'src self) -> Option<&'src str> {
164        self.shorthand_items()
165            .first()
166            .filter(|v| !v.chars().any(is_shorthand_delimiter))
167            .cloned()
168    }
169
170    /// Return the ID attribute from shorthand syntax.
171    ///
172    /// If multiple ID attributes were specified, only the first
173    /// match is returned. (Multiple IDs are not supported.)
174    ///
175    /// You can assign an ID to a block using the shorthand syntax, the longhand
176    /// syntax, or a legacy block anchor.
177    ///
178    /// In the shorthand syntax, you prefix the name with a hash (`#`) in the
179    /// first position attribute:
180    ///
181    /// ```asciidoc
182    /// [#goals]
183    /// * Goal 1
184    /// * Goal 2
185    /// ```
186    ///
187    /// In the longhand syntax, you use a standard named attribute:
188    ///
189    /// ```asciidoc
190    /// [id=goals]
191    /// * Goal 1
192    /// * Goal 2
193    /// ```
194    ///
195    /// In the legacy block anchor syntax, you surround the name with double
196    /// square brackets:
197    ///
198    /// ```asciidoc
199    /// [[goals]]
200    /// * Goal 1
201    /// * Goal 2
202    /// ```
203    pub fn id(&'src self) -> Option<&'src str> {
204        self.shorthand_items()
205            .iter()
206            .find(|v| v.starts_with('#'))
207            .map(|v| &v[1..])
208    }
209
210    /// Return any role attributes that were found in shorthand syntax.
211    ///     
212    /// You can assign one or more roles to blocks and most inline elements
213    /// using the `role` attribute. The `role` attribute is a [named attribute].
214    /// Even though the attribute name is singular, it may contain multiple
215    /// (space-separated) roles. Roles may also be defined using a shorthand
216    /// (dot-prefixed) syntax.
217    ///
218    /// A role:
219    /// 1. adds additional semantics to an element
220    /// 2. can be used to apply additional styling to a group of elements (e.g.,
221    ///    via a CSS class selector)
222    /// 3. may activate additional behavior if recognized by the converter
223    ///
224    /// **TIP:** The `role` attribute in AsciiDoc always get mapped to the
225    /// `class` attribute in the HTML output. In other words, role names are
226    /// synonymous with HTML class names, thus allowing output elements to be
227    /// identified and styled in CSS using class selectors (e.g.,
228    /// `sidebarblock.role1`).
229    ///
230    /// [named attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/#named
231    pub fn roles(&'src self) -> Vec<&'src str> {
232        self.shorthand_items()
233            .iter()
234            .filter(|span| span.starts_with('.'))
235            .map(|span| &span[1..])
236            .collect()
237    }
238
239    /// Return any option attributes that were found in shorthand syntax.
240    ///     
241    /// The `options` attribute (often abbreviated as `opts`) is a versatile
242    /// [named attribute] that can be assigned one or more values. It can be
243    /// defined globally as document attribute as well as a block attribute on
244    /// an individual block.
245    ///
246    /// There is no strict schema for options. Any options which are not
247    /// recognized are ignored.
248    ///
249    /// You can assign one or more options to a block using the shorthand or
250    /// formal syntax for the options attribute.
251    ///
252    /// # Shorthand options syntax for blocks
253    ///
254    /// To assign an option to a block, prefix the value with a percent sign
255    /// (`%`) in an attribute list. The percent sign implicitly sets the
256    /// `options` attribute.
257    ///
258    /// ## Example 1: Sidebar block with an option assigned using the shorthand dot
259    ///
260    /// ```asciidoc
261    /// [%option]
262    /// ****
263    /// This is a sidebar with an option assigned to it, named option.
264    /// ****
265    /// ```
266    ///
267    /// You can assign multiple options to a block by prest
268    /// fixing each value with
269    /// a percent sign (`%`).
270    ///
271    /// ## Example 2: Sidebar with two options assigned using the shorthand dot
272    /// ```asciidoc
273    /// [%option1%option2]
274    /// ****
275    /// This is a sidebar with two options assigned to it, named option1 and option2.
276    /// ****
277    /// ```
278    ///
279    /// # Formal options syntax for blocks
280    ///
281    /// Explicitly set `options` or `opts`, followed by the equals sign (`=`),
282    /// and then the value in an attribute list.
283    ///
284    /// ## Example 3. Sidebar block with an option assigned using the formal syntax
285    /// ```asciidoc
286    /// [opts=option]
287    /// ****
288    /// This is a sidebar with an option assigned to it, named option.
289    /// ****
290    /// ```
291    ///
292    /// Separate multiple option values with commas (`,`).
293    ///
294    /// ## Example 4. Sidebar with three options assigned using the formal syntax
295    /// ```asciidoc
296    /// [opts="option1,option2"]
297    /// ****
298    /// This is a sidebar with two options assigned to it, option1 and option2.
299    /// ****
300    /// ```
301    ///
302    /// [named attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/#named
303    pub fn options(&'src self) -> Vec<&'src str> {
304        self.shorthand_items()
305            .iter()
306            .filter(|v| v.starts_with('%'))
307            .map(|v| &v[1..])
308            .collect()
309    }
310
311    /// Return the attribute's value.
312    ///
313    /// Note that this value will have had special characters and attribute
314    /// value replacements applied to it.
315    pub fn value(&'src self) -> &'src str {
316        self.value.as_ref()
317    }
318}
319
320fn parse_shorthand_items(source: &str, warnings: &mut Vec<WarningType>) -> Vec<usize> {
321    let mut shorthand_item_indices: Vec<usize> = vec![];
322    let mut span = Span::new(source);
323
324    // Look for block style selector.
325    if let Some(block_style_pr) = span.split_at_match_non_empty(is_shorthand_delimiter) {
326        shorthand_item_indices.push(block_style_pr.item.discard_whitespace().byte_offset());
327
328        span = block_style_pr.after;
329    }
330
331    while !span.is_empty() {
332        // Assumption: First character is a delimiter.
333        let after_delimiter = span.discard(1);
334
335        match after_delimiter.position(is_shorthand_delimiter) {
336            None => {
337                if after_delimiter.is_empty() {
338                    warnings.push(WarningType::EmptyShorthandItem);
339                    shorthand_item_indices.push(span.byte_offset());
340                    span = after_delimiter;
341                } else {
342                    shorthand_item_indices.push(span.byte_offset());
343                    span = span.discard_all();
344                }
345            }
346
347            Some(0) => {
348                shorthand_item_indices.push(span.byte_offset());
349                warnings.push(WarningType::EmptyShorthandItem);
350                span = after_delimiter;
351            }
352
353            Some(index) => {
354                let mi: MatchedItem<Span> = span.into_parse_result(index + 1);
355                shorthand_item_indices.push(span.byte_offset());
356                span = mi.after;
357            }
358        }
359    }
360
361    shorthand_item_indices
362}
363
364fn is_shorthand_delimiter(c: char) -> bool {
365    c == '#' || c == '%' || c == '.'
366}
367
368#[derive(Clone, Debug)]
369pub(crate) struct ParseShorthand(pub bool);
370
371fn cowstr_from_source_and_span<'src>(source: &CowStr<'src>, span: &Span<'_>) -> CowStr<'src> {
372    if let CowStr::Borrowed(source) = source {
373        let borrowed: Span<'src> = Span::new(source)
374            .discard(span.byte_offset())
375            .slice_to(..span.len());
376
377        CowStr::Borrowed(borrowed.data())
378    } else {
379        CowStr::from(span.data().to_string())
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    #![allow(clippy::unwrap_used)]
386    use pretty_assertions_sorted::assert_eq;
387
388    use crate::{
389        Parser,
390        attributes::{AttrlistContext, element_attribute::ParseShorthand},
391        strings::CowStr,
392        tests::prelude::*,
393    };
394
395    #[test]
396    fn impl_clone() {
397        // Silly test to mark the #[derive(...)] line as covered.
398        let p = Parser::default();
399
400        let b1 = crate::attributes::ElementAttribute::parse(
401            &CowStr::from("abc"),
402            0,
403            &p,
404            ParseShorthand(false),
405            AttrlistContext::Inline,
406        )
407        .0;
408
409        let b2 = b1.clone();
410
411        assert_eq!(b1, b2);
412    }
413
414    #[test]
415    fn empty_source() {
416        let p = Parser::default();
417
418        let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
419            &CowStr::from(""),
420            0,
421            &p,
422            ParseShorthand(false),
423            AttrlistContext::Inline,
424        );
425
426        assert!(warning_types.is_empty());
427
428        assert_eq!(
429            element_attr,
430            ElementAttribute {
431                name: None,
432                shorthand_items: &[],
433                value: "",
434            }
435        );
436
437        assert!(element_attr.name().is_none());
438        assert!(element_attr.block_style().is_none());
439        assert!(element_attr.id().is_none());
440        assert!(element_attr.roles().is_empty());
441        assert!(element_attr.options().is_empty());
442
443        assert_eq!(offset, 0);
444    }
445
446    #[test]
447    fn only_spaces() {
448        let p = Parser::default();
449
450        let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
451            &CowStr::from("   "),
452            0,
453            &p,
454            ParseShorthand(false),
455            AttrlistContext::Inline,
456        );
457
458        assert!(warning_types.is_empty());
459
460        assert_eq!(
461            element_attr,
462            ElementAttribute {
463                name: None,
464                shorthand_items: &[],
465                value: "",
466            }
467        );
468
469        assert!(element_attr.name().is_none());
470        assert!(element_attr.block_style().is_none());
471        assert!(element_attr.id().is_none());
472        assert!(element_attr.roles().is_empty());
473        assert!(element_attr.options().is_empty());
474
475        assert_eq!(offset, 3);
476    }
477
478    #[test]
479    fn unquoted_and_unnamed_value() {
480        let p = Parser::default();
481
482        let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
483            &CowStr::from("abc"),
484            0,
485            &p,
486            ParseShorthand(false),
487            AttrlistContext::Inline,
488        );
489
490        assert!(warning_types.is_empty());
491
492        assert_eq!(
493            element_attr,
494            ElementAttribute {
495                name: None,
496                shorthand_items: &[],
497                value: "abc",
498            }
499        );
500
501        assert!(element_attr.name().is_none());
502        assert!(element_attr.block_style().is_none());
503        assert!(element_attr.id().is_none());
504        assert!(element_attr.roles().is_empty());
505        assert!(element_attr.options().is_empty());
506
507        assert_eq!(offset, 3);
508    }
509
510    #[test]
511    fn unquoted_stops_at_comma() {
512        let p = Parser::default();
513
514        let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
515            &CowStr::from("abc,def"),
516            0,
517            &p,
518            ParseShorthand(false),
519            AttrlistContext::Inline,
520        );
521
522        assert!(warning_types.is_empty());
523
524        assert_eq!(
525            element_attr,
526            ElementAttribute {
527                name: None,
528                shorthand_items: &[],
529                value: "abc",
530            }
531        );
532
533        assert!(element_attr.name().is_none());
534        assert!(element_attr.block_style().is_none());
535        assert!(element_attr.id().is_none());
536        assert!(element_attr.roles().is_empty());
537        assert!(element_attr.options().is_empty());
538
539        assert_eq!(offset, 3);
540    }
541
542    mod quoted_string {
543        use pretty_assertions_sorted::assert_eq;
544
545        use crate::{
546            Parser,
547            attributes::{AttrlistContext, element_attribute::ParseShorthand},
548            parser::ModificationContext,
549            strings::CowStr,
550            tests::prelude::*,
551            warnings::WarningType,
552        };
553
554        #[test]
555        fn err_unterminated_double_quote() {
556            let p = Parser::default();
557
558            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
559                &CowStr::from("\"xyz"),
560                0,
561                &p,
562                ParseShorthand(false),
563                AttrlistContext::Inline,
564            );
565
566            assert_eq!(
567                element_attr,
568                ElementAttribute {
569                    name: None,
570                    shorthand_items: &[],
571                    value: "\"xyz"
572                }
573            );
574
575            assert!(element_attr.name().is_none());
576            assert!(element_attr.block_style().is_none());
577            assert!(element_attr.id().is_none());
578            assert!(element_attr.roles().is_empty());
579            assert!(element_attr.options().is_empty());
580
581            assert_eq!(offset, 4);
582
583            assert_eq!(
584                warning_types,
585                vec![WarningType::AttributeValueMissingTerminatingQuote]
586            );
587        }
588
589        #[test]
590        fn err_unterminated_double_quote_ends_at_comma() {
591            let p = Parser::default();
592
593            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
594                &CowStr::from("\"xyz,abc"),
595                0,
596                &p,
597                ParseShorthand(false),
598                AttrlistContext::Inline,
599            );
600
601            assert_eq!(
602                element_attr,
603                ElementAttribute {
604                    name: None,
605                    shorthand_items: &[],
606                    value: "\"xyz"
607                }
608            );
609
610            assert!(element_attr.name().is_none());
611            assert!(element_attr.block_style().is_none());
612            assert!(element_attr.id().is_none());
613            assert!(element_attr.roles().is_empty());
614            assert!(element_attr.options().is_empty());
615
616            assert_eq!(offset, 4);
617            assert_eq!(
618                warning_types,
619                vec![WarningType::AttributeValueMissingTerminatingQuote]
620            );
621        }
622
623        #[test]
624        fn double_quoted_string() {
625            let p = Parser::default();
626
627            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
628                &CowStr::from("\"abc\"def"),
629                0,
630                &p,
631                ParseShorthand(false),
632                AttrlistContext::Inline,
633            );
634
635            assert!(warning_types.is_empty());
636
637            assert_eq!(
638                element_attr,
639                ElementAttribute {
640                    name: None,
641                    shorthand_items: &[],
642                    value: "abc"
643                }
644            );
645
646            assert!(element_attr.name().is_none());
647            assert!(element_attr.block_style().is_none());
648            assert!(element_attr.id().is_none());
649            assert!(element_attr.roles().is_empty());
650            assert!(element_attr.options().is_empty());
651
652            assert_eq!(offset, 5);
653        }
654
655        #[test]
656        fn double_quoted_with_escape() {
657            let p = Parser::default();
658
659            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
660                &CowStr::from("\"a\\\"bc\"def"),
661                0,
662                &p,
663                ParseShorthand(false),
664                AttrlistContext::Inline,
665            );
666
667            assert!(warning_types.is_empty());
668
669            assert_eq!(
670                element_attr,
671                ElementAttribute {
672                    name: None,
673                    shorthand_items: &[],
674                    value: "a\"bc"
675                }
676            );
677
678            assert!(element_attr.name().is_none());
679            assert!(element_attr.block_style().is_none());
680            assert!(element_attr.id().is_none());
681            assert!(element_attr.roles().is_empty());
682            assert!(element_attr.options().is_empty());
683
684            assert_eq!(offset, 7);
685        }
686
687        #[test]
688        fn double_quoted_with_single_quote() {
689            let p = Parser::default();
690
691            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
692                &CowStr::from("\"a'bc\"def"),
693                0,
694                &p,
695                ParseShorthand(false),
696                AttrlistContext::Inline,
697            );
698
699            assert!(warning_types.is_empty());
700
701            assert_eq!(
702                element_attr,
703                ElementAttribute {
704                    name: None,
705                    shorthand_items: &[],
706                    value: "a'bc"
707                }
708            );
709
710            assert!(element_attr.name().is_none());
711            assert!(element_attr.block_style().is_none());
712            assert!(element_attr.id().is_none());
713            assert!(element_attr.roles().is_empty());
714            assert!(element_attr.options().is_empty());
715
716            assert_eq!(offset, 6);
717        }
718
719        #[test]
720        fn err_unterminated_single_quote() {
721            let p = Parser::default();
722
723            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
724                &CowStr::from("\'xyz"),
725                0,
726                &p,
727                ParseShorthand(false),
728                AttrlistContext::Inline,
729            );
730
731            assert_eq!(
732                element_attr,
733                ElementAttribute {
734                    name: None,
735                    shorthand_items: &[],
736                    value: "\'xyz"
737                }
738            );
739
740            assert!(element_attr.name().is_none());
741            assert!(element_attr.block_style().is_none());
742            assert!(element_attr.id().is_none());
743            assert!(element_attr.roles().is_empty());
744            assert!(element_attr.options().is_empty());
745
746            assert_eq!(offset, 4);
747
748            assert_eq!(
749                warning_types,
750                vec![WarningType::AttributeValueMissingTerminatingQuote]
751            );
752        }
753
754        #[test]
755        fn err_unterminated_single_quote_ends_at_comma() {
756            let p = Parser::default();
757
758            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
759                &CowStr::from("\'xyz,abc"),
760                0,
761                &p,
762                ParseShorthand(false),
763                AttrlistContext::Inline,
764            );
765
766            assert_eq!(
767                element_attr,
768                ElementAttribute {
769                    name: None,
770                    shorthand_items: &[],
771                    value: "\'xyz"
772                }
773            );
774
775            assert!(element_attr.name().is_none());
776            assert!(element_attr.block_style().is_none());
777            assert!(element_attr.id().is_none());
778            assert!(element_attr.roles().is_empty());
779            assert!(element_attr.options().is_empty());
780
781            assert_eq!(offset, 4);
782            assert_eq!(
783                warning_types,
784                vec![WarningType::AttributeValueMissingTerminatingQuote]
785            );
786        }
787
788        #[test]
789        fn single_quoted_string() {
790            let p = Parser::default();
791
792            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
793                &CowStr::from("'abc'def"),
794                0,
795                &p,
796                ParseShorthand(false),
797                AttrlistContext::Inline,
798            );
799
800            assert!(warning_types.is_empty());
801
802            assert_eq!(
803                element_attr,
804                ElementAttribute {
805                    name: None,
806                    shorthand_items: &[],
807                    value: "abc"
808                }
809            );
810
811            assert!(element_attr.name().is_none());
812            assert!(element_attr.block_style().is_none());
813            assert!(element_attr.id().is_none());
814            assert!(element_attr.roles().is_empty());
815            assert!(element_attr.options().is_empty());
816
817            assert_eq!(offset, 5);
818        }
819
820        #[test]
821        fn single_quoted_with_escape() {
822            let p = Parser::default();
823
824            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
825                &CowStr::from("'a\\'bc'def"),
826                0,
827                &p,
828                ParseShorthand(false),
829                AttrlistContext::Inline,
830            );
831
832            assert!(warning_types.is_empty());
833
834            assert_eq!(
835                element_attr,
836                ElementAttribute {
837                    name: None,
838                    shorthand_items: &[],
839                    value: "a'bc"
840                }
841            );
842
843            assert!(element_attr.name().is_none());
844            assert!(element_attr.block_style().is_none());
845            assert!(element_attr.id().is_none());
846            assert!(element_attr.roles().is_empty());
847            assert!(element_attr.options().is_empty());
848
849            assert_eq!(offset, 7);
850        }
851
852        #[test]
853        fn single_quoted_with_double_quote() {
854            let p = Parser::default();
855
856            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
857                &CowStr::from("'a\"bc'def"),
858                0,
859                &p,
860                ParseShorthand(false),
861                AttrlistContext::Inline,
862            );
863
864            assert!(warning_types.is_empty());
865
866            assert_eq!(
867                element_attr,
868                ElementAttribute {
869                    name: None,
870                    shorthand_items: &[],
871                    value: "a\"bc"
872                }
873            );
874
875            assert!(element_attr.name().is_none());
876            assert!(element_attr.block_style().is_none());
877            assert!(element_attr.id().is_none());
878            assert!(element_attr.roles().is_empty());
879            assert!(element_attr.options().is_empty());
880
881            assert_eq!(offset, 6);
882        }
883
884        #[test]
885        fn single_quoted_gets_substitions() {
886            let p = Parser::default().with_intrinsic_attribute(
887                "foo",
888                "bar",
889                ModificationContext::Anywhere,
890            );
891
892            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
893                &CowStr::from("'*abc* def {foo}'"),
894                0,
895                &p,
896                ParseShorthand(false),
897                AttrlistContext::Block,
898            );
899
900            assert!(warning_types.is_empty());
901
902            assert_eq!(
903                element_attr,
904                ElementAttribute {
905                    name: None,
906                    shorthand_items: &[],
907                    value: "<strong>abc</strong> def bar"
908                }
909            );
910
911            assert!(element_attr.name().is_none());
912            assert!(element_attr.block_style().is_none());
913            assert!(element_attr.id().is_none());
914            assert!(element_attr.roles().is_empty());
915            assert!(element_attr.options().is_empty());
916
917            assert_eq!(offset, 17);
918        }
919    }
920
921    mod named {
922        use pretty_assertions_sorted::assert_eq;
923
924        use crate::{
925            Parser,
926            attributes::{AttrlistContext, element_attribute::ParseShorthand},
927            strings::CowStr,
928            tests::prelude::*,
929        };
930
931        #[test]
932        fn simple_named_value() {
933            let p = Parser::default();
934
935            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
936                &CowStr::from("abc=def"),
937                0,
938                &p,
939                ParseShorthand(false),
940                AttrlistContext::Inline,
941            );
942
943            assert!(warning_types.is_empty());
944
945            assert_eq!(
946                element_attr,
947                ElementAttribute {
948                    name: Some("abc"),
949                    shorthand_items: &[],
950                    value: "def"
951                }
952            );
953
954            assert_eq!(element_attr.name().unwrap(), "abc");
955            assert!(element_attr.block_style().is_none());
956            assert!(element_attr.id().is_none());
957            assert!(element_attr.roles().is_empty());
958            assert!(element_attr.options().is_empty());
959
960            assert_eq!(offset, 7);
961        }
962
963        #[test]
964        fn ignores_spaces_around_equals() {
965            let p = Parser::default();
966
967            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
968                &CowStr::from("abc =  def"),
969                0,
970                &p,
971                ParseShorthand(false),
972                AttrlistContext::Inline,
973            );
974
975            assert!(warning_types.is_empty());
976
977            assert_eq!(
978                element_attr,
979                ElementAttribute {
980                    name: Some("abc"),
981                    shorthand_items: &[],
982                    value: "def"
983                }
984            );
985
986            assert_eq!(element_attr.name().unwrap(), "abc");
987
988            assert_eq!(offset, 10);
989        }
990
991        #[test]
992        fn numeric_name() {
993            let p = Parser::default();
994
995            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
996                &CowStr::from("94-x =def"),
997                0,
998                &p,
999                ParseShorthand(false),
1000                AttrlistContext::Inline,
1001            );
1002
1003            assert!(warning_types.is_empty());
1004
1005            assert_eq!(
1006                element_attr,
1007                ElementAttribute {
1008                    name: Some("94-x"),
1009                    shorthand_items: &[],
1010                    value: "def"
1011                }
1012            );
1013
1014            assert_eq!(element_attr.name().unwrap(), "94-x");
1015            assert!(element_attr.block_style().is_none());
1016            assert!(element_attr.id().is_none());
1017            assert!(element_attr.roles().is_empty());
1018            assert!(element_attr.options().is_empty());
1019
1020            assert_eq!(offset, 9);
1021        }
1022
1023        #[test]
1024        fn quoted_value() {
1025            let p = Parser::default();
1026
1027            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1028                &CowStr::from("abc='def'g"),
1029                0,
1030                &p,
1031                ParseShorthand(false),
1032                AttrlistContext::Inline,
1033            );
1034
1035            assert!(warning_types.is_empty());
1036
1037            assert_eq!(
1038                element_attr,
1039                ElementAttribute {
1040                    name: Some("abc"),
1041                    shorthand_items: &[],
1042                    value: "def"
1043                }
1044            );
1045
1046            assert_eq!(element_attr.name().unwrap(), "abc");
1047            assert!(element_attr.block_style().is_none());
1048            assert!(element_attr.id().is_none());
1049            assert!(element_attr.roles().is_empty());
1050            assert!(element_attr.options().is_empty());
1051
1052            assert_eq!(offset, 9);
1053        }
1054
1055        #[test]
1056        fn fallback_if_no_value() {
1057            let p = Parser::default();
1058
1059            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1060                &CowStr::from("abc="),
1061                0,
1062                &p,
1063                ParseShorthand(false),
1064                AttrlistContext::Inline,
1065            );
1066
1067            assert!(warning_types.is_empty());
1068
1069            assert_eq!(
1070                element_attr,
1071                ElementAttribute {
1072                    name: None,
1073                    shorthand_items: &[],
1074                    value: "abc="
1075                }
1076            );
1077
1078            assert!(element_attr.name().is_none());
1079            assert!(element_attr.block_style().is_none());
1080            assert!(element_attr.id().is_none());
1081            assert!(element_attr.roles().is_empty());
1082            assert!(element_attr.options().is_empty());
1083
1084            assert_eq!(offset, 4);
1085        }
1086
1087        #[test]
1088        fn fallback_if_immediate_comma() {
1089            let p = Parser::default();
1090
1091            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1092                &CowStr::from("abc=,def"),
1093                0,
1094                &p,
1095                ParseShorthand(false),
1096                AttrlistContext::Inline,
1097            );
1098
1099            assert!(warning_types.is_empty());
1100
1101            assert_eq!(
1102                element_attr,
1103                ElementAttribute {
1104                    name: None,
1105                    shorthand_items: &[],
1106                    value: "abc="
1107                }
1108            );
1109
1110            assert!(element_attr.name().is_none());
1111            assert!(element_attr.block_style().is_none());
1112            assert!(element_attr.id().is_none());
1113            assert!(element_attr.roles().is_empty());
1114            assert!(element_attr.options().is_empty());
1115
1116            assert_eq!(offset, 4);
1117        }
1118    }
1119
1120    mod parse_with_shorthand {
1121        use pretty_assertions_sorted::assert_eq;
1122
1123        use crate::{
1124            Parser,
1125            attributes::{AttrlistContext, element_attribute::ParseShorthand},
1126            strings::CowStr,
1127            tests::prelude::*,
1128            warnings::WarningType,
1129        };
1130
1131        #[test]
1132        fn block_style_only() {
1133            let p = Parser::default();
1134
1135            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1136                &CowStr::from("abc"),
1137                0,
1138                &p,
1139                ParseShorthand(true),
1140                AttrlistContext::Inline,
1141            );
1142
1143            assert!(warning_types.is_empty());
1144
1145            assert_eq!(
1146                element_attr,
1147                ElementAttribute {
1148                    name: None,
1149                    shorthand_items: &["abc"],
1150                    value: "abc"
1151                }
1152            );
1153
1154            assert!(element_attr.name().is_none());
1155            assert_eq!(element_attr.shorthand_items(), vec!["abc"]);
1156            assert_eq!(element_attr.block_style().unwrap(), "abc");
1157            assert!(element_attr.id().is_none());
1158            assert!(element_attr.roles().is_empty());
1159            assert!(element_attr.options().is_empty());
1160
1161            assert_eq!(offset, 3);
1162        }
1163
1164        #[test]
1165        fn ignore_if_named_attribute() {
1166            let p = Parser::default();
1167
1168            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1169                &CowStr::from("name=block_style#id"),
1170                0,
1171                &p,
1172                ParseShorthand(true),
1173                AttrlistContext::Inline,
1174            );
1175
1176            assert!(warning_types.is_empty());
1177
1178            assert_eq!(
1179                element_attr,
1180                ElementAttribute {
1181                    name: Some("name"),
1182                    shorthand_items: &[],
1183                    value: "block_style#id"
1184                }
1185            );
1186
1187            assert_eq!(element_attr.name().unwrap(), "name");
1188            assert!(element_attr.shorthand_items().is_empty());
1189            assert!(element_attr.block_style().is_none());
1190            assert!(element_attr.id().is_none());
1191            assert!(element_attr.roles().is_empty());
1192            assert!(element_attr.options().is_empty());
1193
1194            assert_eq!(offset, 19);
1195        }
1196
1197        #[test]
1198        fn error_empty_id() {
1199            let p = Parser::default();
1200
1201            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1202                &CowStr::from("abc#"),
1203                0,
1204                &p,
1205                ParseShorthand(true),
1206                AttrlistContext::Inline,
1207            );
1208
1209            assert_eq!(
1210                element_attr,
1211                ElementAttribute {
1212                    name: None,
1213                    shorthand_items: &["abc"],
1214                    value: "abc#"
1215                }
1216            );
1217
1218            assert_eq!(offset, 4);
1219            assert_eq!(warning_types, vec![WarningType::EmptyShorthandItem]);
1220        }
1221
1222        #[test]
1223        fn error_duplicate_delimiter() {
1224            let p = Parser::default();
1225
1226            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1227                &CowStr::from("abc##id"),
1228                0,
1229                &p,
1230                ParseShorthand(true),
1231                AttrlistContext::Inline,
1232            );
1233
1234            assert_eq!(
1235                element_attr,
1236                ElementAttribute {
1237                    name: None,
1238                    shorthand_items: &["abc", "#id"],
1239                    value: "abc##id"
1240                }
1241            );
1242
1243            assert_eq!(offset, 7);
1244            assert_eq!(warning_types, vec![WarningType::EmptyShorthandItem]);
1245        }
1246
1247        #[test]
1248        fn id_only() {
1249            let p = Parser::default();
1250
1251            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1252                &CowStr::from("#xyz"),
1253                0,
1254                &p,
1255                ParseShorthand(true),
1256                AttrlistContext::Inline,
1257            );
1258
1259            assert!(warning_types.is_empty());
1260
1261            assert_eq!(
1262                element_attr,
1263                ElementAttribute {
1264                    name: None,
1265                    shorthand_items: &["#xyz"],
1266                    value: "#xyz"
1267                }
1268            );
1269
1270            assert!(element_attr.name().is_none());
1271            assert_eq!(element_attr.shorthand_items(), vec!["#xyz"]);
1272            assert!(element_attr.block_style().is_none());
1273            assert_eq!(element_attr.id().unwrap(), "xyz");
1274            assert!(element_attr.roles().is_empty());
1275            assert!(element_attr.options().is_empty());
1276
1277            assert_eq!(offset, 4);
1278        }
1279
1280        #[test]
1281        fn one_role_only() {
1282            let p = Parser::default();
1283
1284            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1285                &CowStr::from(".role1"),
1286                0,
1287                &p,
1288                ParseShorthand(true),
1289                AttrlistContext::Inline,
1290            );
1291
1292            assert!(warning_types.is_empty());
1293
1294            assert_eq!(
1295                element_attr,
1296                ElementAttribute {
1297                    name: None,
1298                    shorthand_items: &[".role1",],
1299                    value: ".role1"
1300                }
1301            );
1302
1303            assert!(element_attr.name().is_none());
1304            assert_eq!(element_attr.shorthand_items(), vec![".role1"]);
1305            assert!(element_attr.block_style().is_none());
1306            assert!(element_attr.id().is_none());
1307            assert_eq!(element_attr.roles(), vec!("role1"));
1308            assert!(element_attr.options().is_empty());
1309
1310            assert_eq!(offset, 6);
1311        }
1312
1313        #[test]
1314        fn multiple_roles() {
1315            let p = Parser::default();
1316
1317            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1318                &CowStr::from(".role1.role2.role3"),
1319                0,
1320                &p,
1321                ParseShorthand(true),
1322                AttrlistContext::Inline,
1323            );
1324
1325            assert!(warning_types.is_empty());
1326
1327            assert_eq!(
1328                element_attr,
1329                ElementAttribute {
1330                    name: None,
1331                    shorthand_items: &[".role1", ".role2", ".role3"],
1332                    value: ".role1.role2.role3"
1333                }
1334            );
1335
1336            assert!(element_attr.name().is_none());
1337
1338            assert_eq!(
1339                element_attr.shorthand_items(),
1340                vec![".role1", ".role2", ".role3"]
1341            );
1342
1343            assert!(element_attr.block_style().is_none());
1344            assert!(element_attr.id().is_none());
1345            assert_eq!(element_attr.roles(), vec!("role1", "role2", "role3",));
1346            assert!(element_attr.options().is_empty());
1347
1348            assert_eq!(offset, 18);
1349        }
1350
1351        #[test]
1352        fn one_option_only() {
1353            let p = Parser::default();
1354
1355            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1356                &CowStr::from("%option1"),
1357                0,
1358                &p,
1359                ParseShorthand(true),
1360                AttrlistContext::Inline,
1361            );
1362
1363            assert!(warning_types.is_empty());
1364
1365            assert_eq!(
1366                element_attr,
1367                ElementAttribute {
1368                    name: None,
1369                    shorthand_items: &["%option1"],
1370                    value: "%option1"
1371                }
1372            );
1373
1374            assert!(element_attr.name().is_none());
1375            assert_eq!(element_attr.shorthand_items(), vec!["%option1"]);
1376            assert!(element_attr.block_style().is_none());
1377            assert!(element_attr.id().is_none());
1378            assert!(element_attr.roles().is_empty());
1379            assert_eq!(element_attr.options(), vec!("option1"));
1380
1381            assert_eq!(offset, 8);
1382        }
1383
1384        #[test]
1385        fn multiple_options() {
1386            let p = Parser::default();
1387
1388            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1389                &CowStr::from("%option1%option2%option3"),
1390                0,
1391                &p,
1392                ParseShorthand(true),
1393                AttrlistContext::Inline,
1394            );
1395
1396            assert!(warning_types.is_empty());
1397
1398            assert_eq!(
1399                element_attr,
1400                ElementAttribute {
1401                    name: None,
1402                    shorthand_items: &["%option1", "%option2", "%option3"],
1403                    value: "%option1%option2%option3"
1404                }
1405            );
1406
1407            assert!(element_attr.name().is_none());
1408
1409            assert_eq!(
1410                element_attr.shorthand_items(),
1411                vec!["%option1", "%option2", "%option3"]
1412            );
1413
1414            assert!(element_attr.block_style().is_none());
1415            assert!(element_attr.id().is_none());
1416            assert!(element_attr.roles().is_empty());
1417            assert_eq!(
1418                element_attr.options(),
1419                vec!("option1", "option2", "option3")
1420            );
1421
1422            assert_eq!(offset, 24);
1423        }
1424
1425        #[test]
1426        fn block_style_and_id() {
1427            let p = Parser::default();
1428
1429            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1430                &CowStr::from("appendix#custom-id"),
1431                0,
1432                &p,
1433                ParseShorthand(true),
1434                AttrlistContext::Inline,
1435            );
1436
1437            assert!(warning_types.is_empty());
1438
1439            assert_eq!(
1440                element_attr,
1441                ElementAttribute {
1442                    name: None,
1443                    shorthand_items: &["appendix", "#custom-id"],
1444                    value: "appendix#custom-id"
1445                }
1446            );
1447
1448            assert!(element_attr.name().is_none());
1449            assert_eq!(
1450                element_attr.shorthand_items(),
1451                vec!["appendix", "#custom-id"]
1452            );
1453            assert_eq!(element_attr.block_style().unwrap(), "appendix",);
1454            assert_eq!(element_attr.id().unwrap(), "custom-id",);
1455            assert!(element_attr.roles().is_empty());
1456            assert!(element_attr.options().is_empty());
1457
1458            assert_eq!(offset, 18);
1459        }
1460
1461        #[test]
1462        fn id_role_and_option() {
1463            let p = Parser::default();
1464
1465            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1466                &CowStr::from("#rules.prominent%incremental"),
1467                0,
1468                &p,
1469                ParseShorthand(true),
1470                AttrlistContext::Inline,
1471            );
1472
1473            assert!(warning_types.is_empty());
1474
1475            assert_eq!(
1476                element_attr,
1477                ElementAttribute {
1478                    name: None,
1479                    shorthand_items: &["#rules", ".prominent", "%incremental"],
1480                    value: "#rules.prominent%incremental"
1481                }
1482            );
1483
1484            assert!(element_attr.name().is_none());
1485
1486            assert_eq!(
1487                element_attr.shorthand_items(),
1488                vec!["#rules", ".prominent", "%incremental"]
1489            );
1490
1491            assert!(element_attr.block_style().is_none());
1492            assert_eq!(element_attr.id().unwrap(), "rules");
1493            assert_eq!(element_attr.roles(), vec!("prominent"));
1494            assert_eq!(element_attr.options(), vec!("incremental"));
1495
1496            assert_eq!(offset, 28);
1497        }
1498    }
1499}