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, Eq, Hash, PartialEq)]
18pub struct ElementAttribute<'src> {
19    name: Option<CowStr<'src>>,
20    value: CowStr<'src>,
21    shorthand_item_indices: Vec<usize>,
22
23    /// The 1-based position of a *positional* (unnamed) attribute within its
24    /// attribute list, counting every comma-delimited entry – named entries and
25    /// blank slots included – the way Asciidoctor numbers positional keys.
26    /// `None` for a named attribute (or an attribute synthesized without a
27    /// known position). Consumed by [`Attrlist::nth_attribute`] so that
28    /// later positionals keep their index even when earlier entries are
29    /// named or blank.
30    ///
31    /// [`Attrlist::nth_attribute`]: crate::attributes::Attrlist::nth_attribute
32    positional_index: Option<usize>,
33
34    /// `true` when this attribute's `value` came from a genuinely
35    /// quote-delimited string (`"…"` or `'…'` with both delimiters
36    /// present), as opposed to an unquoted value or a lone leading quote.
37    /// This distinguishes an explicit empty quoted positional (`""` / `''`,
38    /// which is a real empty-valued attribute) from a blank slot, and gates
39    /// the quote-unescaping and single-quoted substitution that only apply
40    /// to genuinely quoted values.
41    value_is_quoted: bool,
42
43    /// `true` when the normal substitution group was already applied to `value`
44    /// while parsing this attribute (a single-quoted value in a block attribute
45    /// list). Consumers that would otherwise substitute the value themselves –
46    /// such as deriving a block title from a `title=` attribute – use this to
47    /// avoid substituting it a second time (which would, e.g., double-escape
48    /// special characters).
49    value_is_substituted: bool,
50}
51
52impl std::fmt::Debug for ElementAttribute<'_> {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        // `value_is_substituted` is an internal parsing detail and is
55        // deliberately omitted so the debug representation stays focused on the
56        // attribute's observable content.
57        f.debug_struct("ElementAttribute")
58            .field("name", &self.name)
59            .field("value", &self.value)
60            .field("shorthand_item_indices", &self.shorthand_item_indices)
61            .finish()
62    }
63}
64
65impl<'src> ElementAttribute<'src> {
66    pub(crate) fn parse(
67        source_text: &CowStr<'src>,
68        start_index: usize,
69        parser: &Parser,
70        mut parse_shorthand: ParseShorthand,
71        attrlist_context: AttrlistContext,
72    ) -> (Self, usize, Vec<WarningType>) {
73        let mut warnings: Vec<WarningType> = vec![];
74
75        let (name, value, shorthand_item_indices, value_is_quoted, value_is_substituted, offset) = {
76            let mut source = Span::new(source_text.as_ref());
77            source = source.discard(start_index);
78
79            // Skip any leading, non-semantic whitespace before this entry
80            // (Asciidoctor's `skip_blank`). Name detection has to run first, so
81            // without this a name with leading blanks – e.g. `[  first = value]`
82            // or the second/third entries once a comma is consumed – would fail
83            // to be recognized and fall through to a positional literal.
84            source = source.take_whitespace_with_newline().after;
85
86            let (name, after): (Option<Span<'_>>, Span) = match source.take_attr_name() {
87                Some(name) => {
88                    let space = name.after.take_whitespace_with_newline();
89                    match space.after.take_prefix("=") {
90                        Some(equals) => {
91                            let space = equals.after.take_whitespace_with_newline();
92
93                            // `name=` with nothing (or only a comma) after the `=`
94                            // is a named attribute with an empty value, not a
95                            // positional one. The empty value falls out of the
96                            // value scan below (`take_while(c != ',')` yields the
97                            // empty string), so the name is all we need to keep.
98                            (Some(name.item), space.after)
99                        }
100                        None => (None, source),
101                    }
102                }
103                None => (None, source),
104            };
105
106            let after = after.take_whitespace_with_newline().after;
107            let first_char = after.data().chars().next();
108
109            // `value_is_quoted` is set only when the value was genuinely
110            // delimited by a matching pair of quotes (`take_quoted_string`
111            // succeeded). A lone leading quote with no terminator is *not*
112            // quoted: Asciidoctor treats it as a literal (its `single_quoted`
113            // flag is set `unless value.start_with? APOS`), so it receives no
114            // unescaping and no substitution.
115            let mut value_is_quoted = false;
116            let value = match first_char {
117                Some('\'') | Some('"') => match after.take_quoted_string() {
118                    Some(v) => {
119                        parse_shorthand = ParseShorthand(false);
120                        value_is_quoted = true;
121                        v
122                    }
123                    None => {
124                        warnings.push(WarningType::AttributeValueMissingTerminatingQuote);
125                        after.take_while(|c| c != ',').trim_item_trailing_spaces()
126                    }
127                },
128                _ => after.take_while(|c| c != ',').trim_item_trailing_spaces(),
129            };
130
131            let after = value.after;
132            let mut value = cowstr_from_source_and_span(source_text, &value.item);
133            let mut value_is_substituted = false;
134
135            if value_is_quoted && let Some(first) = first_char {
136                let escaped_quote = format!("\\{first}");
137                let mut new_value = value.replace(&escaped_quote, &first.to_string());
138
139                if first == '\'' && attrlist_context == AttrlistContext::Block {
140                    // A single-quoted value in a block attribute list has the
141                    // normal substitution group applied at assignment time.
142                    value_is_substituted = true;
143
144                    let span = Span::new(&new_value);
145                    let mut content = Content::from(span);
146                    SubstitutionGroup::Normal.apply(&mut content, parser, None);
147
148                    if content.rendered.as_ref() != new_value {
149                        new_value = content.rendered.to_string();
150                    }
151                }
152
153                if new_value != *value {
154                    value = CowStr::from(new_value);
155                }
156            }
157
158            let shorthand_item_indices = if name.is_none() && parse_shorthand.0 {
159                parse_shorthand_items(&value, &mut warnings)
160            } else {
161                vec![]
162            };
163
164            let name = name.map(|name| cowstr_from_source_and_span(source_text, &name));
165
166            (
167                name,
168                value,
169                shorthand_item_indices,
170                value_is_quoted,
171                value_is_substituted,
172                after.byte_offset(),
173            )
174        };
175
176        (
177            Self {
178                name,
179                value,
180                shorthand_item_indices,
181                positional_index: None,
182                value_is_quoted,
183                value_is_substituted,
184            },
185            offset,
186            warnings,
187        )
188    }
189
190    /// Synthesize the `source` block-style attribute implied by a
191    /// language-aware fenced code block (`` ```lang ``). This is the first
192    /// positional attribute of the equivalent `[source,<lang>]` attribute list;
193    /// its shorthand items resolve the block style to `source`.
194    pub(crate) fn synthesized_source_style() -> Self {
195        const SOURCE: &str = "source";
196        let mut warnings: Vec<WarningType> = vec![];
197        let shorthand_item_indices = parse_shorthand_items(SOURCE, &mut warnings);
198
199        // `source` is a single shorthand item with no delimiters, so parsing it
200        // can never warn. Guard that invariant rather than plumb an always-empty
201        // list back to the caller.
202        debug_assert!(
203            warnings.is_empty(),
204            "synthesizing the `source` block style should not produce warnings, got: {warnings:?}"
205        );
206
207        Self {
208            name: None,
209            value: CowStr::from(SOURCE),
210            shorthand_item_indices,
211            positional_index: Some(1),
212            value_is_quoted: false,
213            value_is_substituted: false,
214        }
215    }
216
217    /// Construct a bare positional attribute whose value is drawn directly from
218    /// `span`, carrying no shorthand items. Used for the language on a
219    /// language-aware fenced code block (the second positional attribute of the
220    /// equivalent `[source,<lang>]` attribute list).
221    pub(crate) fn positional_from_span(span: Span<'src>) -> Self {
222        Self {
223            name: None,
224            value: CowStr::from(span.data()),
225            shorthand_item_indices: vec![],
226            positional_index: Some(2),
227            value_is_quoted: false,
228            value_is_substituted: false,
229        }
230    }
231
232    /// Return the attribute name, if one was found.
233    pub fn name(&'src self) -> Option<&'src str> {
234        self.name_str()
235    }
236
237    /// Return the attribute name, if one was found.
238    ///
239    /// Unlike [`name`](Self::name), this borrows for the duration of the call
240    /// only, so it can be used on temporary `ElementAttribute` values (for
241    /// example while merging multiple attribute lists).
242    pub(crate) fn name_str(&self) -> Option<&str> {
243        self.name.as_deref()
244    }
245
246    /// Return the shorthand items, if applicable.
247    ///
248    /// Shorthand items are only parsed for certain element attributes. If this
249    /// attribute is not of the appropriate kind, this will return an empty
250    /// list.
251    pub fn shorthand_items(&'src self) -> Vec<&'src str> {
252        self.shorthand_items_internal()
253    }
254
255    /// Same as [`shorthand_items`](Self::shorthand_items), but borrows for the
256    /// duration of the call only so it can be used on temporary values.
257    fn shorthand_items_internal(&self) -> Vec<&str> {
258        let mut result = vec![];
259        let value = self.value.as_ref();
260
261        let mut iter = self.shorthand_item_indices.iter().peekable();
262
263        while let Some(curr) = iter.next() {
264            let mut next_item = if let Some(next) = iter.peek() {
265                &value[*curr..**next]
266            } else {
267                &value[*curr..]
268            };
269
270            if next_item == "#" || next_item == "." || next_item == "%" {
271                continue;
272            }
273
274            next_item = next_item.trim_end();
275
276            if !next_item.is_empty() {
277                result.push(next_item);
278            }
279        }
280
281        result
282    }
283
284    /// Return the block style name from shorthand syntax.
285    pub fn block_style(&'src self) -> Option<&'src str> {
286        self.block_style_internal()
287    }
288
289    /// Same as [`block_style`](Self::block_style), but borrows for the duration
290    /// of the call only.
291    fn block_style_internal(&self) -> Option<&str> {
292        self.shorthand_items_internal()
293            .first()
294            .filter(|v| !v.chars().any(is_shorthand_delimiter))
295            .cloned()
296    }
297
298    /// Return the ID attribute from shorthand syntax.
299    ///
300    /// If multiple ID attributes were specified, only the first
301    /// match is returned. (Multiple IDs are not supported.)
302    ///
303    /// You can assign an ID to a block using the shorthand syntax, the longhand
304    /// syntax, or a legacy block anchor.
305    ///
306    /// In the shorthand syntax, you prefix the name with a hash (`#`) in the
307    /// first position attribute:
308    ///
309    /// ```asciidoc
310    /// [#goals]
311    /// * Goal 1
312    /// * Goal 2
313    /// ```
314    ///
315    /// In the longhand syntax, you use a standard named attribute:
316    ///
317    /// ```asciidoc
318    /// [id=goals]
319    /// * Goal 1
320    /// * Goal 2
321    /// ```
322    ///
323    /// In the legacy block anchor syntax, you surround the name with double
324    /// square brackets:
325    ///
326    /// ```asciidoc
327    /// [[goals]]
328    /// * Goal 1
329    /// * Goal 2
330    /// ```
331    pub fn id(&'src self) -> Option<&'src str> {
332        self.id_internal()
333    }
334
335    /// Same as [`id`](Self::id), but borrows for the duration of the call only.
336    fn id_internal(&self) -> Option<&str> {
337        self.shorthand_items_internal()
338            .into_iter()
339            .find(|v| v.starts_with('#'))
340            .map(|v| &v[1..])
341    }
342
343    /// Return any role attributes that were found in shorthand syntax.
344    ///
345    /// You can assign one or more roles to blocks and most inline elements
346    /// using the `role` attribute. The `role` attribute is a [named attribute].
347    /// Even though the attribute name is singular, it may contain multiple
348    /// (space-separated) roles. Roles may also be defined using a shorthand
349    /// (dot-prefixed) syntax.
350    ///
351    /// A role:
352    /// 1. adds additional semantics to an element
353    /// 2. can be used to apply additional styling to a group of elements (e.g.,
354    ///    via a CSS class selector)
355    /// 3. may activate additional behavior if recognized by the converter
356    ///
357    /// **TIP:** The `role` attribute in AsciiDoc always get mapped to the
358    /// `class` attribute in the HTML output. In other words, role names are
359    /// synonymous with HTML class names, thus allowing output elements to be
360    /// identified and styled in CSS using class selectors (e.g.,
361    /// `sidebarblock.role1`).
362    ///
363    /// [named attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/#named
364    pub fn roles(&'src self) -> Vec<&'src str> {
365        self.roles_internal()
366    }
367
368    /// Same as [`roles`](Self::roles), but borrows for the duration of the call
369    /// only.
370    fn roles_internal(&self) -> Vec<&str> {
371        self.shorthand_items_internal()
372            .into_iter()
373            .filter(|span| span.starts_with('.'))
374            .map(|span| &span[1..])
375            .collect()
376    }
377
378    /// Return any option attributes that were found in shorthand syntax.
379    ///
380    /// The `options` attribute (often abbreviated as `opts`) is a versatile
381    /// [named attribute] that can be assigned one or more values. It can be
382    /// defined globally as document attribute as well as a block attribute on
383    /// an individual block.
384    ///
385    /// There is no strict schema for options. Any options which are not
386    /// recognized are ignored.
387    ///
388    /// You can assign one or more options to a block using the shorthand or
389    /// formal syntax for the options attribute.
390    ///
391    /// # Shorthand options syntax for blocks
392    ///
393    /// To assign an option to a block, prefix the value with a percent sign
394    /// (`%`) in an attribute list. The percent sign implicitly sets the
395    /// `options` attribute.
396    ///
397    /// ## Example 1: Sidebar block with an option assigned using the shorthand dot
398    ///
399    /// ```asciidoc
400    /// [%option]
401    /// ****
402    /// This is a sidebar with an option assigned to it, named option.
403    /// ****
404    /// ```
405    ///
406    /// You can assign multiple options to a block by prest
407    /// fixing each value with
408    /// a percent sign (`%`).
409    ///
410    /// ## Example 2: Sidebar with two options assigned using the shorthand dot
411    /// ```asciidoc
412    /// [%option1%option2]
413    /// ****
414    /// This is a sidebar with two options assigned to it, named option1 and option2.
415    /// ****
416    /// ```
417    ///
418    /// # Formal options syntax for blocks
419    ///
420    /// Explicitly set `options` or `opts`, followed by the equals sign (`=`),
421    /// and then the value in an attribute list.
422    ///
423    /// ## Example 3. Sidebar block with an option assigned using the formal syntax
424    /// ```asciidoc
425    /// [opts=option]
426    /// ****
427    /// This is a sidebar with an option assigned to it, named option.
428    /// ****
429    /// ```
430    ///
431    /// Separate multiple option values with commas (`,`).
432    ///
433    /// ## Example 4. Sidebar with three options assigned using the formal syntax
434    /// ```asciidoc
435    /// [opts="option1,option2"]
436    /// ****
437    /// This is a sidebar with two options assigned to it, option1 and option2.
438    /// ****
439    /// ```
440    ///
441    /// [named attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/#named
442    pub fn options(&'src self) -> Vec<&'src str> {
443        self.options_internal()
444    }
445
446    /// Same as [`options`](Self::options), but borrows for the duration of the
447    /// call only.
448    fn options_internal(&self) -> Vec<&str> {
449        self.shorthand_items_internal()
450            .into_iter()
451            .filter(|v| v.starts_with('%'))
452            .map(|v| &v[1..])
453            .collect()
454    }
455
456    /// Merge the shorthand of two first-position (block style) attributes drawn
457    /// from separate block attribute lines, following Asciidoctor's semantics:
458    ///
459    /// * the block style and ID are taken from the later line if it specifies
460    ///   them, otherwise retained from the earlier line;
461    /// * roles and options accumulate, with the earlier line's values first.
462    ///
463    /// The result is a freshly synthesized positional attribute whose value
464    /// re-encodes the merged shorthand so that the usual accessors continue to
465    /// work.
466    pub(crate) fn merge_block_style_shorthand(earlier: &Self, later: &Self) -> Self {
467        let block_style = later
468            .block_style_internal()
469            .or(earlier.block_style_internal());
470        let id = later.id_internal().or(earlier.id_internal());
471
472        let mut roles = earlier.roles_internal();
473        roles.extend(later.roles_internal());
474
475        let mut options = earlier.options_internal();
476        options.extend(later.options_internal());
477
478        let mut value = String::new();
479        if let Some(block_style) = block_style {
480            value.push_str(block_style);
481        }
482        if let Some(id) = id {
483            value.push('#');
484            value.push_str(id);
485        }
486        for role in &roles {
487            value.push('.');
488            value.push_str(role);
489        }
490        for option in &options {
491            value.push('%');
492            value.push_str(option);
493        }
494
495        // The shorthand string is synthesized entirely from components that
496        // were already parsed and validated when their source lines were read
497        // (a block style, a non-empty ID, and non-empty roles/options, each
498        // separated by a single delimiter). Re-parsing it therefore can never
499        // produce a warning, so rather than plumb an always-empty warning list
500        // back to the caller we assert that invariant – turning a silent
501        // discard into an explicit, regression-guarded one.
502        let mut warnings: Vec<WarningType> = vec![];
503        let shorthand_item_indices = if value.is_empty() {
504            vec![]
505        } else {
506            parse_shorthand_items(&value, &mut warnings)
507        };
508
509        debug_assert!(
510            warnings.is_empty(),
511            "merging block-style shorthand should not produce warnings, got: {warnings:?}"
512        );
513
514        Self {
515            name: None,
516            value: CowStr::from(value),
517            shorthand_item_indices,
518
519            // The merged shorthand is the first positional of the earlier line,
520            // so it keeps that line's position (1).
521            positional_index: earlier.positional_index,
522            value_is_quoted: false,
523            value_is_substituted: false,
524        }
525    }
526
527    /// Return a copy of this first-position (block style) attribute with its
528    /// role shorthand (`.role`) items removed, retaining the block style, ID,
529    /// and options. Used while merging block attribute lines, where a formal
530    /// `role=` entry takes ownership of the resolved role list and the
531    /// shorthand must therefore no longer contribute roles of its own.
532    ///
533    /// If the attribute carries no role shorthand, it is returned unchanged.
534    pub(crate) fn without_shorthand_roles(&self) -> Self {
535        if self.roles_internal().is_empty() {
536            return self.clone();
537        }
538
539        let block_style = self.block_style_internal();
540        let id = self.id_internal();
541        let options = self.options_internal();
542
543        // Rebuild the shorthand string from the retained components, recording
544        // the start offset of each shorthand item as it is appended. The
545        // components were validated (and stripped of their delimiters) when the
546        // source line was parsed, so these offsets match what
547        // `parse_shorthand_items` would produce for the same string – without
548        // re-parsing it (or having to discard its always-empty warnings).
549        let mut value = String::new();
550        let mut shorthand_item_indices: Vec<usize> = vec![];
551
552        if let Some(block_style) = block_style {
553            shorthand_item_indices.push(value.len());
554            value.push_str(block_style);
555        }
556        if let Some(id) = id {
557            shorthand_item_indices.push(value.len());
558            value.push('#');
559            value.push_str(id);
560        }
561        for option in &options {
562            shorthand_item_indices.push(value.len());
563            value.push('%');
564            value.push_str(option);
565        }
566
567        Self {
568            name: None,
569            value: CowStr::from(value),
570            shorthand_item_indices,
571            positional_index: self.positional_index,
572            value_is_quoted: false,
573            value_is_substituted: false,
574        }
575    }
576
577    /// Synthesize a named `role` attribute whose value is the space-separated
578    /// list of resolved roles. Used while merging block attribute lines to
579    /// record the roles resolved under Asciidoctor's running model (a formal
580    /// `role=` replaces, shorthand `.role` appends).
581    pub(crate) fn synthesized_role(value: String) -> Self {
582        Self {
583            name: Some(CowStr::from("role")),
584            value: CowStr::from(value),
585            shorthand_item_indices: vec![],
586            positional_index: None,
587            value_is_quoted: false,
588            value_is_substituted: false,
589        }
590    }
591
592    /// Return the 1-based positional index of this attribute, if it is a
593    /// positional (unnamed) attribute whose position is known. See the
594    /// [`positional_index`](Self::positional_index) field for the numbering
595    /// rules.
596    pub(crate) fn positional_index(&self) -> Option<usize> {
597        self.positional_index
598    }
599
600    /// Record the 1-based positional index of this attribute. Called by the
601    /// attribute-list parser once it knows the entry's position within the
602    /// list.
603    pub(crate) fn set_positional_index(&mut self, index: usize) {
604        self.positional_index = Some(index);
605    }
606
607    /// Returns `true` when this attribute's value came from a genuinely
608    /// quote-delimited string (both delimiters present). See the
609    /// [`value_is_quoted`](Self::value_is_quoted) field.
610    pub(crate) fn value_is_quoted(&self) -> bool {
611        self.value_is_quoted
612    }
613
614    /// Return the attribute's value.
615    ///
616    /// Note that this value will have had special characters and attribute
617    /// value replacements applied to it.
618    pub fn value(&'src self) -> &'src str {
619        self.value.as_ref()
620    }
621
622    /// Return the attribute's value.
623    ///
624    /// Unlike [`value`](Self::value), this borrows for the duration of the call
625    /// only, so it can be used on temporary `ElementAttribute` values (for
626    /// example while an owned attribute list is still being assembled).
627    pub(crate) fn value_str(&self) -> &str {
628        self.value.as_ref()
629    }
630
631    /// Returns `true` when the normal substitution group has already been
632    /// applied to this attribute's [`value`](Self::value) (a single-quoted
633    /// value in a block attribute list). A consumer that would otherwise
634    /// substitute the value itself uses this to avoid substituting it a
635    /// second time.
636    pub(crate) fn value_is_substituted(&self) -> bool {
637        self.value_is_substituted
638    }
639}
640
641fn parse_shorthand_items(source: &str, warnings: &mut Vec<WarningType>) -> Vec<usize> {
642    let mut shorthand_item_indices: Vec<usize> = vec![];
643    let mut span = Span::new(source);
644
645    // Look for block style selector.
646    if let Some(block_style_pr) = span.split_at_match_non_empty(is_shorthand_delimiter) {
647        shorthand_item_indices.push(block_style_pr.item.discard_whitespace().byte_offset());
648
649        span = block_style_pr.after;
650    }
651
652    while !span.is_empty() {
653        // Assumption: First character is a delimiter.
654        let after_delimiter = span.discard(1);
655
656        match after_delimiter.position(is_shorthand_delimiter) {
657            None => {
658                if after_delimiter.is_empty() {
659                    warnings.push(WarningType::EmptyShorthandName);
660                    shorthand_item_indices.push(span.byte_offset());
661                    span = after_delimiter;
662                } else {
663                    shorthand_item_indices.push(span.byte_offset());
664                    span = span.discard_all();
665                }
666            }
667
668            Some(0) => {
669                shorthand_item_indices.push(span.byte_offset());
670                warnings.push(WarningType::EmptyShorthandName);
671                span = after_delimiter;
672            }
673
674            Some(index) => {
675                let mi: MatchedItem<Span> = span.into_parse_result(index + 1);
676                shorthand_item_indices.push(span.byte_offset());
677                span = mi.after;
678            }
679        }
680    }
681
682    shorthand_item_indices
683}
684
685fn is_shorthand_delimiter(c: char) -> bool {
686    c == '#' || c == '%' || c == '.'
687}
688
689#[derive(Clone, Debug)]
690pub(crate) struct ParseShorthand(pub bool);
691
692fn cowstr_from_source_and_span<'src>(source: &CowStr<'src>, span: &Span<'_>) -> CowStr<'src> {
693    if let CowStr::Borrowed(source) = source {
694        let borrowed: Span<'src> = Span::new(source)
695            .discard(span.byte_offset())
696            .slice_to(..span.len());
697
698        CowStr::Borrowed(borrowed.data())
699    } else {
700        CowStr::from(span.data().to_string())
701    }
702}
703
704#[cfg(test)]
705mod tests {
706    #![allow(clippy::unwrap_used)]
707
708    use crate::{
709        attributes::{AttrlistContext, element_attribute::ParseShorthand},
710        strings::CowStr,
711        tests::prelude::*,
712    };
713
714    #[test]
715    fn impl_clone() {
716        // Silly test to mark the #[derive(...)] line as covered.
717        let p = Parser::default();
718
719        let b1 = crate::attributes::ElementAttribute::parse(
720            &CowStr::from("abc"),
721            0,
722            &p,
723            ParseShorthand(false),
724            AttrlistContext::Inline,
725        )
726        .0;
727
728        let b2 = b1.clone();
729
730        assert_eq!(b1, b2);
731    }
732
733    #[test]
734    fn empty_source() {
735        let p = Parser::default();
736
737        let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
738            &CowStr::from(""),
739            0,
740            &p,
741            ParseShorthand(false),
742            AttrlistContext::Inline,
743        );
744
745        assert!(warning_types.is_empty());
746
747        assert_eq!(
748            element_attr,
749            ElementAttribute {
750                name: None,
751                shorthand_items: &[],
752                value: "",
753            }
754        );
755
756        assert!(element_attr.name().is_none());
757        assert!(element_attr.block_style().is_none());
758        assert!(element_attr.id().is_none());
759        assert!(element_attr.roles().is_empty());
760        assert!(element_attr.options().is_empty());
761
762        assert_eq!(offset, 0);
763    }
764
765    #[test]
766    fn only_spaces() {
767        let p = Parser::default();
768
769        let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
770            &CowStr::from("   "),
771            0,
772            &p,
773            ParseShorthand(false),
774            AttrlistContext::Inline,
775        );
776
777        assert!(warning_types.is_empty());
778
779        assert_eq!(
780            element_attr,
781            ElementAttribute {
782                name: None,
783                shorthand_items: &[],
784                value: "",
785            }
786        );
787
788        assert!(element_attr.name().is_none());
789        assert!(element_attr.block_style().is_none());
790        assert!(element_attr.id().is_none());
791        assert!(element_attr.roles().is_empty());
792        assert!(element_attr.options().is_empty());
793
794        assert_eq!(offset, 3);
795    }
796
797    #[test]
798    fn unquoted_and_unnamed_value() {
799        let p = Parser::default();
800
801        let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
802            &CowStr::from("abc"),
803            0,
804            &p,
805            ParseShorthand(false),
806            AttrlistContext::Inline,
807        );
808
809        assert!(warning_types.is_empty());
810
811        assert_eq!(
812            element_attr,
813            ElementAttribute {
814                name: None,
815                shorthand_items: &[],
816                value: "abc",
817            }
818        );
819
820        assert!(element_attr.name().is_none());
821        assert!(element_attr.block_style().is_none());
822        assert!(element_attr.id().is_none());
823        assert!(element_attr.roles().is_empty());
824        assert!(element_attr.options().is_empty());
825
826        assert_eq!(offset, 3);
827    }
828
829    #[test]
830    fn unquoted_stops_at_comma() {
831        let p = Parser::default();
832
833        let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
834            &CowStr::from("abc,def"),
835            0,
836            &p,
837            ParseShorthand(false),
838            AttrlistContext::Inline,
839        );
840
841        assert!(warning_types.is_empty());
842
843        assert_eq!(
844            element_attr,
845            ElementAttribute {
846                name: None,
847                shorthand_items: &[],
848                value: "abc",
849            }
850        );
851
852        assert!(element_attr.name().is_none());
853        assert!(element_attr.block_style().is_none());
854        assert!(element_attr.id().is_none());
855        assert!(element_attr.roles().is_empty());
856        assert!(element_attr.options().is_empty());
857
858        assert_eq!(offset, 3);
859    }
860
861    mod quoted_string {
862        use crate::{
863            attributes::{AttrlistContext, element_attribute::ParseShorthand},
864            strings::CowStr,
865            tests::prelude::*,
866        };
867
868        #[test]
869        fn err_unterminated_double_quote() {
870            let p = Parser::default();
871
872            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
873                &CowStr::from("\"xyz"),
874                0,
875                &p,
876                ParseShorthand(false),
877                AttrlistContext::Inline,
878            );
879
880            assert_eq!(
881                element_attr,
882                ElementAttribute {
883                    name: None,
884                    shorthand_items: &[],
885                    value: "\"xyz"
886                }
887            );
888
889            assert!(element_attr.name().is_none());
890            assert!(element_attr.block_style().is_none());
891            assert!(element_attr.id().is_none());
892            assert!(element_attr.roles().is_empty());
893            assert!(element_attr.options().is_empty());
894
895            assert_eq!(offset, 4);
896
897            assert_eq!(
898                warning_types,
899                vec![WarningType::AttributeValueMissingTerminatingQuote]
900            );
901        }
902
903        #[test]
904        fn err_unterminated_double_quote_ends_at_comma() {
905            let p = Parser::default();
906
907            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
908                &CowStr::from("\"xyz,abc"),
909                0,
910                &p,
911                ParseShorthand(false),
912                AttrlistContext::Inline,
913            );
914
915            assert_eq!(
916                element_attr,
917                ElementAttribute {
918                    name: None,
919                    shorthand_items: &[],
920                    value: "\"xyz"
921                }
922            );
923
924            assert!(element_attr.name().is_none());
925            assert!(element_attr.block_style().is_none());
926            assert!(element_attr.id().is_none());
927            assert!(element_attr.roles().is_empty());
928            assert!(element_attr.options().is_empty());
929
930            assert_eq!(offset, 4);
931            assert_eq!(
932                warning_types,
933                vec![WarningType::AttributeValueMissingTerminatingQuote]
934            );
935        }
936
937        #[test]
938        fn double_quoted_string() {
939            let p = Parser::default();
940
941            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
942                &CowStr::from("\"abc\"def"),
943                0,
944                &p,
945                ParseShorthand(false),
946                AttrlistContext::Inline,
947            );
948
949            assert!(warning_types.is_empty());
950
951            assert_eq!(
952                element_attr,
953                ElementAttribute {
954                    name: None,
955                    shorthand_items: &[],
956                    value: "abc"
957                }
958            );
959
960            assert!(element_attr.name().is_none());
961            assert!(element_attr.block_style().is_none());
962            assert!(element_attr.id().is_none());
963            assert!(element_attr.roles().is_empty());
964            assert!(element_attr.options().is_empty());
965
966            assert_eq!(offset, 5);
967        }
968
969        #[test]
970        fn double_quoted_with_escape() {
971            let p = Parser::default();
972
973            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
974                &CowStr::from("\"a\\\"bc\"def"),
975                0,
976                &p,
977                ParseShorthand(false),
978                AttrlistContext::Inline,
979            );
980
981            assert!(warning_types.is_empty());
982
983            assert_eq!(
984                element_attr,
985                ElementAttribute {
986                    name: None,
987                    shorthand_items: &[],
988                    value: "a\"bc"
989                }
990            );
991
992            assert!(element_attr.name().is_none());
993            assert!(element_attr.block_style().is_none());
994            assert!(element_attr.id().is_none());
995            assert!(element_attr.roles().is_empty());
996            assert!(element_attr.options().is_empty());
997
998            assert_eq!(offset, 7);
999        }
1000
1001        #[test]
1002        fn double_quoted_with_single_quote() {
1003            let p = Parser::default();
1004
1005            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1006                &CowStr::from("\"a'bc\"def"),
1007                0,
1008                &p,
1009                ParseShorthand(false),
1010                AttrlistContext::Inline,
1011            );
1012
1013            assert!(warning_types.is_empty());
1014
1015            assert_eq!(
1016                element_attr,
1017                ElementAttribute {
1018                    name: None,
1019                    shorthand_items: &[],
1020                    value: "a'bc"
1021                }
1022            );
1023
1024            assert!(element_attr.name().is_none());
1025            assert!(element_attr.block_style().is_none());
1026            assert!(element_attr.id().is_none());
1027            assert!(element_attr.roles().is_empty());
1028            assert!(element_attr.options().is_empty());
1029
1030            assert_eq!(offset, 6);
1031        }
1032
1033        #[test]
1034        fn err_unterminated_single_quote() {
1035            let p = Parser::default();
1036
1037            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1038                &CowStr::from("\'xyz"),
1039                0,
1040                &p,
1041                ParseShorthand(false),
1042                AttrlistContext::Inline,
1043            );
1044
1045            assert_eq!(
1046                element_attr,
1047                ElementAttribute {
1048                    name: None,
1049                    shorthand_items: &[],
1050                    value: "\'xyz"
1051                }
1052            );
1053
1054            assert!(element_attr.name().is_none());
1055            assert!(element_attr.block_style().is_none());
1056            assert!(element_attr.id().is_none());
1057            assert!(element_attr.roles().is_empty());
1058            assert!(element_attr.options().is_empty());
1059
1060            assert_eq!(offset, 4);
1061
1062            assert_eq!(
1063                warning_types,
1064                vec![WarningType::AttributeValueMissingTerminatingQuote]
1065            );
1066        }
1067
1068        #[test]
1069        fn err_unterminated_single_quote_ends_at_comma() {
1070            let p = Parser::default();
1071
1072            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1073                &CowStr::from("\'xyz,abc"),
1074                0,
1075                &p,
1076                ParseShorthand(false),
1077                AttrlistContext::Inline,
1078            );
1079
1080            assert_eq!(
1081                element_attr,
1082                ElementAttribute {
1083                    name: None,
1084                    shorthand_items: &[],
1085                    value: "\'xyz"
1086                }
1087            );
1088
1089            assert!(element_attr.name().is_none());
1090            assert!(element_attr.block_style().is_none());
1091            assert!(element_attr.id().is_none());
1092            assert!(element_attr.roles().is_empty());
1093            assert!(element_attr.options().is_empty());
1094
1095            assert_eq!(offset, 4);
1096            assert_eq!(
1097                warning_types,
1098                vec![WarningType::AttributeValueMissingTerminatingQuote]
1099            );
1100        }
1101
1102        #[test]
1103        fn single_quoted_string() {
1104            let p = Parser::default();
1105
1106            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1107                &CowStr::from("'abc'def"),
1108                0,
1109                &p,
1110                ParseShorthand(false),
1111                AttrlistContext::Inline,
1112            );
1113
1114            assert!(warning_types.is_empty());
1115
1116            assert_eq!(
1117                element_attr,
1118                ElementAttribute {
1119                    name: None,
1120                    shorthand_items: &[],
1121                    value: "abc"
1122                }
1123            );
1124
1125            assert!(element_attr.name().is_none());
1126            assert!(element_attr.block_style().is_none());
1127            assert!(element_attr.id().is_none());
1128            assert!(element_attr.roles().is_empty());
1129            assert!(element_attr.options().is_empty());
1130
1131            assert_eq!(offset, 5);
1132        }
1133
1134        #[test]
1135        fn single_quoted_with_escape() {
1136            let p = Parser::default();
1137
1138            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1139                &CowStr::from("'a\\'bc'def"),
1140                0,
1141                &p,
1142                ParseShorthand(false),
1143                AttrlistContext::Inline,
1144            );
1145
1146            assert!(warning_types.is_empty());
1147
1148            assert_eq!(
1149                element_attr,
1150                ElementAttribute {
1151                    name: None,
1152                    shorthand_items: &[],
1153                    value: "a'bc"
1154                }
1155            );
1156
1157            assert!(element_attr.name().is_none());
1158            assert!(element_attr.block_style().is_none());
1159            assert!(element_attr.id().is_none());
1160            assert!(element_attr.roles().is_empty());
1161            assert!(element_attr.options().is_empty());
1162
1163            assert_eq!(offset, 7);
1164        }
1165
1166        #[test]
1167        fn single_quoted_with_double_quote() {
1168            let p = Parser::default();
1169
1170            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1171                &CowStr::from("'a\"bc'def"),
1172                0,
1173                &p,
1174                ParseShorthand(false),
1175                AttrlistContext::Inline,
1176            );
1177
1178            assert!(warning_types.is_empty());
1179
1180            assert_eq!(
1181                element_attr,
1182                ElementAttribute {
1183                    name: None,
1184                    shorthand_items: &[],
1185                    value: "a\"bc"
1186                }
1187            );
1188
1189            assert!(element_attr.name().is_none());
1190            assert!(element_attr.block_style().is_none());
1191            assert!(element_attr.id().is_none());
1192            assert!(element_attr.roles().is_empty());
1193            assert!(element_attr.options().is_empty());
1194
1195            assert_eq!(offset, 6);
1196        }
1197
1198        #[test]
1199        fn single_quoted_gets_substitions() {
1200            let p = Parser::default().with_intrinsic_attribute(
1201                "foo",
1202                "bar",
1203                ModificationContext::Anywhere,
1204            );
1205
1206            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1207                &CowStr::from("'*abc* def {foo}'"),
1208                0,
1209                &p,
1210                ParseShorthand(false),
1211                AttrlistContext::Block,
1212            );
1213
1214            assert!(warning_types.is_empty());
1215
1216            assert_eq!(
1217                element_attr,
1218                ElementAttribute {
1219                    name: None,
1220                    shorthand_items: &[],
1221                    value: "<strong>abc</strong> def bar"
1222                }
1223            );
1224
1225            assert!(element_attr.name().is_none());
1226            assert!(element_attr.block_style().is_none());
1227            assert!(element_attr.id().is_none());
1228            assert!(element_attr.roles().is_empty());
1229            assert!(element_attr.options().is_empty());
1230
1231            assert_eq!(offset, 17);
1232        }
1233    }
1234
1235    mod named {
1236        use crate::{
1237            attributes::{AttrlistContext, element_attribute::ParseShorthand},
1238            strings::CowStr,
1239            tests::prelude::*,
1240        };
1241
1242        #[test]
1243        fn simple_named_value() {
1244            let p = Parser::default();
1245
1246            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1247                &CowStr::from("abc=def"),
1248                0,
1249                &p,
1250                ParseShorthand(false),
1251                AttrlistContext::Inline,
1252            );
1253
1254            assert!(warning_types.is_empty());
1255
1256            assert_eq!(
1257                element_attr,
1258                ElementAttribute {
1259                    name: Some("abc"),
1260                    shorthand_items: &[],
1261                    value: "def"
1262                }
1263            );
1264
1265            assert_eq!(element_attr.name().unwrap(), "abc");
1266            assert!(element_attr.block_style().is_none());
1267            assert!(element_attr.id().is_none());
1268            assert!(element_attr.roles().is_empty());
1269            assert!(element_attr.options().is_empty());
1270
1271            assert_eq!(offset, 7);
1272        }
1273
1274        #[test]
1275        fn ignores_spaces_around_equals() {
1276            let p = Parser::default();
1277
1278            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1279                &CowStr::from("abc =  def"),
1280                0,
1281                &p,
1282                ParseShorthand(false),
1283                AttrlistContext::Inline,
1284            );
1285
1286            assert!(warning_types.is_empty());
1287
1288            assert_eq!(
1289                element_attr,
1290                ElementAttribute {
1291                    name: Some("abc"),
1292                    shorthand_items: &[],
1293                    value: "def"
1294                }
1295            );
1296
1297            assert_eq!(element_attr.name().unwrap(), "abc");
1298
1299            assert_eq!(offset, 10);
1300        }
1301
1302        #[test]
1303        fn numeric_name() {
1304            let p = Parser::default();
1305
1306            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1307                &CowStr::from("94-x =def"),
1308                0,
1309                &p,
1310                ParseShorthand(false),
1311                AttrlistContext::Inline,
1312            );
1313
1314            assert!(warning_types.is_empty());
1315
1316            assert_eq!(
1317                element_attr,
1318                ElementAttribute {
1319                    name: Some("94-x"),
1320                    shorthand_items: &[],
1321                    value: "def"
1322                }
1323            );
1324
1325            assert_eq!(element_attr.name().unwrap(), "94-x");
1326            assert!(element_attr.block_style().is_none());
1327            assert!(element_attr.id().is_none());
1328            assert!(element_attr.roles().is_empty());
1329            assert!(element_attr.options().is_empty());
1330
1331            assert_eq!(offset, 9);
1332        }
1333
1334        #[test]
1335        fn quoted_value() {
1336            let p = Parser::default();
1337
1338            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1339                &CowStr::from("abc='def'g"),
1340                0,
1341                &p,
1342                ParseShorthand(false),
1343                AttrlistContext::Inline,
1344            );
1345
1346            assert!(warning_types.is_empty());
1347
1348            assert_eq!(
1349                element_attr,
1350                ElementAttribute {
1351                    name: Some("abc"),
1352                    shorthand_items: &[],
1353                    value: "def"
1354                }
1355            );
1356
1357            assert_eq!(element_attr.name().unwrap(), "abc");
1358            assert!(element_attr.block_style().is_none());
1359            assert!(element_attr.id().is_none());
1360            assert!(element_attr.roles().is_empty());
1361            assert!(element_attr.options().is_empty());
1362
1363            assert_eq!(offset, 9);
1364        }
1365
1366        #[test]
1367        fn named_with_empty_value() {
1368            let p = Parser::default();
1369
1370            // `name=` with nothing after the `=` is a named attribute whose value
1371            // is the empty string (e.g. `[caption=]` clears a label), not a
1372            // positional attribute with the literal value "abc=".
1373            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1374                &CowStr::from("abc="),
1375                0,
1376                &p,
1377                ParseShorthand(false),
1378                AttrlistContext::Inline,
1379            );
1380
1381            assert!(warning_types.is_empty());
1382
1383            assert_eq!(
1384                element_attr,
1385                ElementAttribute {
1386                    name: Some("abc"),
1387                    shorthand_items: &[],
1388                    value: ""
1389                }
1390            );
1391
1392            assert_eq!(element_attr.name(), Some("abc"));
1393            assert!(element_attr.block_style().is_none());
1394            assert!(element_attr.id().is_none());
1395            assert!(element_attr.roles().is_empty());
1396            assert!(element_attr.options().is_empty());
1397
1398            assert_eq!(offset, 4);
1399        }
1400
1401        #[test]
1402        fn named_with_empty_value_before_comma() {
1403            let p = Parser::default();
1404
1405            // `name=` immediately followed by a comma is likewise a named
1406            // attribute with an empty value; parsing stops at the comma so the
1407            // next attribute can be read separately.
1408            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1409                &CowStr::from("abc=,def"),
1410                0,
1411                &p,
1412                ParseShorthand(false),
1413                AttrlistContext::Inline,
1414            );
1415
1416            assert!(warning_types.is_empty());
1417
1418            assert_eq!(
1419                element_attr,
1420                ElementAttribute {
1421                    name: Some("abc"),
1422                    shorthand_items: &[],
1423                    value: ""
1424                }
1425            );
1426
1427            assert_eq!(element_attr.name(), Some("abc"));
1428            assert!(element_attr.block_style().is_none());
1429            assert!(element_attr.id().is_none());
1430            assert!(element_attr.roles().is_empty());
1431            assert!(element_attr.options().is_empty());
1432
1433            assert_eq!(offset, 4);
1434        }
1435    }
1436
1437    mod parse_with_shorthand {
1438        use crate::{
1439            attributes::{AttrlistContext, element_attribute::ParseShorthand},
1440            strings::CowStr,
1441            tests::prelude::*,
1442        };
1443
1444        #[test]
1445        fn block_style_only() {
1446            let p = Parser::default();
1447
1448            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1449                &CowStr::from("abc"),
1450                0,
1451                &p,
1452                ParseShorthand(true),
1453                AttrlistContext::Inline,
1454            );
1455
1456            assert!(warning_types.is_empty());
1457
1458            assert_eq!(
1459                element_attr,
1460                ElementAttribute {
1461                    name: None,
1462                    shorthand_items: &["abc"],
1463                    value: "abc"
1464                }
1465            );
1466
1467            assert!(element_attr.name().is_none());
1468            assert_eq!(element_attr.shorthand_items(), vec!["abc"]);
1469            assert_eq!(element_attr.block_style().unwrap(), "abc");
1470            assert!(element_attr.id().is_none());
1471            assert!(element_attr.roles().is_empty());
1472            assert!(element_attr.options().is_empty());
1473
1474            assert_eq!(offset, 3);
1475        }
1476
1477        #[test]
1478        fn ignore_if_named_attribute() {
1479            let p = Parser::default();
1480
1481            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1482                &CowStr::from("name=block_style#id"),
1483                0,
1484                &p,
1485                ParseShorthand(true),
1486                AttrlistContext::Inline,
1487            );
1488
1489            assert!(warning_types.is_empty());
1490
1491            assert_eq!(
1492                element_attr,
1493                ElementAttribute {
1494                    name: Some("name"),
1495                    shorthand_items: &[],
1496                    value: "block_style#id"
1497                }
1498            );
1499
1500            assert_eq!(element_attr.name().unwrap(), "name");
1501            assert!(element_attr.shorthand_items().is_empty());
1502            assert!(element_attr.block_style().is_none());
1503            assert!(element_attr.id().is_none());
1504            assert!(element_attr.roles().is_empty());
1505            assert!(element_attr.options().is_empty());
1506
1507            assert_eq!(offset, 19);
1508        }
1509
1510        #[test]
1511        fn error_empty_id() {
1512            let p = Parser::default();
1513
1514            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1515                &CowStr::from("abc#"),
1516                0,
1517                &p,
1518                ParseShorthand(true),
1519                AttrlistContext::Inline,
1520            );
1521
1522            assert_eq!(
1523                element_attr,
1524                ElementAttribute {
1525                    name: None,
1526                    shorthand_items: &["abc"],
1527                    value: "abc#"
1528                }
1529            );
1530
1531            assert_eq!(offset, 4);
1532            assert_eq!(warning_types, vec![WarningType::EmptyShorthandName]);
1533        }
1534
1535        #[test]
1536        fn error_duplicate_delimiter() {
1537            let p = Parser::default();
1538
1539            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1540                &CowStr::from("abc##id"),
1541                0,
1542                &p,
1543                ParseShorthand(true),
1544                AttrlistContext::Inline,
1545            );
1546
1547            assert_eq!(
1548                element_attr,
1549                ElementAttribute {
1550                    name: None,
1551                    shorthand_items: &["abc", "#id"],
1552                    value: "abc##id"
1553                }
1554            );
1555
1556            assert_eq!(offset, 7);
1557            assert_eq!(warning_types, vec![WarningType::EmptyShorthandName]);
1558        }
1559
1560        #[test]
1561        fn id_only() {
1562            let p = Parser::default();
1563
1564            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1565                &CowStr::from("#xyz"),
1566                0,
1567                &p,
1568                ParseShorthand(true),
1569                AttrlistContext::Inline,
1570            );
1571
1572            assert!(warning_types.is_empty());
1573
1574            assert_eq!(
1575                element_attr,
1576                ElementAttribute {
1577                    name: None,
1578                    shorthand_items: &["#xyz"],
1579                    value: "#xyz"
1580                }
1581            );
1582
1583            assert!(element_attr.name().is_none());
1584            assert_eq!(element_attr.shorthand_items(), vec!["#xyz"]);
1585            assert!(element_attr.block_style().is_none());
1586            assert_eq!(element_attr.id().unwrap(), "xyz");
1587            assert!(element_attr.roles().is_empty());
1588            assert!(element_attr.options().is_empty());
1589
1590            assert_eq!(offset, 4);
1591        }
1592
1593        #[test]
1594        fn one_role_only() {
1595            let p = Parser::default();
1596
1597            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1598                &CowStr::from(".role1"),
1599                0,
1600                &p,
1601                ParseShorthand(true),
1602                AttrlistContext::Inline,
1603            );
1604
1605            assert!(warning_types.is_empty());
1606
1607            assert_eq!(
1608                element_attr,
1609                ElementAttribute {
1610                    name: None,
1611                    shorthand_items: &[".role1",],
1612                    value: ".role1"
1613                }
1614            );
1615
1616            assert!(element_attr.name().is_none());
1617            assert_eq!(element_attr.shorthand_items(), vec![".role1"]);
1618            assert!(element_attr.block_style().is_none());
1619            assert!(element_attr.id().is_none());
1620            assert_eq!(element_attr.roles(), vec!("role1"));
1621            assert!(element_attr.options().is_empty());
1622
1623            assert_eq!(offset, 6);
1624        }
1625
1626        #[test]
1627        fn multiple_roles() {
1628            let p = Parser::default();
1629
1630            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1631                &CowStr::from(".role1.role2.role3"),
1632                0,
1633                &p,
1634                ParseShorthand(true),
1635                AttrlistContext::Inline,
1636            );
1637
1638            assert!(warning_types.is_empty());
1639
1640            assert_eq!(
1641                element_attr,
1642                ElementAttribute {
1643                    name: None,
1644                    shorthand_items: &[".role1", ".role2", ".role3"],
1645                    value: ".role1.role2.role3"
1646                }
1647            );
1648
1649            assert!(element_attr.name().is_none());
1650
1651            assert_eq!(
1652                element_attr.shorthand_items(),
1653                vec![".role1", ".role2", ".role3"]
1654            );
1655
1656            assert!(element_attr.block_style().is_none());
1657            assert!(element_attr.id().is_none());
1658            assert_eq!(element_attr.roles(), vec!("role1", "role2", "role3",));
1659            assert!(element_attr.options().is_empty());
1660
1661            assert_eq!(offset, 18);
1662        }
1663
1664        #[test]
1665        fn one_option_only() {
1666            let p = Parser::default();
1667
1668            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1669                &CowStr::from("%option1"),
1670                0,
1671                &p,
1672                ParseShorthand(true),
1673                AttrlistContext::Inline,
1674            );
1675
1676            assert!(warning_types.is_empty());
1677
1678            assert_eq!(
1679                element_attr,
1680                ElementAttribute {
1681                    name: None,
1682                    shorthand_items: &["%option1"],
1683                    value: "%option1"
1684                }
1685            );
1686
1687            assert!(element_attr.name().is_none());
1688            assert_eq!(element_attr.shorthand_items(), vec!["%option1"]);
1689            assert!(element_attr.block_style().is_none());
1690            assert!(element_attr.id().is_none());
1691            assert!(element_attr.roles().is_empty());
1692            assert_eq!(element_attr.options(), vec!("option1"));
1693
1694            assert_eq!(offset, 8);
1695        }
1696
1697        #[test]
1698        fn multiple_options() {
1699            let p = Parser::default();
1700
1701            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1702                &CowStr::from("%option1%option2%option3"),
1703                0,
1704                &p,
1705                ParseShorthand(true),
1706                AttrlistContext::Inline,
1707            );
1708
1709            assert!(warning_types.is_empty());
1710
1711            assert_eq!(
1712                element_attr,
1713                ElementAttribute {
1714                    name: None,
1715                    shorthand_items: &["%option1", "%option2", "%option3"],
1716                    value: "%option1%option2%option3"
1717                }
1718            );
1719
1720            assert!(element_attr.name().is_none());
1721
1722            assert_eq!(
1723                element_attr.shorthand_items(),
1724                vec!["%option1", "%option2", "%option3"]
1725            );
1726
1727            assert!(element_attr.block_style().is_none());
1728            assert!(element_attr.id().is_none());
1729            assert!(element_attr.roles().is_empty());
1730            assert_eq!(
1731                element_attr.options(),
1732                vec!("option1", "option2", "option3")
1733            );
1734
1735            assert_eq!(offset, 24);
1736        }
1737
1738        #[test]
1739        fn block_style_and_id() {
1740            let p = Parser::default();
1741
1742            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1743                &CowStr::from("appendix#custom-id"),
1744                0,
1745                &p,
1746                ParseShorthand(true),
1747                AttrlistContext::Inline,
1748            );
1749
1750            assert!(warning_types.is_empty());
1751
1752            assert_eq!(
1753                element_attr,
1754                ElementAttribute {
1755                    name: None,
1756                    shorthand_items: &["appendix", "#custom-id"],
1757                    value: "appendix#custom-id"
1758                }
1759            );
1760
1761            assert!(element_attr.name().is_none());
1762            assert_eq!(
1763                element_attr.shorthand_items(),
1764                vec!["appendix", "#custom-id"]
1765            );
1766            assert_eq!(element_attr.block_style().unwrap(), "appendix",);
1767            assert_eq!(element_attr.id().unwrap(), "custom-id",);
1768            assert!(element_attr.roles().is_empty());
1769            assert!(element_attr.options().is_empty());
1770
1771            assert_eq!(offset, 18);
1772        }
1773
1774        #[test]
1775        fn id_role_and_option() {
1776            let p = Parser::default();
1777
1778            let (element_attr, offset, warning_types) = crate::attributes::ElementAttribute::parse(
1779                &CowStr::from("#rules.prominent%incremental"),
1780                0,
1781                &p,
1782                ParseShorthand(true),
1783                AttrlistContext::Inline,
1784            );
1785
1786            assert!(warning_types.is_empty());
1787
1788            assert_eq!(
1789                element_attr,
1790                ElementAttribute {
1791                    name: None,
1792                    shorthand_items: &["#rules", ".prominent", "%incremental"],
1793                    value: "#rules.prominent%incremental"
1794                }
1795            );
1796
1797            assert!(element_attr.name().is_none());
1798
1799            assert_eq!(
1800                element_attr.shorthand_items(),
1801                vec!["#rules", ".prominent", "%incremental"]
1802            );
1803
1804            assert!(element_attr.block_style().is_none());
1805            assert_eq!(element_attr.id().unwrap(), "rules");
1806            assert_eq!(element_attr.roles(), vec!("prominent"));
1807            assert_eq!(element_attr.options(), vec!("incremental"));
1808
1809            assert_eq!(offset, 28);
1810        }
1811    }
1812
1813    mod merge_block_style_shorthand {
1814        use crate::{
1815            attributes::{AttrlistContext, element_attribute::ParseShorthand},
1816            strings::CowStr,
1817            tests::prelude::*,
1818        };
1819
1820        fn parse(value: &str) -> crate::attributes::ElementAttribute<'_> {
1821            let p = Parser::default();
1822            crate::attributes::ElementAttribute::parse(
1823                &CowStr::from(value),
1824                0,
1825                &p,
1826                ParseShorthand(true),
1827                AttrlistContext::Block,
1828            )
1829            .0
1830        }
1831
1832        #[test]
1833        fn merges_disjoint_shorthand() {
1834            let earlier = parse("#myid");
1835            let later = parse(".myrole");
1836
1837            let merged =
1838                crate::attributes::ElementAttribute::merge_block_style_shorthand(&earlier, &later);
1839
1840            assert!(merged.name().is_none());
1841            assert_eq!(merged.id().unwrap(), "myid");
1842            assert_eq!(merged.roles(), vec!["myrole"]);
1843        }
1844
1845        #[test]
1846        fn two_empty_positionals_stay_empty() {
1847            // Merging two empty first positionals exercises the empty-value
1848            // branch, which produces an attribute with no shorthand items.
1849            let earlier = parse("");
1850            let later = parse("");
1851
1852            let merged =
1853                crate::attributes::ElementAttribute::merge_block_style_shorthand(&earlier, &later);
1854
1855            assert_eq!(
1856                merged,
1857                ElementAttribute {
1858                    name: None,
1859                    shorthand_items: &[],
1860                    value: "",
1861                }
1862            );
1863
1864            assert!(merged.name().is_none());
1865            assert!(merged.block_style().is_none());
1866            assert!(merged.id().is_none());
1867            assert!(merged.roles().is_empty());
1868            assert!(merged.options().is_empty());
1869        }
1870    }
1871
1872    mod without_shorthand_roles {
1873        use crate::{
1874            attributes::{AttrlistContext, element_attribute::ParseShorthand},
1875            strings::CowStr,
1876            tests::prelude::*,
1877        };
1878
1879        fn parse(value: &str) -> crate::attributes::ElementAttribute<'_> {
1880            let p = Parser::default();
1881            crate::attributes::ElementAttribute::parse(
1882                &CowStr::from(value),
1883                0,
1884                &p,
1885                ParseShorthand(true),
1886                AttrlistContext::Block,
1887            )
1888            .0
1889        }
1890
1891        #[test]
1892        fn drops_roles_keeps_style_id_and_options() {
1893            let attr = parse("sidebar#myid.role1.role2%opt1");
1894            let stripped = attr.without_shorthand_roles();
1895
1896            assert_eq!(stripped.block_style().unwrap(), "sidebar");
1897            assert_eq!(stripped.id().unwrap(), "myid");
1898            assert!(stripped.roles().is_empty());
1899            assert_eq!(stripped.options(), vec!["opt1"]);
1900        }
1901
1902        #[test]
1903        fn returns_clone_when_no_roles_present() {
1904            // With no role shorthand to strip, the attribute is returned as-is.
1905            let attr = parse("sidebar#myid%opt1");
1906            let stripped = attr.without_shorthand_roles();
1907
1908            assert_eq!(stripped, attr);
1909        }
1910
1911        #[test]
1912        fn roles_only_becomes_empty() {
1913            let attr = parse(".role1.role2");
1914            let stripped = attr.without_shorthand_roles();
1915
1916            assert!(stripped.block_style().is_none());
1917            assert!(stripped.id().is_none());
1918            assert!(stripped.roles().is_empty());
1919            assert!(stripped.options().is_empty());
1920        }
1921    }
1922}