asciidoc_parser/attributes/
attrlist.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
use std::{ops::Deref, slice::Iter};

use crate::{
    attributes::ElementAttribute,
    span::MatchedItem,
    warnings::{MatchAndWarnings, Warning, WarningType},
    HasSpan, Span,
};

/// The source text that’s used to define attributes for an element is referred
/// to as an attrlist. An attrlist is always enclosed in a pair of square
/// brackets. This applies for block attributes as well as attributes on a block
/// or inline macro. The processor splits the attrlist into individual attribute
/// entries, determines whether each entry is a positional or named attribute,
/// parses the entry accordingly, and assigns the result as an attribute on the
/// node.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Attrlist<'src> {
    attributes: Vec<ElementAttribute<'src>>,
    source: Span<'src>,
}

impl<'src> Attrlist<'src> {
    /// IMPORTANT: This `source` span passed to this function should NOT include
    /// the opening or closing square brackets for the attrlist. This is because
    /// the rules for closing brackets differ when parsing inline, macro, and
    /// block elements.
    pub(crate) fn parse(source: Span<'src>) -> MatchAndWarnings<'src, MatchedItem<'src, Self>> {
        let mut after = source;
        let mut attributes: Vec<ElementAttribute> = vec![];
        let mut parse_shorthand_items = true;
        let mut warnings: Vec<Warning<'src>> = vec![];

        if source.starts_with('[') && source.ends_with(']') {
            todo!("Parse block anchor syntax (issue #122)");
        }

        loop {
            let mut maybe_attr_and_warnings = if parse_shorthand_items {
                ElementAttribute::parse_with_shorthand(after)
            } else {
                ElementAttribute::parse(after)
            };

            if !maybe_attr_and_warnings.warnings.is_empty() {
                warnings.append(&mut maybe_attr_and_warnings.warnings);
            }

            let maybe_attr = maybe_attr_and_warnings.item;
            let Some(attr) = maybe_attr else {
                break;
            };

            if attr.item.name().is_none() {
                parse_shorthand_items = false;
            }

            attributes.push(attr.item);

            after = attr.after.take_whitespace().after;
            match after.take_prefix(",") {
                Some(comma) => {
                    after = comma.after.take_whitespace().after;
                    if after.starts_with(",") {
                        warnings.push(Warning {
                            source: comma.item,
                            warning: WarningType::EmptyAttributeValue,
                        });
                        after = after.discard(1);
                        continue;
                    }
                }
                None => {
                    break;
                }
            }
        }

        if !after.is_empty() {
            warnings.push(Warning {
                source: after,
                warning: WarningType::MissingCommaAfterQuotedAttributeValue,
            });

            after = after.discard_all();
        }

        MatchAndWarnings {
            item: MatchedItem {
                item: Self { attributes, source },
                after,
            },
            warnings,
        }
    }

    /// Returns an iterator over the attributes contained within
    /// this attrlist.
    pub fn attributes(&'src self) -> Iter<'src, ElementAttribute<'src>> {
        self.attributes.iter()
    }

    /// Returns the first attribute with the given name.
    pub fn named_attribute(&'src self, name: &str) -> Option<&'src ElementAttribute<'src>> {
        self.attributes.iter().find(|attr| {
            if let Some(attr_name) = attr.name() {
                attr_name.deref() == &name
            } else {
                false
            }
        })
    }

    /// Returns the given (1-based) positional attribute.
    ///
    /// IMPORTANT: Named attributes with names are disregarded when counting.
    pub fn nth_attribute(&'src self, n: usize) -> Option<&'src ElementAttribute<'src>> {
        if n == 0 {
            None
        } else {
            self.attributes
                .iter()
                .filter(|attr| attr.name().is_none())
                .nth(n - 1)
        }
    }

    /// Returns the first attribute with the given name or index.
    ///
    /// Some block and macro types provide implicit mappings between attribute
    /// names and positions to permit a shorthand syntax.
    ///
    /// This method will search by name first, and fall back to positional
    /// indexing if the name doesn't yield a match.
    pub fn named_or_positional_attribute(
        &'src self,
        name: &str,
        index: usize,
    ) -> Option<&'src ElementAttribute<'src>> {
        self.named_attribute(name)
            .or_else(|| self.nth_attribute(index))
    }

    /// Returns the ID attribute (if any).
    ///
    /// You can assign an ID to a block using the shorthand syntax, the longhand
    /// syntax, or a legacy block anchor.
    ///
    /// In the shorthand syntax, you prefix the name with a hash (`#`) in the
    /// first position attribute:
    ///
    /// ```ignore
    /// [#goals]
    /// * Goal 1
    /// * Goal 2
    /// ```
    ///
    /// In the longhand syntax, you use a standard named attribute:
    ///
    /// ```ignore
    /// [id=goals]
    /// * Goal 1
    /// * Goal 2
    /// ```
    ///
    /// In the legacy block anchor syntax, you surround the name with double
    /// square brackets:
    ///
    /// ```ignore
    /// [[goals]]
    /// * Goal 1
    /// * Goal 2
    /// ```
    pub fn id(&'src self) -> Option<Span<'src>> {
        self.nth_attribute(1)
            .and_then(|attr1| attr1.id())
            .or_else(|| self.named_attribute("id").map(|attr| attr.raw_value()))
    }
}

impl<'src> HasSpan<'src> for Attrlist<'src> {
    fn span(&'src self) -> &'src Span<'src> {
        &self.source
    }
}