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}