asciidoc_parser/attributes/element_attribute.rs
1use crate::{
2 Parser, Span,
3 attributes::AttrlistContext,
4 content::{Content, SubstitutionGroup},
5 span::MatchedItem,
6 strings::CowStr,
7 warnings::WarningType,
8};
9
10/// This struct represents a single element attribute.
11///
12/// Element attributes define the built-in and user-defined settings and
13/// metadata that can be applied to an individual block element or inline
14/// element in a document (including macros). Although the include directive is
15/// not technically an element, element attributes can also be defined on an
16/// include directive.
17#[derive(Clone, Debug, Eq, PartialEq)]
18pub struct ElementAttribute<'src> {
19 name: Option<CowStr<'src>>,
20 value: CowStr<'src>,
21 shorthand_item_indices: Vec<usize>,
22}
23
24impl<'src> ElementAttribute<'src> {
25 pub(crate) fn parse(
26 source_text: &CowStr<'src>,
27 start_index: usize,
28 parser: &Parser,
29 mut parse_shorthand: ParseShorthand,
30 attrlist_context: AttrlistContext,
31 ) -> (Self, usize, Vec<WarningType>) {
32 let mut warnings: Vec<WarningType> = vec![];
33
34 let (name, value, shorthand_item_indices, offset) = {
35 let mut source = Span::new(source_text.as_ref());
36 source = source.discard(start_index);
37
38 let (name, after): (Option<Span<'_>>, Span) = match source.take_attr_name() {
39 Some(name) => {
40 let space = name.after.take_whitespace_with_newline();
41 match space.after.take_prefix("=") {
42 Some(equals) => {
43 let space = equals.after.take_whitespace_with_newline();
44 if space.after.is_empty() || space.after.starts_with(',') {
45 // TO DO: Is this a warning? Possible spec ambiguity.
46 (None, source)
47 } else {
48 (Some(name.item), space.after)
49 }
50 }
51 None => (None, source),
52 }
53 }
54 None => (None, source),
55 };
56
57 let after = after.take_whitespace_with_newline().after;
58 let first_char = after.data().chars().next();
59
60 let value = match first_char {
61 Some('\'') | Some('"') => match after.take_quoted_string() {
62 Some(v) => {
63 parse_shorthand = ParseShorthand(false);
64 v
65 }
66 None => {
67 warnings.push(WarningType::AttributeValueMissingTerminatingQuote);
68 after.take_while(|c| c != ',').trim_item_trailing_spaces()
69 }
70 },
71 _ => after.take_while(|c| c != ',').trim_item_trailing_spaces(),
72 };
73
74 let after = value.after;
75 let mut value = cowstr_from_source_and_span(source_text, &value.item);
76
77 if let Some(first) = first_char
78 && (first == '\'' || first == '\"')
79 {
80 let escaped_quote = format!("\\{first}");
81 let mut new_value = value.replace(&escaped_quote, &first.to_string());
82
83 if first == '\'' && attrlist_context == AttrlistContext::Block {
84 let span = Span::new(&new_value);
85 let mut content = Content::from(span);
86 SubstitutionGroup::Normal.apply(&mut content, parser, None);
87
88 if content.rendered.as_ref() != new_value {
89 new_value = content.rendered.to_string();
90 }
91 }
92
93 if new_value != *value {
94 value = CowStr::from(new_value);
95 }
96 }
97
98 let shorthand_item_indices = if name.is_none() && parse_shorthand.0 {
99 parse_shorthand_items(&value, &mut warnings)
100 } else {
101 vec![]
102 };
103
104 let name = name.map(|name| cowstr_from_source_and_span(source_text, &name));
105
106 (name, value, shorthand_item_indices, after.byte_offset())
107 };
108
109 (
110 Self {
111 name,
112 value,
113 shorthand_item_indices,
114 },
115 offset,
116 warnings,
117 )
118 }
119
120 /// Return the attribute name, if one was found`.
121 pub fn name(&'src self) -> Option<&'src str> {
122 if let Some(ref name) = self.name {
123 Some(name.as_ref())
124 } else {
125 None
126 }
127 }
128
129 /// Return the shorthand items, if applicable.
130 ///
131 /// Shorthand items are only parsed for certain element attributes. If this
132 /// attribute is not of the appropriate kind, this will return an empty
133 /// list.
134 pub fn shorthand_items(&'src self) -> Vec<&'src str> {
135 let mut result = vec![];
136 let value = self.value.as_ref();
137
138 let mut iter = self.shorthand_item_indices.iter().peekable();
139
140 loop {
141 let Some(curr) = iter.next() else { break };
142 let mut next_item = if let Some(next) = iter.peek() {
143 &value[*curr..**next]
144 } else {
145 &value[*curr..]
146 };
147
148 if next_item == "#" || next_item == "." || next_item == "%" {
149 continue;
150 }
151
152 next_item = next_item.trim_end();
153
154 if !next_item.is_empty() {
155 result.push(next_item);
156 }
157 }
158
159 result
160 }
161
162 /// Return the block style name from shorthand syntax.
163 pub fn block_style(&'src self) -> Option<&'src str> {
164 self.shorthand_items()
165 .first()
166 .filter(|v| !v.chars().any(is_shorthand_delimiter))
167 .cloned()
168 }
169
170 /// Return the ID attribute from shorthand syntax.
171 ///
172 /// If multiple ID attributes were specified, only the first
173 /// match is returned. (Multiple IDs are not supported.)
174 ///
175 /// You can assign an ID to a block using the shorthand syntax, the longhand
176 /// syntax, or a legacy block anchor.
177 ///
178 /// In the shorthand syntax, you prefix the name with a hash (`#`) in the
179 /// first position attribute:
180 ///
181 /// ```asciidoc
182 /// [#goals]
183 /// * Goal 1
184 /// * Goal 2
185 /// ```
186 ///
187 /// In the longhand syntax, you use a standard named attribute:
188 ///
189 /// ```asciidoc
190 /// [id=goals]
191 /// * Goal 1
192 /// * Goal 2
193 /// ```
194 ///
195 /// In the legacy block anchor syntax, you surround the name with double
196 /// square brackets:
197 ///
198 /// ```asciidoc
199 /// [[goals]]
200 /// * Goal 1
201 /// * Goal 2
202 /// ```
203 pub fn id(&'src self) -> Option<&'src str> {
204 self.shorthand_items()
205 .iter()
206 .find(|v| v.starts_with('#'))
207 .map(|v| &v[1..])
208 }
209
210 /// Return any role attributes that were found in shorthand syntax.
211 ///
212 /// You can assign one or more roles to blocks and most inline elements
213 /// using the `role` attribute. The `role` attribute is a [named attribute].
214 /// Even though the attribute name is singular, it may contain multiple
215 /// (space-separated) roles. Roles may also be defined using a shorthand
216 /// (dot-prefixed) syntax.
217 ///
218 /// A role:
219 /// 1. adds additional semantics to an element
220 /// 2. can be used to apply additional styling to a group of elements (e.g.,
221 /// via a CSS class selector)
222 /// 3. may activate additional behavior if recognized by the converter
223 ///
224 /// **TIP:** The `role` attribute in AsciiDoc always get mapped to the
225 /// `class` attribute in the HTML output. In other words, role names are
226 /// synonymous with HTML class names, thus allowing output elements to be
227 /// identified and styled in CSS using class selectors (e.g.,
228 /// `sidebarblock.role1`).
229 ///
230 /// [named attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/#named
231 pub fn roles(&'src self) -> Vec<&'src str> {
232 self.shorthand_items()
233 .iter()
234 .filter(|span| span.starts_with('.'))
235 .map(|span| &span[1..])
236 .collect()
237 }
238
239 /// Return any option attributes that were found in shorthand syntax.
240 ///
241 /// The `options` attribute (often abbreviated as `opts`) is a versatile
242 /// [named attribute] that can be assigned one or more values. It can be
243 /// defined globally as document attribute as well as a block attribute on
244 /// an individual block.
245 ///
246 /// There is no strict schema for options. Any options which are not
247 /// recognized are ignored.
248 ///
249 /// You can assign one or more options to a block using the shorthand or
250 /// formal syntax for the options attribute.
251 ///
252 /// # Shorthand options syntax for blocks
253 ///
254 /// To assign an option to a block, prefix the value with a percent sign
255 /// (`%`) in an attribute list. The percent sign implicitly sets the
256 /// `options` attribute.
257 ///
258 /// ## Example 1: Sidebar block with an option assigned using the shorthand dot
259 ///
260 /// ```asciidoc
261 /// [%option]
262 /// ****
263 /// This is a sidebar with an option assigned to it, named option.
264 /// ****
265 /// ```
266 ///
267 /// You can assign multiple options to a block by prest
268 /// fixing each value with
269 /// a percent sign (`%`).
270 ///
271 /// ## Example 2: Sidebar with two options assigned using the shorthand dot
272 /// ```asciidoc
273 /// [%option1%option2]
274 /// ****
275 /// This is a sidebar with two options assigned to it, named option1 and option2.
276 /// ****
277 /// ```
278 ///
279 /// # Formal options syntax for blocks
280 ///
281 /// Explicitly set `options` or `opts`, followed by the equals sign (`=`),
282 /// and then the value in an attribute list.
283 ///
284 /// ## Example 3. Sidebar block with an option assigned using the formal syntax
285 /// ```asciidoc
286 /// [opts=option]
287 /// ****
288 /// This is a sidebar with an option assigned to it, named option.
289 /// ****
290 /// ```
291 ///
292 /// Separate multiple option values with commas (`,`).
293 ///
294 /// ## Example 4. Sidebar with three options assigned using the formal syntax
295 /// ```asciidoc
296 /// [opts="option1,option2"]
297 /// ****
298 /// This is a sidebar with two options assigned to it, option1 and option2.
299 /// ****
300 /// ```
301 ///
302 /// [named attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/#named
303 pub fn options(&'src self) -> Vec<&'src str> {
304 self.shorthand_items()
305 .iter()
306 .filter(|v| v.starts_with('%'))
307 .map(|v| &v[1..])
308 .collect()
309 }
310
311 /// Return the attribute's value.
312 ///
313 /// Note that this value will have had special characters and attribute
314 /// value replacements applied to it.
315 pub fn value(&'src self) -> &'src str {
316 self.value.as_ref()
317 }
318}
319
320fn parse_shorthand_items(source: &str, warnings: &mut Vec<WarningType>) -> Vec<usize> {
321 let mut shorthand_item_indices: Vec<usize> = vec![];
322 let mut span = Span::new(source);
323
324 // Look for block style selector.
325 if let Some(block_style_pr) = span.split_at_match_non_empty(is_shorthand_delimiter) {
326 shorthand_item_indices.push(block_style_pr.item.discard_whitespace().byte_offset());
327
328 span = block_style_pr.after;
329 }
330
331 while !span.is_empty() {
332 // Assumption: First character is a delimiter.
333 let after_delimiter = span.discard(1);
334
335 match after_delimiter.position(is_shorthand_delimiter) {
336 None => {
337 if after_delimiter.is_empty() {
338 warnings.push(WarningType::EmptyShorthandItem);
339 shorthand_item_indices.push(span.byte_offset());
340 span = after_delimiter;
341 } else {
342 shorthand_item_indices.push(span.byte_offset());
343 span = span.discard_all();
344 }
345 }
346
347 Some(0) => {
348 shorthand_item_indices.push(span.byte_offset());
349 warnings.push(WarningType::EmptyShorthandItem);
350 span = after_delimiter;
351 }
352
353 Some(index) => {
354 let mi: MatchedItem<Span> = span.into_parse_result(index + 1);
355 shorthand_item_indices.push(span.byte_offset());
356 span = mi.after;
357 }
358 }
359 }
360
361 shorthand_item_indices
362}
363
364fn is_shorthand_delimiter(c: char) -> bool {
365 c == '#' || c == '%' || c == '.'
366}
367
368#[derive(Clone, Debug)]
369pub(crate) struct ParseShorthand(pub bool);
370
371fn cowstr_from_source_and_span<'src>(source: &CowStr<'src>, span: &Span<'_>) -> CowStr<'src> {
372 if let CowStr::Borrowed(source) = source {
373 let borrowed: Span<'src> = Span::new(source)
374 .discard(span.byte_offset())
375 .slice_to(..span.len());
376
377 CowStr::Borrowed(borrowed.data())
378 } else {
379 CowStr::from(span.data().to_string())
380 }
381}