Skip to main content

rumdl_lib/config/
flavor.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::str::FromStr;
4
5/// Canonical values shown in user-facing configuration diagnostics, wherever a
6/// flavor name was rejected: a config file's `[per-file-flavor]` and the
7/// command line's own overrides answer with the same list.
8pub const CANONICAL_MARKDOWN_FLAVORS: &str =
9    "standard, mkdocs, mdx, pandoc, quarto, obsidian, kramdown, azure_devops, myst, hugo, mdg, gh-aw";
10
11// ============================================================================
12// Typestate markers for configuration pipeline
13// ============================================================================
14
15/// Marker type for configuration that has been loaded but not yet validated.
16/// This is the initial state after `load_with_discovery()`.
17#[derive(Debug, Clone, Copy, Default)]
18pub struct ConfigLoaded;
19
20/// Marker type for configuration that has been validated.
21/// Only validated configs can be converted to `Config`.
22#[derive(Debug, Clone, Copy, Default)]
23pub struct ConfigValidated;
24
25/// Markdown flavor/dialect enumeration
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
27#[serde(rename_all = "lowercase")]
28pub enum MarkdownFlavor {
29    /// Standard Markdown without flavor-specific adjustments
30    #[serde(rename = "standard", alias = "none", alias = "")]
31    #[default]
32    Standard,
33    /// MkDocs flavor with auto-reference support
34    #[serde(rename = "mkdocs")]
35    MkDocs,
36    /// MDX flavor with JSX and ESM support (.mdx files)
37    #[serde(rename = "mdx")]
38    MDX,
39    /// Pandoc Markdown — fenced divs, attribute lists, citations, definition
40    /// lists, math, and other Pandoc-specific syntax.
41    #[serde(rename = "pandoc")]
42    Pandoc,
43    /// Quarto/RMarkdown flavor for scientific publishing (.qmd, .Rmd files)
44    #[serde(rename = "quarto")]
45    Quarto,
46    /// Obsidian flavor with tag syntax support (#tagname as tags, not headings)
47    #[serde(rename = "obsidian")]
48    Obsidian,
49    /// Kramdown flavor for Jekyll sites with IAL, ALD, and extension block support
50    #[serde(rename = "kramdown")]
51    Kramdown,
52    /// Azure DevOps flavor — treats `:::lang` blocks as opaque code fences
53    #[serde(rename = "azure_devops", alias = "azure", alias = "ado")]
54    AzureDevOps,
55    /// MyST (Markedly Structured Text) flavor — directives, roles, dollar math, % comments
56    #[serde(rename = "myst", alias = "mystmd")]
57    MyST,
58    /// Hugo flavor — GFM plus Goldmark block attribute lists (`{class="a" id="b"}`)
59    #[serde(rename = "hugo", alias = "goldmark")]
60    Hugo,
61    /// Markdown with Gherkin (MDG) flavor for executable specifications (`.feature.md` files)
62    #[serde(rename = "mdg", alias = "markdown_with_gherkin")]
63    MDG,
64    /// GitHub Agentic Workflows — YAML frontmatter and Handlebars-style control directives
65    #[serde(rename = "gh-aw")]
66    GhAw,
67}
68
69/// Custom JSON schema for MarkdownFlavor that includes all accepted values and aliases
70fn markdown_flavor_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
71    schemars::json_schema!({
72        "description": "Markdown flavor/dialect. Accepts: standard, gfm, mkdocs, mdx, pandoc, quarto, obsidian, kramdown, azure_devops, myst, hugo, mdg, gh-aw (preview). Aliases: commonmark/github map to standard, qmd/rmd/rmarkdown map to quarto, jekyll maps to kramdown, azure/ado map to azure_devops, mystmd maps to myst, goldmark maps to hugo, markdown_with_gherkin maps to mdg.",
73        "type": "string",
74        "enum": ["standard", "gfm", "github", "commonmark", "mkdocs", "mdx", "pandoc", "quarto", "qmd", "rmd", "rmarkdown", "obsidian", "kramdown", "jekyll", "azure_devops", "azure", "ado", "myst", "mystmd", "hugo", "goldmark", "mdg", "markdown_with_gherkin", "gh-aw"]
75    })
76}
77
78impl schemars::JsonSchema for MarkdownFlavor {
79    fn schema_name() -> std::borrow::Cow<'static, str> {
80        std::borrow::Cow::Borrowed("MarkdownFlavor")
81    }
82
83    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
84        markdown_flavor_schema(generator)
85    }
86}
87
88impl fmt::Display for MarkdownFlavor {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        match self {
91            MarkdownFlavor::Standard => write!(f, "standard"),
92            MarkdownFlavor::MkDocs => write!(f, "mkdocs"),
93            MarkdownFlavor::MDX => write!(f, "mdx"),
94            MarkdownFlavor::Pandoc => write!(f, "pandoc"),
95            MarkdownFlavor::Quarto => write!(f, "quarto"),
96            MarkdownFlavor::Obsidian => write!(f, "obsidian"),
97            MarkdownFlavor::Kramdown => write!(f, "kramdown"),
98            MarkdownFlavor::AzureDevOps => write!(f, "azure_devops"),
99            MarkdownFlavor::MyST => write!(f, "myst"),
100            MarkdownFlavor::Hugo => write!(f, "hugo"),
101            MarkdownFlavor::MDG => write!(f, "mdg"),
102            MarkdownFlavor::GhAw => write!(f, "gh-aw"),
103        }
104    }
105}
106
107impl FromStr for MarkdownFlavor {
108    type Err = String;
109
110    fn from_str(s: &str) -> Result<Self, Self::Err> {
111        match s.to_lowercase().as_str() {
112            "standard" | "" | "none" => Ok(MarkdownFlavor::Standard),
113            "mkdocs" => Ok(MarkdownFlavor::MkDocs),
114            "mdx" => Ok(MarkdownFlavor::MDX),
115            "pandoc" => Ok(MarkdownFlavor::Pandoc),
116            "quarto" | "qmd" | "rmd" | "rmarkdown" => Ok(MarkdownFlavor::Quarto),
117            "obsidian" => Ok(MarkdownFlavor::Obsidian),
118            "kramdown" | "jekyll" => Ok(MarkdownFlavor::Kramdown),
119            "azure_devops" | "azure" | "ado" => Ok(MarkdownFlavor::AzureDevOps),
120            "myst" | "mystmd" => Ok(MarkdownFlavor::MyST),
121            "hugo" | "goldmark" => Ok(MarkdownFlavor::Hugo),
122            // GFM and CommonMark are aliases for Standard since the base parser
123            // (pulldown-cmark) already supports GFM extensions (tables, task lists,
124            // strikethrough, autolinks, etc.) which are a superset of CommonMark
125            "gfm" | "github" | "commonmark" => Ok(MarkdownFlavor::Standard),
126            "mdg" | "markdown_with_gherkin" => Ok(MarkdownFlavor::MDG),
127            "gh-aw" => Ok(MarkdownFlavor::GhAw),
128            _ => Err(format!("Unknown markdown flavor: {s}")),
129        }
130    }
131}
132
133impl MarkdownFlavor {
134    /// Detect flavor from file extension
135    pub fn from_extension(ext: &str) -> Self {
136        match ext.to_lowercase().as_str() {
137            "mdx" => Self::MDX,
138            "qmd" => Self::Quarto,
139            "rmd" => Self::Quarto,
140            "kramdown" => Self::Kramdown,
141            _ => Self::Standard,
142        }
143    }
144
145    /// Detect flavor from file path
146    pub fn from_path(path: &std::path::Path) -> Self {
147        if path
148            .file_name()
149            .and_then(|name| name.to_str())
150            .is_some_and(|name| name.to_ascii_lowercase().ends_with(".feature.md"))
151        {
152            return Self::MDG;
153        }
154
155        path.extension()
156            .and_then(|e| e.to_str())
157            .map_or(Self::Standard, Self::from_extension)
158    }
159
160    /// Check if this flavor supports ESM imports/exports (MDX-specific)
161    pub fn supports_esm_blocks(self) -> bool {
162        matches!(self, Self::MDX)
163    }
164
165    /// Check if this flavor supports JSX components (MDX-specific)
166    pub fn supports_jsx(self) -> bool {
167        matches!(self, Self::MDX)
168    }
169
170    /// Check if this flavor supports auto-references (MkDocs-specific)
171    pub fn supports_auto_references(self) -> bool {
172        matches!(self, Self::MkDocs)
173    }
174
175    /// Check if this flavor supports kramdown syntax (IALs, ALDs, extension blocks)
176    pub fn supports_kramdown_syntax(self) -> bool {
177        matches!(self, Self::Kramdown)
178    }
179
180    /// Check if this flavor supports attribute lists ({#id .class key="value"})
181    pub fn supports_attr_lists(self) -> bool {
182        matches!(self, Self::MkDocs | Self::Kramdown | Self::Hugo)
183    }
184
185    /// Check if this flavor requires strict (≥4-space) list continuation indent.
186    ///
187    /// Python-Markdown (used by MkDocs) requires 4-space indentation for ordered
188    /// list continuation content, regardless of marker width.
189    pub fn requires_strict_list_indent(self) -> bool {
190        matches!(self, Self::MkDocs)
191    }
192
193    /// True for any flavor that includes Pandoc-style syntax — fenced divs,
194    /// attribute lists, citations, definition lists, math, raw blocks.
195    /// Use this to gate behavior shared by both Pandoc and Quarto users.
196    pub fn is_pandoc_compatible(self) -> bool {
197        matches!(self, Self::Pandoc | Self::Quarto)
198    }
199
200    /// Get a human-readable name for this flavor
201    pub fn name(self) -> &'static str {
202        match self {
203            Self::Standard => "Standard",
204            Self::MkDocs => "MkDocs",
205            Self::MDX => "MDX",
206            Self::Pandoc => "Pandoc",
207            Self::Quarto => "Quarto",
208            Self::Obsidian => "Obsidian",
209            Self::Kramdown => "Kramdown",
210            Self::AzureDevOps => "AzureDevOps",
211            Self::MyST => "MyST",
212            Self::Hugo => "Hugo",
213            Self::MDG => "Markdown with Gherkin",
214            Self::GhAw => "GitHub Agentic Workflows",
215        }
216    }
217
218    /// True only for Azure DevOps flavor, which uses `:::lang` as a code fence.
219    pub fn supports_colon_code_fences(self) -> bool {
220        matches!(self, Self::AzureDevOps)
221    }
222
223    /// True for MyST flavor — supports directive syntax (backtick and colon fences with `{name}`)
224    pub fn supports_myst_directives(self) -> bool {
225        matches!(self, Self::MyST)
226    }
227
228    /// True for MyST flavor — supports role syntax (`{role}`content``)
229    pub fn supports_myst_roles(self) -> bool {
230        matches!(self, Self::MyST)
231    }
232
233    /// True for MyST flavor — supports `%` line comments
234    pub fn supports_myst_comments(self) -> bool {
235        matches!(self, Self::MyST)
236    }
237}
238
239/// Normalizes configuration keys (rule names, option names) to lowercase kebab-case.
240pub fn normalize_key(key: &str) -> String {
241    // If the key looks like a rule name (e.g., MD013), uppercase it
242    if key.len() == 5 && key.to_ascii_lowercase().starts_with("md") && key[2..].chars().all(|c| c.is_ascii_digit()) {
243        key.to_ascii_uppercase()
244    } else {
245        key.replace('_', "-").to_ascii_lowercase()
246    }
247}
248
249/// Warns if a per-file-ignores pattern contains a comma but no braces.
250/// This is a common mistake where users expect "A.md,B.md" to match both files,
251/// but glob syntax requires "{A.md,B.md}" for brace expansion.
252///
253/// The pattern is a key out of the config file, so for one reached through
254/// `extends` it is named rather than shown, and the suggestion that would have
255/// rewritten it becomes a description of the rewrite.
256pub(super) fn warn_comma_without_brace_in_pattern(pattern: &str, config_file: super::types::ConfigRef<'_>) {
257    if pattern.contains(',') && !pattern.contains('{') {
258        if config_file.may_quote_contents() {
259            eprintln!("Warning: Pattern \"{pattern}\" in {config_file} contains a comma but no braces.");
260            eprintln!("  To match multiple files, use brace expansion: \"{{{pattern}}}\"");
261        } else {
262            eprintln!("Warning: A pattern in {config_file} contains a comma but no braces.");
263            eprintln!("  To match multiple files, wrap the pattern in braces for brace expansion.");
264        }
265        eprintln!("  Or use separate entries for each file.");
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    /// Every MarkdownFlavor variant must produce a lowercase, unquoted string via Display.
274    /// This guards against new variants being added without a matching Display arm,
275    /// and against the Display impl regressing to Debug-style output (e.g. "Standard").
276    #[test]
277    fn test_display_all_variants_are_lowercase() {
278        let cases = [
279            (MarkdownFlavor::Standard, "standard"),
280            (MarkdownFlavor::MkDocs, "mkdocs"),
281            (MarkdownFlavor::MDX, "mdx"),
282            (MarkdownFlavor::Pandoc, "pandoc"),
283            (MarkdownFlavor::Quarto, "quarto"),
284            (MarkdownFlavor::Obsidian, "obsidian"),
285            (MarkdownFlavor::Kramdown, "kramdown"),
286            (MarkdownFlavor::AzureDevOps, "azure_devops"),
287            (MarkdownFlavor::MyST, "myst"),
288            (MarkdownFlavor::Hugo, "hugo"),
289            (MarkdownFlavor::MDG, "mdg"),
290            (MarkdownFlavor::GhAw, "gh-aw"),
291        ];
292        assert_eq!(
293            cases.iter().map(|(_, name)| *name).collect::<Vec<_>>().join(", "),
294            CANONICAL_MARKDOWN_FLAVORS,
295            "user-facing canonical flavor diagnostics drifted from the enum matrix"
296        );
297        for (variant, expected) in cases {
298            let displayed = variant.to_string();
299            assert_eq!(
300                displayed, expected,
301                "MarkdownFlavor::{variant:?} Display should produce \"{expected}\", got \"{displayed}\""
302            );
303            // Must be lowercase — no uppercase letters anywhere
304            assert!(
305                displayed.chars().all(|c| !c.is_ascii_uppercase()),
306                "MarkdownFlavor::{variant:?} Display must be entirely lowercase, got \"{displayed}\""
307            );
308        }
309    }
310
311    /// Display output must round-trip through FromStr — every variant's Display string
312    /// must parse back to the same variant.
313    #[test]
314    fn test_display_round_trips_through_from_str() {
315        let variants = [
316            MarkdownFlavor::Standard,
317            MarkdownFlavor::MkDocs,
318            MarkdownFlavor::MDX,
319            MarkdownFlavor::Pandoc,
320            MarkdownFlavor::Quarto,
321            MarkdownFlavor::Obsidian,
322            MarkdownFlavor::Kramdown,
323            MarkdownFlavor::AzureDevOps,
324            MarkdownFlavor::MyST,
325            MarkdownFlavor::Hugo,
326            MarkdownFlavor::MDG,
327            MarkdownFlavor::GhAw,
328        ];
329        for variant in variants {
330            let displayed = variant.to_string();
331            let parsed: MarkdownFlavor = displayed
332                .parse()
333                .unwrap_or_else(|e| panic!("Display string \"{displayed}\" for {variant:?} failed to parse back: {e}"));
334            assert_eq!(
335                parsed, variant,
336                "Display(\"{displayed}\") for {variant:?} round-trips to a different variant: {parsed:?}"
337            );
338        }
339    }
340
341    #[test]
342    fn test_pandoc_from_str() {
343        assert_eq!("pandoc".parse::<MarkdownFlavor>().unwrap(), MarkdownFlavor::Pandoc);
344        assert_eq!("PANDOC".parse::<MarkdownFlavor>().unwrap(), MarkdownFlavor::Pandoc);
345    }
346
347    #[test]
348    fn test_pandoc_name_and_display() {
349        assert_eq!(MarkdownFlavor::Pandoc.name(), "Pandoc");
350        assert_eq!(MarkdownFlavor::Pandoc.to_string(), "pandoc");
351    }
352
353    #[test]
354    fn test_from_extension_does_not_auto_detect_pandoc() {
355        // Pandoc files use .md — must NOT auto-detect to Pandoc.
356        assert_eq!(MarkdownFlavor::from_extension("md"), MarkdownFlavor::Standard);
357        assert_eq!(MarkdownFlavor::from_extension("markdown"), MarkdownFlavor::Standard);
358    }
359
360    #[test]
361    fn test_from_path_does_not_auto_detect_gh_aw() {
362        use std::path::Path;
363
364        assert_eq!(
365            MarkdownFlavor::from_path(Path::new(".github/workflows/triage.md")),
366            MarkdownFlavor::Standard
367        );
368    }
369
370    #[test]
371    fn test_gh_aw_name_and_serde() {
372        assert_eq!("GH-AW".parse::<MarkdownFlavor>().unwrap(), MarkdownFlavor::GhAw);
373        assert_eq!(MarkdownFlavor::GhAw.name(), "GitHub Agentic Workflows");
374        assert_eq!(
375            toml::Value::try_from(MarkdownFlavor::GhAw).unwrap().as_str(),
376            Some("gh-aw")
377        );
378    }
379
380    #[test]
381    fn test_mdg_aliases_and_name() {
382        assert_eq!("MDG".parse::<MarkdownFlavor>().unwrap(), MarkdownFlavor::MDG);
383        assert_eq!(
384            "markdown_with_gherkin".parse::<MarkdownFlavor>().unwrap(),
385            MarkdownFlavor::MDG
386        );
387        assert_eq!(MarkdownFlavor::MDG.name(), "Markdown with Gherkin");
388    }
389
390    #[test]
391    fn test_mdg_serde_names() {
392        assert_eq!(
393            MarkdownFlavor::deserialize(toml::Value::String("markdown_with_gherkin".to_string())).unwrap(),
394            MarkdownFlavor::MDG
395        );
396        assert_eq!(
397            toml::Value::try_from(MarkdownFlavor::MDG).unwrap().as_str(),
398            Some("mdg")
399        );
400    }
401
402    #[test]
403    fn test_from_path_detects_feature_md_compound_suffix() {
404        use std::path::Path;
405
406        assert_eq!(
407            MarkdownFlavor::from_path(Path::new("features/login.feature.md")),
408            MarkdownFlavor::MDG
409        );
410        assert_eq!(
411            MarkdownFlavor::from_path(Path::new("features/login.FEATURE.MD")),
412            MarkdownFlavor::MDG
413        );
414        assert_eq!(
415            MarkdownFlavor::from_path(Path::new("README.md")),
416            MarkdownFlavor::Standard
417        );
418        assert_eq!(
419            MarkdownFlavor::from_path(Path::new("features/login.feature.markdown")),
420            MarkdownFlavor::Standard
421        );
422    }
423
424    #[test]
425    fn test_is_pandoc_compatible() {
426        assert!(MarkdownFlavor::Pandoc.is_pandoc_compatible());
427        assert!(MarkdownFlavor::Quarto.is_pandoc_compatible());
428
429        assert!(!MarkdownFlavor::Standard.is_pandoc_compatible());
430        assert!(!MarkdownFlavor::MkDocs.is_pandoc_compatible());
431        assert!(!MarkdownFlavor::MDX.is_pandoc_compatible());
432        assert!(!MarkdownFlavor::Obsidian.is_pandoc_compatible());
433        assert!(!MarkdownFlavor::Kramdown.is_pandoc_compatible());
434    }
435
436    #[test]
437    fn test_azure_devops_from_str() {
438        assert_eq!(
439            "azure_devops".parse::<MarkdownFlavor>().unwrap(),
440            MarkdownFlavor::AzureDevOps
441        );
442        assert_eq!("azure".parse::<MarkdownFlavor>().unwrap(), MarkdownFlavor::AzureDevOps);
443        assert_eq!("ado".parse::<MarkdownFlavor>().unwrap(), MarkdownFlavor::AzureDevOps);
444        assert_eq!(
445            "AZURE_DEVOPS".parse::<MarkdownFlavor>().unwrap(),
446            MarkdownFlavor::AzureDevOps
447        );
448    }
449
450    #[test]
451    fn test_azure_devops_display_and_round_trip() {
452        assert_eq!(MarkdownFlavor::AzureDevOps.to_string(), "azure_devops");
453        let parsed: MarkdownFlavor = "azure_devops".parse().unwrap();
454        assert_eq!(parsed, MarkdownFlavor::AzureDevOps);
455    }
456
457    #[test]
458    fn test_supports_colon_code_fences() {
459        assert!(MarkdownFlavor::AzureDevOps.supports_colon_code_fences());
460        assert!(!MarkdownFlavor::Standard.supports_colon_code_fences());
461        assert!(!MarkdownFlavor::MkDocs.supports_colon_code_fences());
462        assert!(!MarkdownFlavor::MDX.supports_colon_code_fences());
463        assert!(!MarkdownFlavor::Pandoc.supports_colon_code_fences());
464        assert!(!MarkdownFlavor::Quarto.supports_colon_code_fences());
465        assert!(!MarkdownFlavor::Obsidian.supports_colon_code_fences());
466        assert!(!MarkdownFlavor::Kramdown.supports_colon_code_fences());
467        assert!(!MarkdownFlavor::MyST.supports_colon_code_fences());
468    }
469
470    #[test]
471    fn test_azure_devops_not_pandoc_compatible() {
472        assert!(!MarkdownFlavor::AzureDevOps.is_pandoc_compatible());
473    }
474
475    #[test]
476    fn test_display_all_variants_covers_azure_devops() {
477        let displayed = MarkdownFlavor::AzureDevOps.to_string();
478        assert!(displayed.chars().all(|c| !c.is_ascii_uppercase()));
479    }
480
481    #[test]
482    fn test_myst_from_str() {
483        assert_eq!("myst".parse::<MarkdownFlavor>().unwrap(), MarkdownFlavor::MyST);
484        assert_eq!("MYST".parse::<MarkdownFlavor>().unwrap(), MarkdownFlavor::MyST);
485        assert_eq!("mystmd".parse::<MarkdownFlavor>().unwrap(), MarkdownFlavor::MyST);
486    }
487
488    #[test]
489    fn test_myst_display_and_round_trip() {
490        assert_eq!(MarkdownFlavor::MyST.to_string(), "myst");
491        let parsed: MarkdownFlavor = "myst".parse().unwrap();
492        assert_eq!(parsed, MarkdownFlavor::MyST);
493    }
494
495    #[test]
496    fn test_myst_capabilities() {
497        assert!(MarkdownFlavor::MyST.supports_myst_directives());
498        assert!(MarkdownFlavor::MyST.supports_myst_roles());
499        assert!(MarkdownFlavor::MyST.supports_myst_comments());
500        assert!(!MarkdownFlavor::MyST.is_pandoc_compatible());
501        assert!(!MarkdownFlavor::MyST.supports_colon_code_fences());
502        assert!(!MarkdownFlavor::MyST.supports_jsx());
503
504        assert!(!MarkdownFlavor::Standard.supports_myst_directives());
505        assert!(!MarkdownFlavor::Standard.supports_myst_roles());
506        assert!(!MarkdownFlavor::Standard.supports_myst_comments());
507    }
508
509    #[test]
510    fn test_myst_name() {
511        assert_eq!(MarkdownFlavor::MyST.name(), "MyST");
512    }
513
514    #[test]
515    fn test_hugo_from_str() {
516        assert_eq!("hugo".parse::<MarkdownFlavor>().unwrap(), MarkdownFlavor::Hugo);
517        assert_eq!("HUGO".parse::<MarkdownFlavor>().unwrap(), MarkdownFlavor::Hugo);
518        assert_eq!("goldmark".parse::<MarkdownFlavor>().unwrap(), MarkdownFlavor::Hugo);
519    }
520
521    #[test]
522    fn test_hugo_display_name_and_round_trip() {
523        assert_eq!(MarkdownFlavor::Hugo.to_string(), "hugo");
524        assert_eq!(MarkdownFlavor::Hugo.name(), "Hugo");
525        let parsed: MarkdownFlavor = "hugo".parse().unwrap();
526        assert_eq!(parsed, MarkdownFlavor::Hugo);
527    }
528
529    #[test]
530    fn test_hugo_supports_attr_lists() {
531        // Hugo/Goldmark supports block attribute lists like MkDocs and Kramdown.
532        assert!(MarkdownFlavor::Hugo.supports_attr_lists());
533        assert!(MarkdownFlavor::MkDocs.supports_attr_lists());
534        assert!(MarkdownFlavor::Kramdown.supports_attr_lists());
535
536        // Standard Markdown does not: there, `{class="a"}` is literal text.
537        assert!(!MarkdownFlavor::Standard.supports_attr_lists());
538        assert!(!MarkdownFlavor::Pandoc.supports_attr_lists());
539
540        // Hugo is otherwise plain GFM.
541        assert!(!MarkdownFlavor::Hugo.is_pandoc_compatible());
542        assert!(!MarkdownFlavor::Hugo.supports_myst_directives());
543        assert!(!MarkdownFlavor::Hugo.supports_colon_code_fences());
544    }
545}