Skip to main content

panache_parser/
options.rs

1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3
4/// The flavor of Markdown to parse and format.
5/// Each flavor has a different set of default extensions enabled.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
9pub enum Flavor {
10    /// Standard Pandoc Markdown (default extensions enabled)
11    #[default]
12    Pandoc,
13    /// Quarto (Pandoc + Quarto-specific extensions)
14    Quarto,
15    /// R Markdown (Pandoc + R-specific extensions)
16    #[cfg_attr(feature = "serde", serde(rename = "rmarkdown"))]
17    RMarkdown,
18    /// GitHub Flavored Markdown
19    Gfm,
20    /// CommonMark
21    #[cfg_attr(feature = "serde", serde(alias = "commonmark"))]
22    CommonMark,
23    /// MultiMarkdown
24    #[cfg_attr(feature = "serde", serde(rename = "multimarkdown"))]
25    MultiMarkdown,
26}
27
28/// Pandoc/Markdown extensions configuration.
29/// Each field represents a specific Pandoc extension.
30/// Extensions marked with a comment indicate implementation status.
31#[derive(Debug, Clone, PartialEq)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
33#[cfg_attr(feature = "serde", serde(default))]
34#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
35pub struct Extensions {
36    // ===== Block-level extensions =====
37
38    // Headings
39    /// Require blank line before headers (default: enabled)
40    #[cfg_attr(feature = "serde", serde(alias = "blank_before_header"))]
41    pub blank_before_header: bool,
42    /// Full attribute syntax on headers {#id .class key=value}
43    #[cfg_attr(feature = "serde", serde(alias = "header_attributes"))]
44    pub header_attributes: bool,
45    /// Auto-generate identifiers from headings
46    pub auto_identifiers: bool,
47    /// Use GitHub's algorithm for auto-generated heading identifiers
48    pub gfm_auto_identifiers: bool,
49    /// Implicit header references ([Heading] links to header)
50    pub implicit_header_references: bool,
51
52    // Block quotes
53    /// Require blank line before blockquotes (default: enabled)
54    #[cfg_attr(feature = "serde", serde(alias = "blank_before_blockquote"))]
55    pub blank_before_blockquote: bool,
56
57    // Lists
58    /// Fancy list markers (roman numerals, letters, etc.)
59    #[cfg_attr(feature = "serde", serde(alias = "fancy_lists"))]
60    pub fancy_lists: bool,
61    /// Start ordered lists at arbitrary numbers
62    pub startnum: bool,
63    /// Example lists with (@) markers
64    #[cfg_attr(feature = "serde", serde(alias = "example_lists"))]
65    pub example_lists: bool,
66    /// GitHub-style task lists - [ ] and - [x]
67    #[cfg_attr(feature = "serde", serde(alias = "task_lists"))]
68    pub task_lists: bool,
69    /// Term/definition syntax
70    #[cfg_attr(feature = "serde", serde(alias = "definition_lists"))]
71    pub definition_lists: bool,
72    /// Allow lists without a preceding blank line
73    #[cfg_attr(feature = "serde", serde(alias = "lists_without_preceding_blankline"))]
74    pub lists_without_preceding_blankline: bool,
75
76    // Code blocks
77    /// Fenced code blocks with backticks
78    #[cfg_attr(feature = "serde", serde(alias = "backtick_code_blocks"))]
79    pub backtick_code_blocks: bool,
80    /// Fenced code blocks with tildes
81    #[cfg_attr(feature = "serde", serde(alias = "fenced_code_blocks"))]
82    pub fenced_code_blocks: bool,
83    /// Attributes on fenced code blocks {.language #id}
84    #[cfg_attr(feature = "serde", serde(alias = "fenced_code_attributes"))]
85    pub fenced_code_attributes: bool,
86    /// Executable code syntax (currently fenced chunks like ```{r} / ```{python})
87    pub executable_code: bool,
88    /// R Markdown inline executable code (`...`r ...)
89    pub rmarkdown_inline_code: bool,
90    /// Quarto inline executable code (`...`{r} ...)
91    pub quarto_inline_code: bool,
92    /// Attributes on inline code
93    #[cfg_attr(feature = "serde", serde(alias = "inline_code_attributes"))]
94    pub inline_code_attributes: bool,
95
96    // Tables
97    /// Simple table syntax
98    #[cfg_attr(feature = "serde", serde(alias = "simple_tables"))]
99    pub simple_tables: bool,
100    /// Multiline cell content in tables
101    #[cfg_attr(feature = "serde", serde(alias = "multiline_tables"))]
102    pub multiline_tables: bool,
103    /// Grid-style tables
104    #[cfg_attr(feature = "serde", serde(alias = "grid_tables"))]
105    pub grid_tables: bool,
106    /// Pipe tables (GitHub/PHP Markdown style)
107    #[cfg_attr(feature = "serde", serde(alias = "pipe_tables"))]
108    pub pipe_tables: bool,
109    /// Table captions
110    #[cfg_attr(feature = "serde", serde(alias = "table_captions"))]
111    pub table_captions: bool,
112
113    // Divs
114    /// Fenced divs ::: {.class}
115    #[cfg_attr(feature = "serde", serde(alias = "fenced_divs"))]
116    pub fenced_divs: bool,
117    /// HTML <div> elements
118    #[cfg_attr(feature = "serde", serde(alias = "native_divs"))]
119    pub native_divs: bool,
120
121    // Other block elements
122    /// Line blocks for poetry | prefix
123    #[cfg_attr(feature = "serde", serde(alias = "line_blocks"))]
124    pub line_blocks: bool,
125
126    // ===== Inline elements =====
127
128    // Emphasis
129    /// Underscores don't trigger emphasis in snake_case
130    #[cfg_attr(feature = "serde", serde(alias = "intraword_underscores"))]
131    pub intraword_underscores: bool,
132    /// Strikethrough ~~text~~
133    pub strikeout: bool,
134    /// Superscript and subscript ^super^ ~sub~
135    pub superscript: bool,
136    pub subscript: bool,
137
138    // Links
139    /// Inline links [text](url)
140    #[cfg_attr(feature = "serde", serde(alias = "inline_links"))]
141    pub inline_links: bool,
142    /// Reference links [text][ref]
143    #[cfg_attr(feature = "serde", serde(alias = "reference_links"))]
144    pub reference_links: bool,
145    /// Shortcut reference links [ref] without second []
146    #[cfg_attr(feature = "serde", serde(alias = "shortcut_reference_links"))]
147    pub shortcut_reference_links: bool,
148    /// Attributes on links [text](url){.class}
149    #[cfg_attr(feature = "serde", serde(alias = "link_attributes"))]
150    pub link_attributes: bool,
151    /// Automatic links <http://example.com>
152    pub autolinks: bool,
153
154    // Images
155    /// Inline images ![alt](url)
156    #[cfg_attr(feature = "serde", serde(alias = "inline_images"))]
157    pub inline_images: bool,
158    /// Paragraph with just image becomes figure
159    #[cfg_attr(feature = "serde", serde(alias = "implicit_figures"))]
160    pub implicit_figures: bool,
161
162    // Math
163    /// Dollar-delimited math $x$ and $$equation$$
164    #[cfg_attr(feature = "serde", serde(alias = "tex_math_dollars"))]
165    pub tex_math_dollars: bool,
166    /// [NON-DEFAULT] GFM math: inline $`...`$ and fenced ``` math blocks
167    #[cfg_attr(feature = "serde", serde(alias = "tex_math_gfm"))]
168    pub tex_math_gfm: bool,
169    /// [NON-DEFAULT] Single backslash math \(...\) and \[...\] (RMarkdown default)
170    #[cfg_attr(feature = "serde", serde(alias = "tex_math_single_backslash"))]
171    pub tex_math_single_backslash: bool,
172    /// [NON-DEFAULT] Double backslash math \\(...\\) and \\[...\\]
173    #[cfg_attr(feature = "serde", serde(alias = "tex_math_double_backslash"))]
174    pub tex_math_double_backslash: bool,
175
176    // Footnotes
177    /// Inline footnotes ^[text]
178    #[cfg_attr(feature = "serde", serde(alias = "inline_footnotes"))]
179    pub inline_footnotes: bool,
180    /// Reference footnotes `[^1]` (requires footnote parsing)
181    pub footnotes: bool,
182
183    // Citations
184    /// Citation syntax [@cite]
185    pub citations: bool,
186
187    // Spans
188    /// Bracketed spans [text]{.class}
189    #[cfg_attr(feature = "serde", serde(alias = "bracketed_spans"))]
190    pub bracketed_spans: bool,
191    /// HTML <span> elements
192    #[cfg_attr(feature = "serde", serde(alias = "native_spans"))]
193    pub native_spans: bool,
194
195    // ===== Metadata =====
196    /// YAML metadata block
197    #[cfg_attr(feature = "serde", serde(alias = "yaml_metadata_block"))]
198    pub yaml_metadata_block: bool,
199    /// Pandoc title block (Title/Author/Date)
200    #[cfg_attr(feature = "serde", serde(alias = "pandoc_title_block"))]
201    pub pandoc_title_block: bool,
202    /// [NON-DEFAULT] MultiMarkdown metadata/title block (Key: Value ...)
203    pub mmd_title_block: bool,
204
205    // ===== Raw content =====
206    /// Raw HTML blocks and inline
207    #[cfg_attr(feature = "serde", serde(alias = "raw_html"))]
208    pub raw_html: bool,
209    /// Markdown inside HTML blocks
210    #[cfg_attr(feature = "serde", serde(alias = "markdown_in_html_blocks"))]
211    pub markdown_in_html_blocks: bool,
212    /// LaTeX commands and environments
213    #[cfg_attr(feature = "serde", serde(alias = "raw_tex"))]
214    pub raw_tex: bool,
215    /// Generic raw blocks with {=format} syntax
216    #[cfg_attr(feature = "serde", serde(alias = "raw_attribute"))]
217    pub raw_attribute: bool,
218
219    // ===== Escapes and special characters =====
220    /// Backslash escapes any symbol
221    #[cfg_attr(feature = "serde", serde(alias = "all_symbols_escapable"))]
222    pub all_symbols_escapable: bool,
223    /// Backslash at line end = hard line break
224    #[cfg_attr(feature = "serde", serde(alias = "escaped_line_breaks"))]
225    pub escaped_line_breaks: bool,
226
227    // ===== NON-DEFAULT EXTENSIONS =====
228    // These are disabled by default in Pandoc
229    /// [NON-DEFAULT] Bare URLs become links
230    #[cfg_attr(feature = "serde", serde(alias = "autolink_bare_uris"))]
231    pub autolink_bare_uris: bool,
232    /// [NON-DEFAULT] Newline = <br>
233    #[cfg_attr(feature = "serde", serde(alias = "hard_line_breaks"))]
234    pub hard_line_breaks: bool,
235    /// [NON-DEFAULT] MultiMarkdown style heading identifiers [my-id]
236    pub mmd_header_identifiers: bool,
237    /// [NON-DEFAULT] MultiMarkdown key=value attributes on reference defs
238    pub mmd_link_attributes: bool,
239    /// [NON-DEFAULT] GitHub/CommonMark alerts in blockquotes (`> [!NOTE]`)
240    pub alerts: bool,
241    /// [NON-DEFAULT] :emoji: syntax
242    pub emoji: bool,
243    /// [NON-DEFAULT] Highlighted ==text==
244    pub mark: bool,
245
246    // ===== Quarto-specific extensions =====
247    /// Quarto callout blocks (.callout-note, etc.)
248    #[cfg_attr(feature = "serde", serde(alias = "quarto_callouts"))]
249    pub quarto_callouts: bool,
250    /// Quarto cross-references @fig-id, @tbl-id
251    #[cfg_attr(feature = "serde", serde(alias = "quarto_crossrefs"))]
252    pub quarto_crossrefs: bool,
253    /// Quarto shortcodes {{< name args >}}
254    #[cfg_attr(feature = "serde", serde(alias = "quarto_shortcodes"))]
255    pub quarto_shortcodes: bool,
256    /// Bookdown references \@ref(label) and (\#label)
257    pub bookdown_references: bool,
258    /// Bookdown equation references in LaTeX math blocks (\#eq:label)
259    pub bookdown_equation_references: bool,
260}
261
262impl Default for Extensions {
263    fn default() -> Self {
264        Self::for_flavor(Flavor::default())
265    }
266}
267
268impl Extensions {
269    fn none_defaults() -> Self {
270        Self {
271            alerts: false,
272            all_symbols_escapable: false,
273            auto_identifiers: false,
274            autolink_bare_uris: false,
275            autolinks: false,
276            backtick_code_blocks: false,
277            blank_before_blockquote: false,
278            blank_before_header: false,
279            bookdown_references: false,
280            bookdown_equation_references: false,
281            bracketed_spans: false,
282            citations: false,
283            definition_lists: false,
284            lists_without_preceding_blankline: false,
285            emoji: false,
286            escaped_line_breaks: false,
287            example_lists: false,
288            executable_code: false,
289            rmarkdown_inline_code: false,
290            quarto_inline_code: false,
291            fancy_lists: false,
292            fenced_code_attributes: false,
293            fenced_code_blocks: false,
294            fenced_divs: false,
295            footnotes: false,
296            gfm_auto_identifiers: false,
297            grid_tables: false,
298            hard_line_breaks: false,
299            header_attributes: false,
300            implicit_figures: false,
301            implicit_header_references: false,
302            inline_code_attributes: false,
303            inline_footnotes: false,
304            inline_images: false,
305            inline_links: false,
306            intraword_underscores: false,
307            line_blocks: false,
308            link_attributes: false,
309            mark: false,
310            markdown_in_html_blocks: false,
311            mmd_header_identifiers: false,
312            mmd_link_attributes: false,
313            mmd_title_block: false,
314            multiline_tables: false,
315            native_divs: false,
316            native_spans: false,
317            pandoc_title_block: false,
318            pipe_tables: false,
319            quarto_callouts: false,
320            quarto_crossrefs: false,
321            quarto_shortcodes: false,
322            raw_attribute: false,
323            raw_html: false,
324            raw_tex: false,
325            reference_links: false,
326            shortcut_reference_links: false,
327            simple_tables: false,
328            startnum: false,
329            strikeout: false,
330            subscript: false,
331            superscript: false,
332            table_captions: false,
333            task_lists: false,
334            tex_math_dollars: false,
335            tex_math_double_backslash: false,
336            tex_math_gfm: false,
337            tex_math_single_backslash: false,
338            yaml_metadata_block: false,
339        }
340    }
341
342    /// Get the default extension set for a given flavor.
343    pub fn for_flavor(flavor: Flavor) -> Self {
344        match flavor {
345            Flavor::Pandoc => Self::pandoc_defaults(),
346            Flavor::Quarto => Self::quarto_defaults(),
347            Flavor::RMarkdown => Self::rmarkdown_defaults(),
348            Flavor::Gfm => Self::gfm_defaults(),
349            Flavor::CommonMark => Self::commonmark_defaults(),
350            Flavor::MultiMarkdown => Self::multimarkdown_defaults(),
351        }
352    }
353
354    fn pandoc_defaults() -> Self {
355        Self {
356            // Block-level - enabled by default in Pandoc
357            auto_identifiers: true,
358            blank_before_blockquote: true,
359            blank_before_header: true,
360            gfm_auto_identifiers: false,
361            header_attributes: true,
362            implicit_header_references: true,
363
364            // Lists
365            definition_lists: true,
366            example_lists: true,
367            fancy_lists: true,
368            lists_without_preceding_blankline: false,
369            startnum: true,
370            task_lists: true,
371
372            // Code
373            backtick_code_blocks: true,
374            executable_code: false,
375            rmarkdown_inline_code: false,
376            quarto_inline_code: false,
377            fenced_code_attributes: true,
378            fenced_code_blocks: true,
379            inline_code_attributes: true,
380
381            // Tables
382            grid_tables: true,
383            multiline_tables: true,
384            pipe_tables: true,
385            simple_tables: true,
386            table_captions: true,
387
388            // Divs
389            fenced_divs: true,
390            native_divs: true,
391
392            // Other blocks
393            line_blocks: true,
394
395            // Inline
396            intraword_underscores: true,
397            strikeout: true,
398            subscript: true,
399            superscript: true,
400
401            // Links
402            autolinks: true,
403            inline_links: true,
404            link_attributes: true,
405            reference_links: true,
406            shortcut_reference_links: true,
407
408            // Images
409            implicit_figures: true,
410            inline_images: true,
411
412            // Math
413            tex_math_dollars: true,
414            tex_math_double_backslash: false,
415            tex_math_gfm: false,
416            tex_math_single_backslash: false,
417
418            // Footnotes
419            footnotes: true,
420            inline_footnotes: true,
421
422            // Citations
423            citations: true,
424
425            // Spans
426            bracketed_spans: true,
427            native_spans: true,
428
429            // Metadata
430            mmd_title_block: false,
431            pandoc_title_block: true,
432            yaml_metadata_block: true,
433
434            // Raw
435            markdown_in_html_blocks: false,
436            raw_attribute: true,
437            raw_html: true,
438            raw_tex: true,
439
440            // Escapes
441            all_symbols_escapable: true,
442            escaped_line_breaks: true,
443
444            // Non-default
445            alerts: false,
446            autolink_bare_uris: false,
447            emoji: false,
448            hard_line_breaks: false,
449            mark: false,
450            mmd_header_identifiers: false,
451            mmd_link_attributes: false,
452
453            // Quarto/Bookdown-specific
454            bookdown_references: false,
455            bookdown_equation_references: false,
456            quarto_callouts: false,
457            quarto_crossrefs: false,
458            quarto_shortcodes: false,
459        }
460    }
461
462    fn quarto_defaults() -> Self {
463        let mut ext = Self::pandoc_defaults();
464
465        ext.executable_code = true;
466        ext.rmarkdown_inline_code = true;
467        ext.quarto_inline_code = true;
468        ext.quarto_callouts = true;
469        ext.quarto_crossrefs = true;
470        ext.quarto_shortcodes = true;
471
472        ext
473    }
474
475    fn rmarkdown_defaults() -> Self {
476        let mut ext = Self::pandoc_defaults();
477
478        ext.bookdown_references = true;
479        ext.bookdown_equation_references = true;
480        ext.executable_code = true;
481        ext.rmarkdown_inline_code = true;
482        ext.quarto_inline_code = false;
483        ext.tex_math_dollars = true;
484        ext.tex_math_single_backslash = true;
485
486        ext
487    }
488
489    fn gfm_defaults() -> Self {
490        let mut ext = Self::none_defaults();
491
492        ext.alerts = true;
493        ext.auto_identifiers = true;
494        ext.autolink_bare_uris = true;
495        ext.autolinks = true;
496        ext.backtick_code_blocks = true;
497        ext.emoji = true;
498        ext.fenced_code_blocks = true;
499        ext.footnotes = true;
500        ext.gfm_auto_identifiers = true;
501        ext.inline_links = true;
502        ext.pipe_tables = true;
503        ext.raw_html = true;
504        ext.strikeout = true;
505        ext.task_lists = true;
506        ext.tex_math_dollars = true;
507        ext.tex_math_gfm = true;
508        ext.yaml_metadata_block = true;
509
510        ext
511    }
512
513    fn commonmark_defaults() -> Self {
514        let mut ext = Self::none_defaults();
515        // CommonMark's core grammar is what pandoc's commonmark reader treats
516        // as "not extensions" — they're built into the reader. Panache's
517        // parser still gates each construct on its extension flag, so we have
518        // to enable the CommonMark-mandatory ones explicitly here.
519        //
520        // Notably absent: `all_symbols_escapable`. CommonMark only allows
521        // backslash escapes of ASCII punctuation, and panache's
522        // `all_symbols_escapable` flag widens that to any character — so it
523        // must stay off for CommonMark.
524        ext.autolinks = true;
525        ext.backtick_code_blocks = true;
526        ext.escaped_line_breaks = true;
527        ext.fenced_code_blocks = true;
528        ext.inline_images = true;
529        ext.inline_links = true;
530        ext.intraword_underscores = true;
531        ext.raw_html = true;
532        ext.reference_links = true;
533        ext.shortcut_reference_links = true;
534        ext
535    }
536
537    fn multimarkdown_defaults() -> Self {
538        let mut ext = Self::none_defaults();
539
540        ext.all_symbols_escapable = true;
541        ext.auto_identifiers = true;
542        ext.backtick_code_blocks = true;
543        ext.definition_lists = true;
544        ext.footnotes = true;
545        ext.implicit_figures = true;
546        ext.implicit_header_references = true;
547        ext.intraword_underscores = true;
548        ext.mmd_header_identifiers = true;
549        ext.mmd_link_attributes = true;
550        ext.mmd_title_block = true;
551        ext.pipe_tables = true;
552        ext.raw_attribute = true;
553        ext.raw_html = true;
554        ext.reference_links = true;
555        ext.shortcut_reference_links = true;
556        ext.subscript = true;
557        ext.superscript = true;
558        ext.tex_math_dollars = true;
559        ext.tex_math_double_backslash = true;
560
561        ext
562    }
563
564    /// Merge user-specified extension overrides with flavor defaults.
565    ///
566    /// This is used to support partial extension overrides in config files.
567    /// For example, if a user specifies `flavor = "quarto"` and then sets
568    /// `[extensions] quarto-crossrefs = false`, we want all other extensions
569    /// to use Quarto defaults, not Pandoc defaults.
570    ///
571    /// # Arguments
572    /// * `user_overrides` - Map of extension names to their user-specified values
573    /// * `flavor` - The flavor to use for default values
574    ///
575    /// # Returns
576    /// A new Extensions struct with flavor defaults merged with user overrides
577    pub fn merge_with_flavor(user_overrides: HashMap<String, bool>, flavor: Flavor) -> Self {
578        let defaults = Self::for_flavor(flavor);
579        Self::merge_overrides(defaults, user_overrides)
580    }
581
582    fn merge_overrides(mut base: Extensions, user_overrides: HashMap<String, bool>) -> Self {
583        for (key, value) in user_overrides {
584            let normalized_key = key.replace('_', "-");
585            match normalized_key.as_str() {
586                "blank-before-header" => base.blank_before_header = value,
587                "header-attributes" => base.header_attributes = value,
588                "auto-identifiers" => base.auto_identifiers = value,
589                "gfm-auto-identifiers" => base.gfm_auto_identifiers = value,
590                "implicit-header-references" => base.implicit_header_references = value,
591                "blank-before-blockquote" => base.blank_before_blockquote = value,
592                "fancy-lists" => base.fancy_lists = value,
593                "startnum" => base.startnum = value,
594                "example-lists" => base.example_lists = value,
595                "task-lists" => base.task_lists = value,
596                "definition-lists" => base.definition_lists = value,
597                "lists-without-preceding-blankline" => {
598                    base.lists_without_preceding_blankline = value
599                }
600                "backtick-code-blocks" => base.backtick_code_blocks = value,
601                "fenced-code-blocks" => base.fenced_code_blocks = value,
602                "fenced-code-attributes" => base.fenced_code_attributes = value,
603                "executable-code" => base.executable_code = value,
604                "rmarkdown-inline-code" => base.rmarkdown_inline_code = value,
605                "quarto-inline-code" => base.quarto_inline_code = value,
606                "inline-code-attributes" => base.inline_code_attributes = value,
607                "simple-tables" => base.simple_tables = value,
608                "multiline-tables" => base.multiline_tables = value,
609                "grid-tables" => base.grid_tables = value,
610                "pipe-tables" => base.pipe_tables = value,
611                "table-captions" => base.table_captions = value,
612                "fenced-divs" => base.fenced_divs = value,
613                "native-divs" => base.native_divs = value,
614                "line-blocks" => base.line_blocks = value,
615                "intraword-underscores" => base.intraword_underscores = value,
616                "strikeout" => base.strikeout = value,
617                "superscript" => base.superscript = value,
618                "subscript" => base.subscript = value,
619                "inline-links" => base.inline_links = value,
620                "reference-links" => base.reference_links = value,
621                "shortcut-reference-links" => base.shortcut_reference_links = value,
622                "link-attributes" => base.link_attributes = value,
623                "autolinks" => base.autolinks = value,
624                "inline-images" => base.inline_images = value,
625                "implicit-figures" => base.implicit_figures = value,
626                "tex-math-dollars" => base.tex_math_dollars = value,
627                "tex-math-gfm" => base.tex_math_gfm = value,
628                "tex-math-single-backslash" => base.tex_math_single_backslash = value,
629                "tex-math-double-backslash" => base.tex_math_double_backslash = value,
630                "inline-footnotes" => base.inline_footnotes = value,
631                "footnotes" => base.footnotes = value,
632                "citations" => base.citations = value,
633                "bracketed-spans" => base.bracketed_spans = value,
634                "native-spans" => base.native_spans = value,
635                "yaml-metadata-block" => base.yaml_metadata_block = value,
636                "pandoc-title-block" => base.pandoc_title_block = value,
637                "mmd-title-block" => base.mmd_title_block = value,
638                "raw-html" => base.raw_html = value,
639                "markdown-in-html-blocks" => base.markdown_in_html_blocks = value,
640                "raw-tex" => base.raw_tex = value,
641                "raw-attribute" => base.raw_attribute = value,
642                "all-symbols-escapable" => base.all_symbols_escapable = value,
643                "escaped-line-breaks" => base.escaped_line_breaks = value,
644                "autolink-bare-uris" => base.autolink_bare_uris = value,
645                "hard-line-breaks" => base.hard_line_breaks = value,
646                "mmd-header-identifiers" => base.mmd_header_identifiers = value,
647                "mmd-link-attributes" => base.mmd_link_attributes = value,
648                "alerts" => base.alerts = value,
649                "emoji" => base.emoji = value,
650                "mark" => base.mark = value,
651                "quarto-callouts" => base.quarto_callouts = value,
652                "quarto-crossrefs" => base.quarto_crossrefs = value,
653                "quarto-shortcodes" => base.quarto_shortcodes = value,
654                "bookdown-references" => base.bookdown_references = value,
655                "bookdown-equation-references" => base.bookdown_equation_references = value,
656                _ => {}
657            }
658        }
659        base
660    }
661}
662
663#[cfg(test)]
664mod tests {
665    use super::{Extensions, Flavor};
666    use std::collections::HashMap;
667
668    #[test]
669    fn merge_with_flavor_keeps_known_extension_overrides() {
670        let mut overrides = HashMap::new();
671        overrides.insert("intraword-underscores".to_string(), false);
672        let ext = Extensions::merge_with_flavor(overrides, Flavor::Pandoc);
673        assert!(!ext.intraword_underscores);
674    }
675
676    #[test]
677    fn merge_with_flavor_ignores_unknown_extension_overrides() {
678        let mut overrides = HashMap::new();
679        overrides.insert("smart".to_string(), true);
680        overrides.insert("smart-quotes".to_string(), true);
681        let ext = Extensions::merge_with_flavor(overrides, Flavor::Gfm);
682        assert!(ext.strikeout, "known defaults should remain intact");
683    }
684
685    #[test]
686    fn lists_without_preceding_blankline_defaults_false_for_pandoc_and_gfm() {
687        assert!(!Extensions::for_flavor(Flavor::Pandoc).lists_without_preceding_blankline);
688        assert!(!Extensions::for_flavor(Flavor::Gfm).lists_without_preceding_blankline);
689    }
690
691    #[test]
692    fn merge_with_flavor_accepts_lists_without_preceding_blankline_override() {
693        let mut overrides = HashMap::new();
694        overrides.insert("lists-without-preceding-blankline".to_string(), true);
695        let ext = Extensions::merge_with_flavor(overrides, Flavor::Pandoc);
696        assert!(ext.lists_without_preceding_blankline);
697    }
698}
699
700#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
701#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
702pub enum PandocCompat {
703    /// Alias for Panache's pinned newest supported Pandoc-compat behavior.
704    ///
705    /// This is intentionally NOT "floating upstream latest". It resolves to
706    /// a concrete version that Panache has verified, and is bumped manually.
707    #[cfg_attr(feature = "serde", serde(rename = "latest"))]
708    Latest,
709    /// Match Pandoc 3.7 behavior for ambiguous syntax edge cases.
710    #[cfg_attr(
711        feature = "serde",
712        serde(rename = "3.7", alias = "3-7", alias = "v3.7", alias = "v3-7")
713    )]
714    V3_7,
715    /// Match Pandoc 3.9 behavior for ambiguous syntax edge cases.
716    #[default]
717    #[cfg_attr(
718        feature = "serde",
719        serde(rename = "3.9", alias = "3-9", alias = "v3.9", alias = "v3-9")
720    )]
721    V3_9,
722}
723
724impl PandocCompat {
725    /// Pinned target for `latest`.
726    pub const PINNED_LATEST: Self = Self::V3_9;
727
728    pub fn effective(self) -> Self {
729        match self {
730            Self::Latest => Self::PINNED_LATEST,
731            other => other,
732        }
733    }
734}
735
736/// Parser dialect — the underlying inline tokenization rule set.
737///
738/// Distinct from [`Flavor`]: `Flavor` is the user-facing identity (Pandoc,
739/// Quarto, GFM, etc.) and selects extension defaults; `Dialect` is the
740/// structural parser identity. Several flavors share a dialect — Quarto and
741/// RMarkdown both use `Pandoc`; CommonMark and GFM both use `CommonMark`.
742///
743/// Use this for parser branches whose behavior is fundamentally different
744/// between dialect families (e.g. unmatched backtick run handling). Per-flavor
745/// feature toggles still belong on [`Extensions`].
746#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
747#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
748#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
749pub enum Dialect {
750    /// Pandoc-markdown family. Default for Pandoc, Quarto, RMarkdown,
751    /// MultiMarkdown.
752    #[default]
753    Pandoc,
754    /// CommonMark family. Default for CommonMark and GFM.
755    CommonMark,
756}
757
758impl Dialect {
759    /// Default dialect for a given user-facing flavor.
760    pub fn for_flavor(flavor: Flavor) -> Self {
761        match flavor {
762            Flavor::CommonMark | Flavor::Gfm => Dialect::CommonMark,
763            Flavor::Pandoc | Flavor::Quarto | Flavor::RMarkdown | Flavor::MultiMarkdown => {
764                Dialect::Pandoc
765            }
766        }
767    }
768}
769
770#[derive(Debug, Clone)]
771#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
772#[cfg_attr(feature = "serde", serde(default, rename_all = "kebab-case"))]
773pub struct ParserOptions {
774    pub flavor: Flavor,
775    pub dialect: Dialect,
776    pub extensions: Extensions,
777    /// Compatibility target for ambiguous Pandoc behavior.
778    pub pandoc_compat: PandocCompat,
779    /// Document-level reference link label set, populated by the
780    /// top-level `parse()` function when running CommonMark dialect and
781    /// consulted by inline parsing's bracket resolution pass. `None`
782    /// means "not pre-computed"; the inline pipeline then treats every
783    /// reference-shaped bracket pair conservatively (current behavior),
784    /// which is correct for the Pandoc dialect and a graceful
785    /// degradation for embedded use cases that bypass `parse()`.
786    ///
787    /// Skipped by serde so config files don't try to (de)serialize a
788    /// runtime cache.
789    #[cfg_attr(feature = "serde", serde(skip))]
790    pub refdef_labels: Option<Arc<HashSet<String>>>,
791}
792
793impl Default for ParserOptions {
794    fn default() -> Self {
795        let flavor = Flavor::default();
796        Self {
797            flavor,
798            dialect: Dialect::for_flavor(flavor),
799            extensions: Extensions::for_flavor(flavor),
800            pandoc_compat: PandocCompat::default(),
801            refdef_labels: None,
802        }
803    }
804}
805
806impl ParserOptions {
807    pub fn effective_pandoc_compat(&self) -> PandocCompat {
808        self.pandoc_compat.effective()
809    }
810}