1use std::collections::HashMap;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
6#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
8pub enum Flavor {
9 #[default]
11 Pandoc,
12 Quarto,
14 #[cfg_attr(feature = "serde", serde(rename = "rmarkdown"))]
16 RMarkdown,
17 Gfm,
19 #[cfg_attr(feature = "serde", serde(alias = "commonmark"))]
21 CommonMark,
22 #[cfg_attr(feature = "serde", serde(rename = "multimarkdown"))]
24 MultiMarkdown,
25}
26
27#[derive(Debug, Clone, PartialEq)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32#[cfg_attr(feature = "serde", serde(default))]
33#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
34pub struct Extensions {
35 #[cfg_attr(feature = "serde", serde(alias = "blank_before_header"))]
40 pub blank_before_header: bool,
41 #[cfg_attr(feature = "serde", serde(alias = "header_attributes"))]
43 pub header_attributes: bool,
44 pub auto_identifiers: bool,
46 pub gfm_auto_identifiers: bool,
48 pub implicit_header_references: bool,
50
51 #[cfg_attr(feature = "serde", serde(alias = "blank_before_blockquote"))]
54 pub blank_before_blockquote: bool,
55
56 #[cfg_attr(feature = "serde", serde(alias = "fancy_lists"))]
59 pub fancy_lists: bool,
60 pub startnum: bool,
62 #[cfg_attr(feature = "serde", serde(alias = "example_lists"))]
64 pub example_lists: bool,
65 #[cfg_attr(feature = "serde", serde(alias = "task_lists"))]
67 pub task_lists: bool,
68 #[cfg_attr(feature = "serde", serde(alias = "definition_lists"))]
70 pub definition_lists: bool,
71 #[cfg_attr(feature = "serde", serde(alias = "lists_without_preceding_blankline"))]
73 pub lists_without_preceding_blankline: bool,
74
75 #[cfg_attr(feature = "serde", serde(alias = "backtick_code_blocks"))]
78 pub backtick_code_blocks: bool,
79 #[cfg_attr(feature = "serde", serde(alias = "fenced_code_blocks"))]
81 pub fenced_code_blocks: bool,
82 #[cfg_attr(feature = "serde", serde(alias = "fenced_code_attributes"))]
84 pub fenced_code_attributes: bool,
85 pub executable_code: bool,
87 pub rmarkdown_inline_code: bool,
89 pub quarto_inline_code: bool,
91 #[cfg_attr(feature = "serde", serde(alias = "inline_code_attributes"))]
93 pub inline_code_attributes: bool,
94
95 #[cfg_attr(feature = "serde", serde(alias = "simple_tables"))]
98 pub simple_tables: bool,
99 #[cfg_attr(feature = "serde", serde(alias = "multiline_tables"))]
101 pub multiline_tables: bool,
102 #[cfg_attr(feature = "serde", serde(alias = "grid_tables"))]
104 pub grid_tables: bool,
105 #[cfg_attr(feature = "serde", serde(alias = "pipe_tables"))]
107 pub pipe_tables: bool,
108 #[cfg_attr(feature = "serde", serde(alias = "table_captions"))]
110 pub table_captions: bool,
111
112 #[cfg_attr(feature = "serde", serde(alias = "fenced_divs"))]
115 pub fenced_divs: bool,
116 #[cfg_attr(feature = "serde", serde(alias = "native_divs"))]
118 pub native_divs: bool,
119
120 #[cfg_attr(feature = "serde", serde(alias = "line_blocks"))]
123 pub line_blocks: bool,
124
125 #[cfg_attr(feature = "serde", serde(alias = "intraword_underscores"))]
130 pub intraword_underscores: bool,
131 pub strikeout: bool,
133 pub superscript: bool,
135 pub subscript: bool,
136
137 #[cfg_attr(feature = "serde", serde(alias = "inline_links"))]
140 pub inline_links: bool,
141 #[cfg_attr(feature = "serde", serde(alias = "reference_links"))]
143 pub reference_links: bool,
144 #[cfg_attr(feature = "serde", serde(alias = "shortcut_reference_links"))]
146 pub shortcut_reference_links: bool,
147 #[cfg_attr(feature = "serde", serde(alias = "link_attributes"))]
149 pub link_attributes: bool,
150 pub autolinks: bool,
152
153 #[cfg_attr(feature = "serde", serde(alias = "inline_images"))]
156 pub inline_images: bool,
157 #[cfg_attr(feature = "serde", serde(alias = "implicit_figures"))]
159 pub implicit_figures: bool,
160
161 #[cfg_attr(feature = "serde", serde(alias = "tex_math_dollars"))]
164 pub tex_math_dollars: bool,
165 #[cfg_attr(feature = "serde", serde(alias = "tex_math_gfm"))]
167 pub tex_math_gfm: bool,
168 #[cfg_attr(feature = "serde", serde(alias = "tex_math_single_backslash"))]
170 pub tex_math_single_backslash: bool,
171 #[cfg_attr(feature = "serde", serde(alias = "tex_math_double_backslash"))]
173 pub tex_math_double_backslash: bool,
174
175 #[cfg_attr(feature = "serde", serde(alias = "inline_footnotes"))]
178 pub inline_footnotes: bool,
179 pub footnotes: bool,
181
182 pub citations: bool,
185
186 #[cfg_attr(feature = "serde", serde(alias = "bracketed_spans"))]
189 pub bracketed_spans: bool,
190 #[cfg_attr(feature = "serde", serde(alias = "native_spans"))]
192 pub native_spans: bool,
193
194 #[cfg_attr(feature = "serde", serde(alias = "yaml_metadata_block"))]
197 pub yaml_metadata_block: bool,
198 #[cfg_attr(feature = "serde", serde(alias = "pandoc_title_block"))]
200 pub pandoc_title_block: bool,
201 pub mmd_title_block: bool,
203
204 #[cfg_attr(feature = "serde", serde(alias = "raw_html"))]
207 pub raw_html: bool,
208 #[cfg_attr(feature = "serde", serde(alias = "markdown_in_html_blocks"))]
210 pub markdown_in_html_blocks: bool,
211 #[cfg_attr(feature = "serde", serde(alias = "raw_tex"))]
213 pub raw_tex: bool,
214 #[cfg_attr(feature = "serde", serde(alias = "raw_attribute"))]
216 pub raw_attribute: bool,
217
218 #[cfg_attr(feature = "serde", serde(alias = "all_symbols_escapable"))]
221 pub all_symbols_escapable: bool,
222 #[cfg_attr(feature = "serde", serde(alias = "escaped_line_breaks"))]
224 pub escaped_line_breaks: bool,
225
226 #[cfg_attr(feature = "serde", serde(alias = "autolink_bare_uris"))]
230 pub autolink_bare_uris: bool,
231 #[cfg_attr(feature = "serde", serde(alias = "hard_line_breaks"))]
233 pub hard_line_breaks: bool,
234 pub mmd_header_identifiers: bool,
236 pub mmd_link_attributes: bool,
238 pub alerts: bool,
240 pub emoji: bool,
242 pub mark: bool,
244
245 #[cfg_attr(feature = "serde", serde(alias = "quarto_callouts"))]
248 pub quarto_callouts: bool,
249 #[cfg_attr(feature = "serde", serde(alias = "quarto_crossrefs"))]
251 pub quarto_crossrefs: bool,
252 #[cfg_attr(feature = "serde", serde(alias = "quarto_shortcodes"))]
254 pub quarto_shortcodes: bool,
255 pub bookdown_references: bool,
257 pub bookdown_equation_references: bool,
259}
260
261impl Default for Extensions {
262 fn default() -> Self {
263 Self::for_flavor(Flavor::default())
264 }
265}
266
267impl Extensions {
268 fn none_defaults() -> Self {
269 Self {
270 alerts: false,
271 all_symbols_escapable: false,
272 auto_identifiers: false,
273 autolink_bare_uris: false,
274 autolinks: false,
275 backtick_code_blocks: false,
276 blank_before_blockquote: false,
277 blank_before_header: false,
278 bookdown_references: false,
279 bookdown_equation_references: false,
280 bracketed_spans: false,
281 citations: false,
282 definition_lists: false,
283 lists_without_preceding_blankline: false,
284 emoji: false,
285 escaped_line_breaks: false,
286 example_lists: false,
287 executable_code: false,
288 rmarkdown_inline_code: false,
289 quarto_inline_code: false,
290 fancy_lists: false,
291 fenced_code_attributes: false,
292 fenced_code_blocks: false,
293 fenced_divs: false,
294 footnotes: false,
295 gfm_auto_identifiers: false,
296 grid_tables: false,
297 hard_line_breaks: false,
298 header_attributes: false,
299 implicit_figures: false,
300 implicit_header_references: false,
301 inline_code_attributes: false,
302 inline_footnotes: false,
303 inline_images: false,
304 inline_links: false,
305 intraword_underscores: false,
306 line_blocks: false,
307 link_attributes: false,
308 mark: false,
309 markdown_in_html_blocks: false,
310 mmd_header_identifiers: false,
311 mmd_link_attributes: false,
312 mmd_title_block: false,
313 multiline_tables: false,
314 native_divs: false,
315 native_spans: false,
316 pandoc_title_block: false,
317 pipe_tables: false,
318 quarto_callouts: false,
319 quarto_crossrefs: false,
320 quarto_shortcodes: false,
321 raw_attribute: false,
322 raw_html: false,
323 raw_tex: false,
324 reference_links: false,
325 shortcut_reference_links: false,
326 simple_tables: false,
327 startnum: false,
328 strikeout: false,
329 subscript: false,
330 superscript: false,
331 table_captions: false,
332 task_lists: false,
333 tex_math_dollars: false,
334 tex_math_double_backslash: false,
335 tex_math_gfm: false,
336 tex_math_single_backslash: false,
337 yaml_metadata_block: false,
338 }
339 }
340
341 pub fn for_flavor(flavor: Flavor) -> Self {
343 match flavor {
344 Flavor::Pandoc => Self::pandoc_defaults(),
345 Flavor::Quarto => Self::quarto_defaults(),
346 Flavor::RMarkdown => Self::rmarkdown_defaults(),
347 Flavor::Gfm => Self::gfm_defaults(),
348 Flavor::CommonMark => Self::commonmark_defaults(),
349 Flavor::MultiMarkdown => Self::multimarkdown_defaults(),
350 }
351 }
352
353 fn pandoc_defaults() -> Self {
354 Self {
355 auto_identifiers: true,
357 blank_before_blockquote: true,
358 blank_before_header: true,
359 gfm_auto_identifiers: false,
360 header_attributes: true,
361 implicit_header_references: true,
362
363 definition_lists: true,
365 example_lists: true,
366 fancy_lists: true,
367 lists_without_preceding_blankline: false,
368 startnum: true,
369 task_lists: true,
370
371 backtick_code_blocks: true,
373 executable_code: false,
374 rmarkdown_inline_code: false,
375 quarto_inline_code: false,
376 fenced_code_attributes: true,
377 fenced_code_blocks: true,
378 inline_code_attributes: true,
379
380 grid_tables: true,
382 multiline_tables: true,
383 pipe_tables: true,
384 simple_tables: true,
385 table_captions: true,
386
387 fenced_divs: true,
389 native_divs: true,
390
391 line_blocks: true,
393
394 intraword_underscores: true,
396 strikeout: true,
397 subscript: true,
398 superscript: true,
399
400 autolinks: true,
402 inline_links: true,
403 link_attributes: true,
404 reference_links: true,
405 shortcut_reference_links: true,
406
407 implicit_figures: true,
409 inline_images: true,
410
411 tex_math_dollars: true,
413 tex_math_double_backslash: false,
414 tex_math_gfm: false,
415 tex_math_single_backslash: false,
416
417 footnotes: true,
419 inline_footnotes: true,
420
421 citations: true,
423
424 bracketed_spans: true,
426 native_spans: true,
427
428 mmd_title_block: false,
430 pandoc_title_block: true,
431 yaml_metadata_block: true,
432
433 markdown_in_html_blocks: false,
435 raw_attribute: true,
436 raw_html: true,
437 raw_tex: true,
438
439 all_symbols_escapable: true,
441 escaped_line_breaks: true,
442
443 alerts: false,
445 autolink_bare_uris: false,
446 emoji: false,
447 hard_line_breaks: false,
448 mark: false,
449 mmd_header_identifiers: false,
450 mmd_link_attributes: false,
451
452 bookdown_references: false,
454 bookdown_equation_references: false,
455 quarto_callouts: false,
456 quarto_crossrefs: false,
457 quarto_shortcodes: false,
458 }
459 }
460
461 fn quarto_defaults() -> Self {
462 let mut ext = Self::pandoc_defaults();
463
464 ext.executable_code = true;
465 ext.rmarkdown_inline_code = true;
466 ext.quarto_inline_code = true;
467 ext.quarto_callouts = true;
468 ext.quarto_crossrefs = true;
469 ext.quarto_shortcodes = true;
470
471 ext
472 }
473
474 fn rmarkdown_defaults() -> Self {
475 let mut ext = Self::pandoc_defaults();
476
477 ext.bookdown_references = true;
478 ext.bookdown_equation_references = true;
479 ext.executable_code = true;
480 ext.rmarkdown_inline_code = true;
481 ext.quarto_inline_code = false;
482 ext.tex_math_dollars = true;
483 ext.tex_math_single_backslash = true;
484
485 ext
486 }
487
488 fn gfm_defaults() -> Self {
489 let mut ext = Self::none_defaults();
490
491 ext.alerts = true;
492 ext.auto_identifiers = true;
493 ext.autolink_bare_uris = true;
494 ext.backtick_code_blocks = true;
495 ext.emoji = true;
496 ext.fenced_code_blocks = true;
497 ext.footnotes = true;
498 ext.gfm_auto_identifiers = true;
499 ext.inline_links = true;
500 ext.pipe_tables = true;
501 ext.raw_html = true;
502 ext.strikeout = true;
503 ext.task_lists = true;
504 ext.tex_math_dollars = true;
505 ext.tex_math_gfm = true;
506 ext.yaml_metadata_block = true;
507
508 ext
509 }
510
511 fn commonmark_defaults() -> Self {
512 let mut ext = Self::none_defaults();
513 ext.autolinks = true;
523 ext.backtick_code_blocks = true;
524 ext.escaped_line_breaks = true;
525 ext.fenced_code_blocks = true;
526 ext.inline_images = true;
527 ext.inline_links = true;
528 ext.intraword_underscores = true;
529 ext.raw_html = true;
530 ext.reference_links = true;
531 ext.shortcut_reference_links = true;
532 ext
533 }
534
535 fn multimarkdown_defaults() -> Self {
536 let mut ext = Self::none_defaults();
537
538 ext.all_symbols_escapable = true;
539 ext.auto_identifiers = true;
540 ext.backtick_code_blocks = true;
541 ext.definition_lists = true;
542 ext.footnotes = true;
543 ext.implicit_figures = true;
544 ext.implicit_header_references = true;
545 ext.intraword_underscores = true;
546 ext.mmd_header_identifiers = true;
547 ext.mmd_link_attributes = true;
548 ext.mmd_title_block = true;
549 ext.pipe_tables = true;
550 ext.raw_attribute = true;
551 ext.raw_html = true;
552 ext.reference_links = true;
553 ext.shortcut_reference_links = true;
554 ext.subscript = true;
555 ext.superscript = true;
556 ext.tex_math_dollars = true;
557 ext.tex_math_double_backslash = true;
558
559 ext
560 }
561
562 pub fn merge_with_flavor(user_overrides: HashMap<String, bool>, flavor: Flavor) -> Self {
576 let defaults = Self::for_flavor(flavor);
577 Self::merge_overrides(defaults, user_overrides)
578 }
579
580 fn merge_overrides(mut base: Extensions, user_overrides: HashMap<String, bool>) -> Self {
581 for (key, value) in user_overrides {
582 let normalized_key = key.replace('_', "-");
583 match normalized_key.as_str() {
584 "blank-before-header" => base.blank_before_header = value,
585 "header-attributes" => base.header_attributes = value,
586 "auto-identifiers" => base.auto_identifiers = value,
587 "gfm-auto-identifiers" => base.gfm_auto_identifiers = value,
588 "implicit-header-references" => base.implicit_header_references = value,
589 "blank-before-blockquote" => base.blank_before_blockquote = value,
590 "fancy-lists" => base.fancy_lists = value,
591 "startnum" => base.startnum = value,
592 "example-lists" => base.example_lists = value,
593 "task-lists" => base.task_lists = value,
594 "definition-lists" => base.definition_lists = value,
595 "lists-without-preceding-blankline" => {
596 base.lists_without_preceding_blankline = value
597 }
598 "backtick-code-blocks" => base.backtick_code_blocks = value,
599 "fenced-code-blocks" => base.fenced_code_blocks = value,
600 "fenced-code-attributes" => base.fenced_code_attributes = value,
601 "executable-code" => base.executable_code = value,
602 "rmarkdown-inline-code" => base.rmarkdown_inline_code = value,
603 "quarto-inline-code" => base.quarto_inline_code = value,
604 "inline-code-attributes" => base.inline_code_attributes = value,
605 "simple-tables" => base.simple_tables = value,
606 "multiline-tables" => base.multiline_tables = value,
607 "grid-tables" => base.grid_tables = value,
608 "pipe-tables" => base.pipe_tables = value,
609 "table-captions" => base.table_captions = value,
610 "fenced-divs" => base.fenced_divs = value,
611 "native-divs" => base.native_divs = value,
612 "line-blocks" => base.line_blocks = value,
613 "intraword-underscores" => base.intraword_underscores = value,
614 "strikeout" => base.strikeout = value,
615 "superscript" => base.superscript = value,
616 "subscript" => base.subscript = value,
617 "inline-links" => base.inline_links = value,
618 "reference-links" => base.reference_links = value,
619 "shortcut-reference-links" => base.shortcut_reference_links = value,
620 "link-attributes" => base.link_attributes = value,
621 "autolinks" => base.autolinks = value,
622 "inline-images" => base.inline_images = value,
623 "implicit-figures" => base.implicit_figures = value,
624 "tex-math-dollars" => base.tex_math_dollars = value,
625 "tex-math-gfm" => base.tex_math_gfm = value,
626 "tex-math-single-backslash" => base.tex_math_single_backslash = value,
627 "tex-math-double-backslash" => base.tex_math_double_backslash = value,
628 "inline-footnotes" => base.inline_footnotes = value,
629 "footnotes" => base.footnotes = value,
630 "citations" => base.citations = value,
631 "bracketed-spans" => base.bracketed_spans = value,
632 "native-spans" => base.native_spans = value,
633 "yaml-metadata-block" => base.yaml_metadata_block = value,
634 "pandoc-title-block" => base.pandoc_title_block = value,
635 "mmd-title-block" => base.mmd_title_block = value,
636 "raw-html" => base.raw_html = value,
637 "markdown-in-html-blocks" => base.markdown_in_html_blocks = value,
638 "raw-tex" => base.raw_tex = value,
639 "raw-attribute" => base.raw_attribute = value,
640 "all-symbols-escapable" => base.all_symbols_escapable = value,
641 "escaped-line-breaks" => base.escaped_line_breaks = value,
642 "autolink-bare-uris" => base.autolink_bare_uris = value,
643 "hard-line-breaks" => base.hard_line_breaks = value,
644 "mmd-header-identifiers" => base.mmd_header_identifiers = value,
645 "mmd-link-attributes" => base.mmd_link_attributes = value,
646 "alerts" => base.alerts = value,
647 "emoji" => base.emoji = value,
648 "mark" => base.mark = value,
649 "quarto-callouts" => base.quarto_callouts = value,
650 "quarto-crossrefs" => base.quarto_crossrefs = value,
651 "quarto-shortcodes" => base.quarto_shortcodes = value,
652 "bookdown-references" => base.bookdown_references = value,
653 "bookdown-equation-references" => base.bookdown_equation_references = value,
654 _ => {}
655 }
656 }
657 base
658 }
659}
660
661#[cfg(test)]
662mod tests {
663 use super::{Extensions, Flavor};
664 use std::collections::HashMap;
665
666 #[test]
667 fn merge_with_flavor_keeps_known_extension_overrides() {
668 let mut overrides = HashMap::new();
669 overrides.insert("intraword-underscores".to_string(), false);
670 let ext = Extensions::merge_with_flavor(overrides, Flavor::Pandoc);
671 assert!(!ext.intraword_underscores);
672 }
673
674 #[test]
675 fn merge_with_flavor_ignores_unknown_extension_overrides() {
676 let mut overrides = HashMap::new();
677 overrides.insert("smart".to_string(), true);
678 overrides.insert("smart-quotes".to_string(), true);
679 let ext = Extensions::merge_with_flavor(overrides, Flavor::Gfm);
680 assert!(ext.strikeout, "known defaults should remain intact");
681 }
682
683 #[test]
684 fn lists_without_preceding_blankline_defaults_false_for_pandoc_and_gfm() {
685 assert!(!Extensions::for_flavor(Flavor::Pandoc).lists_without_preceding_blankline);
686 assert!(!Extensions::for_flavor(Flavor::Gfm).lists_without_preceding_blankline);
687 }
688
689 #[test]
690 fn merge_with_flavor_accepts_lists_without_preceding_blankline_override() {
691 let mut overrides = HashMap::new();
692 overrides.insert("lists-without-preceding-blankline".to_string(), true);
693 let ext = Extensions::merge_with_flavor(overrides, Flavor::Pandoc);
694 assert!(ext.lists_without_preceding_blankline);
695 }
696}
697
698#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
699#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
700pub enum PandocCompat {
701 #[cfg_attr(feature = "serde", serde(rename = "latest"))]
706 Latest,
707 #[cfg_attr(
709 feature = "serde",
710 serde(rename = "3.7", alias = "3-7", alias = "v3.7", alias = "v3-7")
711 )]
712 V3_7,
713 #[default]
715 #[cfg_attr(
716 feature = "serde",
717 serde(rename = "3.9", alias = "3-9", alias = "v3.9", alias = "v3-9")
718 )]
719 V3_9,
720}
721
722impl PandocCompat {
723 pub const PINNED_LATEST: Self = Self::V3_9;
725
726 pub fn effective(self) -> Self {
727 match self {
728 Self::Latest => Self::PINNED_LATEST,
729 other => other,
730 }
731 }
732}
733
734#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
745#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
746#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
747pub enum Dialect {
748 #[default]
751 Pandoc,
752 CommonMark,
754}
755
756impl Dialect {
757 pub fn for_flavor(flavor: Flavor) -> Self {
759 match flavor {
760 Flavor::CommonMark | Flavor::Gfm => Dialect::CommonMark,
761 Flavor::Pandoc | Flavor::Quarto | Flavor::RMarkdown | Flavor::MultiMarkdown => {
762 Dialect::Pandoc
763 }
764 }
765 }
766}
767
768#[derive(Debug, Clone)]
769#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
770#[cfg_attr(feature = "serde", serde(default, rename_all = "kebab-case"))]
771pub struct ParserOptions {
772 pub flavor: Flavor,
773 pub dialect: Dialect,
774 pub extensions: Extensions,
775 pub pandoc_compat: PandocCompat,
777}
778
779impl Default for ParserOptions {
780 fn default() -> Self {
781 let flavor = Flavor::default();
782 Self {
783 flavor,
784 dialect: Dialect::for_flavor(flavor),
785 extensions: Extensions::for_flavor(flavor),
786 pandoc_compat: PandocCompat::default(),
787 }
788 }
789}
790
791impl ParserOptions {
792 pub fn effective_pandoc_compat(&self) -> PandocCompat {
793 self.pandoc_compat.effective()
794 }
795}