1macro_rules! define_extensions {
10 ($($(#[$attribute:meta])* $variant:ident => $name:literal),+ $(,)?) => {
11 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14 #[non_exhaustive]
15 pub enum Extension { $($(#[$attribute])* $variant),+ }
16
17 impl Extension {
18 pub const ALL: &'static [Extension] = &[$(Extension::$variant),+];
20 pub const COUNT: usize = Self::ALL.len();
22
23 #[must_use]
25 pub const fn name(self) -> &'static str {
26 match self { $(Extension::$variant => $name),+ }
27 }
28
29 #[must_use]
31 pub fn from_name(name: &str) -> Option<Extension> {
32 match name { $($name => Some(Extension::$variant),)+ _ => None }
33 }
34 }
35 };
36}
37
38define_extensions! {
39 Smart => "smart",
41 Strikeout => "strikeout",
43 Superscript => "superscript",
45 Subscript => "subscript",
47 PipeTables => "pipe_tables",
49 Footnotes => "footnotes",
51 TaskLists => "task_lists",
53 Autolink => "autolink_bare_uris",
55 TexMathDollars => "tex_math_dollars",
57 FencedDivs => "fenced_divs",
59 BracketedSpans => "bracketed_spans",
61 HardLineBreaks => "hard_line_breaks",
63 RawHtml => "raw_html",
65 HeaderAttributes => "header_attributes",
67 FencedCodeAttributes => "fenced_code_attributes",
69 InlineCodeAttributes => "inline_code_attributes",
71 LinkAttributes => "link_attributes",
73 Attributes => "attributes",
75 DefinitionLists => "definition_lists",
77 GridTables => "grid_tables",
79 MultilineTables => "multiline_tables",
81 SimpleTables => "simple_tables",
83 TableCaptions => "table_captions",
85 LineBlocks => "line_blocks",
87 FancyLists => "fancy_lists",
89 ExampleLists => "example_lists",
91 Startnum => "startnum",
93 YamlMetadataBlock => "yaml_metadata_block",
95 PandocTitleBlock => "pandoc_title_block",
97 AutoIdentifiers => "auto_identifiers",
99 GfmAutoIdentifiers => "gfm_auto_identifiers",
102 AsciiIdentifiers => "ascii_identifiers",
104 MmdHeaderIdentifiers => "mmd_header_identifiers",
107 ImplicitHeaderReferences => "implicit_header_references",
109 ImplicitFigures => "implicit_figures",
111 RawAttribute => "raw_attribute",
113 InlineNotes => "inline_notes",
115 NativeDivs => "native_divs",
117 NativeSpans => "native_spans",
119 MarkdownInHtmlBlocks => "markdown_in_html_blocks",
121 MarkdownAttribute => "markdown_attribute",
125 RawTex => "raw_tex",
127 Citations => "citations",
129 TableAttributes => "table_attributes",
131 BlankBeforeBlockquote => "blank_before_blockquote",
133 BlankBeforeHeader => "blank_before_header",
135 Mark => "mark",
137 Emoji => "emoji",
139 Alerts => "alerts",
141 TexMathSingleBackslash => "tex_math_single_backslash",
143 TexMathDoubleBackslash => "tex_math_double_backslash",
145 FencedCodeBlocks => "fenced_code_blocks",
148 BacktickCodeBlocks => "backtick_code_blocks",
150 TexMathGfm => "tex_math_gfm",
153 EscapedLineBreaks => "escaped_line_breaks",
156 IntrawordUnderscores => "intraword_underscores",
159 ListsWithoutPrecedingBlankline => "lists_without_preceding_blankline",
163 Abbreviations => "abbreviations",
165 AllSymbolsEscapable => "all_symbols_escapable",
167 AngleBracketsEscapable => "angle_brackets_escapable",
169 EastAsianLineBreaks => "east_asian_line_breaks",
171 FourSpaceRule => "four_space_rule",
173 Gutenberg => "gutenberg",
175 IgnoreLineBreaks => "ignore_line_breaks",
177 LatexMacros => "latex_macros",
179 LiterateHaskell => "literate_haskell",
181 MmdLinkAttributes => "mmd_link_attributes",
183 MmdTitleBlock => "mmd_title_block",
185 OldDashes => "old_dashes",
187 RawMarkdown => "raw_markdown",
189 RebaseRelativePaths => "rebase_relative_paths",
191 ShortSubsuperscripts => "short_subsuperscripts",
193 ShortcutReferenceLinks => "shortcut_reference_links",
195 SpaceInAtxHeader => "space_in_atx_header",
197 SpacedReferenceLinks => "spaced_reference_links",
199 WikilinksTitleAfterPipe => "wikilinks_title_after_pipe",
201 WikilinksTitleBeforePipe => "wikilinks_title_before_pipe",
203 Styles => "styles",
206 NativeNumbering => "native_numbering",
209 EmptyParagraphs => "empty_paragraphs",
211}
212
213const WORD_BITS: usize = u64::BITS as usize;
214const WORDS: usize = Extension::COUNT.div_ceil(WORD_BITS);
215
216#[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#[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 #[must_use]
241 pub const fn empty() -> Self {
242 Self([0; WORDS])
243 }
244
245 #[must_use]
247 #[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 #[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 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 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 #[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 #[must_use]
297 pub fn is_empty(self) -> bool {
298 self.0.iter().all(|&word| word == 0)
299 }
300
301 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
318pub mod presets {
320 use super::{Extension, Extensions};
321
322 pub const COMMONMARK: Extensions = Extensions::empty();
324
325 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 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 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 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 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 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 pub const MARKDOWN_STRICT: Extensions = Extensions::from_list(&[Extension::RawHtml]);
482
483 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 pub const MARKDOWN_STRICT_READ: Extensions = Extensions::from_list(&[
542 Extension::RawHtml,
543 Extension::ShortcutReferenceLinks,
544 Extension::SpacedReferenceLinks,
545 ]);
546
547 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 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 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 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 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 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 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 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 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 assert!(presets::MARKDOWN.contains(Extension::FencedCodeBlocks));
728 assert!(presets::MARKDOWN.contains(Extension::BacktickCodeBlocks));
729 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}