Skip to main content

asciidoc_parser/blocks/
is_block.rs

1use std::fmt::Debug;
2
3use crate::{
4    Span,
5    attributes::Attrlist,
6    blocks::{Block, is_built_in_context},
7    content::{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 rendered content for this block, if any.
29    ///
30    /// Some blocks (especially compound blocks) do not directly contain
31    /// content. In such cases, this function will return `None`.
32    ///
33    /// This content will contain the text _after_ substitutions have been
34    /// applied.
35    fn rendered_content(&'src self) -> Option<&'src str> {
36        None
37    }
38
39    /// Returns the resolved context for this block.
40    ///
41    /// A block’s context is also sometimes referred to as a name, such as an
42    /// example block, a sidebar block, an admonition block, or a section.
43    ///
44    /// Every block has a context. The context is often implied by the syntax,
45    /// but can be declared explicitly in certain cases. The context is what
46    /// distinguishes one kind of block from another. You can think of the
47    /// context as the block’s type.
48    ///
49    /// For that reason, the context is not defined as an enumeration, but
50    /// rather as a string type that is optimized for the case where predefined
51    /// constants are viable.
52    ///
53    /// A block's context can be replaced by a block style that matches a
54    /// built-in context. Unlike [`raw_context()`], that transformation _is_
55    /// performed by this function.
56    ///
57    /// [`raw_context()`]: Self::raw_context
58    fn resolved_context(&'src self) -> CowStr<'src> {
59        if let Some(declared_style) = self.declared_style() {
60            if is_built_in_context(declared_style) {
61                return declared_style.into();
62            }
63
64            // The `source` style is not itself a context; it specializes the
65            // `listing` context (a source block is a listing block with syntax
66            // highlighting). A `source` style therefore resolves the context to
67            // `listing` – for example, `[source]` placed over a `....` literal
68            // block makes it a listing block.
69            if declared_style == "source" {
70                return "listing".into();
71            }
72
73            // The `abstract` style is not itself a context; it specializes the
74            // `open` context. Asciidoctor rebuilds an `[abstract]` paragraph as
75            // an open block (keeping the simple content model), so the style
76            // resolves a paragraph's context to `open`. On any other context
77            // (including the `--` delimited form, which is already `open`), the
78            // declared style is preserved without changing the context.
79            if declared_style == "abstract" && self.raw_context().as_ref() == "paragraph" {
80                return "open".into();
81            }
82        }
83
84        self.raw_context()
85    }
86
87    /// Returns the raw (uninterpreted) context for this block.
88    ///
89    /// A block’s context is also sometimes referred to as a name, such as an
90    /// example block, a sidebar block, an admonition block, or a section.
91    ///
92    /// Every block has a context. The context is often implied by the syntax,
93    /// but can be declared explicitly in certain cases. The context is what
94    /// distinguishes one kind of block from another. You can think of the
95    /// context as the block’s type.
96    ///
97    /// For that reason, the context is not defined as an enumeration, but
98    /// rather as a string type that is optimized for the case where predefined
99    /// constants are viable.
100    ///
101    /// A block's context can be replaced by a block style that matches a
102    /// built-in context. That transformation is only performed by
103    /// [`resolved_context()`], not this function.
104    ///
105    /// [`resolved_context()`]: Self::resolved_context
106    fn raw_context(&self) -> CowStr<'src>;
107
108    /// Returns the declared (uninterpreted) style for this block.
109    ///
110    /// Above some blocks, you may notice a name at the start of the block
111    /// attribute list (e.g., `[source]` or `[verse]`). The first positional
112    /// (unnamed) attribute in the block attribute list is used to declare the
113    /// block style.
114    ///
115    /// The declared block style is the value the author supplies.
116    ///
117    /// That value is then interpreted and resolved. That interpretation is not
118    /// performed by this function.
119    fn declared_style(&'src self) -> Option<&'src str> {
120        self.attrlist()
121            .and_then(|attrlist| attrlist.nth_attribute(1))
122            .and_then(|attr| attr.block_style())
123    }
124
125    /// Returns the resolved style for this block.
126    ///
127    /// A block's style is usually the value the author declared (see
128    /// [`declared_style()`]), but a block can also acquire a style implicitly
129    /// during parsing. For example, a section carrying the `bibliography` style
130    /// implicitly adds that style to each of its top-level unordered lists, so
131    /// such a list resolves to `bibliography` even though the author declared
132    /// no style on the list itself. This is the crate's analog of
133    /// Asciidoctor's `AbstractBlock#style`.
134    ///
135    /// This transformation _is_ performed by this function, so it may differ
136    /// from [`declared_style()`]. The default implementation returns the
137    /// declared style unchanged; block types that resolve an implicit style
138    /// override it.
139    ///
140    /// [`declared_style()`]: Self::declared_style
141    fn resolved_style(&'src self) -> Option<&'src str> {
142        self.declared_style()
143    }
144
145    /// Returns a mutable slice of the child blocks contained within this block.
146    ///
147    /// The default returns an empty slice; container blocks override it to
148    /// expose their children for in-place passes such as cross-reference
149    /// resolution. This is a low-level extension hook; to read a block's
150    /// children, use its inherent `child_blocks()` accessor or
151    /// [`FindBlocks::child_blocks()`].
152    ///
153    /// [`FindBlocks::child_blocks()`]: crate::blocks::FindBlocks::child_blocks
154    fn child_blocks_mut(&mut self) -> &mut [Block<'src>] {
155        &mut []
156    }
157
158    /// Returns a mutable reference to this block's own resolvable content – its
159    /// body or description-list term – if any.
160    ///
161    /// The default returns `None`; content-bearing blocks override it. This is
162    /// used by in-place passes such as cross-reference resolution. A section
163    /// keeps the default: its heading is resolved by the document-order title
164    /// pass, not the per-content pass.
165    fn content_mut(&mut self) -> Option<&mut Content<'src>> {
166        None
167    }
168
169    /// Returns the ID for this block, if present.
170    ///
171    /// You can assign an ID to a block using the shorthand syntax, the longhand
172    /// syntax, or a legacy block anchor.
173    ///
174    /// In the shorthand syntax, you prefix the name with a hash (`#`) in the
175    /// first position attribute:
176    ///
177    /// ```asciidoc
178    /// [#goals]
179    /// * Goal 1
180    /// * Goal 2
181    /// ```
182    ///
183    /// In the longhand syntax, you use a standard named attribute:
184    ///
185    /// ```asciidoc
186    /// [id=goals]
187    /// * Goal 1
188    /// * Goal 2
189    /// ```
190    ///
191    /// In the legacy block anchor syntax, you surround the name with double
192    /// square brackets:
193    ///
194    /// ```asciidoc
195    /// [[goals]]
196    /// * Goal 1
197    /// * Goal 2
198    /// ```
199    fn id(&'src self) -> Option<&'src str> {
200        self.anchor()
201            .map(|a| a.data())
202            .or_else(|| self.attrlist().and_then(|attrlist| attrlist.id()))
203    }
204
205    /// Returns any role attributes that were found.
206    ///
207    /// You can assign one or more roles to blocks and most inline elements
208    /// using the `role` attribute. The `role` attribute is a [named attribute].
209    /// Even though the attribute name is singular, it may contain multiple
210    /// (space-separated) roles. Roles may also be defined using a shorthand
211    /// (dot-prefixed) syntax.
212    ///
213    /// A role:
214    /// 1. adds additional semantics to an element
215    /// 2. can be used to apply additional styling to a group of elements (e.g.,
216    ///    via a CSS class selector)
217    /// 3. may activate additional behavior if recognized by the converter
218    ///
219    /// **TIP:** The `role` attribute in AsciiDoc always get mapped to the
220    /// `class` attribute in the HTML output. In other words, role names are
221    /// synonymous with HTML class names, thus allowing output elements to be
222    /// identified and styled in CSS using class selectors (e.g.,
223    /// `sidebarblock.role1`).
224    ///
225    /// [named attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/#named
226    fn roles(&'src self) -> Vec<&'src str> {
227        match self.attrlist() {
228            Some(attrlist) => attrlist.roles(),
229            None => vec![],
230        }
231    }
232
233    /// Returns any option attributes that were found.
234    ///
235    /// The `options` attribute (often abbreviated as `opts`) is a versatile
236    /// [named attribute] that can be assigned one or more values. It can be
237    /// defined globally as document attribute as well as a block attribute on
238    /// an individual block.
239    ///
240    /// There is no strict schema for options. Any options which are not
241    /// recognized are ignored.
242    ///
243    /// You can assign one or more options to a block using the shorthand or
244    /// formal syntax for the options attribute.
245    ///
246    /// # Shorthand options syntax for blocks
247    ///
248    /// To assign an option to a block, prefix the value with a percent sign
249    /// (`%`) in an attribute list. The percent sign implicitly sets the
250    /// `options` attribute.
251    ///
252    /// ## Example 1: Sidebar block with an option assigned using the shorthand dot
253    ///
254    /// ```asciidoc
255    /// [%option]
256    /// ****
257    /// This is a sidebar with an option assigned to it, named option.
258    /// ****
259    /// ```
260    ///
261    /// You can assign multiple options to a block by prest
262    /// fixing each value with
263    /// a percent sign (`%`).
264    ///
265    /// ## Example 2: Sidebar with two options assigned using the shorthand dot
266    /// ```asciidoc
267    /// [%option1%option2]
268    /// ****
269    /// This is a sidebar with two options assigned to it, named option1 and option2.
270    /// ****
271    /// ```
272    ///
273    /// # Formal options syntax for blocks
274    ///
275    /// Explicitly set `options` or `opts`, followed by the equals sign (`=`),
276    /// and then the value in an attribute list.
277    ///
278    /// ## Example 3. Sidebar block with an option assigned using the formal syntax
279    /// ```asciidoc
280    /// [opts=option]
281    /// ****
282    /// This is a sidebar with an option assigned to it, named option.
283    /// ****
284    /// ```
285    ///
286    /// Separate multiple option values with commas (`,`).
287    ///
288    /// ## Example 4. Sidebar with three options assigned using the formal syntax
289    /// ```asciidoc
290    /// [opts="option1,option2"]
291    /// ****
292    /// This is a sidebar with two options assigned to it, option1 and option2.
293    /// ****
294    /// ```
295    ///
296    /// [named attribute]: https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/#named
297    fn options(&'src self) -> Vec<&'src str> {
298        match self.attrlist() {
299            Some(attrlist) => attrlist.options(),
300            None => vec![],
301        }
302    }
303
304    /// Returns `true` if this block has the named option.
305    ///
306    /// See [`options()`] for a description of option syntax.
307    ///
308    /// [`options()`]: Self::options
309    fn has_option<N: AsRef<str>>(&'src self, name: N) -> bool {
310        self.attrlist()
311            .is_some_and(|attrlist| attrlist.has_option(name))
312    }
313
314    /// Returns the source text for the title for this block, if present.
315    fn title_source(&'src self) -> Option<Span<'src>>;
316
317    /// Returns the rendered title for this block, if present.
318    fn title(&self) -> Option<&str>;
319
320    /// Returns the caption prefix for this block, if it has one.
321    ///
322    /// A *captionable* block (e.g. an example block or a table) that has a
323    /// [title](Self::title) is given a caption: a label and an automatically
324    /// incremented number that a converter prepends to the title (e.g.
325    /// `"Example 1. "`, including the trailing separator and space). The prefix
326    /// combines a label drawn from a document attribute (e.g.
327    /// `example-caption`) with the block's [number](Self::number).
328    ///
329    /// The caption is absent when the block has no title, when its caption
330    /// attribute has been unset, or when an explicitly empty caption was
331    /// supplied. An explicit `caption` attribute overrides the prefix with a
332    /// verbatim, unnumbered label. The default implementation returns `None`.
333    fn caption(&self) -> Option<&str> {
334        None
335    }
336
337    /// Returns the automatically assigned number for this block, if it has one.
338    ///
339    /// Captionable blocks are numbered, per context, in document order as they
340    /// finish parsing (so a nested captioned block is numbered before its
341    /// container). The number is the bare counter value that appears in the
342    /// block's [caption](Self::caption) prefix. It is absent when the block is
343    /// not captioned, or when its caption comes from an explicit (unnumbered)
344    /// `caption` attribute. The default implementation returns `None`.
345    fn number(&self) -> Option<usize> {
346        None
347    }
348
349    /// Returns the anchor for this block, if present.
350    fn anchor(&'src self) -> Option<Span<'src>>;
351
352    /// Returns the reference text for this block's anchor, if present.
353    fn anchor_reftext(&'src self) -> Option<Span<'src>>;
354
355    /// Returns the attribute list for this block, if present.
356    fn attrlist(&'src self) -> Option<&'src Attrlist<'src>>;
357
358    /// Returns the default substitution group that is applied unless you
359    /// customize the substitutions for a particular element.
360    fn substitution_group(&'src self) -> SubstitutionGroup {
361        SubstitutionGroup::Normal
362    }
363}
364
365/// The content model of a block determines what kind of content the block can
366/// have (if any) and how that content is processed.
367#[derive(Clone, Copy, Eq, Hash, PartialEq)]
368pub enum ContentModel {
369    /// A block that may only contain other blocks (e.g., a section)
370    Compound,
371
372    /// A block that's treated as contiguous lines of paragraph text (and
373    /// subject to normal substitutions) (e.g., a paragraph block)
374    Simple,
375
376    /// A block that holds verbatim text (displayed "as is") (and subject to
377    /// verbatim substitutions) (e.g., a listing block)
378    Verbatim,
379
380    /// A block that holds unprocessed content passed directly through to the
381    /// output with no substitutions applied (e.g., a passthrough block)
382    Raw,
383
384    /// A block that has no content (e.g., an image block).
385    Empty,
386
387    /// A special content model reserved for tables that enforces a fixed
388    /// structure.
389    Table,
390}
391
392impl std::fmt::Debug for ContentModel {
393    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
394        match self {
395            ContentModel::Compound => write!(f, "ContentModel::Compound"),
396            ContentModel::Simple => write!(f, "ContentModel::Simple"),
397            ContentModel::Verbatim => write!(f, "ContentModel::Verbatim"),
398            ContentModel::Raw => write!(f, "ContentModel::Raw"),
399            ContentModel::Empty => write!(f, "ContentModel::Empty"),
400            ContentModel::Table => write!(f, "ContentModel::Table"),
401        }
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    #![allow(clippy::unwrap_used)]
408
409    mod content_model {
410        mod impl_debug {
411            use crate::blocks::ContentModel;
412
413            #[test]
414            fn compound() {
415                let content_model = ContentModel::Compound;
416                let debug_output = format!("{:?}", content_model);
417                assert_eq!(debug_output, "ContentModel::Compound");
418            }
419
420            #[test]
421            fn simple() {
422                let content_model = ContentModel::Simple;
423                let debug_output = format!("{:?}", content_model);
424                assert_eq!(debug_output, "ContentModel::Simple");
425            }
426
427            #[test]
428            fn verbatim() {
429                let content_model = ContentModel::Verbatim;
430                let debug_output = format!("{:?}", content_model);
431                assert_eq!(debug_output, "ContentModel::Verbatim");
432            }
433
434            #[test]
435            fn raw() {
436                let content_model = ContentModel::Raw;
437                let debug_output = format!("{:?}", content_model);
438                assert_eq!(debug_output, "ContentModel::Raw");
439            }
440
441            #[test]
442            fn empty() {
443                let content_model = ContentModel::Empty;
444                let debug_output = format!("{:?}", content_model);
445                assert_eq!(debug_output, "ContentModel::Empty");
446            }
447
448            #[test]
449            fn table() {
450                let content_model = ContentModel::Table;
451                let debug_output = format!("{:?}", content_model);
452                assert_eq!(debug_output, "ContentModel::Table");
453            }
454        }
455    }
456}