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    #[cfg_attr(feature = "serde", serde(alias = "commonmark"))]
21    CommonMark,
22    /// MultiMarkdown
23    #[cfg_attr(feature = "serde", serde(rename = "multimarkdown"))]
24    MultiMarkdown,
25}
26
27/// Pandoc/Markdown extensions configuration.
28/// Each field represents a specific Pandoc extension.
29/// Extensions marked with a comment indicate implementation status.
30#[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    // ===== Block-level extensions =====
36
37    // Headings
38    /// Require blank line before headers (default: enabled)
39    #[cfg_attr(feature = "serde", serde(alias = "blank_before_header"))]
40    pub blank_before_header: bool,
41    /// Full attribute syntax on headers {#id .class key=value}
42    #[cfg_attr(feature = "serde", serde(alias = "header_attributes"))]
43    pub header_attributes: bool,
44    /// Auto-generate identifiers from headings
45    pub auto_identifiers: bool,
46    /// Use GitHub's algorithm for auto-generated heading identifiers
47    pub gfm_auto_identifiers: bool,
48    /// Implicit header references ([Heading] links to header)
49    pub implicit_header_references: bool,
50
51    // Block quotes
52    /// Require blank line before blockquotes (default: enabled)
53    #[cfg_attr(feature = "serde", serde(alias = "blank_before_blockquote"))]
54    pub blank_before_blockquote: bool,
55
56    // Lists
57    /// Fancy list markers (roman numerals, letters, etc.)
58    #[cfg_attr(feature = "serde", serde(alias = "fancy_lists"))]
59    pub fancy_lists: bool,
60    /// Start ordered lists at arbitrary numbers
61    pub startnum: bool,
62    /// Example lists with (@) markers
63    #[cfg_attr(feature = "serde", serde(alias = "example_lists"))]
64    pub example_lists: bool,
65    /// GitHub-style task lists - [ ] and - [x]
66    #[cfg_attr(feature = "serde", serde(alias = "task_lists"))]
67    pub task_lists: bool,
68    /// Term/definition syntax
69    #[cfg_attr(feature = "serde", serde(alias = "definition_lists"))]
70    pub definition_lists: bool,
71    /// Allow lists without a preceding blank line
72    #[cfg_attr(feature = "serde", serde(alias = "lists_without_preceding_blankline"))]
73    pub lists_without_preceding_blankline: bool,
74
75    // Code blocks
76    /// Fenced code blocks with backticks
77    #[cfg_attr(feature = "serde", serde(alias = "backtick_code_blocks"))]
78    pub backtick_code_blocks: bool,
79    /// Fenced code blocks with tildes
80    #[cfg_attr(feature = "serde", serde(alias = "fenced_code_blocks"))]
81    pub fenced_code_blocks: bool,
82    /// Attributes on fenced code blocks {.language #id}
83    #[cfg_attr(feature = "serde", serde(alias = "fenced_code_attributes"))]
84    pub fenced_code_attributes: bool,
85    /// Executable code syntax (currently fenced chunks like ```{r} / ```{python})
86    pub executable_code: bool,
87    /// R Markdown inline executable code (`...`r ...)
88    pub rmarkdown_inline_code: bool,
89    /// Quarto inline executable code (`...`{r} ...)
90    pub quarto_inline_code: bool,
91    /// Attributes on inline code
92    #[cfg_attr(feature = "serde", serde(alias = "inline_code_attributes"))]
93    pub inline_code_attributes: bool,
94
95    // Tables
96    /// Simple table syntax
97    #[cfg_attr(feature = "serde", serde(alias = "simple_tables"))]
98    pub simple_tables: bool,
99    /// Multiline cell content in tables
100    #[cfg_attr(feature = "serde", serde(alias = "multiline_tables"))]
101    pub multiline_tables: bool,
102    /// Grid-style tables
103    #[cfg_attr(feature = "serde", serde(alias = "grid_tables"))]
104    pub grid_tables: bool,
105    /// Pipe tables (GitHub/PHP Markdown style)
106    #[cfg_attr(feature = "serde", serde(alias = "pipe_tables"))]
107    pub pipe_tables: bool,
108    /// Table captions
109    #[cfg_attr(feature = "serde", serde(alias = "table_captions"))]
110    pub table_captions: bool,
111
112    // Divs
113    /// Fenced divs ::: {.class}
114    #[cfg_attr(feature = "serde", serde(alias = "fenced_divs"))]
115    pub fenced_divs: bool,
116    /// HTML <div> elements
117    #[cfg_attr(feature = "serde", serde(alias = "native_divs"))]
118    pub native_divs: bool,
119
120    // Other block elements
121    /// Line blocks for poetry | prefix
122    #[cfg_attr(feature = "serde", serde(alias = "line_blocks"))]
123    pub line_blocks: bool,
124
125    // ===== Inline elements =====
126
127    // Emphasis
128    /// Underscores don't trigger emphasis in snake_case
129    #[cfg_attr(feature = "serde", serde(alias = "intraword_underscores"))]
130    pub intraword_underscores: bool,
131    /// Strikethrough ~~text~~
132    pub strikeout: bool,
133    /// Superscript and subscript ^super^ ~sub~
134    pub superscript: bool,
135    pub subscript: bool,
136
137    // Links
138    /// Inline links [text](url)
139    #[cfg_attr(feature = "serde", serde(alias = "inline_links"))]
140    pub inline_links: bool,
141    /// Reference links [text][ref]
142    #[cfg_attr(feature = "serde", serde(alias = "reference_links"))]
143    pub reference_links: bool,
144    /// Shortcut reference links [ref] without second []
145    #[cfg_attr(feature = "serde", serde(alias = "shortcut_reference_links"))]
146    pub shortcut_reference_links: bool,
147    /// Attributes on links [text](url){.class}
148    #[cfg_attr(feature = "serde", serde(alias = "link_attributes"))]
149    pub link_attributes: bool,
150    /// Automatic links <http://example.com>
151    pub autolinks: bool,
152
153    // Images
154    /// Inline images ![alt](url)
155    #[cfg_attr(feature = "serde", serde(alias = "inline_images"))]
156    pub inline_images: bool,
157    /// Paragraph with just image becomes figure
158    #[cfg_attr(feature = "serde", serde(alias = "implicit_figures"))]
159    pub implicit_figures: bool,
160
161    // Math
162    /// Dollar-delimited math $x$ and $$equation$$
163    #[cfg_attr(feature = "serde", serde(alias = "tex_math_dollars"))]
164    pub tex_math_dollars: bool,
165    /// [NON-DEFAULT] GFM math: inline $`...`$ and fenced ``` math blocks
166    #[cfg_attr(feature = "serde", serde(alias = "tex_math_gfm"))]
167    pub tex_math_gfm: bool,
168    /// [NON-DEFAULT] Single backslash math \(...\) and \[...\] (RMarkdown default)
169    #[cfg_attr(feature = "serde", serde(alias = "tex_math_single_backslash"))]
170    pub tex_math_single_backslash: bool,
171    /// [NON-DEFAULT] Double backslash math \\(...\\) and \\[...\\]
172    #[cfg_attr(feature = "serde", serde(alias = "tex_math_double_backslash"))]
173    pub tex_math_double_backslash: bool,
174
175    // Footnotes
176    /// Inline footnotes ^[text]
177    #[cfg_attr(feature = "serde", serde(alias = "inline_footnotes"))]
178    pub inline_footnotes: bool,
179    /// Reference footnotes `[^1]` (requires footnote parsing)
180    pub footnotes: bool,
181
182    // Citations
183    /// Citation syntax [@cite]
184    pub citations: bool,
185
186    // Spans
187    /// Bracketed spans [text]{.class}
188    #[cfg_attr(feature = "serde", serde(alias = "bracketed_spans"))]
189    pub bracketed_spans: bool,
190    /// HTML <span> elements
191    #[cfg_attr(feature = "serde", serde(alias = "native_spans"))]
192    pub native_spans: bool,
193
194    // ===== Metadata =====
195    /// YAML metadata block
196    #[cfg_attr(feature = "serde", serde(alias = "yaml_metadata_block"))]
197    pub yaml_metadata_block: bool,
198    /// Pandoc title block (Title/Author/Date)
199    #[cfg_attr(feature = "serde", serde(alias = "pandoc_title_block"))]
200    pub pandoc_title_block: bool,
201    /// [NON-DEFAULT] MultiMarkdown metadata/title block (Key: Value ...)
202    pub mmd_title_block: bool,
203
204    // ===== Raw content =====
205    /// Raw HTML blocks and inline
206    #[cfg_attr(feature = "serde", serde(alias = "raw_html"))]
207    pub raw_html: bool,
208    /// Markdown inside HTML blocks
209    #[cfg_attr(feature = "serde", serde(alias = "markdown_in_html_blocks"))]
210    pub markdown_in_html_blocks: bool,
211    /// LaTeX commands and environments
212    #[cfg_attr(feature = "serde", serde(alias = "raw_tex"))]
213    pub raw_tex: bool,
214    /// Generic raw blocks with {=format} syntax
215    #[cfg_attr(feature = "serde", serde(alias = "raw_attribute"))]
216    pub raw_attribute: bool,
217
218    // ===== Escapes and special characters =====
219    /// Backslash escapes any symbol
220    #[cfg_attr(feature = "serde", serde(alias = "all_symbols_escapable"))]
221    pub all_symbols_escapable: bool,
222    /// Backslash at line end = hard line break
223    #[cfg_attr(feature = "serde", serde(alias = "escaped_line_breaks"))]
224    pub escaped_line_breaks: bool,
225
226    // ===== NON-DEFAULT EXTENSIONS =====
227    // These are disabled by default in Pandoc
228    /// [NON-DEFAULT] Bare URLs become links
229    #[cfg_attr(feature = "serde", serde(alias = "autolink_bare_uris"))]
230    pub autolink_bare_uris: bool,
231    /// [NON-DEFAULT] Newline = <br>
232    #[cfg_attr(feature = "serde", serde(alias = "hard_line_breaks"))]
233    pub hard_line_breaks: bool,
234    /// [NON-DEFAULT] MultiMarkdown style heading identifiers [my-id]
235    pub mmd_header_identifiers: bool,
236    /// [NON-DEFAULT] MultiMarkdown key=value attributes on reference defs
237    pub mmd_link_attributes: bool,
238    /// [NON-DEFAULT] GitHub/CommonMark alerts in blockquotes (`> [!NOTE]`)
239    pub alerts: bool,
240    /// [NON-DEFAULT] :emoji: syntax
241    pub emoji: bool,
242    /// [NON-DEFAULT] Highlighted ==text==
243    pub mark: bool,
244
245    // ===== Quarto-specific extensions =====
246    /// Quarto callout blocks (.callout-note, etc.)
247    #[cfg_attr(feature = "serde", serde(alias = "quarto_callouts"))]
248    pub quarto_callouts: bool,
249    /// Quarto cross-references @fig-id, @tbl-id
250    #[cfg_attr(feature = "serde", serde(alias = "quarto_crossrefs"))]
251    pub quarto_crossrefs: bool,
252    /// Quarto shortcodes {{< name args >}}
253    #[cfg_attr(feature = "serde", serde(alias = "quarto_shortcodes"))]
254    pub quarto_shortcodes: bool,
255    /// Bookdown references \@ref(label) and (\#label)
256    pub bookdown_references: bool,
257    /// Bookdown equation references in LaTeX math blocks (\#eq:label)
258    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    /// Get the default extension set for a given flavor.
342    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            // Block-level - enabled by default in Pandoc
356            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            // Lists
364            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            // Code
372            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            // Tables
381            grid_tables: true,
382            multiline_tables: true,
383            pipe_tables: true,
384            simple_tables: true,
385            table_captions: true,
386
387            // Divs
388            fenced_divs: true,
389            native_divs: true,
390
391            // Other blocks
392            line_blocks: true,
393
394            // Inline
395            intraword_underscores: true,
396            strikeout: true,
397            subscript: true,
398            superscript: true,
399
400            // Links
401            autolinks: true,
402            inline_links: true,
403            link_attributes: true,
404            reference_links: true,
405            shortcut_reference_links: true,
406
407            // Images
408            implicit_figures: true,
409            inline_images: true,
410
411            // Math
412            tex_math_dollars: true,
413            tex_math_double_backslash: false,
414            tex_math_gfm: false,
415            tex_math_single_backslash: false,
416
417            // Footnotes
418            footnotes: true,
419            inline_footnotes: true,
420
421            // Citations
422            citations: true,
423
424            // Spans
425            bracketed_spans: true,
426            native_spans: true,
427
428            // Metadata
429            mmd_title_block: false,
430            pandoc_title_block: true,
431            yaml_metadata_block: true,
432
433            // Raw
434            markdown_in_html_blocks: false,
435            raw_attribute: true,
436            raw_html: true,
437            raw_tex: true,
438
439            // Escapes
440            all_symbols_escapable: true,
441            escaped_line_breaks: true,
442
443            // Non-default
444            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            // Quarto/Bookdown-specific
453            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        // CommonMark's core grammar is what pandoc's commonmark reader treats
514        // as "not extensions" — they're built into the reader. Panache's
515        // parser still gates each construct on its extension flag, so we have
516        // to enable the CommonMark-mandatory ones explicitly here.
517        //
518        // Notably absent: `all_symbols_escapable`. CommonMark only allows
519        // backslash escapes of ASCII punctuation, and panache's
520        // `all_symbols_escapable` flag widens that to any character — so it
521        // must stay off for CommonMark.
522        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    /// Merge user-specified extension overrides with flavor defaults.
563    ///
564    /// This is used to support partial extension overrides in config files.
565    /// For example, if a user specifies `flavor = "quarto"` and then sets
566    /// `[extensions] quarto-crossrefs = false`, we want all other extensions
567    /// to use Quarto defaults, not Pandoc defaults.
568    ///
569    /// # Arguments
570    /// * `user_overrides` - Map of extension names to their user-specified values
571    /// * `flavor` - The flavor to use for default values
572    ///
573    /// # Returns
574    /// A new Extensions struct with flavor defaults merged with user overrides
575    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    /// Alias for Panache's pinned newest supported Pandoc-compat behavior.
702    ///
703    /// This is intentionally NOT "floating upstream latest". It resolves to
704    /// a concrete version that Panache has verified, and is bumped manually.
705    #[cfg_attr(feature = "serde", serde(rename = "latest"))]
706    Latest,
707    /// Match Pandoc 3.7 behavior for ambiguous syntax edge cases.
708    #[cfg_attr(
709        feature = "serde",
710        serde(rename = "3.7", alias = "3-7", alias = "v3.7", alias = "v3-7")
711    )]
712    V3_7,
713    /// Match Pandoc 3.9 behavior for ambiguous syntax edge cases.
714    #[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    /// Pinned target for `latest`.
724    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/// Parser dialect — the underlying inline tokenization rule set.
735///
736/// Distinct from [`Flavor`]: `Flavor` is the user-facing identity (Pandoc,
737/// Quarto, GFM, etc.) and selects extension defaults; `Dialect` is the
738/// structural parser identity. Several flavors share a dialect — Quarto and
739/// RMarkdown both use `Pandoc`; CommonMark and GFM both use `CommonMark`.
740///
741/// Use this for parser branches whose behavior is fundamentally different
742/// between dialect families (e.g. unmatched backtick run handling). Per-flavor
743/// feature toggles still belong on [`Extensions`].
744#[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    /// Pandoc-markdown family. Default for Pandoc, Quarto, RMarkdown,
749    /// MultiMarkdown.
750    #[default]
751    Pandoc,
752    /// CommonMark family. Default for CommonMark and GFM.
753    CommonMark,
754}
755
756impl Dialect {
757    /// Default dialect for a given user-facing flavor.
758    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    /// Compatibility target for ambiguous Pandoc behavior.
776    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}