Skip to main content

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