1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
9pub enum Flavor {
10 #[default]
12 Pandoc,
13 Quarto,
15 #[cfg_attr(feature = "serde", serde(rename = "rmarkdown"))]
17 RMarkdown,
18 Gfm,
20 #[cfg_attr(feature = "serde", serde(alias = "commonmark"))]
22 CommonMark,
23 #[cfg_attr(feature = "serde", serde(rename = "multimarkdown"))]
25 MultiMarkdown,
26}
27
28#[derive(Debug, Clone, PartialEq)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
33#[cfg_attr(feature = "serde", serde(default))]
34#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
35pub struct Extensions {
36 #[cfg_attr(feature = "serde", serde(alias = "blank_before_header"))]
41 pub blank_before_header: bool,
42 #[cfg_attr(feature = "serde", serde(alias = "header_attributes"))]
44 pub header_attributes: bool,
45 pub auto_identifiers: bool,
47 pub gfm_auto_identifiers: bool,
49 pub implicit_header_references: bool,
51
52 #[cfg_attr(feature = "serde", serde(alias = "blank_before_blockquote"))]
55 pub blank_before_blockquote: bool,
56
57 #[cfg_attr(feature = "serde", serde(alias = "fancy_lists"))]
60 pub fancy_lists: bool,
61 pub startnum: bool,
63 #[cfg_attr(feature = "serde", serde(alias = "example_lists"))]
65 pub example_lists: bool,
66 #[cfg_attr(feature = "serde", serde(alias = "task_lists"))]
68 pub task_lists: bool,
69 #[cfg_attr(feature = "serde", serde(alias = "definition_lists"))]
71 pub definition_lists: bool,
72 #[cfg_attr(feature = "serde", serde(alias = "lists_without_preceding_blankline"))]
74 pub lists_without_preceding_blankline: bool,
75
76 #[cfg_attr(feature = "serde", serde(alias = "backtick_code_blocks"))]
79 pub backtick_code_blocks: bool,
80 #[cfg_attr(feature = "serde", serde(alias = "fenced_code_blocks"))]
82 pub fenced_code_blocks: bool,
83 #[cfg_attr(feature = "serde", serde(alias = "fenced_code_attributes"))]
85 pub fenced_code_attributes: bool,
86 pub executable_code: bool,
88 pub rmarkdown_inline_code: bool,
90 pub quarto_inline_code: bool,
92 #[cfg_attr(feature = "serde", serde(alias = "inline_code_attributes"))]
94 pub inline_code_attributes: bool,
95
96 #[cfg_attr(feature = "serde", serde(alias = "simple_tables"))]
99 pub simple_tables: bool,
100 #[cfg_attr(feature = "serde", serde(alias = "multiline_tables"))]
102 pub multiline_tables: bool,
103 #[cfg_attr(feature = "serde", serde(alias = "grid_tables"))]
105 pub grid_tables: bool,
106 #[cfg_attr(feature = "serde", serde(alias = "pipe_tables"))]
108 pub pipe_tables: bool,
109 #[cfg_attr(feature = "serde", serde(alias = "table_captions"))]
111 pub table_captions: bool,
112
113 #[cfg_attr(feature = "serde", serde(alias = "fenced_divs"))]
116 pub fenced_divs: bool,
117 #[cfg_attr(feature = "serde", serde(alias = "native_divs"))]
119 pub native_divs: bool,
120
121 #[cfg_attr(feature = "serde", serde(alias = "line_blocks"))]
124 pub line_blocks: bool,
125
126 #[cfg_attr(feature = "serde", serde(alias = "intraword_underscores"))]
131 pub intraword_underscores: bool,
132 pub strikeout: bool,
134 pub superscript: bool,
136 pub subscript: bool,
137
138 #[cfg_attr(feature = "serde", serde(alias = "inline_links"))]
141 pub inline_links: bool,
142 #[cfg_attr(feature = "serde", serde(alias = "reference_links"))]
144 pub reference_links: bool,
145 #[cfg_attr(feature = "serde", serde(alias = "shortcut_reference_links"))]
147 pub shortcut_reference_links: bool,
148 #[cfg_attr(feature = "serde", serde(alias = "link_attributes"))]
150 pub link_attributes: bool,
151 pub autolinks: bool,
153
154 #[cfg_attr(feature = "serde", serde(alias = "inline_images"))]
157 pub inline_images: bool,
158 #[cfg_attr(feature = "serde", serde(alias = "implicit_figures"))]
160 pub implicit_figures: bool,
161
162 #[cfg_attr(feature = "serde", serde(alias = "tex_math_dollars"))]
165 pub tex_math_dollars: bool,
166 #[cfg_attr(feature = "serde", serde(alias = "tex_math_gfm"))]
168 pub tex_math_gfm: bool,
169 #[cfg_attr(feature = "serde", serde(alias = "tex_math_single_backslash"))]
171 pub tex_math_single_backslash: bool,
172 #[cfg_attr(feature = "serde", serde(alias = "tex_math_double_backslash"))]
174 pub tex_math_double_backslash: bool,
175
176 #[cfg_attr(feature = "serde", serde(alias = "inline_footnotes"))]
179 pub inline_footnotes: bool,
180 pub footnotes: bool,
182
183 pub citations: bool,
186
187 #[cfg_attr(feature = "serde", serde(alias = "bracketed_spans"))]
190 pub bracketed_spans: bool,
191 #[cfg_attr(feature = "serde", serde(alias = "native_spans"))]
193 pub native_spans: bool,
194
195 #[cfg_attr(feature = "serde", serde(alias = "yaml_metadata_block"))]
198 pub yaml_metadata_block: bool,
199 #[cfg_attr(feature = "serde", serde(alias = "pandoc_title_block"))]
201 pub pandoc_title_block: bool,
202 pub mmd_title_block: bool,
204
205 #[cfg_attr(feature = "serde", serde(alias = "raw_html"))]
208 pub raw_html: bool,
209 #[cfg_attr(feature = "serde", serde(alias = "markdown_in_html_blocks"))]
211 pub markdown_in_html_blocks: bool,
212 #[cfg_attr(feature = "serde", serde(alias = "raw_tex"))]
214 pub raw_tex: bool,
215 #[cfg_attr(feature = "serde", serde(alias = "raw_attribute"))]
217 pub raw_attribute: bool,
218
219 #[cfg_attr(feature = "serde", serde(alias = "all_symbols_escapable"))]
222 pub all_symbols_escapable: bool,
223 #[cfg_attr(feature = "serde", serde(alias = "escaped_line_breaks"))]
225 pub escaped_line_breaks: bool,
226
227 #[cfg_attr(feature = "serde", serde(alias = "autolink_bare_uris"))]
231 pub autolink_bare_uris: bool,
232 #[cfg_attr(feature = "serde", serde(alias = "hard_line_breaks"))]
234 pub hard_line_breaks: bool,
235 pub mmd_header_identifiers: bool,
237 pub mmd_link_attributes: bool,
239 pub alerts: bool,
241 pub emoji: bool,
243 pub mark: bool,
245
246 #[cfg_attr(feature = "serde", serde(alias = "quarto_callouts"))]
249 pub quarto_callouts: bool,
250 #[cfg_attr(feature = "serde", serde(alias = "quarto_crossrefs"))]
252 pub quarto_crossrefs: bool,
253 #[cfg_attr(feature = "serde", serde(alias = "quarto_shortcodes"))]
255 pub quarto_shortcodes: bool,
256 pub bookdown_references: bool,
258 pub bookdown_equation_references: bool,
260}
261
262impl Default for Extensions {
263 fn default() -> Self {
264 Self::for_flavor(Flavor::default())
265 }
266}
267
268impl Extensions {
269 fn none_defaults() -> Self {
270 Self {
271 alerts: false,
272 all_symbols_escapable: false,
273 auto_identifiers: false,
274 autolink_bare_uris: false,
275 autolinks: false,
276 backtick_code_blocks: false,
277 blank_before_blockquote: false,
278 blank_before_header: false,
279 bookdown_references: false,
280 bookdown_equation_references: false,
281 bracketed_spans: false,
282 citations: false,
283 definition_lists: false,
284 lists_without_preceding_blankline: false,
285 emoji: false,
286 escaped_line_breaks: false,
287 example_lists: false,
288 executable_code: false,
289 rmarkdown_inline_code: false,
290 quarto_inline_code: false,
291 fancy_lists: false,
292 fenced_code_attributes: false,
293 fenced_code_blocks: false,
294 fenced_divs: false,
295 footnotes: false,
296 gfm_auto_identifiers: false,
297 grid_tables: false,
298 hard_line_breaks: false,
299 header_attributes: false,
300 implicit_figures: false,
301 implicit_header_references: false,
302 inline_code_attributes: false,
303 inline_footnotes: false,
304 inline_images: false,
305 inline_links: false,
306 intraword_underscores: false,
307 line_blocks: false,
308 link_attributes: false,
309 mark: false,
310 markdown_in_html_blocks: false,
311 mmd_header_identifiers: false,
312 mmd_link_attributes: false,
313 mmd_title_block: false,
314 multiline_tables: false,
315 native_divs: false,
316 native_spans: false,
317 pandoc_title_block: false,
318 pipe_tables: false,
319 quarto_callouts: false,
320 quarto_crossrefs: false,
321 quarto_shortcodes: false,
322 raw_attribute: false,
323 raw_html: false,
324 raw_tex: false,
325 reference_links: false,
326 shortcut_reference_links: false,
327 simple_tables: false,
328 startnum: false,
329 strikeout: false,
330 subscript: false,
331 superscript: false,
332 table_captions: false,
333 task_lists: false,
334 tex_math_dollars: false,
335 tex_math_double_backslash: false,
336 tex_math_gfm: false,
337 tex_math_single_backslash: false,
338 yaml_metadata_block: false,
339 }
340 }
341
342 pub fn for_flavor(flavor: Flavor) -> Self {
344 match flavor {
345 Flavor::Pandoc => Self::pandoc_defaults(),
346 Flavor::Quarto => Self::quarto_defaults(),
347 Flavor::RMarkdown => Self::rmarkdown_defaults(),
348 Flavor::Gfm => Self::gfm_defaults(),
349 Flavor::CommonMark => Self::commonmark_defaults(),
350 Flavor::MultiMarkdown => Self::multimarkdown_defaults(),
351 }
352 }
353
354 fn pandoc_defaults() -> Self {
355 Self {
356 auto_identifiers: true,
358 blank_before_blockquote: true,
359 blank_before_header: true,
360 gfm_auto_identifiers: false,
361 header_attributes: true,
362 implicit_header_references: true,
363
364 definition_lists: true,
366 example_lists: true,
367 fancy_lists: true,
368 lists_without_preceding_blankline: false,
369 startnum: true,
370 task_lists: true,
371
372 backtick_code_blocks: true,
374 executable_code: false,
375 rmarkdown_inline_code: false,
376 quarto_inline_code: false,
377 fenced_code_attributes: true,
378 fenced_code_blocks: true,
379 inline_code_attributes: true,
380
381 grid_tables: true,
383 multiline_tables: true,
384 pipe_tables: true,
385 simple_tables: true,
386 table_captions: true,
387
388 fenced_divs: true,
390 native_divs: true,
391
392 line_blocks: true,
394
395 intraword_underscores: true,
397 strikeout: true,
398 subscript: true,
399 superscript: true,
400
401 autolinks: true,
403 inline_links: true,
404 link_attributes: true,
405 reference_links: true,
406 shortcut_reference_links: true,
407
408 implicit_figures: true,
410 inline_images: true,
411
412 tex_math_dollars: true,
414 tex_math_double_backslash: false,
415 tex_math_gfm: false,
416 tex_math_single_backslash: false,
417
418 footnotes: true,
420 inline_footnotes: true,
421
422 citations: true,
424
425 bracketed_spans: true,
427 native_spans: true,
428
429 mmd_title_block: false,
431 pandoc_title_block: true,
432 yaml_metadata_block: true,
433
434 markdown_in_html_blocks: false,
436 raw_attribute: true,
437 raw_html: true,
438 raw_tex: true,
439
440 all_symbols_escapable: true,
442 escaped_line_breaks: true,
443
444 alerts: false,
446 autolink_bare_uris: false,
447 emoji: false,
448 hard_line_breaks: false,
449 mark: false,
450 mmd_header_identifiers: false,
451 mmd_link_attributes: false,
452
453 bookdown_references: false,
455 bookdown_equation_references: false,
456 quarto_callouts: false,
457 quarto_crossrefs: false,
458 quarto_shortcodes: false,
459 }
460 }
461
462 fn quarto_defaults() -> Self {
463 let mut ext = Self::pandoc_defaults();
464
465 ext.executable_code = true;
466 ext.rmarkdown_inline_code = true;
467 ext.quarto_inline_code = true;
468 ext.quarto_callouts = true;
469 ext.quarto_crossrefs = true;
470 ext.quarto_shortcodes = true;
471
472 ext
473 }
474
475 fn rmarkdown_defaults() -> Self {
476 let mut ext = Self::pandoc_defaults();
477
478 ext.bookdown_references = true;
479 ext.bookdown_equation_references = true;
480 ext.executable_code = true;
481 ext.rmarkdown_inline_code = true;
482 ext.quarto_inline_code = false;
483 ext.tex_math_dollars = true;
484 ext.tex_math_single_backslash = true;
485
486 ext
487 }
488
489 fn gfm_defaults() -> Self {
490 let mut ext = Self::none_defaults();
491
492 ext.alerts = true;
493 ext.auto_identifiers = true;
494 ext.autolink_bare_uris = true;
495 ext.autolinks = true;
496 ext.backtick_code_blocks = true;
497 ext.emoji = true;
498 ext.fenced_code_blocks = true;
499 ext.footnotes = true;
500 ext.gfm_auto_identifiers = true;
501 ext.inline_links = true;
502 ext.pipe_tables = true;
503 ext.raw_html = true;
504 ext.strikeout = true;
505 ext.task_lists = true;
506 ext.tex_math_dollars = true;
507 ext.tex_math_gfm = true;
508 ext.yaml_metadata_block = true;
509
510 ext
511 }
512
513 fn commonmark_defaults() -> Self {
514 let mut ext = Self::none_defaults();
515 ext.autolinks = true;
525 ext.backtick_code_blocks = true;
526 ext.escaped_line_breaks = true;
527 ext.fenced_code_blocks = true;
528 ext.inline_images = true;
529 ext.inline_links = true;
530 ext.intraword_underscores = true;
531 ext.raw_html = true;
532 ext.reference_links = true;
533 ext.shortcut_reference_links = true;
534 ext
535 }
536
537 fn multimarkdown_defaults() -> Self {
538 let mut ext = Self::none_defaults();
539
540 ext.all_symbols_escapable = true;
541 ext.auto_identifiers = true;
542 ext.backtick_code_blocks = true;
543 ext.definition_lists = true;
544 ext.footnotes = true;
545 ext.implicit_figures = true;
546 ext.implicit_header_references = true;
547 ext.intraword_underscores = true;
548 ext.mmd_header_identifiers = true;
549 ext.mmd_link_attributes = true;
550 ext.mmd_title_block = true;
551 ext.pipe_tables = true;
552 ext.raw_attribute = true;
553 ext.raw_html = true;
554 ext.reference_links = true;
555 ext.shortcut_reference_links = true;
556 ext.subscript = true;
557 ext.superscript = true;
558 ext.tex_math_dollars = true;
559 ext.tex_math_double_backslash = true;
560
561 ext
562 }
563
564 pub fn merge_with_flavor(user_overrides: HashMap<String, bool>, flavor: Flavor) -> Self {
578 let defaults = Self::for_flavor(flavor);
579 Self::merge_overrides(defaults, user_overrides)
580 }
581
582 fn merge_overrides(mut base: Extensions, user_overrides: HashMap<String, bool>) -> Self {
583 for (key, value) in user_overrides {
584 let normalized_key = key.replace('_', "-");
585 match normalized_key.as_str() {
586 "blank-before-header" => base.blank_before_header = value,
587 "header-attributes" => base.header_attributes = value,
588 "auto-identifiers" => base.auto_identifiers = value,
589 "gfm-auto-identifiers" => base.gfm_auto_identifiers = value,
590 "implicit-header-references" => base.implicit_header_references = value,
591 "blank-before-blockquote" => base.blank_before_blockquote = value,
592 "fancy-lists" => base.fancy_lists = value,
593 "startnum" => base.startnum = value,
594 "example-lists" => base.example_lists = value,
595 "task-lists" => base.task_lists = value,
596 "definition-lists" => base.definition_lists = value,
597 "lists-without-preceding-blankline" => {
598 base.lists_without_preceding_blankline = value
599 }
600 "backtick-code-blocks" => base.backtick_code_blocks = value,
601 "fenced-code-blocks" => base.fenced_code_blocks = value,
602 "fenced-code-attributes" => base.fenced_code_attributes = value,
603 "executable-code" => base.executable_code = value,
604 "rmarkdown-inline-code" => base.rmarkdown_inline_code = value,
605 "quarto-inline-code" => base.quarto_inline_code = value,
606 "inline-code-attributes" => base.inline_code_attributes = value,
607 "simple-tables" => base.simple_tables = value,
608 "multiline-tables" => base.multiline_tables = value,
609 "grid-tables" => base.grid_tables = value,
610 "pipe-tables" => base.pipe_tables = value,
611 "table-captions" => base.table_captions = value,
612 "fenced-divs" => base.fenced_divs = value,
613 "native-divs" => base.native_divs = value,
614 "line-blocks" => base.line_blocks = value,
615 "intraword-underscores" => base.intraword_underscores = value,
616 "strikeout" => base.strikeout = value,
617 "superscript" => base.superscript = value,
618 "subscript" => base.subscript = value,
619 "inline-links" => base.inline_links = value,
620 "reference-links" => base.reference_links = value,
621 "shortcut-reference-links" => base.shortcut_reference_links = value,
622 "link-attributes" => base.link_attributes = value,
623 "autolinks" => base.autolinks = value,
624 "inline-images" => base.inline_images = value,
625 "implicit-figures" => base.implicit_figures = value,
626 "tex-math-dollars" => base.tex_math_dollars = value,
627 "tex-math-gfm" => base.tex_math_gfm = value,
628 "tex-math-single-backslash" => base.tex_math_single_backslash = value,
629 "tex-math-double-backslash" => base.tex_math_double_backslash = value,
630 "inline-footnotes" => base.inline_footnotes = value,
631 "footnotes" => base.footnotes = value,
632 "citations" => base.citations = value,
633 "bracketed-spans" => base.bracketed_spans = value,
634 "native-spans" => base.native_spans = value,
635 "yaml-metadata-block" => base.yaml_metadata_block = value,
636 "pandoc-title-block" => base.pandoc_title_block = value,
637 "mmd-title-block" => base.mmd_title_block = value,
638 "raw-html" => base.raw_html = value,
639 "markdown-in-html-blocks" => base.markdown_in_html_blocks = value,
640 "raw-tex" => base.raw_tex = value,
641 "raw-attribute" => base.raw_attribute = value,
642 "all-symbols-escapable" => base.all_symbols_escapable = value,
643 "escaped-line-breaks" => base.escaped_line_breaks = value,
644 "autolink-bare-uris" => base.autolink_bare_uris = value,
645 "hard-line-breaks" => base.hard_line_breaks = value,
646 "mmd-header-identifiers" => base.mmd_header_identifiers = value,
647 "mmd-link-attributes" => base.mmd_link_attributes = value,
648 "alerts" => base.alerts = value,
649 "emoji" => base.emoji = value,
650 "mark" => base.mark = value,
651 "quarto-callouts" => base.quarto_callouts = value,
652 "quarto-crossrefs" => base.quarto_crossrefs = value,
653 "quarto-shortcodes" => base.quarto_shortcodes = value,
654 "bookdown-references" => base.bookdown_references = value,
655 "bookdown-equation-references" => base.bookdown_equation_references = value,
656 _ => {}
657 }
658 }
659 base
660 }
661}
662
663#[cfg(test)]
664mod tests {
665 use super::{Extensions, Flavor};
666 use std::collections::HashMap;
667
668 #[test]
669 fn merge_with_flavor_keeps_known_extension_overrides() {
670 let mut overrides = HashMap::new();
671 overrides.insert("intraword-underscores".to_string(), false);
672 let ext = Extensions::merge_with_flavor(overrides, Flavor::Pandoc);
673 assert!(!ext.intraword_underscores);
674 }
675
676 #[test]
677 fn merge_with_flavor_ignores_unknown_extension_overrides() {
678 let mut overrides = HashMap::new();
679 overrides.insert("smart".to_string(), true);
680 overrides.insert("smart-quotes".to_string(), true);
681 let ext = Extensions::merge_with_flavor(overrides, Flavor::Gfm);
682 assert!(ext.strikeout, "known defaults should remain intact");
683 }
684
685 #[test]
686 fn lists_without_preceding_blankline_defaults_false_for_pandoc_and_gfm() {
687 assert!(!Extensions::for_flavor(Flavor::Pandoc).lists_without_preceding_blankline);
688 assert!(!Extensions::for_flavor(Flavor::Gfm).lists_without_preceding_blankline);
689 }
690
691 #[test]
692 fn merge_with_flavor_accepts_lists_without_preceding_blankline_override() {
693 let mut overrides = HashMap::new();
694 overrides.insert("lists-without-preceding-blankline".to_string(), true);
695 let ext = Extensions::merge_with_flavor(overrides, Flavor::Pandoc);
696 assert!(ext.lists_without_preceding_blankline);
697 }
698}
699
700#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
701#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
702pub enum PandocCompat {
703 #[cfg_attr(feature = "serde", serde(rename = "latest"))]
708 Latest,
709 #[cfg_attr(
711 feature = "serde",
712 serde(rename = "3.7", alias = "3-7", alias = "v3.7", alias = "v3-7")
713 )]
714 V3_7,
715 #[default]
717 #[cfg_attr(
718 feature = "serde",
719 serde(rename = "3.9", alias = "3-9", alias = "v3.9", alias = "v3-9")
720 )]
721 V3_9,
722}
723
724impl PandocCompat {
725 pub const PINNED_LATEST: Self = Self::V3_9;
727
728 pub fn effective(self) -> Self {
729 match self {
730 Self::Latest => Self::PINNED_LATEST,
731 other => other,
732 }
733 }
734}
735
736#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
747#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
748#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
749pub enum Dialect {
750 #[default]
753 Pandoc,
754 CommonMark,
756}
757
758impl Dialect {
759 pub fn for_flavor(flavor: Flavor) -> Self {
761 match flavor {
762 Flavor::CommonMark | Flavor::Gfm => Dialect::CommonMark,
763 Flavor::Pandoc | Flavor::Quarto | Flavor::RMarkdown | Flavor::MultiMarkdown => {
764 Dialect::Pandoc
765 }
766 }
767 }
768}
769
770#[derive(Debug, Clone)]
771#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
772#[cfg_attr(feature = "serde", serde(default, rename_all = "kebab-case"))]
773pub struct ParserOptions {
774 pub flavor: Flavor,
775 pub dialect: Dialect,
776 pub extensions: Extensions,
777 pub pandoc_compat: PandocCompat,
779 #[cfg_attr(feature = "serde", serde(skip))]
790 pub refdef_labels: Option<Arc<HashSet<String>>>,
791}
792
793impl Default for ParserOptions {
794 fn default() -> Self {
795 let flavor = Flavor::default();
796 Self {
797 flavor,
798 dialect: Dialect::for_flavor(flavor),
799 extensions: Extensions::for_flavor(flavor),
800 pandoc_compat: PandocCompat::default(),
801 refdef_labels: None,
802 }
803 }
804}
805
806impl ParserOptions {
807 pub fn effective_pandoc_compat(&self) -> PandocCompat {
808 self.pandoc_compat.effective()
809 }
810}