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. [`presets`] holds the per-flavor sets; strict
5//! `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// `from_list` indexing is sound only while each discriminant equals its `ALL` position; the
217// assertion turns a contiguity break into a build failure, not an out-of-bounds index.
218#[allow(clippy::indexing_slicing)]
219const _: () = {
220    let mut i = 0;
221    while i < Extension::ALL.len() {
222        assert!(Extension::ALL[i] as usize == i);
223        i += 1;
224    }
225};
226
227/// A deterministic, allocation-free set of [`Extension`]s, backed by a fixed array of 64-bit words
228/// indexed by each variant's position in [`Extension::ALL`].
229#[derive(Clone, Copy, PartialEq, Eq)]
230pub struct Extensions([u64; WORDS]);
231
232impl Default for Extensions {
233    fn default() -> Self {
234        Self::empty()
235    }
236}
237
238impl Extensions {
239    /// The empty set (strict `CommonMark`).
240    #[must_use]
241    pub const fn empty() -> Self {
242        Self([0; WORDS])
243    }
244
245    /// The set containing exactly `list`. Const so presets are `const` values.
246    #[must_use]
247    // In bounds by the contiguity assertion; slice `get` is not const-usable on the pinned toolchain.
248    #[allow(clippy::indexing_slicing)]
249    pub const fn from_list(list: &[Extension]) -> Self {
250        let mut words = [0u64; WORDS];
251        let mut i = 0;
252        while i < list.len() {
253            let bit = list[i] as usize;
254            words[bit / WORD_BITS] |= 1u64 << (bit % WORD_BITS);
255            i += 1;
256        }
257        Self(words)
258    }
259
260    /// Whether `ext` is in the set.
261    #[must_use]
262    pub fn contains(self, ext: Extension) -> bool {
263        let bit = ext as usize;
264        self.0
265            .get(bit / WORD_BITS)
266            .is_some_and(|word| (word >> (bit % WORD_BITS)) & 1 == 1)
267    }
268
269    /// Adds `ext` to the set.
270    pub fn insert(&mut self, ext: Extension) {
271        let bit = ext as usize;
272        if let Some(word) = self.0.get_mut(bit / WORD_BITS) {
273            *word |= 1u64 << (bit % WORD_BITS);
274        }
275    }
276
277    /// Removes `ext` from the set.
278    pub fn remove(&mut self, ext: Extension) {
279        let bit = ext as usize;
280        if let Some(word) = self.0.get_mut(bit / WORD_BITS) {
281            *word &= !(1u64 << (bit % WORD_BITS));
282        }
283    }
284
285    /// The union of this set and `other`.
286    #[must_use]
287    pub fn union(self, other: Extensions) -> Extensions {
288        let mut words = self.0;
289        for (word, &add) in words.iter_mut().zip(other.0.iter()) {
290            *word |= add;
291        }
292        Extensions(words)
293    }
294
295    /// Whether the set is empty.
296    #[must_use]
297    pub fn is_empty(self) -> bool {
298        self.0.iter().all(|&word| word == 0)
299    }
300
301    /// The set's extensions in [`Extension::ALL`] (deterministic) order.
302    pub fn iter(self) -> impl Iterator<Item = Extension> {
303        Extension::ALL
304            .iter()
305            .copied()
306            .filter(move |&ext| self.contains(ext))
307    }
308}
309
310impl core::fmt::Debug for Extensions {
311    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
312        f.debug_set()
313            .entries(self.iter().map(Extension::name))
314            .finish()
315    }
316}
317
318/// Per-flavor extension sets.
319pub mod presets {
320    use super::{Extension, Extensions};
321
322    /// Strict `CommonMark`: no extensions.
323    pub const COMMONMARK: Extensions = Extensions::empty();
324
325    /// `GitHub`-Flavored Markdown.
326    pub const GFM: Extensions = Extensions::from_list(&[
327        Extension::Strikeout,
328        Extension::PipeTables,
329        Extension::BacktickCodeBlocks,
330        Extension::TaskLists,
331        Extension::Autolink,
332        Extension::Footnotes,
333        Extension::TexMathDollars,
334        Extension::TexMathGfm,
335        Extension::GfmAutoIdentifiers,
336        Extension::RawHtml,
337        Extension::Emoji,
338        Extension::Alerts,
339    ]);
340
341    /// The `commonmark_x` dialect: `CommonMark` with a broad set of inline and block extensions
342    /// enabled. `backtick_code_blocks` is additionally carried because the shared Markdown engine
343    /// fences code on that flag, which `CommonMark` does natively.
344    pub const COMMONMARK_X: Extensions = Extensions::from_list(&[
345        Extension::Smart,
346        Extension::Strikeout,
347        Extension::Superscript,
348        Extension::Subscript,
349        Extension::PipeTables,
350        Extension::Footnotes,
351        Extension::TaskLists,
352        Extension::TexMathDollars,
353        Extension::FencedDivs,
354        Extension::BracketedSpans,
355        Extension::BacktickCodeBlocks,
356        Extension::RawHtml,
357        Extension::RawAttribute,
358        Extension::Attributes,
359        Extension::HeaderAttributes,
360        Extension::FencedCodeAttributes,
361        Extension::InlineCodeAttributes,
362        Extension::LinkAttributes,
363        Extension::DefinitionLists,
364        Extension::FancyLists,
365        Extension::GfmAutoIdentifiers,
366        Extension::ImplicitHeaderReferences,
367        Extension::Emoji,
368        Extension::Alerts,
369    ]);
370
371    /// The extended Markdown dialect: the broad default extension set.
372    pub const MARKDOWN: Extensions = Extensions::from_list(&[
373        Extension::AllSymbolsEscapable,
374        Extension::Smart,
375        Extension::Strikeout,
376        Extension::Superscript,
377        Extension::Subscript,
378        Extension::PipeTables,
379        Extension::Footnotes,
380        Extension::TaskLists,
381        Extension::TexMathDollars,
382        Extension::FencedDivs,
383        Extension::BracketedSpans,
384        Extension::RawHtml,
385        Extension::HeaderAttributes,
386        Extension::FencedCodeAttributes,
387        Extension::FencedCodeBlocks,
388        Extension::BacktickCodeBlocks,
389        Extension::InlineCodeAttributes,
390        Extension::LinkAttributes,
391        Extension::DefinitionLists,
392        Extension::GridTables,
393        Extension::MultilineTables,
394        Extension::SimpleTables,
395        Extension::TableCaptions,
396        Extension::LineBlocks,
397        Extension::FancyLists,
398        Extension::ExampleLists,
399        Extension::Startnum,
400        Extension::YamlMetadataBlock,
401        Extension::PandocTitleBlock,
402        Extension::AutoIdentifiers,
403        Extension::ImplicitHeaderReferences,
404        Extension::ImplicitFigures,
405        Extension::RawAttribute,
406        Extension::InlineNotes,
407        Extension::NativeDivs,
408        Extension::NativeSpans,
409        Extension::MarkdownInHtmlBlocks,
410        Extension::RawTex,
411        Extension::Citations,
412        Extension::TableAttributes,
413        Extension::BlankBeforeBlockquote,
414        Extension::BlankBeforeHeader,
415        Extension::EscapedLineBreaks,
416        Extension::IntrawordUnderscores,
417        Extension::SpaceInAtxHeader,
418    ]);
419
420    /// The legacy GitHub Markdown dialect (`markdown_github`): the variants that exist and affect
421    /// writer output. No smart typography, math, spans, or fenced divs.
422    pub const MARKDOWN_GITHUB: Extensions = Extensions::from_list(&[
423        Extension::Strikeout,
424        Extension::PipeTables,
425        Extension::Footnotes,
426        Extension::TaskLists,
427        Extension::Autolink,
428        Extension::RawHtml,
429        Extension::FencedCodeBlocks,
430        Extension::BacktickCodeBlocks,
431        Extension::AutoIdentifiers,
432        Extension::GfmAutoIdentifiers,
433        Extension::Emoji,
434        Extension::Alerts,
435        Extension::IntrawordUnderscores,
436    ]);
437
438    /// The PHP Markdown Extra dialect (`markdown_phpextra`): the variants that exist and affect
439    /// writer output. It has no backtick code fences, so code fences are written with tildes, and
440    /// no smart typography, math, strikeout, spans, or fenced divs.
441    pub const MARKDOWN_PHPEXTRA: Extensions = Extensions::from_list(&[
442        Extension::DefinitionLists,
443        Extension::FencedCodeBlocks,
444        Extension::Footnotes,
445        Extension::HeaderAttributes,
446        Extension::IntrawordUnderscores,
447        Extension::LinkAttributes,
448        Extension::MarkdownAttribute,
449        Extension::PipeTables,
450        Extension::RawHtml,
451    ]);
452
453    /// The `MultiMarkdown` dialect (`markdown_mmd`): the variants that exist and affect writer
454    /// output. It has no header attribute blocks, strikeout, task lists, smart typography, spans,
455    /// or fenced divs. With `tex_math_dollars` on and taking precedence, a `tex_math_double_backslash`
456    /// surface would not change this dialect's writer output, so it is left out of the preset and math
457    /// is emitted as `$…$`.
458    pub const MARKDOWN_MMD: Extensions = Extensions::from_list(&[
459        Extension::AutoIdentifiers,
460        Extension::BacktickCodeBlocks,
461        Extension::DefinitionLists,
462        Extension::Footnotes,
463        Extension::ImplicitFigures,
464        Extension::ImplicitHeaderReferences,
465        Extension::IntrawordUnderscores,
466        Extension::MarkdownAttribute,
467        Extension::MmdHeaderIdentifiers,
468        Extension::PipeTables,
469        Extension::RawAttribute,
470        Extension::RawHtml,
471        Extension::Subscript,
472        Extension::Superscript,
473        Extension::TexMathDollars,
474    ]);
475
476    /// The original Markdown dialect (`markdown_strict`): only raw HTML. With no fenced or backtick
477    /// code, tables, definition lists, footnotes, task lists, math, or any attribute syntax, every
478    /// richer construct falls back to indented code, an HTML block, or a raw glyph. Lacking
479    /// `intraword_underscores`, every `_` is escaped; lacking `pipe_tables`, a literal `|` is left
480    /// unescaped.
481    pub const MARKDOWN_STRICT: Extensions = Extensions::from_list(&[Extension::RawHtml]);
482
483    // Reader defaults are broader than writer presets: readers enable every parseable construct.
484    // Some entries are not yet branched on by the engine; recorded so they take effect once modeled.
485
486    /// Reader defaults for the extended Markdown dialect (`markdown`): the broad writer set plus the
487    /// two constructs the writer preset omits, user-defined `LaTeX` macro expansion and the shortcut
488    /// reference-link form.
489    pub const MARKDOWN_READ: Extensions = Extensions::from_list(&[
490        Extension::AllSymbolsEscapable,
491        Extension::AutoIdentifiers,
492        Extension::BacktickCodeBlocks,
493        Extension::BlankBeforeBlockquote,
494        Extension::BlankBeforeHeader,
495        Extension::BracketedSpans,
496        Extension::Citations,
497        Extension::DefinitionLists,
498        Extension::EscapedLineBreaks,
499        Extension::ExampleLists,
500        Extension::FancyLists,
501        Extension::FencedCodeAttributes,
502        Extension::FencedCodeBlocks,
503        Extension::FencedDivs,
504        Extension::Footnotes,
505        Extension::GridTables,
506        Extension::HeaderAttributes,
507        Extension::ImplicitFigures,
508        Extension::ImplicitHeaderReferences,
509        Extension::InlineCodeAttributes,
510        Extension::InlineNotes,
511        Extension::IntrawordUnderscores,
512        Extension::LatexMacros,
513        Extension::LineBlocks,
514        Extension::LinkAttributes,
515        Extension::MarkdownInHtmlBlocks,
516        Extension::MultilineTables,
517        Extension::NativeDivs,
518        Extension::NativeSpans,
519        Extension::PandocTitleBlock,
520        Extension::PipeTables,
521        Extension::RawAttribute,
522        Extension::RawHtml,
523        Extension::RawTex,
524        Extension::ShortcutReferenceLinks,
525        Extension::SimpleTables,
526        Extension::Smart,
527        Extension::SpaceInAtxHeader,
528        Extension::Startnum,
529        Extension::Strikeout,
530        Extension::Subscript,
531        Extension::Superscript,
532        Extension::TableAttributes,
533        Extension::TableCaptions,
534        Extension::TaskLists,
535        Extension::TexMathDollars,
536        Extension::YamlMetadataBlock,
537    ]);
538
539    /// Reader defaults for the original Markdown dialect (`markdown_strict`): only raw HTML, plus the
540    /// shortcut and spaced reference-link forms.
541    pub const MARKDOWN_STRICT_READ: Extensions = Extensions::from_list(&[
542        Extension::RawHtml,
543        Extension::ShortcutReferenceLinks,
544        Extension::SpacedReferenceLinks,
545    ]);
546
547    /// Reader defaults for the GitHub Markdown dialect (`markdown_github`): the GitHub construct
548    /// set, including lists that open without a preceding blank line and the escaping and
549    /// heading-spacing leniencies.
550    pub const MARKDOWN_GITHUB_READ: Extensions = Extensions::from_list(&[
551        Extension::Alerts,
552        Extension::AllSymbolsEscapable,
553        Extension::AutoIdentifiers,
554        Extension::Autolink,
555        Extension::BacktickCodeBlocks,
556        Extension::Emoji,
557        Extension::FencedCodeBlocks,
558        Extension::Footnotes,
559        Extension::GfmAutoIdentifiers,
560        Extension::IntrawordUnderscores,
561        Extension::ListsWithoutPrecedingBlankline,
562        Extension::PipeTables,
563        Extension::RawHtml,
564        Extension::ShortcutReferenceLinks,
565        Extension::SpaceInAtxHeader,
566        Extension::Strikeout,
567        Extension::TaskLists,
568    ]);
569
570    /// Reader defaults for the PHP Markdown Extra dialect (`markdown_phpextra`): the writer set
571    /// plus abbreviations and the reference-link forms.
572    pub const MARKDOWN_PHPEXTRA_READ: Extensions = Extensions::from_list(&[
573        Extension::Abbreviations,
574        Extension::DefinitionLists,
575        Extension::FencedCodeBlocks,
576        Extension::Footnotes,
577        Extension::HeaderAttributes,
578        Extension::IntrawordUnderscores,
579        Extension::LinkAttributes,
580        Extension::MarkdownAttribute,
581        Extension::PipeTables,
582        Extension::RawHtml,
583        Extension::ShortcutReferenceLinks,
584        Extension::SpacedReferenceLinks,
585    ]);
586
587    /// Reader defaults for the `MultiMarkdown` dialect (`markdown_mmd`): the writer set plus
588    /// `MultiMarkdown`'s link-attribute and title-block forms, single-character sub/superscripts,
589    /// the reference-link forms, the all-symbols escaping leniency, and the double-backslash math
590    /// delimiters.
591    pub const MARKDOWN_MMD_READ: Extensions = Extensions::from_list(&[
592        Extension::AllSymbolsEscapable,
593        Extension::AutoIdentifiers,
594        Extension::BacktickCodeBlocks,
595        Extension::DefinitionLists,
596        Extension::Footnotes,
597        Extension::ImplicitFigures,
598        Extension::ImplicitHeaderReferences,
599        Extension::IntrawordUnderscores,
600        Extension::MarkdownAttribute,
601        Extension::MmdHeaderIdentifiers,
602        Extension::MmdLinkAttributes,
603        Extension::MmdTitleBlock,
604        Extension::PipeTables,
605        Extension::RawAttribute,
606        Extension::RawHtml,
607        Extension::ShortSubsuperscripts,
608        Extension::ShortcutReferenceLinks,
609        Extension::SpacedReferenceLinks,
610        Extension::Subscript,
611        Extension::Superscript,
612        Extension::TexMathDollars,
613        Extension::TexMathDoubleBackslash,
614    ]);
615
616    /// Reader defaults for `GitHub`-Flavored Markdown (`gfm`): the writer set plus recognition of a
617    /// `---`-delimited YAML metadata block, which the writer preset omits.
618    pub const GFM_READ: Extensions = Extensions::from_list(&[
619        Extension::Alerts,
620        Extension::Autolink,
621        Extension::BacktickCodeBlocks,
622        Extension::Emoji,
623        Extension::Footnotes,
624        Extension::GfmAutoIdentifiers,
625        Extension::PipeTables,
626        Extension::RawHtml,
627        Extension::Strikeout,
628        Extension::TaskLists,
629        Extension::TexMathDollars,
630        Extension::TexMathGfm,
631        Extension::YamlMetadataBlock,
632    ]);
633
634    /// Reader defaults for the `commonmark_x` dialect: the writer set plus recognition of a
635    /// `---`-delimited YAML metadata block, which the writer preset omits.
636    pub const COMMONMARK_X_READ: Extensions = Extensions::from_list(&[
637        Extension::Alerts,
638        Extension::Attributes,
639        Extension::BacktickCodeBlocks,
640        Extension::BracketedSpans,
641        Extension::DefinitionLists,
642        Extension::Emoji,
643        Extension::FancyLists,
644        Extension::FencedCodeAttributes,
645        Extension::FencedDivs,
646        Extension::Footnotes,
647        Extension::GfmAutoIdentifiers,
648        Extension::HeaderAttributes,
649        Extension::ImplicitHeaderReferences,
650        Extension::InlineCodeAttributes,
651        Extension::LinkAttributes,
652        Extension::PipeTables,
653        Extension::RawAttribute,
654        Extension::RawHtml,
655        Extension::Smart,
656        Extension::Strikeout,
657        Extension::Subscript,
658        Extension::Superscript,
659        Extension::TaskLists,
660        Extension::TexMathDollars,
661        Extension::YamlMetadataBlock,
662    ]);
663}
664
665#[cfg(test)]
666mod tests {
667    use super::{Extension, Extensions, presets};
668
669    #[test]
670    fn words_cover_every_variant() {
671        // Every variant's bit index must land inside the backing array.
672        for ext in Extension::ALL {
673            assert!((*ext as usize) / super::WORD_BITS < super::WORDS);
674        }
675    }
676
677    #[test]
678    fn insert_remove_contains_round_trip() {
679        let mut set = Extensions::empty();
680        assert!(set.is_empty());
681        assert!(!set.contains(Extension::Footnotes));
682        set.insert(Extension::Footnotes);
683        assert!(set.contains(Extension::Footnotes));
684        assert!(!set.is_empty());
685        set.remove(Extension::Footnotes);
686        assert!(!set.contains(Extension::Footnotes));
687        assert!(set.is_empty());
688    }
689
690    #[test]
691    fn from_list_and_iter_follow_declaration_order() {
692        let set = Extensions::from_list(&[Extension::PipeTables, Extension::Smart]);
693        let collected: Vec<Extension> = set.iter().collect();
694        // `iter` yields in `ALL` order, regardless of `from_list` argument order.
695        assert_eq!(collected, vec![Extension::Smart, Extension::PipeTables]);
696    }
697
698    #[test]
699    fn commonmark_preset_is_empty_gfm_is_not() {
700        assert!(presets::COMMONMARK.is_empty());
701        assert!(presets::GFM.contains(Extension::Strikeout));
702        assert!(presets::GFM.contains(Extension::TaskLists));
703        assert!(presets::GFM.contains(Extension::PipeTables));
704        // GFM has no subscript/superscript; those belong to the broader Markdown dialects.
705        assert!(!presets::GFM.contains(Extension::Subscript));
706        assert!(!presets::GFM.contains(Extension::Superscript));
707    }
708
709    #[test]
710    fn markdown_and_commonmark_x_presets_are_broad() {
711        assert!(presets::MARKDOWN.contains(Extension::DefinitionLists));
712        assert!(presets::MARKDOWN.contains(Extension::YamlMetadataBlock));
713        assert!(presets::MARKDOWN.contains(Extension::Smart));
714        assert!(presets::COMMONMARK_X.contains(Extension::FencedDivs));
715        assert!(presets::COMMONMARK_X.contains(Extension::Attributes));
716        // The strict CommonMark dialect keeps none of these.
717        assert!(presets::COMMONMARK.is_empty());
718    }
719
720    #[test]
721    fn code_and_math_surface_variants_round_trip_and_seed_presets() {
722        for token in ["fenced_code_blocks", "backtick_code_blocks", "tex_math_gfm"] {
723            let ext = Extension::from_name(token).expect("a declared variant");
724            assert_eq!(ext.name(), token);
725        }
726        // The Markdown dialect fences code with both backtick and tilde forms.
727        assert!(presets::MARKDOWN.contains(Extension::FencedCodeBlocks));
728        assert!(presets::MARKDOWN.contains(Extension::BacktickCodeBlocks));
729        // GFM fences with backticks and renders math in its own surface; it has no tilde-fence form.
730        assert!(presets::GFM.contains(Extension::BacktickCodeBlocks));
731        assert!(presets::GFM.contains(Extension::TexMathGfm));
732        assert!(!presets::GFM.contains(Extension::FencedCodeBlocks));
733    }
734
735    #[test]
736    fn names_are_stable() {
737        assert_eq!(Extension::Footnotes.name(), "footnotes");
738        assert_eq!(Extension::Autolink.name(), "autolink_bare_uris");
739        assert_eq!(Extension::HardLineBreaks.name(), "hard_line_breaks");
740        assert_eq!(Extension::RawHtml.name(), "raw_html");
741    }
742
743    #[test]
744    fn from_name_round_trips_every_variant() {
745        for ext in Extension::ALL {
746            assert_eq!(Extension::from_name(ext.name()), Some(*ext));
747        }
748        assert_eq!(Extension::from_name("not_an_extension"), None);
749        assert_eq!(Extension::from_name(""), None);
750    }
751
752    #[test]
753    fn union_combines_both_sides() {
754        let a = Extensions::from_list(&[Extension::Strikeout]);
755        let b = Extensions::from_list(&[Extension::Subscript]);
756        let combined = a.union(b);
757        assert!(combined.contains(Extension::Strikeout));
758        assert!(combined.contains(Extension::Subscript));
759        assert!(!combined.contains(Extension::Superscript));
760        assert_eq!(a.union(Extensions::empty()), a);
761    }
762}