asciidoc_parser/blocks/is_block.rs
1use std::{fmt::Debug, slice::Iter};
2
3use crate::{
4 Span,
5 attributes::Attrlist,
6 blocks::{Block, is_built_in_context},
7 content::SubstitutionGroup,
8 strings::CowStr,
9};
10
11/// **Block elements** form the main structure of an AsciiDoc document, starting
12/// with the document itself.
13///
14/// A block element (aka **block**) is a discrete, line-oriented chunk of
15/// content in an AsciiDoc document. Once parsed, that chunk of content becomes
16/// a block element in the parsed document model. Certain blocks may contain
17/// other blocks, so we say that blocks can be nested. The converter visits each
18/// block in turn, in document order, converting it to a corresponding chunk of
19/// output.
20///
21/// This trait implements many of the same core methods as the [`Block`] enum
22/// but provides a mechanism for third-party code to extend the behavior of
23/// blocks.
24pub trait IsBlock<'src>: Debug + Eq + PartialEq {
25 /// Returns the [`ContentModel`] for this block.
26 fn content_model(&self) -> ContentModel;
27
28 /// Returns the resolved context for this block.
29 ///
30 /// A block’s context is also sometimes referred to as a name, such as an
31 /// example block, a sidebar block, an admonition block, or a section.
32 ///
33 /// Every block has a context. The context is often implied by the syntax,
34 /// but can be declared explicitly in certain cases. The context is what
35 /// distinguishes one kind of block from another. You can think of the
36 /// context as the block’s type.
37 ///
38 /// For that reason, the context is not defined as an enumeration, but
39 /// rather as a string type that is optimized for the case where predefined
40 /// constants are viable.
41 ///
42 /// A block's context can be replaced by a block style that matches a
43 /// built-in context. Unlike [`raw_context()`], that transformation _is_
44 /// performed by this function.
45 ///
46 /// [`raw_context()`]: Self::raw_context
47 fn resolved_context(&'src self) -> CowStr<'src> {
48 if let Some(declared_style) = self.declared_style()
49 && is_built_in_context(declared_style)
50 {
51 return declared_style.into();
52 }
53
54 self.raw_context()
55 }
56
57 /// Returns the raw (uninterpreted) context for this block.
58 ///
59 /// A block’s context is also sometimes referred to as a name, such as an
60 /// example block, a sidebar block, an admonition block, or a section.
61 ///
62 /// Every block has a context. The context is often implied by the syntax,
63 /// but can be declared explicitly in certain cases. The context is what
64 /// distinguishes one kind of block from another. You can think of the
65 /// context as the block’s type.
66 ///
67 /// For that reason, the context is not defined as an enumeration, but
68 /// rather as a string type that is optimized for the case where predefined
69 /// constants are viable.
70 ///
71 /// A block's context can be replaced by a block style that matches a
72 /// built-in context. That transformation is only performed by
73 /// [`resolved_context()`], not this function.
74 ///
75 /// [`resolved_context()`]: Self::resolved_context
76 fn raw_context(&self) -> CowStr<'src>;
77
78 /// Returns the declared (uninterpreted) style for this block.
79 ///
80 /// Above some blocks, you may notice a name at the start of the block
81 /// attribute list (e.g., `[source]` or `[verse]`). The first positional
82 /// (unnamed) attribute in the block attribute list is used to declare the
83 /// block style.
84 ///
85 /// The declared block style is the value the author supplies.
86 ///
87 /// That value is then interpreted and resolved. That interpretation is not
88 /// performed by this function.
89 fn declared_style(&'src self) -> Option<&'src str> {
90 self.attrlist()
91 .and_then(|attrlist| attrlist.nth_attribute(1))
92 .and_then(|attr| attr.block_style())
93 }
94
95 /// Returns an iterator over the nested blocks contained within
96 /// this block.
97 ///
98 /// Many block types do not have nested blocks so the default implementation
99 /// returns an empty iterator.
100 fn nested_blocks(&'src self) -> Iter<'src, Block<'src>> {
101 const NO_BLOCKS: &[Block<'static>] = &[];
102 NO_BLOCKS.iter()
103 }
104
105 /// Returns the ID for this block, if present.
106 ///
107 /// You can assign an ID to a block using the shorthand syntax, the longhand
108 /// syntax, or a legacy block anchor.
109 ///
110 /// In the shorthand syntax, you prefix the name with a hash (`#`) in the
111 /// first position attribute:
112 ///
113 /// ```asciidoc
114 /// [#goals]
115 /// * Goal 1
116 /// * Goal 2
117 /// ```
118 ///
119 /// In the longhand syntax, you use a standard named attribute:
120 ///
121 /// ```asciidoc
122 /// [id=goals]
123 /// * Goal 1
124 /// * Goal 2
125 /// ```
126 ///
127 /// In the legacy block anchor syntax, you surround the name with double
128 /// square brackets:
129 ///
130 /// ```asciidoc
131 /// [[goals]]
132 /// * Goal 1
133 /// * Goal 2
134 /// ```
135 fn id(&'src self) -> Option<&'src str> {
136 self.anchor()
137 .map(|a| a.data())
138 .or_else(|| self.attrlist().and_then(|attrlist| attrlist.id()))
139 }
140
141 /// Returns any role attributes that were found.
142 ///
143 /// You can assign one or more roles to blocks and most inline elements
144 /// using the `role` attribute. The `role` attribute is a [named attribute].
145 /// Even though the attribute name is singular, it may contain multiple
146 /// (space-separated) roles. Roles may also be defined using a shorthand
147 /// (dot-prefixed) syntax.
148 ///
149 /// A role:
150 /// 1. adds additional semantics to an element
151 /// 2. can be used to apply additional styling to a group of elements (e.g.,
152 /// via a CSS class selector)
153 /// 3. may activate additional behavior if recognized by the converter
154 ///
155 /// **TIP:** The `role` attribute in AsciiDoc always get mapped to the
156 /// `class` attribute in the HTML output. In other words, role names are
157 /// synonymous with HTML class names, thus allowing output elements to be
158 /// identified and styled in CSS using class selectors (e.g.,
159 /// `sidebarblock.role1`).
160 ///
161 /// [named attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/#named
162 fn roles(&'src self) -> Vec<&'src str> {
163 match self.attrlist() {
164 Some(attrlist) => attrlist.roles(),
165 None => vec![],
166 }
167 }
168
169 /// Returns any option attributes that were found.
170 ///
171 /// The `options` attribute (often abbreviated as `opts`) is a versatile
172 /// [named attribute] that can be assigned one or more values. It can be
173 /// defined globally as document attribute as well as a block attribute on
174 /// an individual block.
175 ///
176 /// There is no strict schema for options. Any options which are not
177 /// recognized are ignored.
178 ///
179 /// You can assign one or more options to a block using the shorthand or
180 /// formal syntax for the options attribute.
181 ///
182 /// # Shorthand options syntax for blocks
183 ///
184 /// To assign an option to a block, prefix the value with a percent sign
185 /// (`%`) in an attribute list. The percent sign implicitly sets the
186 /// `options` attribute.
187 ///
188 /// ## Example 1: Sidebar block with an option assigned using the shorthand dot
189 ///
190 /// ```asciidoc
191 /// [%option]
192 /// ****
193 /// This is a sidebar with an option assigned to it, named option.
194 /// ****
195 /// ```
196 ///
197 /// You can assign multiple options to a block by prest
198 /// fixing each value with
199 /// a percent sign (`%`).
200 ///
201 /// ## Example 2: Sidebar with two options assigned using the shorthand dot
202 /// ```asciidoc
203 /// [%option1%option2]
204 /// ****
205 /// This is a sidebar with two options assigned to it, named option1 and option2.
206 /// ****
207 /// ```
208 ///
209 /// # Formal options syntax for blocks
210 ///
211 /// Explicitly set `options` or `opts`, followed by the equals sign (`=`),
212 /// and then the value in an attribute list.
213 ///
214 /// ## Example 3. Sidebar block with an option assigned using the formal syntax
215 /// ```asciidoc
216 /// [opts=option]
217 /// ****
218 /// This is a sidebar with an option assigned to it, named option.
219 /// ****
220 /// ```
221 ///
222 /// Separate multiple option values with commas (`,`).
223 ///
224 /// ## Example 4. Sidebar with three options assigned using the formal syntax
225 /// ```asciidoc
226 /// [opts="option1,option2"]
227 /// ****
228 /// This is a sidebar with two options assigned to it, option1 and option2.
229 /// ****
230 /// ```
231 ///
232 /// [named attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/#named
233 fn options(&'src self) -> Vec<&'src str> {
234 match self.attrlist() {
235 Some(attrlist) => attrlist.options(),
236 None => vec![],
237 }
238 }
239
240 /// Returns `true` if this block has the named option.
241 ///
242 /// See [`options()`] for a description of option syntax.
243 ///
244 /// [`options()`]: Self::options
245 fn has_option<N: AsRef<str>>(&'src self, name: N) -> bool {
246 self.attrlist()
247 .is_some_and(|attrlist| attrlist.has_option(name))
248 }
249
250 /// Returns the source text for the title for this block, if present.
251 fn title_source(&'src self) -> Option<Span<'src>>;
252
253 /// Returns the rendered title for this block, if present.
254 fn title(&self) -> Option<&str>;
255
256 /// Returns the anchor for this block, if present.
257 fn anchor(&'src self) -> Option<Span<'src>>;
258
259 /// Returns the reference text for this block's anchor, if present.
260 fn anchor_reftext(&'src self) -> Option<Span<'src>>;
261
262 /// Returns the attribute list for this block, if present.
263 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>>;
264
265 /// Returns the default substitution group that is applied unless you
266 /// customize the substitutions for a particular element.
267 fn substitution_group(&'src self) -> SubstitutionGroup {
268 SubstitutionGroup::Normal
269 }
270}
271
272/// The content model of a block determines what kind of content the block can
273/// have (if any) and how that content is processed.
274#[derive(Clone, Copy, Eq, PartialEq)]
275pub enum ContentModel {
276 /// A block that may only contain other blocks (e.g., a section)
277 Compound,
278
279 /// A block that's treated as contiguous lines of paragraph text (and
280 /// subject to normal substitutions) (e.g., a paragraph block)
281 Simple,
282
283 /// A block that holds verbatim text (displayed "as is") (and subject to
284 /// verbatim substitutions) (e.g., a listing block)
285 Verbatim,
286
287 /// A block that holds unprocessed content passed directly through to the
288 /// output with no substitutions applied (e.g., a passthrough block)
289 Raw,
290
291 /// A block that has no content (e.g., an image block)
292 Empty,
293
294 /// A special content model reserved for tables that enforces a fixed
295 /// structure
296 Table,
297}
298
299impl std::fmt::Debug for ContentModel {
300 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301 match self {
302 ContentModel::Compound => write!(f, "ContentModel::Compound"),
303 ContentModel::Simple => write!(f, "ContentModel::Simple"),
304 ContentModel::Verbatim => write!(f, "ContentModel::Verbatim"),
305 ContentModel::Raw => write!(f, "ContentModel::Raw"),
306 ContentModel::Empty => write!(f, "ContentModel::Empty"),
307 ContentModel::Table => write!(f, "ContentModel::Table"),
308 }
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 #![allow(clippy::unwrap_used)]
315
316 mod content_model {
317 mod impl_debug {
318 use pretty_assertions_sorted::assert_eq;
319
320 use crate::blocks::ContentModel;
321
322 #[test]
323 fn compound() {
324 let content_model = ContentModel::Compound;
325 let debug_output = format!("{:?}", content_model);
326 assert_eq!(debug_output, "ContentModel::Compound");
327 }
328
329 #[test]
330 fn simple() {
331 let content_model = ContentModel::Simple;
332 let debug_output = format!("{:?}", content_model);
333 assert_eq!(debug_output, "ContentModel::Simple");
334 }
335
336 #[test]
337 fn verbatim() {
338 let content_model = ContentModel::Verbatim;
339 let debug_output = format!("{:?}", content_model);
340 assert_eq!(debug_output, "ContentModel::Verbatim");
341 }
342
343 #[test]
344 fn raw() {
345 let content_model = ContentModel::Raw;
346 let debug_output = format!("{:?}", content_model);
347 assert_eq!(debug_output, "ContentModel::Raw");
348 }
349
350 #[test]
351 fn empty() {
352 let content_model = ContentModel::Empty;
353 let debug_output = format!("{:?}", content_model);
354 assert_eq!(debug_output, "ContentModel::Empty");
355 }
356
357 #[test]
358 fn table() {
359 let content_model = ContentModel::Table;
360 let debug_output = format!("{:?}", content_model);
361 assert_eq!(debug_output, "ContentModel::Table");
362 }
363 }
364 }
365}