Skip to main content

panache_parser/
options.rs

1use std::collections::HashMap;
2
3/// The flavor of Markdown to parse and format.
4/// Each flavor has a different set of default extensions enabled.
5#[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    /// Standard Pandoc Markdown (default extensions enabled)
10    #[default]
11    Pandoc,
12    /// Quarto (Pandoc + Quarto-specific extensions)
13    Quarto,
14    /// R Markdown (Pandoc + R-specific extensions)
15    #[cfg_attr(feature = "serde", serde(rename = "rmarkdown"))]
16    RMarkdown,
17    /// GitHub Flavored Markdown
18    Gfm,
19    /// CommonMark
20    CommonMark,
21    /// MultiMarkdown
22    #[cfg_attr(feature = "serde", serde(rename = "multimarkdown"))]
23    MultiMarkdown,
24}
25
26/// Pandoc/Markdown extensions configuration.
27/// Each field represents a specific Pandoc extension.
28/// Extensions marked with a comment indicate implementation status.
29#[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    // ===== Block-level extensions =====
35
36    // Headings
37    /// Require blank line before headers (default: enabled)
38    #[cfg_attr(feature = "serde", serde(alias = "blank_before_header"))]
39    pub blank_before_header: bool,
40    /// Full attribute syntax on headers {#id .class key=value}
41    #[cfg_attr(feature = "serde", serde(alias = "header_attributes"))]
42    pub header_attributes: bool,
43    /// Auto-generate identifiers from headings
44    pub auto_identifiers: bool,
45    /// Use GitHub's algorithm for auto-generated heading identifiers
46    pub gfm_auto_identifiers: bool,
47    /// Implicit header references ([Heading] links to header)
48    pub implicit_header_references: bool,
49
50    // Block quotes
51    /// Require blank line before blockquotes (default: enabled)
52    #[cfg_attr(feature = "serde", serde(alias = "blank_before_blockquote"))]
53    pub blank_before_blockquote: bool,
54
55    // Lists
56    /// Fancy list markers (roman numerals, letters, etc.)
57    #[cfg_attr(feature = "serde", serde(alias = "fancy_lists"))]
58    pub fancy_lists: bool,
59    /// Start ordered lists at arbitrary numbers
60    pub startnum: bool,
61    /// Example lists with (@) markers
62    #[cfg_attr(feature = "serde", serde(alias = "example_lists"))]
63    pub example_lists: bool,
64    /// GitHub-style task lists - [ ] and - [x]
65    #[cfg_attr(feature = "serde", serde(alias = "task_lists"))]
66    pub task_lists: bool,
67    /// Term/definition syntax
68    #[cfg_attr(feature = "serde", serde(alias = "definition_lists"))]
69    pub definition_lists: bool,
70    /// Allow lists without a preceding blank line
71    #[cfg_attr(feature = "serde", serde(alias = "lists_without_preceding_blankline"))]
72    pub lists_without_preceding_blankline: bool,
73
74    // Code blocks
75    /// Fenced code blocks with backticks
76    #[cfg_attr(feature = "serde", serde(alias = "backtick_code_blocks"))]
77    pub backtick_code_blocks: bool,
78    /// Fenced code blocks with tildes
79    #[cfg_attr(feature = "serde", serde(alias = "fenced_code_blocks"))]
80    pub fenced_code_blocks: bool,
81    /// Attributes on fenced code blocks {.language #id}
82    #[cfg_attr(feature = "serde", serde(alias = "fenced_code_attributes"))]
83    pub fenced_code_attributes: bool,
84    /// Executable code syntax (currently fenced chunks like ```{r} / ```{python})
85    pub executable_code: bool,
86    /// R Markdown inline executable code (`...`r ...)
87    pub rmarkdown_inline_code: bool,
88    /// Quarto inline executable code (`...`{r} ...)
89    pub quarto_inline_code: bool,
90    /// Attributes on inline code
91    #[cfg_attr(feature = "serde", serde(alias = "inline_code_attributes"))]
92    pub inline_code_attributes: bool,
93
94    // Tables
95    /// Simple table syntax
96    #[cfg_attr(feature = "serde", serde(alias = "simple_tables"))]
97    pub simple_tables: bool,
98    /// Multiline cell content in tables
99    #[cfg_attr(feature = "serde", serde(alias = "multiline_tables"))]
100    pub multiline_tables: bool,
101    /// Grid-style tables
102    #[cfg_attr(feature = "serde", serde(alias = "grid_tables"))]
103    pub grid_tables: bool,
104    /// Pipe tables (GitHub/PHP Markdown style)
105    #[cfg_attr(feature = "serde", serde(alias = "pipe_tables"))]
106    pub pipe_tables: bool,
107    /// Table captions
108    #[cfg_attr(feature = "serde", serde(alias = "table_captions"))]
109    pub table_captions: bool,
110
111    // Divs
112    /// Fenced divs ::: {.class}
113    #[cfg_attr(feature = "serde", serde(alias = "fenced_divs"))]
114    pub fenced_divs: bool,
115    /// HTML <div> elements
116    #[cfg_attr(feature = "serde", serde(alias = "native_divs"))]
117    pub native_divs: bool,
118
119    // Other block elements
120    /// Line blocks for poetry | prefix
121    #[cfg_attr(feature = "serde", serde(alias = "line_blocks"))]
122    pub line_blocks: bool,
123
124    // ===== Inline elements =====
125
126    // Emphasis
127    /// Underscores don't trigger emphasis in snake_case
128    #[cfg_attr(feature = "serde", serde(alias = "intraword_underscores"))]
129    pub intraword_underscores: bool,
130    /// Strikethrough ~~text~~
131    pub strikeout: bool,
132    /// Superscript and subscript ^super^ ~sub~
133    pub superscript: bool,
134    pub subscript: bool,
135
136    // Links
137    /// Inline links [text](url)
138    #[cfg_attr(feature = "serde", serde(alias = "inline_links"))]
139    pub inline_links: bool,
140    /// Reference links [text][ref]
141    #[cfg_attr(feature = "serde", serde(alias = "reference_links"))]
142    pub reference_links: bool,
143    /// Shortcut reference links [ref] without second []
144    #[cfg_attr(feature = "serde", serde(alias = "shortcut_reference_links"))]
145    pub shortcut_reference_links: bool,
146    /// Attributes on links [text](url){.class}
147    #[cfg_attr(feature = "serde", serde(alias = "link_attributes"))]
148    pub link_attributes: bool,
149    /// Automatic links <http://example.com>
150    pub autolinks: bool,
151
152    // Images
153    /// Inline images ![alt](url)
154    #[cfg_attr(feature = "serde", serde(alias = "inline_images"))]
155    pub inline_images: bool,
156    /// Paragraph with just image becomes figure
157    #[cfg_attr(feature = "serde", serde(alias = "implicit_figures"))]
158    pub implicit_figures: bool,
159
160    // Math
161    /// Dollar-delimited math $x$ and $$equation$$
162    #[cfg_attr(feature = "serde", serde(alias = "tex_math_dollars"))]
163    pub tex_math_dollars: bool,
164    /// [NON-DEFAULT] GFM math: inline $`...`$ and fenced ``` math blocks
165    #[cfg_attr(feature = "serde", serde(alias = "tex_math_gfm"))]
166    pub tex_math_gfm: bool,
167    /// [NON-DEFAULT] Single backslash math \(...\) and \[...\] (RMarkdown default)
168    #[cfg_attr(feature = "serde", serde(alias = "tex_math_single_backslash"))]
169    pub tex_math_single_backslash: bool,
170    /// [NON-DEFAULT] Double backslash math \\(...\\) and \\[...\\]
171    #[cfg_attr(feature = "serde", serde(alias = "tex_math_double_backslash"))]
172    pub tex_math_double_backslash: bool,
173
174    // Footnotes
175    /// Inline footnotes ^[text]
176    #[cfg_attr(feature = "serde", serde(alias = "inline_footnotes"))]
177    pub inline_footnotes: bool,
178    /// Reference footnotes `[^1]` (requires footnote parsing)
179    pub footnotes: bool,
180
181    // Citations
182    /// Citation syntax [@cite]
183    pub citations: bool,
184
185    // Spans
186    /// Bracketed spans [text]{.class}
187    #[cfg_attr(feature = "serde", serde(alias = "bracketed_spans"))]
188    pub bracketed_spans: bool,
189    /// HTML <span> elements
190    #[cfg_attr(feature = "serde", serde(alias = "native_spans"))]
191    pub native_spans: bool,
192
193    // ===== Metadata =====
194    /// YAML metadata block
195    #[cfg_attr(feature = "serde", serde(alias = "yaml_metadata_block"))]
196    pub yaml_metadata_block: bool,
197    /// Pandoc title block (Title/Author/Date)
198    #[cfg_attr(feature = "serde", serde(alias = "pandoc_title_block"))]
199    pub pandoc_title_block: bool,
200    /// [NON-DEFAULT] MultiMarkdown metadata/title block (Key: Value ...)
201    pub mmd_title_block: bool,
202
203    // ===== Raw content =====
204    /// Raw HTML blocks and inline
205    #[cfg_attr(feature = "serde", serde(alias = "raw_html"))]
206    pub raw_html: bool,
207    /// Markdown inside HTML blocks
208    #[cfg_attr(feature = "serde", serde(alias = "markdown_in_html_blocks"))]
209    pub markdown_in_html_blocks: bool,
210    /// LaTeX commands and environments
211    #[cfg_attr(feature = "serde", serde(alias = "raw_tex"))]
212    pub raw_tex: bool,
213    /// Generic raw blocks with {=format} syntax
214    #[cfg_attr(feature = "serde", serde(alias = "raw_attribute"))]
215    pub raw_attribute: bool,
216
217    // ===== Escapes and special characters =====
218    /// Backslash escapes any symbol
219    #[cfg_attr(feature = "serde", serde(alias = "all_symbols_escapable"))]
220    pub all_symbols_escapable: bool,
221    /// Backslash at line end = hard line break
222    #[cfg_attr(feature = "serde", serde(alias = "escaped_line_breaks"))]
223    pub escaped_line_breaks: bool,
224
225    // ===== NON-DEFAULT EXTENSIONS =====
226    // These are disabled by default in Pandoc
227    /// [NON-DEFAULT] Bare URLs become links
228    #[cfg_attr(feature = "serde", serde(alias = "autolink_bare_uris"))]
229    pub autolink_bare_uris: bool,
230    /// [NON-DEFAULT] Newline = <br>
231    #[cfg_attr(feature = "serde", serde(alias = "hard_line_breaks"))]
232    pub hard_line_breaks: bool,
233    /// [NON-DEFAULT] MultiMarkdown style heading identifiers [my-id]
234    pub mmd_header_identifiers: bool,
235    /// [NON-DEFAULT] MultiMarkdown key=value attributes on reference defs
236    pub mmd_link_attributes: bool,
237    /// [NON-DEFAULT] GitHub/CommonMark alerts in blockquotes (`> [!NOTE]`)
238    pub alerts: bool,
239    /// [NON-DEFAULT] :emoji: syntax
240    pub emoji: bool,
241    /// [NON-DEFAULT] Highlighted ==text==
242    pub mark: bool,
243
244    // ===== Quarto-specific extensions =====
245    /// Quarto callout blocks (.callout-note, etc.)
246    #[cfg_attr(feature = "serde", serde(alias = "quarto_callouts"))]
247    pub quarto_callouts: bool,
248    /// Quarto cross-references @fig-id, @tbl-id
249    #[cfg_attr(feature = "serde", serde(alias = "quarto_crossrefs"))]
250    pub quarto_crossrefs: bool,
251    /// Quarto shortcodes {{< name args >}}
252    #[cfg_attr(feature = "serde", serde(alias = "quarto_shortcodes"))]
253    pub quarto_shortcodes: bool,
254    /// Bookdown references \@ref(label) and (\#label)
255    pub bookdown_references: bool,
256    /// Bookdown equation references in LaTeX math blocks (\#eq:label)
257    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    /// Get the default extension set for a given flavor.
341    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            // Block-level - enabled by default in Pandoc
355            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            // Lists
363            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            // Code
371            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            // Tables
380            grid_tables: true,
381            multiline_tables: true,
382            pipe_tables: true,
383            simple_tables: true,
384            table_captions: true,
385
386            // Divs
387            fenced_divs: true,
388            native_divs: true,
389
390            // Other blocks
391            line_blocks: true,
392
393            // Inline
394            intraword_underscores: true,
395            strikeout: true,
396            subscript: true,
397            superscript: true,
398
399            // Links
400            autolinks: true,
401            inline_links: true,
402            link_attributes: true,
403            reference_links: true,
404            shortcut_reference_links: true,
405
406            // Images
407            implicit_figures: true,
408            inline_images: true,
409
410            // Math
411            tex_math_dollars: true,
412            tex_math_double_backslash: false,
413            tex_math_gfm: false,
414            tex_math_single_backslash: false,
415
416            // Footnotes
417            footnotes: true,
418            inline_footnotes: true,
419
420            // Citations
421            citations: true,
422
423            // Spans
424            bracketed_spans: true,
425            native_spans: true,
426
427            // Metadata
428            mmd_title_block: false,
429            pandoc_title_block: true,
430            yaml_metadata_block: true,
431
432            // Raw
433            markdown_in_html_blocks: false,
434            raw_attribute: true,
435            raw_html: true,
436            raw_tex: true,
437
438            // Escapes
439            all_symbols_escapable: true,
440            escaped_line_breaks: true,
441
442            // Non-default
443            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            // Quarto/Bookdown-specific
452            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    /// Merge user-specified extension overrides with flavor defaults.
544    ///
545    /// This is used to support partial extension overrides in config files.
546    /// For example, if a user specifies `flavor = "quarto"` and then sets
547    /// `[extensions] quarto-crossrefs = false`, we want all other extensions
548    /// to use Quarto defaults, not Pandoc defaults.
549    ///
550    /// # Arguments
551    /// * `user_overrides` - Map of extension names to their user-specified values
552    /// * `flavor` - The flavor to use for default values
553    ///
554    /// # Returns
555    /// A new Extensions struct with flavor defaults merged with user overrides
556    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    /// Alias for Panache's pinned newest supported Pandoc-compat behavior.
683    ///
684    /// This is intentionally NOT "floating upstream latest". It resolves to
685    /// a concrete version that Panache has verified, and is bumped manually.
686    #[cfg_attr(feature = "serde", serde(rename = "latest"))]
687    Latest,
688    /// Match Pandoc 3.7 behavior for ambiguous syntax edge cases.
689    #[cfg_attr(
690        feature = "serde",
691        serde(rename = "3.7", alias = "3-7", alias = "v3.7", alias = "v3-7")
692    )]
693    V3_7,
694    /// Match Pandoc 3.9 behavior for ambiguous syntax edge cases.
695    #[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    /// Pinned target for `latest`.
705    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    /// Compatibility target for ambiguous Pandoc behavior.
722    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}