Skip to main content

asciidoc_parser/blocks/
context.rs

1pub(crate) fn is_built_in_context(context: &str) -> bool {
2    BuiltInContext::from_str(context).is_some()
3}
4
5/// The set of built-in block contexts defined by the AsciiDoc language.
6///
7/// A block's context (see [`IsBlock::resolved_context`]) is a stringly-typed
8/// value so that third-party extensions can introduce their own contexts. The
9/// contexts understood natively by this parser, however, are drawn from a fixed
10/// vocabulary. This enum names that vocabulary so a downstream consumer (for
11/// example, a converter that dispatches on
12/// [`resolved_context`](crate::blocks::IsBlock::resolved_context)) can match
13/// against a shared definition rather than hard-coding and maintaining its own
14/// copy of the context strings.
15///
16/// Use [`from_str`](Self::from_str) to classify a context string (such as the
17/// value returned by
18/// [`resolved_context`](crate::blocks::IsBlock::resolved_context)) and
19/// [`as_str`](Self::as_str) to recover the canonical string for a variant.
20///
21/// [`IsBlock::resolved_context`]: crate::blocks::IsBlock::resolved_context
22/// [`resolved_context`]: crate::blocks::IsBlock::resolved_context
23#[derive(Clone, Copy, Eq, PartialEq, Hash)]
24#[non_exhaustive]
25pub enum BuiltInContext {
26    /// The `admonition` context.
27    Admonition,
28
29    /// The `audio` context.
30    Audio,
31
32    /// The `colist` (callout list) context.
33    Colist,
34
35    /// The `dlist` (description list) context.
36    Dlist,
37
38    /// The `document` context.
39    Document,
40
41    /// The `example` context.
42    Example,
43
44    /// The `floating_title` (discrete heading) context.
45    FloatingTitle,
46
47    /// The `image` context.
48    Image,
49
50    /// The `list_item` context.
51    ListItem,
52
53    /// The `listing` context.
54    Listing,
55
56    /// The `literal` context.
57    Literal,
58
59    /// The `olist` (ordered list) context.
60    Olist,
61
62    /// The `open` context.
63    Open,
64
65    /// The `page_break` context.
66    PageBreak,
67
68    /// The `paragraph` context.
69    Paragraph,
70
71    /// The `pass` (passthrough) context.
72    Pass,
73
74    /// The `preamble` context.
75    Preamble,
76
77    /// The `quote` context.
78    Quote,
79
80    /// The `section` context.
81    Section,
82
83    /// The `sidebar` context.
84    Sidebar,
85
86    /// The `stem` context.
87    Stem,
88
89    /// The `table` context.
90    Table,
91
92    /// The `table_cell` context.
93    TableCell,
94
95    /// The `thematic_break` context.
96    ThematicBreak,
97
98    /// The `toc` context.
99    Toc,
100
101    /// The `ulist` (unordered list) context.
102    Ulist,
103
104    /// The `verse` context.
105    Verse,
106
107    /// The `video` context.
108    Video,
109}
110
111impl BuiltInContext {
112    /// Every built-in context, in a stable order.
113    ///
114    /// This lets a consumer enumerate the full vocabulary (for example, to
115    /// build a dispatch table keyed by [`as_str`](Self::as_str)).
116    pub const ALL: &'static [BuiltInContext] = &[
117        Self::Admonition,
118        Self::Audio,
119        Self::Colist,
120        Self::Dlist,
121        Self::Document,
122        Self::Example,
123        Self::FloatingTitle,
124        Self::Image,
125        Self::ListItem,
126        Self::Listing,
127        Self::Literal,
128        Self::Olist,
129        Self::Open,
130        Self::PageBreak,
131        Self::Paragraph,
132        Self::Pass,
133        Self::Preamble,
134        Self::Quote,
135        Self::Section,
136        Self::Sidebar,
137        Self::Stem,
138        Self::Table,
139        Self::TableCell,
140        Self::ThematicBreak,
141        Self::Toc,
142        Self::Ulist,
143        Self::Verse,
144        Self::Video,
145    ];
146
147    /// Returns the canonical context string for this variant (e.g.,
148    /// `"paragraph"`).
149    ///
150    /// This is the same string that [`resolved_context`] yields for a block of
151    /// this context.
152    ///
153    /// [`resolved_context`]: crate::blocks::IsBlock::resolved_context
154    pub fn as_str(self) -> &'static str {
155        match self {
156            Self::Admonition => "admonition",
157            Self::Audio => "audio",
158            Self::Colist => "colist",
159            Self::Dlist => "dlist",
160            Self::Document => "document",
161            Self::Example => "example",
162            Self::FloatingTitle => "floating_title",
163            Self::Image => "image",
164            Self::ListItem => "list_item",
165            Self::Listing => "listing",
166            Self::Literal => "literal",
167            Self::Olist => "olist",
168            Self::Open => "open",
169            Self::PageBreak => "page_break",
170            Self::Paragraph => "paragraph",
171            Self::Pass => "pass",
172            Self::Preamble => "preamble",
173            Self::Quote => "quote",
174            Self::Section => "section",
175            Self::Sidebar => "sidebar",
176            Self::Stem => "stem",
177            Self::Table => "table",
178            Self::TableCell => "table_cell",
179            Self::ThematicBreak => "thematic_break",
180            Self::Toc => "toc",
181            Self::Ulist => "ulist",
182            Self::Verse => "verse",
183            Self::Video => "video",
184        }
185    }
186
187    /// Resolves a context string to its [`BuiltInContext`] variant, or `None`
188    /// if the string is not a built-in context.
189    ///
190    /// The match is case-sensitive: built-in contexts are always lowercase.
191    // This is deliberately an inherent method returning `Option` (rather than an
192    // implementation of `std::str::FromStr`, which would return `Result`): a
193    // non-built-in context is an ordinary, expected outcome here, not an error.
194    #[allow(clippy::should_implement_trait)]
195    pub fn from_str(context: &str) -> Option<Self> {
196        Some(match context {
197            "admonition" => Self::Admonition,
198            "audio" => Self::Audio,
199            "colist" => Self::Colist,
200            "dlist" => Self::Dlist,
201            "document" => Self::Document,
202            "example" => Self::Example,
203            "floating_title" => Self::FloatingTitle,
204            "image" => Self::Image,
205            "list_item" => Self::ListItem,
206            "listing" => Self::Listing,
207            "literal" => Self::Literal,
208            "olist" => Self::Olist,
209            "open" => Self::Open,
210            "page_break" => Self::PageBreak,
211            "paragraph" => Self::Paragraph,
212            "pass" => Self::Pass,
213            "preamble" => Self::Preamble,
214            "quote" => Self::Quote,
215            "section" => Self::Section,
216            "sidebar" => Self::Sidebar,
217            "stem" => Self::Stem,
218            "table" => Self::Table,
219            "table_cell" => Self::TableCell,
220            "thematic_break" => Self::ThematicBreak,
221            "toc" => Self::Toc,
222            "ulist" => Self::Ulist,
223            "verse" => Self::Verse,
224            "video" => Self::Video,
225            _ => return None,
226        })
227    }
228}
229
230impl std::fmt::Debug for BuiltInContext {
231    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232        write!(f, "BuiltInContext::{}", {
233            match self {
234                Self::Admonition => "Admonition",
235                Self::Audio => "Audio",
236                Self::Colist => "Colist",
237                Self::Dlist => "Dlist",
238                Self::Document => "Document",
239                Self::Example => "Example",
240                Self::FloatingTitle => "FloatingTitle",
241                Self::Image => "Image",
242                Self::ListItem => "ListItem",
243                Self::Listing => "Listing",
244                Self::Literal => "Literal",
245                Self::Olist => "Olist",
246                Self::Open => "Open",
247                Self::PageBreak => "PageBreak",
248                Self::Paragraph => "Paragraph",
249                Self::Pass => "Pass",
250                Self::Preamble => "Preamble",
251                Self::Quote => "Quote",
252                Self::Section => "Section",
253                Self::Sidebar => "Sidebar",
254                Self::Stem => "Stem",
255                Self::Table => "Table",
256                Self::TableCell => "TableCell",
257                Self::ThematicBreak => "ThematicBreak",
258                Self::Toc => "Toc",
259                Self::Ulist => "Ulist",
260                Self::Verse => "Verse",
261                Self::Video => "Video",
262            }
263        })
264    }
265}
266
267/// The five admonition types provided by the AsciiDoc language.
268///
269/// An admonition draws attention to a statement by taking it out of the
270/// content's flow and labeling it with a priority. The variant is determined by
271/// the assigned type (i.e., the uppercase label), which is specified either as
272/// a special paragraph prefix (e.g., `NOTE:`) or as the block style in an
273/// attribute list (e.g., `[NOTE]`).
274#[derive(Clone, Copy, Eq, PartialEq)]
275pub enum AdmonitionVariant {
276    /// The `NOTE` admonition type.
277    Note,
278
279    /// The `TIP` admonition type.
280    Tip,
281
282    /// The `IMPORTANT` admonition type.
283    Important,
284
285    /// The `CAUTION` admonition type.
286    Caution,
287
288    /// The `WARNING` admonition type.
289    Warning,
290}
291
292impl AdmonitionVariant {
293    /// Resolve an uppercase style label (e.g., `NOTE`) to its admonition
294    /// variant, if it names one.
295    ///
296    /// The match is case-sensitive: the label must be uppercase, per the
297    /// AsciiDoc language specification.
298    pub(crate) fn from_style(style: &str) -> Option<Self> {
299        Some(match style {
300            "NOTE" => Self::Note,
301            "TIP" => Self::Tip,
302            "IMPORTANT" => Self::Important,
303            "CAUTION" => Self::Caution,
304            "WARNING" => Self::Warning,
305            _ => return None,
306        })
307    }
308
309    /// Returns the uppercase style label for this variant (e.g., `NOTE`).
310    pub fn style(self) -> &'static str {
311        match self {
312            Self::Note => "NOTE",
313            Self::Tip => "TIP",
314            Self::Important => "IMPORTANT",
315            Self::Caution => "CAUTION",
316            Self::Warning => "WARNING",
317        }
318    }
319
320    /// Returns the lowercase name for this variant (e.g., `note`).
321    ///
322    /// This is the name used for the block's CSS class and for the
323    /// `<type>-caption` document attribute that controls the label text.
324    pub fn name(self) -> &'static str {
325        match self {
326            Self::Note => "note",
327            Self::Tip => "tip",
328            Self::Important => "important",
329            Self::Caution => "caution",
330            Self::Warning => "warning",
331        }
332    }
333
334    /// Returns the default caption (label) text for this variant (e.g.,
335    /// `Note`).
336    ///
337    /// This text is shown to the reader (in place of an icon) unless it is
338    /// overridden by setting the `<type>-caption` document attribute.
339    pub fn default_caption(self) -> &'static str {
340        match self {
341            Self::Note => "Note",
342            Self::Tip => "Tip",
343            Self::Important => "Important",
344            Self::Caution => "Caution",
345            Self::Warning => "Warning",
346        }
347    }
348}
349
350impl std::fmt::Debug for AdmonitionVariant {
351    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352        match self {
353            Self::Note => write!(f, "AdmonitionVariant::Note"),
354            Self::Tip => write!(f, "AdmonitionVariant::Tip"),
355            Self::Important => write!(f, "AdmonitionVariant::Important"),
356            Self::Caution => write!(f, "AdmonitionVariant::Caution"),
357            Self::Warning => write!(f, "AdmonitionVariant::Warning"),
358        }
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    mod built_in_context {
365        use crate::blocks::{BuiltInContext, context::is_built_in_context};
366
367        #[test]
368        fn round_trips_every_variant() {
369            // Every variant's `as_str` must resolve back to the same variant,
370            // and each canonical string is recognized as a built-in context.
371            for &context in BuiltInContext::ALL {
372                assert_eq!(BuiltInContext::from_str(context.as_str()), Some(context));
373                assert!(is_built_in_context(context.as_str()));
374            }
375        }
376
377        #[test]
378        fn all_is_complete_and_deduplicated() {
379            // `ALL` should list each of the 28 built-in contexts exactly once.
380            assert_eq!(BuiltInContext::ALL.len(), 28);
381
382            let mut seen: Vec<&str> = BuiltInContext::ALL.iter().map(|c| c.as_str()).collect();
383            seen.sort_unstable();
384            seen.dedup();
385            assert_eq!(seen.len(), BuiltInContext::ALL.len());
386        }
387
388        #[test]
389        fn known_contexts() {
390            assert_eq!(
391                BuiltInContext::from_str("paragraph"),
392                Some(BuiltInContext::Paragraph)
393            );
394            assert_eq!(BuiltInContext::Listing.as_str(), "listing");
395            assert_eq!(BuiltInContext::TableCell.as_str(), "table_cell");
396        }
397
398        #[test]
399        fn unknown_context_is_none() {
400            assert_eq!(BuiltInContext::from_str("verbatim"), None);
401            // The match is case-sensitive.
402            assert_eq!(BuiltInContext::from_str("Paragraph"), None);
403            assert!(!is_built_in_context("not-a-context"));
404        }
405
406        #[test]
407        fn impl_debug() {
408            // Spot-check one exact rendering...
409            assert_eq!(
410                format!("{:?}", BuiltInContext::FloatingTitle),
411                "BuiltInContext::FloatingTitle"
412            );
413
414            // ...then exercise every variant's `Debug` arm, checking each
415            // renders as a distinct `BuiltInContext::`-prefixed name.
416            let mut seen = std::collections::HashSet::new();
417            for &context in BuiltInContext::ALL {
418                let debug = format!("{context:?}");
419                assert!(debug.starts_with("BuiltInContext::"));
420                assert!(!debug.contains(' '));
421                assert!(seen.insert(debug), "duplicate Debug rendering");
422            }
423            assert_eq!(seen.len(), BuiltInContext::ALL.len());
424        }
425    }
426}