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    /// mdsvex (Svelte-flavored Markdown: CommonMark + Svelte template syntax)
27    #[cfg_attr(feature = "serde", serde(rename = "mdsvex"))]
28    Mdsvex,
29    /// MyST (CommonMark + Sphinx/MyST directives, roles, and targets)
30    #[cfg_attr(feature = "serde", serde(rename = "myst"))]
31    Myst,
32}
33
34/// Pandoc/Markdown extensions configuration.
35/// Each field represents a specific Pandoc extension.
36/// Extensions marked with a comment indicate implementation status.
37#[derive(Debug, Clone, PartialEq)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39#[cfg_attr(feature = "serde", serde(default))]
40#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
41#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
42pub struct Extensions {
43    // ===== Block-level extensions =====
44
45    // Headings
46    /// Require blank line before headers (default: enabled)
47    #[cfg_attr(feature = "serde", serde(alias = "blank_before_header"))]
48    pub blank_before_header: bool,
49    /// Full attribute syntax on headers {#id .class key=value}
50    #[cfg_attr(feature = "serde", serde(alias = "header_attributes"))]
51    pub header_attributes: bool,
52    /// Auto-generate identifiers from headings
53    pub auto_identifiers: bool,
54    /// Use GitHub's algorithm for auto-generated heading identifiers
55    pub gfm_auto_identifiers: bool,
56    /// Implicit header references ([Heading] links to header)
57    pub implicit_header_references: bool,
58
59    // Block quotes
60    /// Require blank line before blockquotes (default: enabled)
61    #[cfg_attr(feature = "serde", serde(alias = "blank_before_blockquote"))]
62    pub blank_before_blockquote: bool,
63
64    // Lists
65    /// Fancy list markers (roman numerals, letters, etc.)
66    #[cfg_attr(feature = "serde", serde(alias = "fancy_lists"))]
67    pub fancy_lists: bool,
68    /// Start ordered lists at arbitrary numbers
69    pub startnum: bool,
70    /// Example lists with (@) markers
71    #[cfg_attr(feature = "serde", serde(alias = "example_lists"))]
72    pub example_lists: bool,
73    /// GitHub-style task lists - [ ] and - [x]
74    #[cfg_attr(feature = "serde", serde(alias = "task_lists"))]
75    pub task_lists: bool,
76    /// Term/definition syntax
77    #[cfg_attr(feature = "serde", serde(alias = "definition_lists"))]
78    pub definition_lists: bool,
79    /// Allow lists without a preceding blank line
80    #[cfg_attr(feature = "serde", serde(alias = "lists_without_preceding_blankline"))]
81    pub lists_without_preceding_blankline: bool,
82    /// [NON-DEFAULT] Pandoc <= 2.0 list semantics: continuation paragraphs and
83    /// nested lists require four-space (one tab-width) indentation
84    #[cfg_attr(feature = "serde", serde(alias = "four_space_rule"))]
85    pub four_space_rule: bool,
86
87    // Code blocks
88    /// Fenced code blocks with backticks
89    #[cfg_attr(feature = "serde", serde(alias = "backtick_code_blocks"))]
90    pub backtick_code_blocks: bool,
91    /// Fenced code blocks with tildes
92    #[cfg_attr(feature = "serde", serde(alias = "fenced_code_blocks"))]
93    pub fenced_code_blocks: bool,
94    /// Attributes on fenced code blocks {.language #id}
95    #[cfg_attr(feature = "serde", serde(alias = "fenced_code_attributes"))]
96    pub fenced_code_attributes: bool,
97    /// Executable code syntax (currently fenced chunks like ```{r} / ```{python})
98    pub executable_code: bool,
99    /// R Markdown inline executable code (`...`r ...)
100    pub rmarkdown_inline_code: bool,
101    /// Quarto inline executable code (`...`{r} ...)
102    pub quarto_inline_code: bool,
103    /// Attributes on inline code
104    #[cfg_attr(feature = "serde", serde(alias = "inline_code_attributes"))]
105    pub inline_code_attributes: bool,
106
107    // Tables
108    /// Simple table syntax
109    #[cfg_attr(feature = "serde", serde(alias = "simple_tables"))]
110    pub simple_tables: bool,
111    /// Multiline cell content in tables
112    #[cfg_attr(feature = "serde", serde(alias = "multiline_tables"))]
113    pub multiline_tables: bool,
114    /// Grid-style tables
115    #[cfg_attr(feature = "serde", serde(alias = "grid_tables"))]
116    pub grid_tables: bool,
117    /// Pipe tables (GitHub/PHP Markdown style)
118    #[cfg_attr(feature = "serde", serde(alias = "pipe_tables"))]
119    pub pipe_tables: bool,
120    /// Table captions
121    #[cfg_attr(feature = "serde", serde(alias = "table_captions"))]
122    pub table_captions: bool,
123
124    // Divs
125    /// Fenced divs ::: {.class}
126    #[cfg_attr(feature = "serde", serde(alias = "fenced_divs"))]
127    pub fenced_divs: bool,
128    /// HTML <div> elements
129    #[cfg_attr(feature = "serde", serde(alias = "native_divs"))]
130    pub native_divs: bool,
131
132    // Other block elements
133    /// Line blocks for poetry | prefix
134    #[cfg_attr(feature = "serde", serde(alias = "line_blocks"))]
135    pub line_blocks: bool,
136
137    // ===== Inline elements =====
138
139    // Emphasis
140    /// Underscores don't trigger emphasis in snake_case
141    #[cfg_attr(feature = "serde", serde(alias = "intraword_underscores"))]
142    pub intraword_underscores: bool,
143    /// Strikethrough ~~text~~
144    pub strikeout: bool,
145    /// Superscript and subscript ^super^ ~sub~
146    pub superscript: bool,
147    pub subscript: bool,
148
149    // Links
150    /// Inline links [text](url)
151    #[cfg_attr(feature = "serde", serde(alias = "inline_links"))]
152    pub inline_links: bool,
153    /// Reference links [text][ref]
154    #[cfg_attr(feature = "serde", serde(alias = "reference_links"))]
155    pub reference_links: bool,
156    /// Shortcut reference links [ref] without second []
157    #[cfg_attr(feature = "serde", serde(alias = "shortcut_reference_links"))]
158    pub shortcut_reference_links: bool,
159    /// Attributes on links [text](url){.class}
160    #[cfg_attr(feature = "serde", serde(alias = "link_attributes"))]
161    pub link_attributes: bool,
162    /// Automatic links <http://example.com>
163    pub autolinks: bool,
164
165    // Images
166    /// Inline images ![alt](url)
167    #[cfg_attr(feature = "serde", serde(alias = "inline_images"))]
168    pub inline_images: bool,
169    /// Paragraph with just image becomes figure
170    #[cfg_attr(feature = "serde", serde(alias = "implicit_figures"))]
171    pub implicit_figures: bool,
172
173    // Math
174    /// Dollar-delimited math $x$ and $$equation$$
175    #[cfg_attr(feature = "serde", serde(alias = "tex_math_dollars"))]
176    pub tex_math_dollars: bool,
177    /// [NON-DEFAULT] GFM math: inline $`...`$ and fenced ``` math blocks
178    #[cfg_attr(feature = "serde", serde(alias = "tex_math_gfm"))]
179    pub tex_math_gfm: bool,
180    /// [NON-DEFAULT] Single backslash math \(...\) and \[...\] (RMarkdown default)
181    #[cfg_attr(feature = "serde", serde(alias = "tex_math_single_backslash"))]
182    pub tex_math_single_backslash: bool,
183    /// [NON-DEFAULT] Double backslash math \\(...\\) and \\[...\\]
184    #[cfg_attr(feature = "serde", serde(alias = "tex_math_double_backslash"))]
185    pub tex_math_double_backslash: bool,
186
187    // Footnotes
188    /// Inline footnotes ^[text]
189    #[cfg_attr(feature = "serde", serde(alias = "inline_footnotes"))]
190    pub inline_footnotes: bool,
191    /// Reference footnotes `[^1]` (requires footnote parsing)
192    pub footnotes: bool,
193
194    // Citations
195    /// Citation syntax [@cite]
196    pub citations: bool,
197
198    // Spans
199    /// Bracketed spans [text]{.class}
200    #[cfg_attr(feature = "serde", serde(alias = "bracketed_spans"))]
201    pub bracketed_spans: bool,
202    /// HTML <span> elements
203    #[cfg_attr(feature = "serde", serde(alias = "native_spans"))]
204    pub native_spans: bool,
205
206    // ===== Metadata =====
207    /// YAML metadata block
208    #[cfg_attr(feature = "serde", serde(alias = "yaml_metadata_block"))]
209    pub yaml_metadata_block: bool,
210    /// Pandoc title block (Title/Author/Date)
211    #[cfg_attr(feature = "serde", serde(alias = "pandoc_title_block"))]
212    pub pandoc_title_block: bool,
213    /// [NON-DEFAULT] MultiMarkdown metadata/title block (Key: Value ...)
214    pub mmd_title_block: bool,
215
216    // ===== Raw content =====
217    /// Raw HTML blocks and inline
218    #[cfg_attr(feature = "serde", serde(alias = "raw_html"))]
219    pub raw_html: bool,
220    /// Markdown inside HTML blocks
221    #[cfg_attr(feature = "serde", serde(alias = "markdown_in_html_blocks"))]
222    pub markdown_in_html_blocks: bool,
223    /// LaTeX commands and environments
224    #[cfg_attr(feature = "serde", serde(alias = "raw_tex"))]
225    pub raw_tex: bool,
226    /// Generic raw blocks with {=format} syntax
227    #[cfg_attr(feature = "serde", serde(alias = "raw_attribute"))]
228    pub raw_attribute: bool,
229
230    // ===== Escapes and special characters =====
231    /// Backslash escapes any symbol
232    #[cfg_attr(feature = "serde", serde(alias = "all_symbols_escapable"))]
233    pub all_symbols_escapable: bool,
234    /// Backslash at line end = hard line break
235    #[cfg_attr(feature = "serde", serde(alias = "escaped_line_breaks"))]
236    pub escaped_line_breaks: bool,
237
238    // ===== NON-DEFAULT EXTENSIONS =====
239    // These are disabled by default in Pandoc
240    /// [NON-DEFAULT] Bare URLs become links
241    #[cfg_attr(feature = "serde", serde(alias = "autolink_bare_uris"))]
242    pub autolink_bare_uris: bool,
243    /// [NON-DEFAULT] Newline = <br>
244    #[cfg_attr(feature = "serde", serde(alias = "hard_line_breaks"))]
245    pub hard_line_breaks: bool,
246    /// [NON-DEFAULT] Ignore soft breaks between two East Asian wide characters
247    #[cfg_attr(feature = "serde", serde(alias = "east_asian_line_breaks"))]
248    pub east_asian_line_breaks: bool,
249    /// [NON-DEFAULT] MultiMarkdown style heading identifiers [my-id]
250    pub mmd_header_identifiers: bool,
251    /// [NON-DEFAULT] MultiMarkdown key=value attributes on reference defs
252    pub mmd_link_attributes: bool,
253    /// [NON-DEFAULT] GitHub/CommonMark alerts in blockquotes (`> [!NOTE]`)
254    pub alerts: bool,
255    /// [NON-DEFAULT] python-markdown admonitions (`!!! note`) with 4-space
256    /// indented content
257    #[cfg_attr(feature = "serde", serde(alias = "python_markdown_admonitions"))]
258    pub python_markdown_admonitions: bool,
259    /// [NON-DEFAULT] pymdownx details: collapsible admonitions (`???`/`???+`)
260    #[cfg_attr(feature = "serde", serde(alias = "pymdownx_details"))]
261    pub pymdownx_details: bool,
262    /// [NON-DEFAULT] :emoji: syntax
263    pub emoji: bool,
264    /// [NON-DEFAULT] Highlighted ==text==
265    pub mark: bool,
266    /// [NON-DEFAULT] Pandoc wikilinks with title after pipe: `[[url|title]]`
267    #[cfg_attr(feature = "serde", serde(alias = "wikilinks_title_after_pipe"))]
268    pub wikilinks_title_after_pipe: bool,
269    /// [NON-DEFAULT] Pandoc wikilinks with title before pipe: `[[title|url]]`
270    #[cfg_attr(feature = "serde", serde(alias = "wikilinks_title_before_pipe"))]
271    pub wikilinks_title_before_pipe: bool,
272    /// [NON-DEFAULT] Allow whitespace between reference link brackets: `[foo] [bar]`
273    #[cfg_attr(feature = "serde", serde(alias = "spaced_reference_links"))]
274    pub spaced_reference_links: bool,
275
276    // ===== Quarto-specific extensions =====
277    /// Quarto callout blocks (.callout-note, etc.)
278    #[cfg_attr(feature = "serde", serde(alias = "quarto_callouts"))]
279    pub quarto_callouts: bool,
280    /// Quarto cross-references @fig-id, @tbl-id
281    #[cfg_attr(feature = "serde", serde(alias = "quarto_crossrefs"))]
282    pub quarto_crossrefs: bool,
283    /// Quarto shortcodes {{< name args >}}
284    #[cfg_attr(feature = "serde", serde(alias = "quarto_shortcodes"))]
285    pub quarto_shortcodes: bool,
286    /// Bookdown references \@ref(label) and (\#label)
287    pub bookdown_references: bool,
288    /// Bookdown equation references in LaTeX math blocks (\#eq:label)
289    pub bookdown_equation_references: bool,
290
291    // ===== mdsvex-specific extensions =====
292    /// [NON-DEFAULT] Svelte template syntax: `{#if}`/`{:else}`/`{/each}` block
293    /// logic, `{@html ...}`/`{@const ...}` tags, and `{expr}` interpolation.
294    /// Parsed as opaque, lossless spans (content preserved verbatim).
295    #[cfg_attr(feature = "serde", serde(alias = "svelte_template"))]
296    pub svelte_template: bool,
297
298    // ===== MyST-specific extensions =====
299    /// [NON-DEFAULT] MyST directives: ```` ```{name} ```` (and, with
300    /// `myst_colon_fence`, `:::{name}`) blocks carrying `:key: value` options
301    /// and a markdown body.
302    #[cfg_attr(feature = "serde", serde(alias = "myst_directives"))]
303    pub myst_directives: bool,
304    /// [NON-DEFAULT] MyST inline roles: `` {name}`content` ``.
305    #[cfg_attr(feature = "serde", serde(alias = "myst_roles"))]
306    pub myst_roles: bool,
307    /// [NON-DEFAULT] MyST targets `(label)=` preceding a block.
308    #[cfg_attr(feature = "serde", serde(alias = "myst_targets"))]
309    pub myst_targets: bool,
310    /// [NON-DEFAULT] MyST inline substitutions `{{ name }}`.
311    #[cfg_attr(feature = "serde", serde(alias = "myst_substitutions"))]
312    pub myst_substitutions: bool,
313    /// [NON-DEFAULT] MyST line comments beginning with `%`.
314    #[cfg_attr(feature = "serde", serde(alias = "myst_comments"))]
315    pub myst_comments: bool,
316    /// [NON-DEFAULT] MyST colon-fence directive form `:::{name}` (off by default
317    /// even in MyST, matching `myst-parser`'s opt-in `colon_fence` extension).
318    #[cfg_attr(feature = "serde", serde(alias = "myst_colon_fence"))]
319    pub myst_colon_fence: bool,
320    /// [NON-DEFAULT] MyST block breaks: a line of 3+ `+` markers (`+++`),
321    /// optionally carrying trailing cell metadata. `myst-parser` loads these via
322    /// `myst_block_plugin`, so they are on by default for `Flavor::Myst`.
323    #[cfg_attr(feature = "serde", serde(alias = "myst_block_breaks"))]
324    pub myst_block_breaks: bool,
325}
326
327impl Default for Extensions {
328    fn default() -> Self {
329        Self::for_flavor(Flavor::default())
330    }
331}
332
333impl Extensions {
334    fn none_defaults() -> Self {
335        Self {
336            alerts: false,
337            all_symbols_escapable: false,
338            auto_identifiers: false,
339            autolink_bare_uris: false,
340            autolinks: false,
341            backtick_code_blocks: false,
342            blank_before_blockquote: false,
343            blank_before_header: false,
344            bookdown_references: false,
345            bookdown_equation_references: false,
346            bracketed_spans: false,
347            citations: false,
348            definition_lists: false,
349            lists_without_preceding_blankline: false,
350            emoji: false,
351            escaped_line_breaks: false,
352            example_lists: false,
353            executable_code: false,
354            rmarkdown_inline_code: false,
355            quarto_inline_code: false,
356            fancy_lists: false,
357            fenced_code_attributes: false,
358            fenced_code_blocks: false,
359            fenced_divs: false,
360            footnotes: false,
361            four_space_rule: false,
362            gfm_auto_identifiers: false,
363            grid_tables: false,
364            east_asian_line_breaks: false,
365            hard_line_breaks: false,
366            header_attributes: false,
367            implicit_figures: false,
368            implicit_header_references: false,
369            inline_code_attributes: false,
370            inline_footnotes: false,
371            inline_images: false,
372            inline_links: false,
373            intraword_underscores: false,
374            line_blocks: false,
375            link_attributes: false,
376            mark: false,
377            markdown_in_html_blocks: false,
378            mmd_header_identifiers: false,
379            mmd_link_attributes: false,
380            mmd_title_block: false,
381            multiline_tables: false,
382            native_divs: false,
383            native_spans: false,
384            pandoc_title_block: false,
385            pipe_tables: false,
386            python_markdown_admonitions: false,
387            pymdownx_details: false,
388            quarto_callouts: false,
389            quarto_crossrefs: false,
390            quarto_shortcodes: false,
391            raw_attribute: false,
392            raw_html: false,
393            raw_tex: false,
394            reference_links: false,
395            shortcut_reference_links: false,
396            simple_tables: false,
397            startnum: false,
398            strikeout: false,
399            subscript: false,
400            superscript: false,
401            svelte_template: false,
402            myst_directives: false,
403            myst_roles: false,
404            myst_targets: false,
405            myst_substitutions: false,
406            myst_comments: false,
407            myst_colon_fence: false,
408            myst_block_breaks: false,
409            table_captions: false,
410            task_lists: false,
411            tex_math_dollars: false,
412            tex_math_double_backslash: false,
413            tex_math_gfm: false,
414            tex_math_single_backslash: false,
415            wikilinks_title_after_pipe: false,
416            wikilinks_title_before_pipe: false,
417            spaced_reference_links: false,
418            yaml_metadata_block: false,
419        }
420    }
421
422    /// Get the default extension set for a given flavor.
423    pub fn for_flavor(flavor: Flavor) -> Self {
424        match flavor {
425            Flavor::Pandoc => Self::pandoc_defaults(),
426            Flavor::Quarto => Self::quarto_defaults(),
427            Flavor::RMarkdown => Self::rmarkdown_defaults(),
428            Flavor::Gfm => Self::gfm_defaults(),
429            Flavor::CommonMark => Self::commonmark_defaults(),
430            Flavor::MultiMarkdown => Self::multimarkdown_defaults(),
431            Flavor::Mdsvex => Self::mdsvex_defaults(),
432            Flavor::Myst => Self::myst_defaults(),
433        }
434    }
435
436    fn pandoc_defaults() -> Self {
437        Self {
438            // Block-level - enabled by default in Pandoc
439            auto_identifiers: true,
440            blank_before_blockquote: true,
441            blank_before_header: true,
442            gfm_auto_identifiers: false,
443            header_attributes: true,
444            implicit_header_references: true,
445
446            // Lists
447            definition_lists: true,
448            example_lists: true,
449            fancy_lists: true,
450            lists_without_preceding_blankline: false,
451            startnum: true,
452            task_lists: true,
453
454            // Code
455            backtick_code_blocks: true,
456            executable_code: false,
457            rmarkdown_inline_code: false,
458            quarto_inline_code: false,
459            fenced_code_attributes: true,
460            fenced_code_blocks: true,
461            inline_code_attributes: true,
462
463            // Tables
464            grid_tables: true,
465            multiline_tables: true,
466            pipe_tables: true,
467            simple_tables: true,
468            table_captions: true,
469
470            // Divs
471            fenced_divs: true,
472            native_divs: true,
473
474            // Other blocks
475            line_blocks: true,
476
477            // Inline
478            intraword_underscores: true,
479            strikeout: true,
480            subscript: true,
481            superscript: true,
482
483            // Links
484            autolinks: true,
485            inline_links: true,
486            link_attributes: true,
487            reference_links: true,
488            shortcut_reference_links: true,
489
490            // Images
491            implicit_figures: true,
492            inline_images: true,
493
494            // Math
495            tex_math_dollars: true,
496            tex_math_double_backslash: false,
497            tex_math_gfm: false,
498            tex_math_single_backslash: false,
499
500            // Footnotes
501            footnotes: true,
502            inline_footnotes: true,
503
504            // Citations
505            citations: true,
506
507            // Spans
508            bracketed_spans: true,
509            native_spans: true,
510
511            // Metadata
512            mmd_title_block: false,
513            pandoc_title_block: true,
514            yaml_metadata_block: true,
515
516            // Raw
517            markdown_in_html_blocks: false,
518            raw_attribute: true,
519            raw_html: true,
520            raw_tex: true,
521
522            // Escapes
523            all_symbols_escapable: true,
524            escaped_line_breaks: true,
525
526            // Non-default
527            alerts: false,
528            python_markdown_admonitions: false,
529            pymdownx_details: false,
530            autolink_bare_uris: false,
531            east_asian_line_breaks: false,
532            emoji: false,
533            four_space_rule: false,
534            hard_line_breaks: false,
535            mark: false,
536            mmd_header_identifiers: false,
537            mmd_link_attributes: false,
538
539            // Quarto/Bookdown-specific
540            bookdown_references: false,
541            bookdown_equation_references: false,
542            quarto_callouts: false,
543            quarto_crossrefs: false,
544            quarto_shortcodes: false,
545
546            // Wikilinks (opt-in, no flavor default)
547            wikilinks_title_after_pipe: false,
548            wikilinks_title_before_pipe: false,
549
550            // Spaced reference links (opt-in)
551            spaced_reference_links: false,
552
553            // mdsvex (opt-in, mdsvex flavor only)
554            svelte_template: false,
555
556            // MyST (opt-in, myst flavor only)
557            myst_directives: false,
558            myst_roles: false,
559            myst_targets: false,
560            myst_substitutions: false,
561            myst_comments: false,
562            myst_colon_fence: false,
563            myst_block_breaks: false,
564        }
565    }
566
567    fn quarto_defaults() -> Self {
568        let mut ext = Self::pandoc_defaults();
569
570        ext.executable_code = true;
571        ext.rmarkdown_inline_code = true;
572        ext.quarto_inline_code = true;
573        ext.quarto_callouts = true;
574        ext.quarto_crossrefs = true;
575        ext.quarto_shortcodes = true;
576
577        ext
578    }
579
580    fn rmarkdown_defaults() -> Self {
581        let mut ext = Self::pandoc_defaults();
582
583        ext.bookdown_references = true;
584        ext.bookdown_equation_references = true;
585        ext.executable_code = true;
586        ext.rmarkdown_inline_code = true;
587        ext.quarto_inline_code = false;
588        ext.tex_math_dollars = true;
589        ext.tex_math_single_backslash = true;
590
591        ext
592    }
593
594    fn gfm_defaults() -> Self {
595        let mut ext = Self::none_defaults();
596
597        ext.alerts = true;
598        ext.auto_identifiers = true;
599        ext.autolink_bare_uris = true;
600        ext.autolinks = true;
601        ext.backtick_code_blocks = true;
602        ext.emoji = true;
603        ext.fenced_code_blocks = true;
604        ext.footnotes = true;
605        ext.gfm_auto_identifiers = true;
606        ext.inline_images = true;
607        ext.inline_links = true;
608        ext.pipe_tables = true;
609        ext.raw_html = true;
610        ext.reference_links = true;
611        ext.shortcut_reference_links = true;
612        ext.strikeout = true;
613        ext.task_lists = true;
614        ext.tex_math_dollars = true;
615        ext.tex_math_gfm = true;
616        ext.yaml_metadata_block = true;
617
618        ext
619    }
620
621    fn commonmark_defaults() -> Self {
622        let mut ext = Self::none_defaults();
623        // CommonMark's core grammar is what pandoc's commonmark reader treats
624        // as "not extensions" — they're built into the reader. Panache's
625        // parser still gates each construct on its extension flag, so we have
626        // to enable the CommonMark-mandatory ones explicitly here.
627        //
628        // Notably absent: `all_symbols_escapable`. CommonMark only allows
629        // backslash escapes of ASCII punctuation, and panache's
630        // `all_symbols_escapable` flag widens that to any character — so it
631        // must stay off for CommonMark.
632        ext.autolinks = true;
633        ext.backtick_code_blocks = true;
634        ext.escaped_line_breaks = true;
635        ext.fenced_code_blocks = true;
636        ext.inline_images = true;
637        ext.inline_links = true;
638        ext.intraword_underscores = true;
639        ext.raw_html = true;
640        ext.reference_links = true;
641        ext.shortcut_reference_links = true;
642        ext
643    }
644
645    fn multimarkdown_defaults() -> Self {
646        let mut ext = Self::none_defaults();
647
648        ext.all_symbols_escapable = true;
649        ext.auto_identifiers = true;
650        ext.backtick_code_blocks = true;
651        ext.definition_lists = true;
652        ext.footnotes = true;
653        ext.implicit_figures = true;
654        ext.implicit_header_references = true;
655        ext.intraword_underscores = true;
656        ext.mmd_header_identifiers = true;
657        ext.mmd_link_attributes = true;
658        ext.mmd_title_block = true;
659        ext.pipe_tables = true;
660        ext.raw_attribute = true;
661        ext.raw_html = true;
662        ext.reference_links = true;
663        ext.shortcut_reference_links = true;
664        ext.subscript = true;
665        ext.superscript = true;
666        ext.tex_math_dollars = true;
667        ext.tex_math_double_backslash = true;
668
669        ext
670    }
671
672    fn mdsvex_defaults() -> Self {
673        // mdsvex (≤0.12.x) builds on `remark-parse@8`, the pre-micromark parser
674        // whose options default to `gfm: true`. So tables, strikethrough, bare
675        // autolinks, and task lists work out of the box with **no** plugins —
676        // confirmed by the official "Svex up your markdown" getting-started
677        // (which uses a pipe table) and by real plugin-free `svelte.config.js`
678        // setups. `remark-gfm` is only needed on *modern* remark, which mdsvex
679        // does not use. A CommonMark-only default would wrongly reflow those
680        // tables as prose.
681        //
682        // We therefore start from the GFM extension set (still the CommonMark
683        // *dialect* per `Dialect::for_flavor`, and still with every Pandoc
684        // attribute extension off, so `{` stays free for Svelte), then trim the
685        // extras that remark-parse@8's `gfm: true` does NOT include: footnotes
686        // (a separate, default-off option), math (needs remark-math), emoji
687        // (needs remark-gemoji), and GitHub alerts (postdates this remark).
688        let mut ext = Self::gfm_defaults();
689
690        ext.footnotes = false;
691        ext.tex_math_dollars = false;
692        ext.tex_math_gfm = false;
693        ext.emoji = false;
694        ext.alerts = false;
695
696        ext.svelte_template = true;
697
698        ext
699    }
700
701    fn myst_defaults() -> Self {
702        // MyST is a CommonMark superset (markdown-it based), so we start from
703        // the CommonMark extension set and layer MyST's always-on constructs on
704        // top. Per the project decision, only MyST's *core* constructs are on by
705        // default — directives, roles, targets, and comments. Every MyST markup
706        // extension (`dollarmath`, `amsmath`, `colon_fence`, `deflist`,
707        // `fieldlist`, `tasklist`, `substitution`, `attrs_*`, `linkify`,
708        // `strikethrough`, `smartquotes`) is opt-in, matching a bare
709        // `myst-parser` install. Users enable them individually via config.
710        let mut ext = Self::commonmark_defaults();
711
712        ext.myst_directives = true;
713        ext.myst_roles = true;
714        ext.myst_targets = true;
715        ext.myst_comments = true;
716        ext.myst_block_breaks = true;
717
718        // MyST is GFM-flavored, not bare CommonMark: `myst-parser` builds its
719        // markdown-it parser with `.enable("table")` and the footnote plugin on
720        // by default. The plugin is loaded as `footnote_plugin, inline=False`,
721        // so reference footnotes (`[^ref]`) are on but inline footnotes
722        // (`^[...]`) stay off, matching a bare `myst-parser` install. (The
723        // remaining markup extensions below stay opt-in.)
724        ext.pipe_tables = true;
725        ext.footnotes = true;
726        ext.inline_footnotes = false;
727
728        // `myst-parser`'s default parser loads `front_matter_plugin`, which
729        // treats a leading `---`-delimited block as YAML front matter. Without
730        // this, panache mis-parses it as a setext heading plus a thematic
731        // break. The plugin only fires at the document start; panache's
732        // `yaml_metadata_block` covers that leading case.
733        ext.yaml_metadata_block = true;
734
735        // Opt-in MyST extensions (off here, overridable via [extensions]):
736        // colon_fence, substitution, and the markup extensions that map onto
737        // existing flags (tex_math_dollars, definition_lists, task_lists,
738        // strikeout, footnotes, ...).
739        ext.myst_substitutions = false;
740        ext.myst_colon_fence = false;
741
742        ext
743    }
744
745    /// Merge user-specified extension overrides with flavor defaults.
746    ///
747    /// This is used to support partial extension overrides in config files.
748    /// For example, if a user specifies `flavor = "quarto"` and then sets
749    /// `[extensions] quarto-crossrefs = false`, we want all other extensions
750    /// to use Quarto defaults, not Pandoc defaults.
751    ///
752    /// # Arguments
753    /// * `user_overrides` - Map of extension names to their user-specified values
754    /// * `flavor` - The flavor to use for default values
755    ///
756    /// # Returns
757    /// A new Extensions struct with flavor defaults merged with user overrides
758    pub fn merge_with_flavor(user_overrides: HashMap<String, bool>, flavor: Flavor) -> Self {
759        let defaults = Self::for_flavor(flavor);
760        Self::merge_overrides(defaults, user_overrides)
761    }
762
763    /// Apply `user_overrides` on top of an already-resolved `Extensions`.
764    /// Unknown keys are silently ignored (mirrors the panache.toml loader).
765    /// Use this when overriding individual extensions on top of a config that
766    /// has already merged flavor defaults + file-based overrides (e.g. CLI
767    /// `-o extensions.<name>=<bool>`).
768    pub fn apply_overrides(&mut self, user_overrides: HashMap<String, bool>) {
769        *self = Self::merge_overrides(self.clone(), user_overrides);
770    }
771
772    fn merge_overrides(mut base: Extensions, user_overrides: HashMap<String, bool>) -> Self {
773        for (key, value) in user_overrides {
774            base.set_by_name(&key, value);
775        }
776        base
777    }
778}
779
780/// Define the canonical mapping between kebab-case extension names (as users
781/// write them in `[extensions]`) and the corresponding `Extensions` fields.
782/// Drives both the runtime setter and the public name list, so adding an
783/// extension means editing exactly one table.
784macro_rules! known_extensions {
785    ( $( $kebab:literal => $field:ident ),* $(,)? ) => {
786        impl Extensions {
787            /// Canonical kebab-case names accepted in `[extensions]`. Used by
788            /// the config loader's typo check and by the JSON Schema
789            /// generator. Snake_case is also accepted at runtime via
790            /// normalization in [`Extensions::set_by_name`].
791            pub const KNOWN_NAMES: &'static [&'static str] = &[ $($kebab),* ];
792
793            /// True if `name` (in either kebab- or snake-case) is a known
794            /// extension key.
795            pub fn is_known_name(name: &str) -> bool {
796                let normalized = name.replace('_', "-");
797                Self::KNOWN_NAMES.iter().any(|k| *k == normalized)
798            }
799
800            /// Set the named extension on `self`, returning `true` if `name`
801            /// matched a known field. Kebab- and snake-case are accepted.
802            fn set_by_name(&mut self, name: &str, value: bool) -> bool {
803                match name.replace('_', "-").as_str() {
804                    $( $kebab => { self.$field = value; true } )*
805                    _ => false,
806                }
807            }
808        }
809    };
810}
811
812known_extensions! {
813    "blank-before-header" => blank_before_header,
814    "header-attributes" => header_attributes,
815    "auto-identifiers" => auto_identifiers,
816    "gfm-auto-identifiers" => gfm_auto_identifiers,
817    "implicit-header-references" => implicit_header_references,
818    "blank-before-blockquote" => blank_before_blockquote,
819    "fancy-lists" => fancy_lists,
820    "startnum" => startnum,
821    "example-lists" => example_lists,
822    "task-lists" => task_lists,
823    "definition-lists" => definition_lists,
824    "lists-without-preceding-blankline" => lists_without_preceding_blankline,
825    "four-space-rule" => four_space_rule,
826    "backtick-code-blocks" => backtick_code_blocks,
827    "fenced-code-blocks" => fenced_code_blocks,
828    "fenced-code-attributes" => fenced_code_attributes,
829    "executable-code" => executable_code,
830    "rmarkdown-inline-code" => rmarkdown_inline_code,
831    "quarto-inline-code" => quarto_inline_code,
832    "inline-code-attributes" => inline_code_attributes,
833    "simple-tables" => simple_tables,
834    "multiline-tables" => multiline_tables,
835    "grid-tables" => grid_tables,
836    "pipe-tables" => pipe_tables,
837    "table-captions" => table_captions,
838    "fenced-divs" => fenced_divs,
839    "native-divs" => native_divs,
840    "line-blocks" => line_blocks,
841    "intraword-underscores" => intraword_underscores,
842    "strikeout" => strikeout,
843    "superscript" => superscript,
844    "subscript" => subscript,
845    "inline-links" => inline_links,
846    "reference-links" => reference_links,
847    "shortcut-reference-links" => shortcut_reference_links,
848    "link-attributes" => link_attributes,
849    "autolinks" => autolinks,
850    "inline-images" => inline_images,
851    "implicit-figures" => implicit_figures,
852    "tex-math-dollars" => tex_math_dollars,
853    "tex-math-gfm" => tex_math_gfm,
854    "tex-math-single-backslash" => tex_math_single_backslash,
855    "tex-math-double-backslash" => tex_math_double_backslash,
856    "inline-footnotes" => inline_footnotes,
857    "footnotes" => footnotes,
858    "citations" => citations,
859    "bracketed-spans" => bracketed_spans,
860    "native-spans" => native_spans,
861    "yaml-metadata-block" => yaml_metadata_block,
862    "pandoc-title-block" => pandoc_title_block,
863    "mmd-title-block" => mmd_title_block,
864    "raw-html" => raw_html,
865    "markdown-in-html-blocks" => markdown_in_html_blocks,
866    "raw-tex" => raw_tex,
867    "raw-attribute" => raw_attribute,
868    "all-symbols-escapable" => all_symbols_escapable,
869    "escaped-line-breaks" => escaped_line_breaks,
870    "autolink-bare-uris" => autolink_bare_uris,
871    "hard-line-breaks" => hard_line_breaks,
872    "east-asian-line-breaks" => east_asian_line_breaks,
873    "mmd-header-identifiers" => mmd_header_identifiers,
874    "mmd-link-attributes" => mmd_link_attributes,
875    "alerts" => alerts,
876    "python-markdown-admonitions" => python_markdown_admonitions,
877    "pymdownx-details" => pymdownx_details,
878    "emoji" => emoji,
879    "mark" => mark,
880    "quarto-callouts" => quarto_callouts,
881    "quarto-crossrefs" => quarto_crossrefs,
882    "quarto-shortcodes" => quarto_shortcodes,
883    "bookdown-references" => bookdown_references,
884    "bookdown-equation-references" => bookdown_equation_references,
885    "wikilinks-title-after-pipe" => wikilinks_title_after_pipe,
886    "wikilinks-title-before-pipe" => wikilinks_title_before_pipe,
887    "spaced-reference-links" => spaced_reference_links,
888    "svelte-template" => svelte_template,
889    "myst-directives" => myst_directives,
890    "myst-roles" => myst_roles,
891    "myst-targets" => myst_targets,
892    "myst-substitutions" => myst_substitutions,
893    "myst-comments" => myst_comments,
894    "myst-colon-fence" => myst_colon_fence,
895    "myst-block-breaks" => myst_block_breaks,
896}
897
898#[cfg(test)]
899mod tests {
900    use super::{Dialect, Extensions, Flavor};
901    use std::collections::HashMap;
902
903    #[test]
904    fn merge_with_flavor_keeps_known_extension_overrides() {
905        let mut overrides = HashMap::new();
906        overrides.insert("intraword-underscores".to_string(), false);
907        let ext = Extensions::merge_with_flavor(overrides, Flavor::Pandoc);
908        assert!(!ext.intraword_underscores);
909    }
910
911    #[test]
912    fn merge_with_flavor_ignores_unknown_extension_overrides() {
913        let mut overrides = HashMap::new();
914        overrides.insert("smart".to_string(), true);
915        overrides.insert("smart-quotes".to_string(), true);
916        let ext = Extensions::merge_with_flavor(overrides, Flavor::Gfm);
917        assert!(ext.strikeout, "known defaults should remain intact");
918    }
919
920    #[test]
921    fn lists_without_preceding_blankline_defaults_false_for_pandoc_and_gfm() {
922        assert!(!Extensions::for_flavor(Flavor::Pandoc).lists_without_preceding_blankline);
923        assert!(!Extensions::for_flavor(Flavor::Gfm).lists_without_preceding_blankline);
924    }
925
926    #[test]
927    fn merge_with_flavor_accepts_lists_without_preceding_blankline_override() {
928        let mut overrides = HashMap::new();
929        overrides.insert("lists-without-preceding-blankline".to_string(), true);
930        let ext = Extensions::merge_with_flavor(overrides, Flavor::Pandoc);
931        assert!(ext.lists_without_preceding_blankline);
932    }
933
934    #[test]
935    fn four_space_rule_defaults_off_for_every_flavor() {
936        for flavor in [
937            Flavor::Pandoc,
938            Flavor::Quarto,
939            Flavor::RMarkdown,
940            Flavor::Gfm,
941            Flavor::CommonMark,
942            Flavor::MultiMarkdown,
943            Flavor::Mdsvex,
944            Flavor::Myst,
945        ] {
946            assert!(
947                !Extensions::for_flavor(flavor).four_space_rule,
948                "four_space_rule should be off by default for {flavor:?}"
949            );
950        }
951    }
952
953    #[test]
954    fn svelte_template_defaults_off_for_non_mdsvex_flavors() {
955        for flavor in [
956            Flavor::Pandoc,
957            Flavor::Quarto,
958            Flavor::RMarkdown,
959            Flavor::Gfm,
960            Flavor::CommonMark,
961            Flavor::MultiMarkdown,
962            Flavor::Myst,
963        ] {
964            assert!(
965                !Extensions::for_flavor(flavor).svelte_template,
966                "svelte_template should be off by default for {flavor:?}"
967            );
968        }
969    }
970
971    #[test]
972    fn myst_uses_commonmark_dialect() {
973        assert_eq!(Dialect::for_flavor(Flavor::Myst), Dialect::CommonMark);
974    }
975
976    #[test]
977    fn myst_defaults_enable_core_constructs_only() {
978        let ext = Extensions::for_flavor(Flavor::Myst);
979
980        // MyST core constructs are on by default.
981        assert!(ext.myst_directives);
982        assert!(ext.myst_roles);
983        assert!(ext.myst_targets);
984        assert!(ext.myst_comments);
985        assert!(ext.myst_block_breaks);
986
987        // MyST is a GFM-flavored superset, not bare CommonMark: markdown-it's
988        // `table` and footnote rules are on by default in `myst-parser`. The
989        // footnote plugin is loaded with `inline=False`, so reference footnotes
990        // are on but inline footnotes (`^[...]`) stay off.
991        assert!(ext.pipe_tables);
992        assert!(ext.footnotes);
993        assert!(!ext.inline_footnotes);
994
995        // `myst-parser` loads `front_matter_plugin` by default, so a leading
996        // `---` YAML block is metadata, not a setext heading + thematic break.
997        assert!(ext.yaml_metadata_block);
998
999        // MyST markup extensions are opt-in (off by default), matching a bare
1000        // `myst-parser` install.
1001        assert!(!ext.myst_colon_fence);
1002        assert!(!ext.myst_substitutions);
1003        assert!(!ext.tex_math_dollars);
1004        assert!(!ext.definition_lists);
1005        assert!(!ext.task_lists);
1006        assert!(!ext.strikeout);
1007
1008        // CommonMark superset: CommonMark core on, Pandoc `{...}` constructs off
1009        // so braces are free for directives/roles.
1010        assert!(ext.inline_links);
1011        assert!(ext.backtick_code_blocks);
1012        assert!(!ext.fenced_divs);
1013        assert!(!ext.bracketed_spans);
1014        assert!(!ext.header_attributes);
1015    }
1016
1017    #[test]
1018    fn myst_core_constructs_off_for_other_flavors() {
1019        for flavor in [
1020            Flavor::Pandoc,
1021            Flavor::Quarto,
1022            Flavor::RMarkdown,
1023            Flavor::Gfm,
1024            Flavor::CommonMark,
1025            Flavor::MultiMarkdown,
1026            Flavor::Mdsvex,
1027        ] {
1028            let ext = Extensions::for_flavor(flavor);
1029            assert!(
1030                !ext.myst_directives
1031                    && !ext.myst_roles
1032                    && !ext.myst_targets
1033                    && !ext.myst_comments
1034                    && !ext.myst_block_breaks,
1035                "MyST core constructs should be off by default for {flavor:?}"
1036            );
1037        }
1038    }
1039
1040    #[test]
1041    fn mdsvex_defaults_match_remark_parse_8_gfm() {
1042        let ext = Extensions::for_flavor(Flavor::Mdsvex);
1043
1044        // The Svelte template layer is on; raw HTML and frontmatter pass through.
1045        assert!(ext.svelte_template);
1046        assert!(ext.raw_html);
1047        assert!(ext.yaml_metadata_block);
1048
1049        // remark-parse@8 defaults to `gfm: true`, so these work with no plugins.
1050        assert!(ext.pipe_tables);
1051        assert!(ext.strikeout);
1052        assert!(ext.task_lists);
1053        assert!(ext.autolink_bare_uris);
1054
1055        // Extras that remark-parse@8's gfm does NOT include stay off.
1056        assert!(!ext.footnotes);
1057        assert!(!ext.tex_math_dollars);
1058        assert!(!ext.emoji);
1059        assert!(!ext.alerts);
1060
1061        // CommonMark dialect, so the Pandoc `{...}` attribute constructs are off
1062        // — this is what frees `{` for Svelte template syntax.
1063        assert_eq!(Dialect::for_flavor(Flavor::Mdsvex), Dialect::CommonMark);
1064        assert!(!ext.header_attributes);
1065        assert!(!ext.bracketed_spans);
1066        assert!(!ext.fenced_divs);
1067        assert!(!ext.raw_attribute);
1068        assert!(!ext.inline_code_attributes);
1069    }
1070
1071    #[test]
1072    fn merge_with_flavor_accepts_four_space_rule_override() {
1073        let mut overrides = HashMap::new();
1074        overrides.insert("four-space-rule".to_string(), true);
1075        let ext = Extensions::merge_with_flavor(overrides, Flavor::Pandoc);
1076        assert!(ext.four_space_rule);
1077    }
1078}
1079
1080#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1081#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1082pub enum PandocCompat {
1083    /// Alias for Panache's pinned newest supported Pandoc-compat behavior.
1084    ///
1085    /// This is intentionally NOT "floating upstream latest". It resolves to
1086    /// a concrete version that Panache has verified, and is bumped manually.
1087    #[cfg_attr(feature = "serde", serde(rename = "latest"))]
1088    Latest,
1089    /// Match Pandoc 3.7 behavior for ambiguous syntax edge cases.
1090    #[cfg_attr(
1091        feature = "serde",
1092        serde(rename = "3.7", alias = "3-7", alias = "v3.7", alias = "v3-7")
1093    )]
1094    V3_7,
1095    /// Match Pandoc 3.9 behavior for ambiguous syntax edge cases.
1096    #[default]
1097    #[cfg_attr(
1098        feature = "serde",
1099        serde(rename = "3.9", alias = "3-9", alias = "v3.9", alias = "v3-9")
1100    )]
1101    V3_9,
1102}
1103
1104impl PandocCompat {
1105    /// Pinned target for `latest`.
1106    pub const PINNED_LATEST: Self = Self::V3_9;
1107
1108    pub fn effective(self) -> Self {
1109        match self {
1110            Self::Latest => Self::PINNED_LATEST,
1111            other => other,
1112        }
1113    }
1114}
1115
1116/// Parser dialect — the underlying inline tokenization rule set.
1117///
1118/// Distinct from [`Flavor`]: `Flavor` is the user-facing identity (Pandoc,
1119/// Quarto, GFM, etc.) and selects extension defaults; `Dialect` is the
1120/// structural parser identity. Several flavors share a dialect — Quarto and
1121/// RMarkdown both use `Pandoc`; CommonMark and GFM both use `CommonMark`.
1122///
1123/// Use this for parser branches whose behavior is fundamentally different
1124/// between dialect families (e.g. unmatched backtick run handling). Per-flavor
1125/// feature toggles still belong on [`Extensions`].
1126#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1127#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1128#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
1129#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1130pub enum Dialect {
1131    /// Pandoc-markdown family. Default for Pandoc, Quarto, RMarkdown,
1132    /// MultiMarkdown.
1133    #[default]
1134    Pandoc,
1135    /// CommonMark family. Default for CommonMark and GFM.
1136    CommonMark,
1137}
1138
1139impl Dialect {
1140    /// Default dialect for a given user-facing flavor.
1141    pub fn for_flavor(flavor: Flavor) -> Self {
1142        match flavor {
1143            Flavor::CommonMark | Flavor::Gfm | Flavor::Mdsvex | Flavor::Myst => Dialect::CommonMark,
1144            Flavor::Pandoc | Flavor::Quarto | Flavor::RMarkdown | Flavor::MultiMarkdown => {
1145                Dialect::Pandoc
1146            }
1147        }
1148    }
1149}
1150
1151#[derive(Debug, Clone)]
1152#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1153#[cfg_attr(feature = "serde", serde(default, rename_all = "kebab-case"))]
1154#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1155pub struct ParserOptions {
1156    pub flavor: Flavor,
1157    pub dialect: Dialect,
1158    pub extensions: Extensions,
1159    /// Compatibility target for ambiguous Pandoc behavior.
1160    pub pandoc_compat: PandocCompat,
1161    /// Additional cross-reference key prefixes (beyond the Quarto built-ins
1162    /// recognized by [`crate::parser::inlines::citations::is_quarto_crossref_key`])
1163    /// that should parse as cross-references rather than citations. Populated
1164    /// from the top-level `crossref-prefixes` config key so documents relying on
1165    /// crossref-injecting extensions (e.g. pseudocode's `@algo-`) don't have
1166    /// those references misclassified as citations.
1167    #[cfg_attr(feature = "serde", serde(default, alias = "crossref_prefixes"))]
1168    pub crossref_prefixes: Vec<String>,
1169    /// Document-level reference link label set, populated by the
1170    /// top-level `parse()` function when running CommonMark dialect and
1171    /// consulted by inline parsing's bracket resolution pass. `None`
1172    /// means "not pre-computed"; the inline pipeline then treats every
1173    /// reference-shaped bracket pair conservatively (current behavior),
1174    /// which is correct for the Pandoc dialect and a graceful
1175    /// degradation for embedded use cases that bypass `parse()`.
1176    ///
1177    /// Skipped by serde so config files don't try to (de)serialize a
1178    /// runtime cache.
1179    #[cfg_attr(feature = "serde", serde(skip))]
1180    pub refdef_labels: Option<Arc<HashSet<String>>>,
1181}
1182
1183impl Default for ParserOptions {
1184    fn default() -> Self {
1185        let flavor = Flavor::default();
1186        Self {
1187            flavor,
1188            dialect: Dialect::for_flavor(flavor),
1189            extensions: Extensions::for_flavor(flavor),
1190            pandoc_compat: PandocCompat::default(),
1191            crossref_prefixes: Vec::new(),
1192            refdef_labels: None,
1193        }
1194    }
1195}
1196
1197impl ParserOptions {
1198    pub fn effective_pandoc_compat(&self) -> PandocCompat {
1199        self.pandoc_compat.effective()
1200    }
1201}
1202
1203#[cfg(feature = "schema")]
1204impl schemars::JsonSchema for Flavor {
1205    fn schema_name() -> std::borrow::Cow<'static, str> {
1206        "Flavor".into()
1207    }
1208
1209    fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1210        // Include serde aliases so the schema accepts every spelling the
1211        // parser accepts (e.g. `commonmark` alongside the kebab-case
1212        // `common-mark` canonical form).
1213        schemars::json_schema!({
1214            "type": "string",
1215            "description": "Markdown flavor to parse and format against.",
1216            "enum": [
1217                "pandoc",
1218                "quarto",
1219                "rmarkdown",
1220                "gfm",
1221                "common-mark",
1222                "commonmark",
1223                "multimarkdown",
1224                "mdsvex",
1225                "myst"
1226            ]
1227        })
1228    }
1229}
1230
1231#[cfg(feature = "schema")]
1232impl schemars::JsonSchema for PandocCompat {
1233    fn schema_name() -> std::borrow::Cow<'static, str> {
1234        "PandocCompat".into()
1235    }
1236
1237    fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1238        schemars::json_schema!({
1239            "type": "string",
1240            "description": "Compatibility target for ambiguous Pandoc behavior.",
1241            "enum": [
1242                "latest",
1243                "3.7", "3-7", "v3.7", "v3-7",
1244                "3.9", "3-9", "v3.9", "v3-9"
1245            ]
1246        })
1247    }
1248}