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}