1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::str::FromStr;
4
5#[derive(Debug, Clone, Copy, Default)]
12pub struct ConfigLoaded;
13
14#[derive(Debug, Clone, Copy, Default)]
17pub struct ConfigValidated;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
21#[serde(rename_all = "lowercase")]
22pub enum MarkdownFlavor {
23 #[serde(rename = "standard", alias = "none", alias = "")]
25 #[default]
26 Standard,
27 #[serde(rename = "mkdocs")]
29 MkDocs,
30 #[serde(rename = "mdx")]
32 MDX,
33 #[serde(rename = "quarto")]
35 Quarto,
36 #[serde(rename = "obsidian")]
38 Obsidian,
39 #[serde(rename = "kramdown")]
41 Kramdown,
42}
43
44fn markdown_flavor_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
46 schemars::json_schema!({
47 "description": "Markdown flavor/dialect. Accepts: standard, gfm, mkdocs, mdx, quarto, obsidian, kramdown. Aliases: commonmark/github map to standard, qmd/rmd/rmarkdown map to quarto, jekyll maps to kramdown.",
48 "type": "string",
49 "enum": ["standard", "gfm", "github", "commonmark", "mkdocs", "mdx", "quarto", "qmd", "rmd", "rmarkdown", "obsidian", "kramdown", "jekyll"]
50 })
51}
52
53impl schemars::JsonSchema for MarkdownFlavor {
54 fn schema_name() -> std::borrow::Cow<'static, str> {
55 std::borrow::Cow::Borrowed("MarkdownFlavor")
56 }
57
58 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
59 markdown_flavor_schema(generator)
60 }
61}
62
63impl fmt::Display for MarkdownFlavor {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 MarkdownFlavor::Standard => write!(f, "standard"),
67 MarkdownFlavor::MkDocs => write!(f, "mkdocs"),
68 MarkdownFlavor::MDX => write!(f, "mdx"),
69 MarkdownFlavor::Quarto => write!(f, "quarto"),
70 MarkdownFlavor::Obsidian => write!(f, "obsidian"),
71 MarkdownFlavor::Kramdown => write!(f, "kramdown"),
72 }
73 }
74}
75
76impl FromStr for MarkdownFlavor {
77 type Err = String;
78
79 fn from_str(s: &str) -> Result<Self, Self::Err> {
80 match s.to_lowercase().as_str() {
81 "standard" | "" | "none" => Ok(MarkdownFlavor::Standard),
82 "mkdocs" => Ok(MarkdownFlavor::MkDocs),
83 "mdx" => Ok(MarkdownFlavor::MDX),
84 "quarto" | "qmd" | "rmd" | "rmarkdown" => Ok(MarkdownFlavor::Quarto),
85 "obsidian" => Ok(MarkdownFlavor::Obsidian),
86 "kramdown" | "jekyll" => Ok(MarkdownFlavor::Kramdown),
87 "gfm" | "github" | "commonmark" => Ok(MarkdownFlavor::Standard),
91 _ => Err(format!("Unknown markdown flavor: {s}")),
92 }
93 }
94}
95
96impl MarkdownFlavor {
97 pub fn from_extension(ext: &str) -> Self {
99 match ext.to_lowercase().as_str() {
100 "mdx" => Self::MDX,
101 "qmd" => Self::Quarto,
102 "rmd" => Self::Quarto,
103 "kramdown" => Self::Kramdown,
104 _ => Self::Standard,
105 }
106 }
107
108 pub fn from_path(path: &std::path::Path) -> Self {
110 path.extension()
111 .and_then(|e| e.to_str())
112 .map_or(Self::Standard, Self::from_extension)
113 }
114
115 pub fn supports_esm_blocks(self) -> bool {
117 matches!(self, Self::MDX)
118 }
119
120 pub fn supports_jsx(self) -> bool {
122 matches!(self, Self::MDX)
123 }
124
125 pub fn supports_auto_references(self) -> bool {
127 matches!(self, Self::MkDocs)
128 }
129
130 pub fn supports_kramdown_syntax(self) -> bool {
132 matches!(self, Self::Kramdown)
133 }
134
135 pub fn supports_attr_lists(self) -> bool {
137 matches!(self, Self::MkDocs | Self::Kramdown)
138 }
139
140 pub fn requires_strict_list_indent(self) -> bool {
145 matches!(self, Self::MkDocs)
146 }
147
148 pub fn name(self) -> &'static str {
150 match self {
151 Self::Standard => "Standard",
152 Self::MkDocs => "MkDocs",
153 Self::MDX => "MDX",
154 Self::Quarto => "Quarto",
155 Self::Obsidian => "Obsidian",
156 Self::Kramdown => "Kramdown",
157 }
158 }
159}
160
161pub fn normalize_key(key: &str) -> String {
163 if key.len() == 5 && key.to_ascii_lowercase().starts_with("md") && key[2..].chars().all(|c| c.is_ascii_digit()) {
165 key.to_ascii_uppercase()
166 } else {
167 key.replace('_', "-").to_ascii_lowercase()
168 }
169}
170
171pub(super) fn warn_comma_without_brace_in_pattern(pattern: &str, config_file: &str) {
175 if pattern.contains(',') && !pattern.contains('{') {
176 eprintln!("Warning: Pattern \"{pattern}\" in {config_file} contains a comma but no braces.");
177 eprintln!(" To match multiple files, use brace expansion: \"{{{pattern}}}\"");
178 eprintln!(" Or use separate entries for each file.");
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
190 fn test_display_all_variants_are_lowercase() {
191 let cases = [
192 (MarkdownFlavor::Standard, "standard"),
193 (MarkdownFlavor::MkDocs, "mkdocs"),
194 (MarkdownFlavor::MDX, "mdx"),
195 (MarkdownFlavor::Quarto, "quarto"),
196 (MarkdownFlavor::Obsidian, "obsidian"),
197 (MarkdownFlavor::Kramdown, "kramdown"),
198 ];
199 for (variant, expected) in cases {
200 let displayed = variant.to_string();
201 assert_eq!(
202 displayed, expected,
203 "MarkdownFlavor::{variant:?} Display should produce \"{expected}\", got \"{displayed}\""
204 );
205 assert!(
207 displayed.chars().all(|c| !c.is_ascii_uppercase()),
208 "MarkdownFlavor::{variant:?} Display must be entirely lowercase, got \"{displayed}\""
209 );
210 }
211 }
212
213 #[test]
216 fn test_display_round_trips_through_from_str() {
217 let variants = [
218 MarkdownFlavor::Standard,
219 MarkdownFlavor::MkDocs,
220 MarkdownFlavor::MDX,
221 MarkdownFlavor::Quarto,
222 MarkdownFlavor::Obsidian,
223 MarkdownFlavor::Kramdown,
224 ];
225 for variant in variants {
226 let displayed = variant.to_string();
227 let parsed: MarkdownFlavor = displayed
228 .parse()
229 .unwrap_or_else(|e| panic!("Display string \"{displayed}\" for {variant:?} failed to parse back: {e}"));
230 assert_eq!(
231 parsed, variant,
232 "Display(\"{displayed}\") for {variant:?} round-trips to a different variant: {parsed:?}"
233 );
234 }
235 }
236}