Skip to main content

carta_core/
extensions.rs

1//! Format extensions: the set of optional syntax features a reader or writer may honor.
2//!
3//! [`Extension`] is one named feature; [`Extensions`] is a deterministic, allocation-free set of them
4//! backed by a fixed array of 64-bit words. The set carries no 128-variant ceiling, so it scales to
5//! the full extension set. [`presets`] holds the per-flavor sets; strict `CommonMark` is the empty set.
6
7/// Generates the [`Extension`] enum together with the `ALL`/`COUNT`/`name` metadata, keeping the
8/// variant list as the single source of truth for the bitset sizing in [`Extensions`].
9macro_rules! define_extensions {
10    ($($variant:ident => $name:literal),+ $(,)?) => {
11        /// A single format extension. Each variant's position in [`Extension::ALL`] is its bit
12        /// index in [`Extensions`].
13        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14        #[non_exhaustive]
15        pub enum Extension { $($variant),+ }
16
17        impl Extension {
18            /// Every extension, in declaration order.
19            pub const ALL: &'static [Extension] = &[$(Extension::$variant),+];
20            /// The number of distinct extensions.
21            pub const COUNT: usize = Self::ALL.len();
22
23            /// The extension's identifier (e.g. `"footnotes"`).
24            #[must_use]
25            pub const fn name(self) -> &'static str {
26                match self { $(Extension::$variant => $name),+ }
27            }
28
29            /// The extension named `name`, or `None` if no extension uses that identifier.
30            #[must_use]
31            pub fn from_name(name: &str) -> Option<Extension> {
32                match name { $($name => Some(Extension::$variant),)+ _ => None }
33            }
34        }
35    };
36}
37
38define_extensions! {
39    Smart => "smart",
40    Strikeout => "strikeout",
41    Superscript => "superscript",
42    Subscript => "subscript",
43    PipeTables => "pipe_tables",
44    Footnotes => "footnotes",
45    TaskLists => "task_lists",
46    Autolink => "autolink_bare_uris",
47    TexMathDollars => "tex_math_dollars",
48    FencedDivs => "fenced_divs",
49    BracketedSpans => "bracketed_spans",
50    HardLineBreaks => "hard_line_breaks",
51    RawHtml => "raw_html",
52    // Attribute-bearing syntax: `{#id .class key=val}` on headers, code, links/images, and spans.
53    HeaderAttributes => "header_attributes",
54    FencedCodeAttributes => "fenced_code_attributes",
55    InlineCodeAttributes => "inline_code_attributes",
56    LinkAttributes => "link_attributes",
57    // The combined attribute toggle (enables the attribute syntaxes as a group).
58    Attributes => "attributes",
59    // Block constructs.
60    DefinitionLists => "definition_lists",
61    GridTables => "grid_tables",
62    MultilineTables => "multiline_tables",
63    SimpleTables => "simple_tables",
64    TableCaptions => "table_captions",
65    LineBlocks => "line_blocks",
66    // List-marker richness.
67    FancyLists => "fancy_lists",
68    ExampleLists => "example_lists",
69    Startnum => "startnum",
70    // Document metadata.
71    YamlMetadataBlock => "yaml_metadata_block",
72    PandocTitleBlock => "pandoc_title_block",
73    // Header identifiers and the references they enable.
74    AutoIdentifiers => "auto_identifiers",
75    GfmAutoIdentifiers => "gfm_auto_identifiers",
76    // Fold a derived identifier down to ASCII, dropping diacritics before the slug is formed.
77    AsciiIdentifiers => "ascii_identifiers",
78    // A header's explicit identifier is written in MultiMarkdown's trailing `[id]` form rather than
79    // the `{#id}` attribute block.
80    MmdHeaderIdentifiers => "mmd_header_identifiers",
81    ImplicitHeaderReferences => "implicit_header_references",
82    // Bare images with a caption become figures.
83    ImplicitFigures => "implicit_figures",
84    // Raw passthrough: `` `code`{=fmt} `` inline and ```` ```{=fmt} ```` fenced blocks.
85    RawAttribute => "raw_attribute",
86    // A `^[…]` inline note expands to a footnote in place.
87    InlineNotes => "inline_notes",
88    // Block-level `<div>`/inline `<span>` HTML become `Div`/`Span`, with markdown parsed inside.
89    NativeDivs => "native_divs",
90    NativeSpans => "native_spans",
91    // Markdown is parsed inside block-level HTML, which is otherwise split tag-by-tag.
92    MarkdownInHtmlBlocks => "markdown_in_html_blocks",
93    // A `<div>`/`<span>` emitted for a div/span carries a `data-markdown="1"` marker so its contents
94    // are still parsed as Markdown; this also forces a div with no native syntax into an HTML wrap.
95    MarkdownAttribute => "markdown_attribute",
96    // Inline raw TeX (`\command{…}`, `\begin{env}…\end{env}`) passes through verbatim.
97    RawTex => "raw_tex",
98    // `[@key]` / `@key` citation references.
99    Citations => "citations",
100    // An attribute block on a table's caption line attaches to the table.
101    TableAttributes => "table_attributes",
102    // A blank line is required before a blockquote / header, so neither interrupts a paragraph.
103    BlankBeforeBlockquote => "blank_before_blockquote",
104    BlankBeforeHeader => "blank_before_header",
105    // `==text==` highlight spans.
106    Mark => "mark",
107    // `:name:` emoji shortcodes.
108    Emoji => "emoji",
109    // `> [!NOTE]`-style admonition blockquotes become classed divs.
110    Alerts => "alerts",
111    // Single- and double-backslash math delimiters: `\(…\)`/`\[…\]` and `\\(…\\)`/`\\[…\\]`.
112    TexMathSingleBackslash => "tex_math_single_backslash",
113    TexMathDoubleBackslash => "tex_math_double_backslash",
114    // Code-block surface: backtick-fenced ```` ``` ```` and tilde-fenced `~~~` blocks. When neither is
115    // available the writer drops to the four-space indented form.
116    FencedCodeBlocks => "fenced_code_blocks",
117    BacktickCodeBlocks => "backtick_code_blocks",
118    // GitHub math surface: inline `` $`…`$ `` and a ```` ```math ```` display block, as opposed to the
119    // `$…$`/`$$…$$` dollar form.
120    TexMathGfm => "tex_math_gfm",
121    // A backslash at a line's end is a hard line break, written as a trailing `\`; without it the
122    // writer falls back to two trailing spaces.
123    EscapedLineBreaks => "escaped_line_breaks",
124    // An underscore inside a word opens no emphasis, so the writer leaves intra-word `_` literal;
125    // without it every `_` is escaped so the strict reader cannot start emphasis mid-word.
126    IntrawordUnderscores => "intraword_underscores",
127    // A list may begin directly after a paragraph line with no intervening blank line, interrupting
128    // it; without it a list marker on the line after a paragraph folds into that paragraph.
129    ListsWithoutPrecedingBlankline => "lists_without_preceding_blankline",
130    // `*[SHY]: Soft hyphen` abbreviation definitions, applied to later occurrences of the term.
131    Abbreviations => "abbreviations",
132    // A backslash escapes any symbol, not only the ASCII-punctuation subset.
133    AllSymbolsEscapable => "all_symbols_escapable",
134    // A backslash before `<` or `>` escapes the angle bracket.
135    AngleBracketsEscapable => "angle_brackets_escapable",
136    // Line breaks between East Asian wide characters carry no width and are dropped.
137    EastAsianLineBreaks => "east_asian_line_breaks",
138    // An indented code block requires four spaces of indentation rather than one tab stop.
139    FourSpaceRule => "four_space_rule",
140    // Typographic conventions of the Project Gutenberg style for plain-text output.
141    Gutenberg => "gutenberg",
142    // Soft line breaks within a paragraph are discarded rather than kept as spaces.
143    IgnoreLineBreaks => "ignore_line_breaks",
144    // User-defined LaTeX macros are expanded in math and raw TeX.
145    LatexMacros => "latex_macros",
146    // Bird-track (`> `) literate-program code sections.
147    LiterateHaskell => "literate_haskell",
148    // An attribute block following a link or image in the MultiMarkdown position.
149    MmdLinkAttributes => "mmd_link_attributes",
150    // A MultiMarkdown metadata block at the top of the document.
151    MmdTitleBlock => "mmd_title_block",
152    // `-` and `--` map to en/em dashes under the older dash convention.
153    OldDashes => "old_dashes",
154    // A raw block or inline may be written directly as Markdown for round-tripping.
155    RawMarkdown => "raw_markdown",
156    // Relative paths in links and images are rebased onto the source file's location.
157    RebaseRelativePaths => "rebase_relative_paths",
158    // `~x` / `^x` subscript and superscript bind only the single following character.
159    ShortSubsuperscripts => "short_subsuperscripts",
160    // A defined label may be referenced by `[label]` alone, with no following `[]` or `(…)`.
161    ShortcutReferenceLinks => "shortcut_reference_links",
162    // An ATX header requires a space between the opening `#` run and the heading text.
163    SpaceInAtxHeader => "space_in_atx_header",
164    // A reference link's label and its following `[id]` may be separated by whitespace.
165    SpacedReferenceLinks => "spaced_reference_links",
166    // `[[target|title]]` wiki links, with the title following the pipe.
167    WikilinksTitleAfterPipe => "wikilinks_title_after_pipe",
168    // `[[title|target]]` wiki links, with the title preceding the pipe.
169    WikilinksTitleBeforePipe => "wikilinks_title_before_pipe",
170}
171
172const WORD_BITS: usize = u64::BITS as usize;
173const WORDS: usize = Extension::COUNT.div_ceil(WORD_BITS);
174
175// The bitset indexing in `from_list` is sound only while each variant's discriminant equals its
176// position in `ALL` (so every `ext as usize` lands in `0..COUNT`). The macro emits no explicit
177// discriminants, so this holds — asserted at compile time here, turning a future edit that breaks
178// contiguity into a build failure rather than an out-of-bounds index.
179#[allow(clippy::indexing_slicing)]
180const _: () = {
181    let mut i = 0;
182    while i < Extension::ALL.len() {
183        assert!(Extension::ALL[i] as usize == i);
184        i += 1;
185    }
186};
187
188/// A deterministic, allocation-free set of [`Extension`]s, backed by a fixed array of 64-bit words
189/// indexed by each variant's position in [`Extension::ALL`].
190#[derive(Clone, Copy, PartialEq, Eq)]
191pub struct Extensions([u64; WORDS]);
192
193impl Default for Extensions {
194    fn default() -> Self {
195        Self::empty()
196    }
197}
198
199impl Extensions {
200    /// The empty set (strict `CommonMark`).
201    #[must_use]
202    pub const fn empty() -> Self {
203        Self([0; WORDS])
204    }
205
206    /// The set containing exactly `list`. Const so presets are `const` values.
207    #[must_use]
208    // Const indexing: contiguity (asserted above) gives `bit < COUNT`, so `bit / WORD_BITS < WORDS`;
209    // `i < list.len()`. Both indices are in bounds, and slice `get` is not usable across all const
210    // contexts on the pinned toolchain.
211    #[allow(clippy::indexing_slicing)]
212    pub const fn from_list(list: &[Extension]) -> Self {
213        let mut words = [0u64; WORDS];
214        let mut i = 0;
215        while i < list.len() {
216            let bit = list[i] as usize;
217            words[bit / WORD_BITS] |= 1u64 << (bit % WORD_BITS);
218            i += 1;
219        }
220        Self(words)
221    }
222
223    /// Whether `ext` is in the set.
224    #[must_use]
225    pub fn contains(self, ext: Extension) -> bool {
226        let bit = ext as usize;
227        self.0
228            .get(bit / WORD_BITS)
229            .is_some_and(|word| (word >> (bit % WORD_BITS)) & 1 == 1)
230    }
231
232    /// Adds `ext` to the set.
233    pub fn insert(&mut self, ext: Extension) {
234        let bit = ext as usize;
235        if let Some(word) = self.0.get_mut(bit / WORD_BITS) {
236            *word |= 1u64 << (bit % WORD_BITS);
237        }
238    }
239
240    /// Removes `ext` from the set.
241    pub fn remove(&mut self, ext: Extension) {
242        let bit = ext as usize;
243        if let Some(word) = self.0.get_mut(bit / WORD_BITS) {
244            *word &= !(1u64 << (bit % WORD_BITS));
245        }
246    }
247
248    /// The union of this set and `other`.
249    #[must_use]
250    pub fn union(self, other: Extensions) -> Extensions {
251        let mut words = self.0;
252        for (word, &add) in words.iter_mut().zip(other.0.iter()) {
253            *word |= add;
254        }
255        Extensions(words)
256    }
257
258    /// Whether the set is empty.
259    #[must_use]
260    pub fn is_empty(self) -> bool {
261        self.0.iter().all(|&word| word == 0)
262    }
263
264    /// The set's extensions in [`Extension::ALL`] (deterministic) order.
265    pub fn iter(self) -> impl Iterator<Item = Extension> {
266        Extension::ALL
267            .iter()
268            .copied()
269            .filter(move |&ext| self.contains(ext))
270    }
271}
272
273impl core::fmt::Debug for Extensions {
274    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
275        f.debug_set()
276            .entries(self.iter().map(Extension::name))
277            .finish()
278    }
279}
280
281/// Per-flavor extension sets.
282pub mod presets {
283    use super::{Extension, Extensions};
284
285    /// Strict `CommonMark`: no extensions.
286    pub const COMMONMARK: Extensions = Extensions::empty();
287
288    /// `GitHub`-Flavored Markdown.
289    pub const GFM: Extensions = Extensions::from_list(&[
290        Extension::Strikeout,
291        Extension::PipeTables,
292        Extension::BacktickCodeBlocks,
293        Extension::TaskLists,
294        Extension::Autolink,
295        Extension::Footnotes,
296        Extension::TexMathDollars,
297        Extension::TexMathGfm,
298        Extension::GfmAutoIdentifiers,
299        Extension::RawHtml,
300        Extension::Emoji,
301        Extension::Alerts,
302    ]);
303
304    /// The `commonmark_x` dialect: `CommonMark` with a broad set of inline and block extensions
305    /// enabled. `backtick_code_blocks` is additionally carried because the shared Markdown engine
306    /// fences code on that flag, which `CommonMark` does natively.
307    pub const COMMONMARK_X: Extensions = Extensions::from_list(&[
308        Extension::Smart,
309        Extension::Strikeout,
310        Extension::Superscript,
311        Extension::Subscript,
312        Extension::PipeTables,
313        Extension::Footnotes,
314        Extension::TaskLists,
315        Extension::TexMathDollars,
316        Extension::FencedDivs,
317        Extension::BracketedSpans,
318        Extension::BacktickCodeBlocks,
319        Extension::RawHtml,
320        Extension::RawAttribute,
321        Extension::Attributes,
322        Extension::HeaderAttributes,
323        Extension::FencedCodeAttributes,
324        Extension::InlineCodeAttributes,
325        Extension::LinkAttributes,
326        Extension::DefinitionLists,
327        Extension::FancyLists,
328        Extension::GfmAutoIdentifiers,
329        Extension::ImplicitHeaderReferences,
330        Extension::Emoji,
331        Extension::Alerts,
332    ]);
333
334    /// The extended Markdown dialect: the broad default extension set.
335    pub const MARKDOWN: Extensions = Extensions::from_list(&[
336        Extension::AllSymbolsEscapable,
337        Extension::Smart,
338        Extension::Strikeout,
339        Extension::Superscript,
340        Extension::Subscript,
341        Extension::PipeTables,
342        Extension::Footnotes,
343        Extension::TaskLists,
344        Extension::TexMathDollars,
345        Extension::FencedDivs,
346        Extension::BracketedSpans,
347        Extension::RawHtml,
348        Extension::HeaderAttributes,
349        Extension::FencedCodeAttributes,
350        Extension::FencedCodeBlocks,
351        Extension::BacktickCodeBlocks,
352        Extension::InlineCodeAttributes,
353        Extension::LinkAttributes,
354        Extension::DefinitionLists,
355        Extension::GridTables,
356        Extension::MultilineTables,
357        Extension::SimpleTables,
358        Extension::TableCaptions,
359        Extension::LineBlocks,
360        Extension::FancyLists,
361        Extension::ExampleLists,
362        Extension::Startnum,
363        Extension::YamlMetadataBlock,
364        Extension::PandocTitleBlock,
365        Extension::AutoIdentifiers,
366        Extension::ImplicitHeaderReferences,
367        Extension::ImplicitFigures,
368        Extension::RawAttribute,
369        Extension::InlineNotes,
370        Extension::NativeDivs,
371        Extension::NativeSpans,
372        Extension::MarkdownInHtmlBlocks,
373        Extension::RawTex,
374        Extension::Citations,
375        Extension::TableAttributes,
376        Extension::BlankBeforeBlockquote,
377        Extension::BlankBeforeHeader,
378        Extension::EscapedLineBreaks,
379        Extension::IntrawordUnderscores,
380        Extension::SpaceInAtxHeader,
381    ]);
382
383    /// The legacy GitHub Markdown dialect (`markdown_github`). The set is restricted to the
384    /// variants that exist and affect writer output: backtick-fenced code, pipe tables, strikeout,
385    /// task lists, footnotes, autolinking, emoji, and alerts, but no smart typography, math, spans,
386    /// or fenced divs.
387    pub const MARKDOWN_GITHUB: Extensions = Extensions::from_list(&[
388        Extension::Strikeout,
389        Extension::PipeTables,
390        Extension::Footnotes,
391        Extension::TaskLists,
392        Extension::Autolink,
393        Extension::RawHtml,
394        Extension::FencedCodeBlocks,
395        Extension::BacktickCodeBlocks,
396        Extension::AutoIdentifiers,
397        Extension::GfmAutoIdentifiers,
398        Extension::Emoji,
399        Extension::Alerts,
400        Extension::IntrawordUnderscores,
401    ]);
402
403    /// The PHP Markdown Extra dialect (`markdown_phpextra`). The set is restricted to the variants
404    /// that exist and affect writer output: definition lists, fenced (tilde) code blocks, footnotes,
405    /// header and link attributes, pipe tables, and raw HTML. It has no backtick code fences, so code
406    /// fences are written with tildes, and no smart typography, math, strikeout, spans, or fenced divs.
407    pub const MARKDOWN_PHPEXTRA: Extensions = Extensions::from_list(&[
408        Extension::DefinitionLists,
409        Extension::FencedCodeBlocks,
410        Extension::Footnotes,
411        Extension::HeaderAttributes,
412        Extension::IntrawordUnderscores,
413        Extension::LinkAttributes,
414        Extension::MarkdownAttribute,
415        Extension::PipeTables,
416        Extension::RawHtml,
417    ]);
418
419    /// The `MultiMarkdown` dialect (`markdown_mmd`). The set is restricted to the variants that
420    /// exist and affect writer output: backtick-fenced code, definition lists, footnotes, pipe
421    /// tables, implicit figures and header references, sub/superscript, dollar math, raw HTML and raw
422    /// attributes, auto identifiers, `MultiMarkdown`'s trailing `[id]` header identifiers, and the
423    /// `data-markdown`
424    /// div marker. It has no header attribute blocks, strikeout, task lists, smart typography, spans,
425    /// or fenced divs. With `tex_math_dollars` on and taking precedence, a `tex_math_double_backslash`
426    /// surface would not change this dialect's writer output, so it is left out of the preset and math
427    /// is emitted as `$…$`.
428    pub const MARKDOWN_MMD: Extensions = Extensions::from_list(&[
429        Extension::AutoIdentifiers,
430        Extension::BacktickCodeBlocks,
431        Extension::DefinitionLists,
432        Extension::Footnotes,
433        Extension::ImplicitFigures,
434        Extension::ImplicitHeaderReferences,
435        Extension::IntrawordUnderscores,
436        Extension::MarkdownAttribute,
437        Extension::MmdHeaderIdentifiers,
438        Extension::PipeTables,
439        Extension::RawAttribute,
440        Extension::RawHtml,
441        Extension::Subscript,
442        Extension::Superscript,
443        Extension::TexMathDollars,
444    ]);
445
446    /// The original Markdown dialect (`markdown_strict`). The set is restricted to the variants that
447    /// exist and affect writer output — only raw HTML. With no fenced or backtick code, tables,
448    /// definition lists,
449    /// footnotes, task lists, math, or any attribute syntax, every richer construct falls back to
450    /// indented code, an HTML block, or a raw glyph. Lacking `intraword_underscores`, every `_` is
451    /// escaped; lacking `pipe_tables`, a literal `|` is left unescaped.
452    pub const MARKDOWN_STRICT: Extensions = Extensions::from_list(&[Extension::RawHtml]);
453
454    // The reader default sets below are broader than the writer presets above: a reader enables every
455    // construct the dialect can parse, whereas the writer presets carry only the extensions that shape
456    // the emitted text. Some entries name constructs the shared Markdown engine does not yet branch on;
457    // they are recorded so the dialect's default surface is complete and takes effect once modeled.
458
459    /// Reader defaults for the original Markdown dialect (`markdown_strict`): only raw HTML, plus the
460    /// shortcut and spaced reference-link forms.
461    pub const MARKDOWN_STRICT_READ: Extensions = Extensions::from_list(&[
462        Extension::RawHtml,
463        Extension::ShortcutReferenceLinks,
464        Extension::SpacedReferenceLinks,
465    ]);
466
467    /// Reader defaults for the GitHub Markdown dialect (`markdown_github`): the GitHub construct set —
468    /// strikeout, task lists, pipe tables, footnotes, bare-URI autolinking, emoji, alerts, backtick and
469    /// fenced code, auto identifiers in both forms, intra-word underscores, lists that open without a
470    /// preceding blank line, and the escaping/heading-spacing leniencies.
471    pub const MARKDOWN_GITHUB_READ: Extensions = Extensions::from_list(&[
472        Extension::Alerts,
473        Extension::AllSymbolsEscapable,
474        Extension::AutoIdentifiers,
475        Extension::Autolink,
476        Extension::BacktickCodeBlocks,
477        Extension::Emoji,
478        Extension::FencedCodeBlocks,
479        Extension::Footnotes,
480        Extension::GfmAutoIdentifiers,
481        Extension::IntrawordUnderscores,
482        Extension::ListsWithoutPrecedingBlankline,
483        Extension::PipeTables,
484        Extension::RawHtml,
485        Extension::ShortcutReferenceLinks,
486        Extension::SpaceInAtxHeader,
487        Extension::Strikeout,
488        Extension::TaskLists,
489    ]);
490
491    /// Reader defaults for the PHP Markdown Extra dialect (`markdown_phpextra`): abbreviations,
492    /// definition lists, fenced code, footnotes, header and link attributes, intra-word underscores,
493    /// the `data-markdown` div marker, pipe tables, raw HTML, and the reference-link forms.
494    pub const MARKDOWN_PHPEXTRA_READ: Extensions = Extensions::from_list(&[
495        Extension::Abbreviations,
496        Extension::DefinitionLists,
497        Extension::FencedCodeBlocks,
498        Extension::Footnotes,
499        Extension::HeaderAttributes,
500        Extension::IntrawordUnderscores,
501        Extension::LinkAttributes,
502        Extension::MarkdownAttribute,
503        Extension::PipeTables,
504        Extension::RawHtml,
505        Extension::ShortcutReferenceLinks,
506        Extension::SpacedReferenceLinks,
507    ]);
508
509    /// Reader defaults for the `MultiMarkdown` dialect (`markdown_mmd`): auto identifiers, backtick
510    /// code, definition lists, footnotes, implicit figures and header references, intra-word
511    /// underscores, the `data-markdown` div marker, `MultiMarkdown`'s trailing `[id]` header
512    /// identifiers, its link-attribute and title-block forms, pipe tables, raw HTML and raw attributes,
513    /// single-character sub/superscripts, the reference-link forms, sub/superscript spans, dollar math,
514    /// and the double-backslash math delimiters.
515    pub const MARKDOWN_MMD_READ: Extensions = Extensions::from_list(&[
516        Extension::AllSymbolsEscapable,
517        Extension::AutoIdentifiers,
518        Extension::BacktickCodeBlocks,
519        Extension::DefinitionLists,
520        Extension::Footnotes,
521        Extension::ImplicitFigures,
522        Extension::ImplicitHeaderReferences,
523        Extension::IntrawordUnderscores,
524        Extension::MarkdownAttribute,
525        Extension::MmdHeaderIdentifiers,
526        Extension::MmdLinkAttributes,
527        Extension::MmdTitleBlock,
528        Extension::PipeTables,
529        Extension::RawAttribute,
530        Extension::RawHtml,
531        Extension::ShortSubsuperscripts,
532        Extension::ShortcutReferenceLinks,
533        Extension::SpacedReferenceLinks,
534        Extension::Subscript,
535        Extension::Superscript,
536        Extension::TexMathDollars,
537        Extension::TexMathDoubleBackslash,
538    ]);
539}
540
541#[cfg(test)]
542mod tests {
543    use super::{Extension, Extensions, presets};
544
545    #[test]
546    fn words_cover_every_variant() {
547        // Every variant's bit index must land inside the backing array.
548        for ext in Extension::ALL {
549            assert!((*ext as usize) / super::WORD_BITS < super::WORDS);
550        }
551    }
552
553    #[test]
554    fn insert_remove_contains_round_trip() {
555        let mut set = Extensions::empty();
556        assert!(set.is_empty());
557        assert!(!set.contains(Extension::Footnotes));
558        set.insert(Extension::Footnotes);
559        assert!(set.contains(Extension::Footnotes));
560        assert!(!set.is_empty());
561        set.remove(Extension::Footnotes);
562        assert!(!set.contains(Extension::Footnotes));
563        assert!(set.is_empty());
564    }
565
566    #[test]
567    fn from_list_and_iter_follow_declaration_order() {
568        let set = Extensions::from_list(&[Extension::PipeTables, Extension::Smart]);
569        let collected: Vec<Extension> = set.iter().collect();
570        // `iter` yields in `ALL` order, regardless of `from_list` argument order.
571        assert_eq!(collected, vec![Extension::Smart, Extension::PipeTables]);
572    }
573
574    #[test]
575    fn commonmark_preset_is_empty_gfm_is_not() {
576        assert!(presets::COMMONMARK.is_empty());
577        assert!(presets::GFM.contains(Extension::Strikeout));
578        assert!(presets::GFM.contains(Extension::TaskLists));
579        assert!(presets::GFM.contains(Extension::PipeTables));
580        // GFM has no subscript/superscript; those belong to the broader Markdown dialects.
581        assert!(!presets::GFM.contains(Extension::Subscript));
582        assert!(!presets::GFM.contains(Extension::Superscript));
583    }
584
585    #[test]
586    fn markdown_and_commonmark_x_presets_are_broad() {
587        assert!(presets::MARKDOWN.contains(Extension::DefinitionLists));
588        assert!(presets::MARKDOWN.contains(Extension::YamlMetadataBlock));
589        assert!(presets::MARKDOWN.contains(Extension::Smart));
590        assert!(presets::COMMONMARK_X.contains(Extension::FencedDivs));
591        assert!(presets::COMMONMARK_X.contains(Extension::Attributes));
592        // The strict CommonMark dialect keeps none of these.
593        assert!(presets::COMMONMARK.is_empty());
594    }
595
596    #[test]
597    fn code_and_math_surface_variants_round_trip_and_seed_presets() {
598        for token in ["fenced_code_blocks", "backtick_code_blocks", "tex_math_gfm"] {
599            let ext = Extension::from_name(token).expect("a declared variant");
600            assert_eq!(ext.name(), token);
601        }
602        // The Markdown dialect fences code with both backtick and tilde forms.
603        assert!(presets::MARKDOWN.contains(Extension::FencedCodeBlocks));
604        assert!(presets::MARKDOWN.contains(Extension::BacktickCodeBlocks));
605        // GFM fences with backticks and renders math in its own surface; it has no tilde-fence form.
606        assert!(presets::GFM.contains(Extension::BacktickCodeBlocks));
607        assert!(presets::GFM.contains(Extension::TexMathGfm));
608        assert!(!presets::GFM.contains(Extension::FencedCodeBlocks));
609    }
610
611    #[test]
612    fn names_are_stable() {
613        assert_eq!(Extension::Footnotes.name(), "footnotes");
614        assert_eq!(Extension::Autolink.name(), "autolink_bare_uris");
615        assert_eq!(Extension::HardLineBreaks.name(), "hard_line_breaks");
616        assert_eq!(Extension::RawHtml.name(), "raw_html");
617    }
618
619    #[test]
620    fn from_name_round_trips_every_variant() {
621        for ext in Extension::ALL {
622            assert_eq!(Extension::from_name(ext.name()), Some(*ext));
623        }
624        assert_eq!(Extension::from_name("not_an_extension"), None);
625        assert_eq!(Extension::from_name(""), None);
626    }
627
628    #[test]
629    fn union_combines_both_sides() {
630        let a = Extensions::from_list(&[Extension::Strikeout]);
631        let b = Extensions::from_list(&[Extension::Subscript]);
632        let combined = a.union(b);
633        assert!(combined.contains(Extension::Strikeout));
634        assert!(combined.contains(Extension::Subscript));
635        assert!(!combined.contains(Extension::Superscript));
636        assert_eq!(a.union(Extensions::empty()), a);
637    }
638}