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