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