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 CommonMark,
21 #[cfg_attr(feature = "serde", serde(rename = "multimarkdown"))]
23 MultiMarkdown,
24}
25
26#[derive(Debug, Clone, PartialEq)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31#[cfg_attr(feature = "serde", serde(default))]
32#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
33pub struct Extensions {
34 #[cfg_attr(feature = "serde", serde(alias = "blank_before_header"))]
39 pub blank_before_header: bool,
40 #[cfg_attr(feature = "serde", serde(alias = "header_attributes"))]
42 pub header_attributes: bool,
43 pub auto_identifiers: bool,
45 pub gfm_auto_identifiers: bool,
47 pub implicit_header_references: bool,
49
50 #[cfg_attr(feature = "serde", serde(alias = "blank_before_blockquote"))]
53 pub blank_before_blockquote: bool,
54
55 #[cfg_attr(feature = "serde", serde(alias = "fancy_lists"))]
58 pub fancy_lists: bool,
59 pub startnum: bool,
61 #[cfg_attr(feature = "serde", serde(alias = "example_lists"))]
63 pub example_lists: bool,
64 #[cfg_attr(feature = "serde", serde(alias = "task_lists"))]
66 pub task_lists: bool,
67 #[cfg_attr(feature = "serde", serde(alias = "definition_lists"))]
69 pub definition_lists: bool,
70 #[cfg_attr(feature = "serde", serde(alias = "lists_without_preceding_blankline"))]
72 pub lists_without_preceding_blankline: bool,
73
74 #[cfg_attr(feature = "serde", serde(alias = "backtick_code_blocks"))]
77 pub backtick_code_blocks: bool,
78 #[cfg_attr(feature = "serde", serde(alias = "fenced_code_blocks"))]
80 pub fenced_code_blocks: bool,
81 #[cfg_attr(feature = "serde", serde(alias = "fenced_code_attributes"))]
83 pub fenced_code_attributes: bool,
84 pub executable_code: bool,
86 pub rmarkdown_inline_code: bool,
88 pub quarto_inline_code: bool,
90 #[cfg_attr(feature = "serde", serde(alias = "inline_code_attributes"))]
92 pub inline_code_attributes: bool,
93
94 #[cfg_attr(feature = "serde", serde(alias = "simple_tables"))]
97 pub simple_tables: bool,
98 #[cfg_attr(feature = "serde", serde(alias = "multiline_tables"))]
100 pub multiline_tables: bool,
101 #[cfg_attr(feature = "serde", serde(alias = "grid_tables"))]
103 pub grid_tables: bool,
104 #[cfg_attr(feature = "serde", serde(alias = "pipe_tables"))]
106 pub pipe_tables: bool,
107 #[cfg_attr(feature = "serde", serde(alias = "table_captions"))]
109 pub table_captions: bool,
110
111 #[cfg_attr(feature = "serde", serde(alias = "fenced_divs"))]
114 pub fenced_divs: bool,
115 #[cfg_attr(feature = "serde", serde(alias = "native_divs"))]
117 pub native_divs: bool,
118
119 #[cfg_attr(feature = "serde", serde(alias = "line_blocks"))]
122 pub line_blocks: bool,
123
124 #[cfg_attr(feature = "serde", serde(alias = "intraword_underscores"))]
129 pub intraword_underscores: bool,
130 pub strikeout: bool,
132 pub superscript: bool,
134 pub subscript: bool,
135
136 #[cfg_attr(feature = "serde", serde(alias = "inline_links"))]
139 pub inline_links: bool,
140 #[cfg_attr(feature = "serde", serde(alias = "reference_links"))]
142 pub reference_links: bool,
143 #[cfg_attr(feature = "serde", serde(alias = "shortcut_reference_links"))]
145 pub shortcut_reference_links: bool,
146 #[cfg_attr(feature = "serde", serde(alias = "link_attributes"))]
148 pub link_attributes: bool,
149 pub autolinks: bool,
151
152 #[cfg_attr(feature = "serde", serde(alias = "inline_images"))]
155 pub inline_images: bool,
156 #[cfg_attr(feature = "serde", serde(alias = "implicit_figures"))]
158 pub implicit_figures: bool,
159
160 #[cfg_attr(feature = "serde", serde(alias = "tex_math_dollars"))]
163 pub tex_math_dollars: bool,
164 #[cfg_attr(feature = "serde", serde(alias = "tex_math_gfm"))]
166 pub tex_math_gfm: bool,
167 #[cfg_attr(feature = "serde", serde(alias = "tex_math_single_backslash"))]
169 pub tex_math_single_backslash: bool,
170 #[cfg_attr(feature = "serde", serde(alias = "tex_math_double_backslash"))]
172 pub tex_math_double_backslash: bool,
173
174 #[cfg_attr(feature = "serde", serde(alias = "inline_footnotes"))]
177 pub inline_footnotes: bool,
178 pub footnotes: bool,
180
181 pub citations: bool,
184
185 #[cfg_attr(feature = "serde", serde(alias = "bracketed_spans"))]
188 pub bracketed_spans: bool,
189 #[cfg_attr(feature = "serde", serde(alias = "native_spans"))]
191 pub native_spans: bool,
192
193 #[cfg_attr(feature = "serde", serde(alias = "yaml_metadata_block"))]
196 pub yaml_metadata_block: bool,
197 #[cfg_attr(feature = "serde", serde(alias = "pandoc_title_block"))]
199 pub pandoc_title_block: bool,
200 pub mmd_title_block: bool,
202
203 #[cfg_attr(feature = "serde", serde(alias = "raw_html"))]
206 pub raw_html: bool,
207 #[cfg_attr(feature = "serde", serde(alias = "markdown_in_html_blocks"))]
209 pub markdown_in_html_blocks: bool,
210 #[cfg_attr(feature = "serde", serde(alias = "raw_tex"))]
212 pub raw_tex: bool,
213 #[cfg_attr(feature = "serde", serde(alias = "raw_attribute"))]
215 pub raw_attribute: bool,
216
217 #[cfg_attr(feature = "serde", serde(alias = "all_symbols_escapable"))]
220 pub all_symbols_escapable: bool,
221 #[cfg_attr(feature = "serde", serde(alias = "escaped_line_breaks"))]
223 pub escaped_line_breaks: bool,
224
225 #[cfg_attr(feature = "serde", serde(alias = "autolink_bare_uris"))]
229 pub autolink_bare_uris: bool,
230 #[cfg_attr(feature = "serde", serde(alias = "hard_line_breaks"))]
232 pub hard_line_breaks: bool,
233 pub mmd_header_identifiers: bool,
235 pub mmd_link_attributes: bool,
237 pub alerts: bool,
239 pub emoji: bool,
241 pub mark: bool,
243
244 #[cfg_attr(feature = "serde", serde(alias = "quarto_callouts"))]
247 pub quarto_callouts: bool,
248 #[cfg_attr(feature = "serde", serde(alias = "quarto_crossrefs"))]
250 pub quarto_crossrefs: bool,
251 #[cfg_attr(feature = "serde", serde(alias = "quarto_shortcodes"))]
253 pub quarto_shortcodes: bool,
254 pub bookdown_references: bool,
256 pub bookdown_equation_references: bool,
258}
259
260impl Default for Extensions {
261 fn default() -> Self {
262 Self::for_flavor(Flavor::default())
263 }
264}
265
266impl Extensions {
267 fn none_defaults() -> Self {
268 Self {
269 alerts: false,
270 all_symbols_escapable: false,
271 auto_identifiers: false,
272 autolink_bare_uris: false,
273 autolinks: false,
274 backtick_code_blocks: false,
275 blank_before_blockquote: false,
276 blank_before_header: false,
277 bookdown_references: false,
278 bookdown_equation_references: false,
279 bracketed_spans: false,
280 citations: false,
281 definition_lists: false,
282 lists_without_preceding_blankline: false,
283 emoji: false,
284 escaped_line_breaks: false,
285 example_lists: false,
286 executable_code: false,
287 rmarkdown_inline_code: false,
288 quarto_inline_code: false,
289 fancy_lists: false,
290 fenced_code_attributes: false,
291 fenced_code_blocks: false,
292 fenced_divs: false,
293 footnotes: false,
294 gfm_auto_identifiers: false,
295 grid_tables: false,
296 hard_line_breaks: false,
297 header_attributes: false,
298 implicit_figures: false,
299 implicit_header_references: false,
300 inline_code_attributes: false,
301 inline_footnotes: false,
302 inline_images: false,
303 inline_links: false,
304 intraword_underscores: false,
305 line_blocks: false,
306 link_attributes: false,
307 mark: false,
308 markdown_in_html_blocks: false,
309 mmd_header_identifiers: false,
310 mmd_link_attributes: false,
311 mmd_title_block: false,
312 multiline_tables: false,
313 native_divs: false,
314 native_spans: false,
315 pandoc_title_block: false,
316 pipe_tables: false,
317 quarto_callouts: false,
318 quarto_crossrefs: false,
319 quarto_shortcodes: false,
320 raw_attribute: false,
321 raw_html: false,
322 raw_tex: false,
323 reference_links: false,
324 shortcut_reference_links: false,
325 simple_tables: false,
326 startnum: false,
327 strikeout: false,
328 subscript: false,
329 superscript: false,
330 table_captions: false,
331 task_lists: false,
332 tex_math_dollars: false,
333 tex_math_double_backslash: false,
334 tex_math_gfm: false,
335 tex_math_single_backslash: false,
336 yaml_metadata_block: false,
337 }
338 }
339
340 pub fn for_flavor(flavor: Flavor) -> Self {
342 match flavor {
343 Flavor::Pandoc => Self::pandoc_defaults(),
344 Flavor::Quarto => Self::quarto_defaults(),
345 Flavor::RMarkdown => Self::rmarkdown_defaults(),
346 Flavor::Gfm => Self::gfm_defaults(),
347 Flavor::CommonMark => Self::commonmark_defaults(),
348 Flavor::MultiMarkdown => Self::multimarkdown_defaults(),
349 }
350 }
351
352 fn pandoc_defaults() -> Self {
353 Self {
354 auto_identifiers: true,
356 blank_before_blockquote: true,
357 blank_before_header: true,
358 gfm_auto_identifiers: false,
359 header_attributes: true,
360 implicit_header_references: true,
361
362 definition_lists: true,
364 example_lists: true,
365 fancy_lists: true,
366 lists_without_preceding_blankline: false,
367 startnum: true,
368 task_lists: true,
369
370 backtick_code_blocks: true,
372 executable_code: false,
373 rmarkdown_inline_code: false,
374 quarto_inline_code: false,
375 fenced_code_attributes: true,
376 fenced_code_blocks: true,
377 inline_code_attributes: true,
378
379 grid_tables: true,
381 multiline_tables: true,
382 pipe_tables: true,
383 simple_tables: true,
384 table_captions: true,
385
386 fenced_divs: true,
388 native_divs: true,
389
390 line_blocks: true,
392
393 intraword_underscores: true,
395 strikeout: true,
396 subscript: true,
397 superscript: true,
398
399 autolinks: true,
401 inline_links: true,
402 link_attributes: true,
403 reference_links: true,
404 shortcut_reference_links: true,
405
406 implicit_figures: true,
408 inline_images: true,
409
410 tex_math_dollars: true,
412 tex_math_double_backslash: false,
413 tex_math_gfm: false,
414 tex_math_single_backslash: false,
415
416 footnotes: true,
418 inline_footnotes: true,
419
420 citations: true,
422
423 bracketed_spans: true,
425 native_spans: true,
426
427 mmd_title_block: false,
429 pandoc_title_block: true,
430 yaml_metadata_block: true,
431
432 markdown_in_html_blocks: false,
434 raw_attribute: true,
435 raw_html: true,
436 raw_tex: true,
437
438 all_symbols_escapable: true,
440 escaped_line_breaks: true,
441
442 alerts: false,
444 autolink_bare_uris: false,
445 emoji: false,
446 hard_line_breaks: false,
447 mark: false,
448 mmd_header_identifiers: false,
449 mmd_link_attributes: false,
450
451 bookdown_references: false,
453 bookdown_equation_references: false,
454 quarto_callouts: false,
455 quarto_crossrefs: false,
456 quarto_shortcodes: false,
457 }
458 }
459
460 fn quarto_defaults() -> Self {
461 let mut ext = Self::pandoc_defaults();
462
463 ext.executable_code = true;
464 ext.rmarkdown_inline_code = true;
465 ext.quarto_inline_code = true;
466 ext.quarto_callouts = true;
467 ext.quarto_crossrefs = true;
468 ext.quarto_shortcodes = true;
469
470 ext
471 }
472
473 fn rmarkdown_defaults() -> Self {
474 let mut ext = Self::pandoc_defaults();
475
476 ext.bookdown_references = true;
477 ext.bookdown_equation_references = true;
478 ext.executable_code = true;
479 ext.rmarkdown_inline_code = true;
480 ext.quarto_inline_code = false;
481 ext.tex_math_dollars = true;
482 ext.tex_math_single_backslash = true;
483
484 ext
485 }
486
487 fn gfm_defaults() -> Self {
488 let mut ext = Self::none_defaults();
489
490 ext.alerts = true;
491 ext.auto_identifiers = true;
492 ext.autolink_bare_uris = true;
493 ext.backtick_code_blocks = true;
494 ext.emoji = true;
495 ext.fenced_code_blocks = true;
496 ext.footnotes = true;
497 ext.gfm_auto_identifiers = true;
498 ext.inline_links = true;
499 ext.pipe_tables = true;
500 ext.raw_html = true;
501 ext.strikeout = true;
502 ext.task_lists = true;
503 ext.tex_math_dollars = true;
504 ext.tex_math_gfm = true;
505 ext.yaml_metadata_block = true;
506
507 ext
508 }
509
510 fn commonmark_defaults() -> Self {
511 let mut ext = Self::none_defaults();
512 ext.raw_html = true;
513 ext
514 }
515
516 fn multimarkdown_defaults() -> Self {
517 let mut ext = Self::none_defaults();
518
519 ext.all_symbols_escapable = true;
520 ext.auto_identifiers = true;
521 ext.backtick_code_blocks = true;
522 ext.definition_lists = true;
523 ext.footnotes = true;
524 ext.implicit_figures = true;
525 ext.implicit_header_references = true;
526 ext.intraword_underscores = true;
527 ext.mmd_header_identifiers = true;
528 ext.mmd_link_attributes = true;
529 ext.mmd_title_block = true;
530 ext.pipe_tables = true;
531 ext.raw_attribute = true;
532 ext.raw_html = true;
533 ext.reference_links = true;
534 ext.shortcut_reference_links = true;
535 ext.subscript = true;
536 ext.superscript = true;
537 ext.tex_math_dollars = true;
538 ext.tex_math_double_backslash = true;
539
540 ext
541 }
542
543 pub fn merge_with_flavor(user_overrides: HashMap<String, bool>, flavor: Flavor) -> Self {
557 let defaults = Self::for_flavor(flavor);
558 Self::merge_overrides(defaults, user_overrides)
559 }
560
561 fn merge_overrides(mut base: Extensions, user_overrides: HashMap<String, bool>) -> Self {
562 for (key, value) in user_overrides {
563 let normalized_key = key.replace('_', "-");
564 match normalized_key.as_str() {
565 "blank-before-header" => base.blank_before_header = value,
566 "header-attributes" => base.header_attributes = value,
567 "auto-identifiers" => base.auto_identifiers = value,
568 "gfm-auto-identifiers" => base.gfm_auto_identifiers = value,
569 "implicit-header-references" => base.implicit_header_references = value,
570 "blank-before-blockquote" => base.blank_before_blockquote = value,
571 "fancy-lists" => base.fancy_lists = value,
572 "startnum" => base.startnum = value,
573 "example-lists" => base.example_lists = value,
574 "task-lists" => base.task_lists = value,
575 "definition-lists" => base.definition_lists = value,
576 "lists-without-preceding-blankline" => {
577 base.lists_without_preceding_blankline = value
578 }
579 "backtick-code-blocks" => base.backtick_code_blocks = value,
580 "fenced-code-blocks" => base.fenced_code_blocks = value,
581 "fenced-code-attributes" => base.fenced_code_attributes = value,
582 "executable-code" => base.executable_code = value,
583 "rmarkdown-inline-code" => base.rmarkdown_inline_code = value,
584 "quarto-inline-code" => base.quarto_inline_code = value,
585 "inline-code-attributes" => base.inline_code_attributes = value,
586 "simple-tables" => base.simple_tables = value,
587 "multiline-tables" => base.multiline_tables = value,
588 "grid-tables" => base.grid_tables = value,
589 "pipe-tables" => base.pipe_tables = value,
590 "table-captions" => base.table_captions = value,
591 "fenced-divs" => base.fenced_divs = value,
592 "native-divs" => base.native_divs = value,
593 "line-blocks" => base.line_blocks = value,
594 "intraword-underscores" => base.intraword_underscores = value,
595 "strikeout" => base.strikeout = value,
596 "superscript" => base.superscript = value,
597 "subscript" => base.subscript = value,
598 "inline-links" => base.inline_links = value,
599 "reference-links" => base.reference_links = value,
600 "shortcut-reference-links" => base.shortcut_reference_links = value,
601 "link-attributes" => base.link_attributes = value,
602 "autolinks" => base.autolinks = value,
603 "inline-images" => base.inline_images = value,
604 "implicit-figures" => base.implicit_figures = value,
605 "tex-math-dollars" => base.tex_math_dollars = value,
606 "tex-math-gfm" => base.tex_math_gfm = value,
607 "tex-math-single-backslash" => base.tex_math_single_backslash = value,
608 "tex-math-double-backslash" => base.tex_math_double_backslash = value,
609 "inline-footnotes" => base.inline_footnotes = value,
610 "footnotes" => base.footnotes = value,
611 "citations" => base.citations = value,
612 "bracketed-spans" => base.bracketed_spans = value,
613 "native-spans" => base.native_spans = value,
614 "yaml-metadata-block" => base.yaml_metadata_block = value,
615 "pandoc-title-block" => base.pandoc_title_block = value,
616 "mmd-title-block" => base.mmd_title_block = value,
617 "raw-html" => base.raw_html = value,
618 "markdown-in-html-blocks" => base.markdown_in_html_blocks = value,
619 "raw-tex" => base.raw_tex = value,
620 "raw-attribute" => base.raw_attribute = value,
621 "all-symbols-escapable" => base.all_symbols_escapable = value,
622 "escaped-line-breaks" => base.escaped_line_breaks = value,
623 "autolink-bare-uris" => base.autolink_bare_uris = value,
624 "hard-line-breaks" => base.hard_line_breaks = value,
625 "mmd-header-identifiers" => base.mmd_header_identifiers = value,
626 "mmd-link-attributes" => base.mmd_link_attributes = value,
627 "alerts" => base.alerts = value,
628 "emoji" => base.emoji = value,
629 "mark" => base.mark = value,
630 "quarto-callouts" => base.quarto_callouts = value,
631 "quarto-crossrefs" => base.quarto_crossrefs = value,
632 "quarto-shortcodes" => base.quarto_shortcodes = value,
633 "bookdown-references" => base.bookdown_references = value,
634 "bookdown-equation-references" => base.bookdown_equation_references = value,
635 _ => {}
636 }
637 }
638 base
639 }
640}
641
642#[cfg(test)]
643mod tests {
644 use super::{Extensions, Flavor};
645 use std::collections::HashMap;
646
647 #[test]
648 fn merge_with_flavor_keeps_known_extension_overrides() {
649 let mut overrides = HashMap::new();
650 overrides.insert("intraword-underscores".to_string(), false);
651 let ext = Extensions::merge_with_flavor(overrides, Flavor::Pandoc);
652 assert!(!ext.intraword_underscores);
653 }
654
655 #[test]
656 fn merge_with_flavor_ignores_unknown_extension_overrides() {
657 let mut overrides = HashMap::new();
658 overrides.insert("smart".to_string(), true);
659 overrides.insert("smart-quotes".to_string(), true);
660 let ext = Extensions::merge_with_flavor(overrides, Flavor::Gfm);
661 assert!(ext.strikeout, "known defaults should remain intact");
662 }
663
664 #[test]
665 fn lists_without_preceding_blankline_defaults_false_for_pandoc_and_gfm() {
666 assert!(!Extensions::for_flavor(Flavor::Pandoc).lists_without_preceding_blankline);
667 assert!(!Extensions::for_flavor(Flavor::Gfm).lists_without_preceding_blankline);
668 }
669
670 #[test]
671 fn merge_with_flavor_accepts_lists_without_preceding_blankline_override() {
672 let mut overrides = HashMap::new();
673 overrides.insert("lists-without-preceding-blankline".to_string(), true);
674 let ext = Extensions::merge_with_flavor(overrides, Flavor::Pandoc);
675 assert!(ext.lists_without_preceding_blankline);
676 }
677}
678
679#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
680#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
681pub enum PandocCompat {
682 #[cfg_attr(feature = "serde", serde(rename = "latest"))]
687 Latest,
688 #[cfg_attr(
690 feature = "serde",
691 serde(rename = "3.7", alias = "3-7", alias = "v3.7", alias = "v3-7")
692 )]
693 V3_7,
694 #[default]
696 #[cfg_attr(
697 feature = "serde",
698 serde(rename = "3.9", alias = "3-9", alias = "v3.9", alias = "v3-9")
699 )]
700 V3_9,
701}
702
703impl PandocCompat {
704 pub const PINNED_LATEST: Self = Self::V3_9;
706
707 pub fn effective(self) -> Self {
708 match self {
709 Self::Latest => Self::PINNED_LATEST,
710 other => other,
711 }
712 }
713}
714
715#[derive(Debug, Clone)]
716#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
717#[cfg_attr(feature = "serde", serde(default, rename_all = "kebab-case"))]
718pub struct ParserOptions {
719 pub flavor: Flavor,
720 pub extensions: Extensions,
721 pub pandoc_compat: PandocCompat,
723}
724
725impl Default for ParserOptions {
726 fn default() -> Self {
727 let flavor = Flavor::default();
728 Self {
729 flavor,
730 extensions: Extensions::for_flavor(flavor),
731 pandoc_compat: PandocCompat::default(),
732 }
733 }
734}
735
736impl ParserOptions {
737 pub fn effective_pandoc_compat(&self) -> PandocCompat {
738 self.pandoc_compat.effective()
739 }
740}