asciidoc_parser/attributes/attrlist.rs
1use std::slice::Iter;
2
3use crate::{
4 HasSpan, Parser, Span,
5 attributes::{ElementAttribute, element_attribute::ParseShorthand},
6 content::{Content, SubstitutionStep},
7 span::MatchedItem,
8 strings::CowStr,
9 warnings::{MatchAndWarnings, Warning, WarningType},
10};
11
12/// The source text that’s used to define attributes for an element is referred
13/// to as an **attrlist.** An attrlist is always enclosed in a pair of square
14/// brackets. This applies for block attributes as well as attributes on a block
15/// or inline macro. The processor splits the attrlist into individual attribute
16/// entries, determines whether each entry is a positional or named attribute,
17/// parses the entry accordingly, and assigns the result as an attribute on the
18/// node.
19#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct Attrlist<'src> {
21 attributes: Vec<ElementAttribute<'src>>,
22 source: Span<'src>,
23}
24
25impl<'src> Attrlist<'src> {
26 /// **IMPORTANT:** This `source` span passed to this function should NOT
27 /// include the opening or closing square brackets for the attrlist.
28 /// This is because the rules for closing brackets differ when parsing
29 /// inline, macro, and block elements.
30 pub(crate) fn parse(
31 source: Span<'src>,
32 parser: &Parser,
33 ) -> MatchAndWarnings<'src, MatchedItem<'src, Self>> {
34 let mut attributes: Vec<ElementAttribute> = vec![];
35 let mut parse_shorthand_items = true;
36 let mut warnings: Vec<Warning<'src>> = vec![];
37
38 // Apply attribute value substitutions before parsing attrlist content.
39 let source_cow = if source.contains('{') && source.contains('}') {
40 let mut content = Content::from(source);
41 SubstitutionStep::AttributeReferences.apply(&mut content, parser, None);
42 CowStr::from(content.rendered.to_string())
43 } else {
44 CowStr::from(source.data())
45 };
46
47 if source_cow.starts_with('[') && source_cow.ends_with(']') {
48 todo!("Parse block anchor syntax (issue #122)");
49 }
50
51 let mut index = 0;
52
53 let after_index = loop {
54 let (maybe_attr, warning_types) = ElementAttribute::parse(
55 &source_cow,
56 index,
57 parser,
58 ParseShorthand(parse_shorthand_items),
59 );
60
61 // Because we do attribute value substitution early on in parsing, we can't
62 // pinpoint the exact location of warnings in an attribute list. For that
63 // reason, individual attribute parsing only returns the warning type and we
64 // then map it back to the entire attrlist source.
65 for warning_type in warning_types {
66 warnings.push(Warning {
67 source,
68 warning: warning_type,
69 });
70 }
71
72 let Some((attr, new_index)) = maybe_attr else {
73 break index;
74 };
75
76 if attr.name().is_none() {
77 parse_shorthand_items = false;
78 }
79
80 let mut after = Span::new(source_cow.as_ref()).discard(new_index);
81
82 if attr.name().is_none()
83 && attr.value().is_empty()
84 && after.is_empty()
85 && attributes.is_empty()
86 {
87 break index;
88 }
89
90 attributes.push(attr);
91
92 after = after.take_whitespace().after;
93
94 match after.take_prefix(",") {
95 Some(comma) => {
96 after = comma.after.take_whitespace().after;
97
98 if after.starts_with(',') {
99 warnings.push(Warning {
100 source,
101 warning: WarningType::EmptyAttributeValue,
102 });
103 after = after.discard(1);
104 index = after.byte_offset();
105 continue;
106 }
107
108 index = after.byte_offset();
109 }
110 None => {
111 break after.byte_offset();
112 }
113 }
114 };
115
116 if after_index < source_cow.len() {
117 warnings.push(Warning {
118 source,
119 warning: WarningType::MissingCommaAfterQuotedAttributeValue,
120 });
121 }
122
123 MatchAndWarnings {
124 item: MatchedItem {
125 item: Self { attributes, source },
126 after: source.discard_all(),
127 },
128 warnings,
129 }
130 }
131
132 /// Returns an iterator over the attributes contained within
133 /// this attrlist.
134 pub fn attributes(&'src self) -> Iter<'src, ElementAttribute<'src>> {
135 self.attributes.iter()
136 }
137
138 /// Returns the first attribute with the given name.
139 pub fn named_attribute(&'src self, name: &str) -> Option<&'src ElementAttribute<'src>> {
140 self.attributes.iter().find(|attr| {
141 if let Some(attr_name) = attr.name() {
142 attr_name == name
143 } else {
144 false
145 }
146 })
147 }
148
149 /// Returns the given (1-based) positional attribute.
150 ///
151 /// **IMPORTANT:** Named attributes with names are disregarded when
152 /// counting.
153 pub fn nth_attribute(&'src self, n: usize) -> Option<&'src ElementAttribute<'src>> {
154 if n == 0 {
155 None
156 } else {
157 self.attributes
158 .iter()
159 .filter(|attr| attr.name().is_none())
160 .nth(n - 1)
161 }
162 }
163
164 /// Returns the first attribute with the given name or (1-based) index.
165 ///
166 /// Some block and macro types provide implicit mappings between attribute
167 /// names and positions to permit a shorthand syntax.
168 ///
169 /// This method will search by name first, and fall back to positional
170 /// indexing if the name doesn't yield a match.
171 pub fn named_or_positional_attribute(
172 &'src self,
173 name: &str,
174 index: usize,
175 ) -> Option<&'src ElementAttribute<'src>> {
176 self.named_attribute(name)
177 .or_else(|| self.nth_attribute(index))
178 }
179
180 /// Returns the ID attribute (if any).
181 ///
182 /// You can assign an ID to a block using the shorthand syntax, the longhand
183 /// syntax, or a legacy block anchor.
184 ///
185 /// In the shorthand syntax, you prefix the name with a hash (`#`) in the
186 /// first position attribute:
187 ///
188 /// ```asciidoc
189 /// [#goals]
190 /// * Goal 1
191 /// * Goal 2
192 /// ```
193 ///
194 /// In the longhand syntax, you use a standard named attribute:
195 ///
196 /// ```asciidoc
197 /// [id=goals]
198 /// * Goal 1
199 /// * Goal 2
200 /// ```
201 ///
202 /// In the legacy block anchor syntax, you surround the name with double
203 /// square brackets:
204 ///
205 /// ```asciidoc
206 /// [[goals]]
207 /// * Goal 1
208 /// * Goal 2
209 /// ```
210 pub fn id(&'src self) -> Option<&'src str> {
211 self.nth_attribute(1)
212 .and_then(|attr1| attr1.id())
213 .or_else(|| self.named_attribute("id").map(|attr| attr.value()))
214 }
215
216 /// Returns any role attributes that were found.
217 ///
218 /// You can assign one or more roles to blocks and most inline elements
219 /// using the `role` attribute. The `role` attribute is a [named attribute].
220 /// Even though the attribute name is singular, it may contain multiple
221 /// (space-separated) roles. Roles may also be defined using a shorthand
222 /// (dot-prefixed) syntax.
223 ///
224 /// A role:
225 /// 1. adds additional semantics to an element
226 /// 2. can be used to apply additional styling to a group of elements (e.g.,
227 /// via a CSS class selector)
228 /// 3. may activate additional behavior if recognized by the converter
229 ///
230 /// **TIP:** The `role` attribute in AsciiDoc always get mapped to the
231 /// `class` attribute in the HTML output. In other words, role names are
232 /// synonymous with HTML class names, thus allowing output elements to be
233 /// identified and styled in CSS using class selectors (e.g.,
234 /// `sidebarblock.role1`).
235 ///
236 /// [named attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/#named
237 pub fn roles(&'src self) -> Vec<&'src str> {
238 let mut roles = self
239 .nth_attribute(1)
240 .map(|attr1| attr1.roles())
241 .unwrap_or_default();
242
243 if let Some(role_attr) = self.named_attribute("role") {
244 let mut role_span = Span::new(role_attr.value());
245 let mut formal_roles: Vec<&'src str> = vec![];
246 role_span = role_span.take_while(|c| c == ' ').after;
247
248 while !role_span.is_empty() {
249 let mi = role_span.take_while(|c| c != ' ');
250 if !mi.item.is_empty() {
251 formal_roles.push(mi.item.data());
252 }
253 role_span = mi.after.take_while(|c| c == ' ').after;
254 }
255
256 roles.append(&mut formal_roles);
257 }
258
259 roles
260 }
261
262 /// Returns any option attributes that were found.
263 ///
264 /// The `options` attribute (often abbreviated as `opts`) is a versatile
265 /// [named attribute] that can be assigned one or more values. It can be
266 /// defined globally as document attribute as well as a block attribute on
267 /// an individual block.
268 ///
269 /// There is no strict schema for options. Any options which are not
270 /// recognized are ignored.
271 ///
272 /// You can assign one or more options to a block using the shorthand or
273 /// formal syntax for the options attribute.
274 ///
275 /// # Shorthand options syntax for blocks
276 ///
277 /// To assign an option to a block, prefix the value with a percent sign
278 /// (`%`) in an attribute list. The percent sign implicitly sets the
279 /// `options` attribute.
280 ///
281 /// ## Example 1: Sidebar block with an option assigned using the shorthand dot
282 ///
283 /// ```asciidoc
284 /// [%option]
285 /// ****
286 /// This is a sidebar with an option assigned to it, named option.
287 /// ****
288 /// ```
289 ///
290 /// You can assign multiple options to a block by prefixing each value with
291 /// a percent sign (`%`).
292 ///
293 /// ## Example 2: Sidebar with two options assigned using the shorthand dot
294 /// ```asciidoc
295 /// [%option1%option2]
296 /// ****
297 /// This is a sidebar with two options assigned to it, named option1 and option2.
298 /// ****
299 /// ```
300 ///
301 /// # Formal options syntax for blocks
302 ///
303 /// Explicitly set `options` or `opts`, followed by the equals sign (`=`),
304 /// and then the value in an attribute list.
305 ///
306 /// ## Example 3. Sidebar block with an option assigned using the formal syntax
307 /// ```asciidoc
308 /// [opts=option]
309 /// ****
310 /// This is a sidebar with an option assigned to it, named option.
311 /// ****
312 /// ```
313 ///
314 /// Separate multiple option values with commas (`,`).
315 ///
316 /// ## Example 4. Sidebar with three options assigned using the formal syntax
317 /// ```asciidoc
318 /// [opts="option1,option2"]
319 /// ****
320 /// This is a sidebar with two options assigned to it, option1 and option2.
321 /// ****
322 /// ```
323 ///
324 /// [named attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/#named
325 pub fn options(&'src self) -> Vec<&'src str> {
326 let mut options = self
327 .nth_attribute(1)
328 .map(|attr1| attr1.options())
329 .unwrap_or_default();
330
331 if let Some(option_attr) = self.named_attribute("opts") {
332 let mut option_span = Span::new(option_attr.value());
333 let mut formal_options: Vec<&'src str> = vec![];
334 option_span = option_span.take_while(|c| c == ',').after;
335
336 while !option_span.is_empty() {
337 let mi = option_span.take_while(|c| c != ',');
338 if !mi.item.is_empty() {
339 formal_options.push(mi.item.data());
340 }
341 option_span = mi.after.take_while(|c| c == ',').after;
342 }
343
344 options.append(&mut formal_options);
345 }
346
347 if let Some(option_attr) = self.named_attribute("options") {
348 let mut option_span = Span::new(option_attr.value());
349 let mut formal_options: Vec<&'_ str> = vec![];
350 option_span = option_span.take_while(|c| c == ',').after;
351
352 while !option_span.is_empty() {
353 let mi = option_span.take_while(|c| c != ',');
354 if !mi.item.is_empty() {
355 formal_options.push(mi.item.data());
356 }
357 option_span = mi.after.take_while(|c| c == ',').after;
358 }
359
360 options.append(&mut formal_options);
361 }
362
363 options
364 }
365
366 /// Returns `true` if this attribute list has the named option.
367 ///
368 /// See [`options()`] for a description of option syntax.
369 ///
370 /// [`options()`]: Self::options
371 pub fn has_option<N: AsRef<str>>(&'src self, name: N) -> bool {
372 // PERF: Might help to optimize away the construction of the options Vec.
373 let options = self.options();
374 let name = name.as_ref();
375 options.contains(&name)
376 }
377}
378
379impl<'src> HasSpan<'src> for Attrlist<'src> {
380 fn span(&self) -> Span<'src> {
381 self.source
382 }
383}