1macro_rules! define_extensions {
10 ($($variant:ident => $name:literal),+ $(,)?) => {
11 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14 #[non_exhaustive]
15 pub enum Extension { $($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",
40 Strikeout => "strikeout",
41 Superscript => "superscript",
42 Subscript => "subscript",
43 PipeTables => "pipe_tables",
44 Footnotes => "footnotes",
45 TaskLists => "task_lists",
46 Autolink => "autolink_bare_uris",
47 TexMathDollars => "tex_math_dollars",
48 FencedDivs => "fenced_divs",
49 BracketedSpans => "bracketed_spans",
50 HardLineBreaks => "hard_line_breaks",
51 RawHtml => "raw_html",
52 HeaderAttributes => "header_attributes",
54 FencedCodeAttributes => "fenced_code_attributes",
55 InlineCodeAttributes => "inline_code_attributes",
56 LinkAttributes => "link_attributes",
57 Attributes => "attributes",
59 DefinitionLists => "definition_lists",
61 GridTables => "grid_tables",
62 MultilineTables => "multiline_tables",
63 SimpleTables => "simple_tables",
64 TableCaptions => "table_captions",
65 LineBlocks => "line_blocks",
66 FancyLists => "fancy_lists",
68 ExampleLists => "example_lists",
69 Startnum => "startnum",
70 YamlMetadataBlock => "yaml_metadata_block",
72 PandocTitleBlock => "pandoc_title_block",
73 AutoIdentifiers => "auto_identifiers",
75 GfmAutoIdentifiers => "gfm_auto_identifiers",
76 AsciiIdentifiers => "ascii_identifiers",
78 MmdHeaderIdentifiers => "mmd_header_identifiers",
81 ImplicitHeaderReferences => "implicit_header_references",
82 ImplicitFigures => "implicit_figures",
84 RawAttribute => "raw_attribute",
86 InlineNotes => "inline_notes",
88 NativeDivs => "native_divs",
90 NativeSpans => "native_spans",
91 MarkdownInHtmlBlocks => "markdown_in_html_blocks",
93 MarkdownAttribute => "markdown_attribute",
96 RawTex => "raw_tex",
98 Citations => "citations",
100 TableAttributes => "table_attributes",
102 BlankBeforeBlockquote => "blank_before_blockquote",
104 BlankBeforeHeader => "blank_before_header",
105 Mark => "mark",
107 Emoji => "emoji",
109 Alerts => "alerts",
111 TexMathSingleBackslash => "tex_math_single_backslash",
113 TexMathDoubleBackslash => "tex_math_double_backslash",
114 FencedCodeBlocks => "fenced_code_blocks",
117 BacktickCodeBlocks => "backtick_code_blocks",
118 TexMathGfm => "tex_math_gfm",
121 EscapedLineBreaks => "escaped_line_breaks",
124 IntrawordUnderscores => "intraword_underscores",
127 ListsWithoutPrecedingBlankline => "lists_without_preceding_blankline",
130 Abbreviations => "abbreviations",
132 AllSymbolsEscapable => "all_symbols_escapable",
134 AngleBracketsEscapable => "angle_brackets_escapable",
136 EastAsianLineBreaks => "east_asian_line_breaks",
138 FourSpaceRule => "four_space_rule",
140 Gutenberg => "gutenberg",
142 IgnoreLineBreaks => "ignore_line_breaks",
144 LatexMacros => "latex_macros",
146 LiterateHaskell => "literate_haskell",
148 MmdLinkAttributes => "mmd_link_attributes",
150 MmdTitleBlock => "mmd_title_block",
152 OldDashes => "old_dashes",
154 RawMarkdown => "raw_markdown",
156 RebaseRelativePaths => "rebase_relative_paths",
158 ShortSubsuperscripts => "short_subsuperscripts",
160 ShortcutReferenceLinks => "shortcut_reference_links",
162 SpaceInAtxHeader => "space_in_atx_header",
164 SpacedReferenceLinks => "spaced_reference_links",
166 WikilinksTitleAfterPipe => "wikilinks_title_after_pipe",
168 WikilinksTitleBeforePipe => "wikilinks_title_before_pipe",
170}
171
172const WORD_BITS: usize = u64::BITS as usize;
173const WORDS: usize = Extension::COUNT.div_ceil(WORD_BITS);
174
175#[allow(clippy::indexing_slicing)]
180const _: () = {
181 let mut i = 0;
182 while i < Extension::ALL.len() {
183 assert!(Extension::ALL[i] as usize == i);
184 i += 1;
185 }
186};
187
188#[derive(Clone, Copy, PartialEq, Eq)]
191pub struct Extensions([u64; WORDS]);
192
193impl Default for Extensions {
194 fn default() -> Self {
195 Self::empty()
196 }
197}
198
199impl Extensions {
200 #[must_use]
202 pub const fn empty() -> Self {
203 Self([0; WORDS])
204 }
205
206 #[must_use]
208 #[allow(clippy::indexing_slicing)]
212 pub const fn from_list(list: &[Extension]) -> Self {
213 let mut words = [0u64; WORDS];
214 let mut i = 0;
215 while i < list.len() {
216 let bit = list[i] as usize;
217 words[bit / WORD_BITS] |= 1u64 << (bit % WORD_BITS);
218 i += 1;
219 }
220 Self(words)
221 }
222
223 #[must_use]
225 pub fn contains(self, ext: Extension) -> bool {
226 let bit = ext as usize;
227 self.0
228 .get(bit / WORD_BITS)
229 .is_some_and(|word| (word >> (bit % WORD_BITS)) & 1 == 1)
230 }
231
232 pub fn insert(&mut self, ext: Extension) {
234 let bit = ext as usize;
235 if let Some(word) = self.0.get_mut(bit / WORD_BITS) {
236 *word |= 1u64 << (bit % WORD_BITS);
237 }
238 }
239
240 pub fn remove(&mut self, ext: Extension) {
242 let bit = ext as usize;
243 if let Some(word) = self.0.get_mut(bit / WORD_BITS) {
244 *word &= !(1u64 << (bit % WORD_BITS));
245 }
246 }
247
248 #[must_use]
250 pub fn union(self, other: Extensions) -> Extensions {
251 let mut words = self.0;
252 for (word, &add) in words.iter_mut().zip(other.0.iter()) {
253 *word |= add;
254 }
255 Extensions(words)
256 }
257
258 #[must_use]
260 pub fn is_empty(self) -> bool {
261 self.0.iter().all(|&word| word == 0)
262 }
263
264 pub fn iter(self) -> impl Iterator<Item = Extension> {
266 Extension::ALL
267 .iter()
268 .copied()
269 .filter(move |&ext| self.contains(ext))
270 }
271}
272
273impl core::fmt::Debug for Extensions {
274 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
275 f.debug_set()
276 .entries(self.iter().map(Extension::name))
277 .finish()
278 }
279}
280
281pub mod presets {
283 use super::{Extension, Extensions};
284
285 pub const COMMONMARK: Extensions = Extensions::empty();
287
288 pub const GFM: Extensions = Extensions::from_list(&[
290 Extension::Strikeout,
291 Extension::PipeTables,
292 Extension::BacktickCodeBlocks,
293 Extension::TaskLists,
294 Extension::Autolink,
295 Extension::Footnotes,
296 Extension::TexMathDollars,
297 Extension::TexMathGfm,
298 Extension::GfmAutoIdentifiers,
299 Extension::RawHtml,
300 Extension::Emoji,
301 Extension::Alerts,
302 ]);
303
304 pub const COMMONMARK_X: Extensions = Extensions::from_list(&[
308 Extension::Smart,
309 Extension::Strikeout,
310 Extension::Superscript,
311 Extension::Subscript,
312 Extension::PipeTables,
313 Extension::Footnotes,
314 Extension::TaskLists,
315 Extension::TexMathDollars,
316 Extension::FencedDivs,
317 Extension::BracketedSpans,
318 Extension::BacktickCodeBlocks,
319 Extension::RawHtml,
320 Extension::RawAttribute,
321 Extension::Attributes,
322 Extension::HeaderAttributes,
323 Extension::FencedCodeAttributes,
324 Extension::InlineCodeAttributes,
325 Extension::LinkAttributes,
326 Extension::DefinitionLists,
327 Extension::FancyLists,
328 Extension::GfmAutoIdentifiers,
329 Extension::ImplicitHeaderReferences,
330 Extension::Emoji,
331 Extension::Alerts,
332 ]);
333
334 pub const MARKDOWN: Extensions = Extensions::from_list(&[
336 Extension::AllSymbolsEscapable,
337 Extension::Smart,
338 Extension::Strikeout,
339 Extension::Superscript,
340 Extension::Subscript,
341 Extension::PipeTables,
342 Extension::Footnotes,
343 Extension::TaskLists,
344 Extension::TexMathDollars,
345 Extension::FencedDivs,
346 Extension::BracketedSpans,
347 Extension::RawHtml,
348 Extension::HeaderAttributes,
349 Extension::FencedCodeAttributes,
350 Extension::FencedCodeBlocks,
351 Extension::BacktickCodeBlocks,
352 Extension::InlineCodeAttributes,
353 Extension::LinkAttributes,
354 Extension::DefinitionLists,
355 Extension::GridTables,
356 Extension::MultilineTables,
357 Extension::SimpleTables,
358 Extension::TableCaptions,
359 Extension::LineBlocks,
360 Extension::FancyLists,
361 Extension::ExampleLists,
362 Extension::Startnum,
363 Extension::YamlMetadataBlock,
364 Extension::PandocTitleBlock,
365 Extension::AutoIdentifiers,
366 Extension::ImplicitHeaderReferences,
367 Extension::ImplicitFigures,
368 Extension::RawAttribute,
369 Extension::InlineNotes,
370 Extension::NativeDivs,
371 Extension::NativeSpans,
372 Extension::MarkdownInHtmlBlocks,
373 Extension::RawTex,
374 Extension::Citations,
375 Extension::TableAttributes,
376 Extension::BlankBeforeBlockquote,
377 Extension::BlankBeforeHeader,
378 Extension::EscapedLineBreaks,
379 Extension::IntrawordUnderscores,
380 Extension::SpaceInAtxHeader,
381 ]);
382
383 pub const MARKDOWN_GITHUB: Extensions = Extensions::from_list(&[
388 Extension::Strikeout,
389 Extension::PipeTables,
390 Extension::Footnotes,
391 Extension::TaskLists,
392 Extension::Autolink,
393 Extension::RawHtml,
394 Extension::FencedCodeBlocks,
395 Extension::BacktickCodeBlocks,
396 Extension::AutoIdentifiers,
397 Extension::GfmAutoIdentifiers,
398 Extension::Emoji,
399 Extension::Alerts,
400 Extension::IntrawordUnderscores,
401 ]);
402
403 pub const MARKDOWN_PHPEXTRA: Extensions = Extensions::from_list(&[
408 Extension::DefinitionLists,
409 Extension::FencedCodeBlocks,
410 Extension::Footnotes,
411 Extension::HeaderAttributes,
412 Extension::IntrawordUnderscores,
413 Extension::LinkAttributes,
414 Extension::MarkdownAttribute,
415 Extension::PipeTables,
416 Extension::RawHtml,
417 ]);
418
419 pub const MARKDOWN_MMD: Extensions = Extensions::from_list(&[
429 Extension::AutoIdentifiers,
430 Extension::BacktickCodeBlocks,
431 Extension::DefinitionLists,
432 Extension::Footnotes,
433 Extension::ImplicitFigures,
434 Extension::ImplicitHeaderReferences,
435 Extension::IntrawordUnderscores,
436 Extension::MarkdownAttribute,
437 Extension::MmdHeaderIdentifiers,
438 Extension::PipeTables,
439 Extension::RawAttribute,
440 Extension::RawHtml,
441 Extension::Subscript,
442 Extension::Superscript,
443 Extension::TexMathDollars,
444 ]);
445
446 pub const MARKDOWN_STRICT: Extensions = Extensions::from_list(&[Extension::RawHtml]);
453
454 pub const MARKDOWN_STRICT_READ: Extensions = Extensions::from_list(&[
462 Extension::RawHtml,
463 Extension::ShortcutReferenceLinks,
464 Extension::SpacedReferenceLinks,
465 ]);
466
467 pub const MARKDOWN_GITHUB_READ: Extensions = Extensions::from_list(&[
472 Extension::Alerts,
473 Extension::AllSymbolsEscapable,
474 Extension::AutoIdentifiers,
475 Extension::Autolink,
476 Extension::BacktickCodeBlocks,
477 Extension::Emoji,
478 Extension::FencedCodeBlocks,
479 Extension::Footnotes,
480 Extension::GfmAutoIdentifiers,
481 Extension::IntrawordUnderscores,
482 Extension::ListsWithoutPrecedingBlankline,
483 Extension::PipeTables,
484 Extension::RawHtml,
485 Extension::ShortcutReferenceLinks,
486 Extension::SpaceInAtxHeader,
487 Extension::Strikeout,
488 Extension::TaskLists,
489 ]);
490
491 pub const MARKDOWN_PHPEXTRA_READ: Extensions = Extensions::from_list(&[
495 Extension::Abbreviations,
496 Extension::DefinitionLists,
497 Extension::FencedCodeBlocks,
498 Extension::Footnotes,
499 Extension::HeaderAttributes,
500 Extension::IntrawordUnderscores,
501 Extension::LinkAttributes,
502 Extension::MarkdownAttribute,
503 Extension::PipeTables,
504 Extension::RawHtml,
505 Extension::ShortcutReferenceLinks,
506 Extension::SpacedReferenceLinks,
507 ]);
508
509 pub const MARKDOWN_MMD_READ: Extensions = Extensions::from_list(&[
516 Extension::AllSymbolsEscapable,
517 Extension::AutoIdentifiers,
518 Extension::BacktickCodeBlocks,
519 Extension::DefinitionLists,
520 Extension::Footnotes,
521 Extension::ImplicitFigures,
522 Extension::ImplicitHeaderReferences,
523 Extension::IntrawordUnderscores,
524 Extension::MarkdownAttribute,
525 Extension::MmdHeaderIdentifiers,
526 Extension::MmdLinkAttributes,
527 Extension::MmdTitleBlock,
528 Extension::PipeTables,
529 Extension::RawAttribute,
530 Extension::RawHtml,
531 Extension::ShortSubsuperscripts,
532 Extension::ShortcutReferenceLinks,
533 Extension::SpacedReferenceLinks,
534 Extension::Subscript,
535 Extension::Superscript,
536 Extension::TexMathDollars,
537 Extension::TexMathDoubleBackslash,
538 ]);
539}
540
541#[cfg(test)]
542mod tests {
543 use super::{Extension, Extensions, presets};
544
545 #[test]
546 fn words_cover_every_variant() {
547 for ext in Extension::ALL {
549 assert!((*ext as usize) / super::WORD_BITS < super::WORDS);
550 }
551 }
552
553 #[test]
554 fn insert_remove_contains_round_trip() {
555 let mut set = Extensions::empty();
556 assert!(set.is_empty());
557 assert!(!set.contains(Extension::Footnotes));
558 set.insert(Extension::Footnotes);
559 assert!(set.contains(Extension::Footnotes));
560 assert!(!set.is_empty());
561 set.remove(Extension::Footnotes);
562 assert!(!set.contains(Extension::Footnotes));
563 assert!(set.is_empty());
564 }
565
566 #[test]
567 fn from_list_and_iter_follow_declaration_order() {
568 let set = Extensions::from_list(&[Extension::PipeTables, Extension::Smart]);
569 let collected: Vec<Extension> = set.iter().collect();
570 assert_eq!(collected, vec![Extension::Smart, Extension::PipeTables]);
572 }
573
574 #[test]
575 fn commonmark_preset_is_empty_gfm_is_not() {
576 assert!(presets::COMMONMARK.is_empty());
577 assert!(presets::GFM.contains(Extension::Strikeout));
578 assert!(presets::GFM.contains(Extension::TaskLists));
579 assert!(presets::GFM.contains(Extension::PipeTables));
580 assert!(!presets::GFM.contains(Extension::Subscript));
582 assert!(!presets::GFM.contains(Extension::Superscript));
583 }
584
585 #[test]
586 fn markdown_and_commonmark_x_presets_are_broad() {
587 assert!(presets::MARKDOWN.contains(Extension::DefinitionLists));
588 assert!(presets::MARKDOWN.contains(Extension::YamlMetadataBlock));
589 assert!(presets::MARKDOWN.contains(Extension::Smart));
590 assert!(presets::COMMONMARK_X.contains(Extension::FencedDivs));
591 assert!(presets::COMMONMARK_X.contains(Extension::Attributes));
592 assert!(presets::COMMONMARK.is_empty());
594 }
595
596 #[test]
597 fn code_and_math_surface_variants_round_trip_and_seed_presets() {
598 for token in ["fenced_code_blocks", "backtick_code_blocks", "tex_math_gfm"] {
599 let ext = Extension::from_name(token).expect("a declared variant");
600 assert_eq!(ext.name(), token);
601 }
602 assert!(presets::MARKDOWN.contains(Extension::FencedCodeBlocks));
604 assert!(presets::MARKDOWN.contains(Extension::BacktickCodeBlocks));
605 assert!(presets::GFM.contains(Extension::BacktickCodeBlocks));
607 assert!(presets::GFM.contains(Extension::TexMathGfm));
608 assert!(!presets::GFM.contains(Extension::FencedCodeBlocks));
609 }
610
611 #[test]
612 fn names_are_stable() {
613 assert_eq!(Extension::Footnotes.name(), "footnotes");
614 assert_eq!(Extension::Autolink.name(), "autolink_bare_uris");
615 assert_eq!(Extension::HardLineBreaks.name(), "hard_line_breaks");
616 assert_eq!(Extension::RawHtml.name(), "raw_html");
617 }
618
619 #[test]
620 fn from_name_round_trips_every_variant() {
621 for ext in Extension::ALL {
622 assert_eq!(Extension::from_name(ext.name()), Some(*ext));
623 }
624 assert_eq!(Extension::from_name("not_an_extension"), None);
625 assert_eq!(Extension::from_name(""), None);
626 }
627
628 #[test]
629 fn union_combines_both_sides() {
630 let a = Extensions::from_list(&[Extension::Strikeout]);
631 let b = Extensions::from_list(&[Extension::Subscript]);
632 let combined = a.union(b);
633 assert!(combined.contains(Extension::Strikeout));
634 assert!(combined.contains(Extension::Subscript));
635 assert!(!combined.contains(Extension::Superscript));
636 assert_eq!(a.union(Extensions::empty()), a);
637 }
638}