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