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 = "pandoc")]
36 Pandoc,
37 #[serde(rename = "quarto")]
39 Quarto,
40 #[serde(rename = "obsidian")]
42 Obsidian,
43 #[serde(rename = "kramdown")]
45 Kramdown,
46}
47
48fn markdown_flavor_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
50 schemars::json_schema!({
51 "description": "Markdown flavor/dialect. Accepts: standard, gfm, mkdocs, mdx, pandoc, quarto, obsidian, kramdown. Aliases: commonmark/github map to standard, qmd/rmd/rmarkdown map to quarto, jekyll maps to kramdown.",
52 "type": "string",
53 "enum": ["standard", "gfm", "github", "commonmark", "mkdocs", "mdx", "pandoc", "quarto", "qmd", "rmd", "rmarkdown", "obsidian", "kramdown", "jekyll"]
54 })
55}
56
57impl schemars::JsonSchema for MarkdownFlavor {
58 fn schema_name() -> std::borrow::Cow<'static, str> {
59 std::borrow::Cow::Borrowed("MarkdownFlavor")
60 }
61
62 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
63 markdown_flavor_schema(generator)
64 }
65}
66
67impl fmt::Display for MarkdownFlavor {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 match self {
70 MarkdownFlavor::Standard => write!(f, "standard"),
71 MarkdownFlavor::MkDocs => write!(f, "mkdocs"),
72 MarkdownFlavor::MDX => write!(f, "mdx"),
73 MarkdownFlavor::Pandoc => write!(f, "pandoc"),
74 MarkdownFlavor::Quarto => write!(f, "quarto"),
75 MarkdownFlavor::Obsidian => write!(f, "obsidian"),
76 MarkdownFlavor::Kramdown => write!(f, "kramdown"),
77 }
78 }
79}
80
81impl FromStr for MarkdownFlavor {
82 type Err = String;
83
84 fn from_str(s: &str) -> Result<Self, Self::Err> {
85 match s.to_lowercase().as_str() {
86 "standard" | "" | "none" => Ok(MarkdownFlavor::Standard),
87 "mkdocs" => Ok(MarkdownFlavor::MkDocs),
88 "mdx" => Ok(MarkdownFlavor::MDX),
89 "pandoc" => Ok(MarkdownFlavor::Pandoc),
90 "quarto" | "qmd" | "rmd" | "rmarkdown" => Ok(MarkdownFlavor::Quarto),
91 "obsidian" => Ok(MarkdownFlavor::Obsidian),
92 "kramdown" | "jekyll" => Ok(MarkdownFlavor::Kramdown),
93 "gfm" | "github" | "commonmark" => Ok(MarkdownFlavor::Standard),
97 _ => Err(format!("Unknown markdown flavor: {s}")),
98 }
99 }
100}
101
102impl MarkdownFlavor {
103 pub fn from_extension(ext: &str) -> Self {
105 match ext.to_lowercase().as_str() {
106 "mdx" => Self::MDX,
107 "qmd" => Self::Quarto,
108 "rmd" => Self::Quarto,
109 "kramdown" => Self::Kramdown,
110 _ => Self::Standard,
111 }
112 }
113
114 pub fn from_path(path: &std::path::Path) -> Self {
116 path.extension()
117 .and_then(|e| e.to_str())
118 .map_or(Self::Standard, Self::from_extension)
119 }
120
121 pub fn supports_esm_blocks(self) -> bool {
123 matches!(self, Self::MDX)
124 }
125
126 pub fn supports_jsx(self) -> bool {
128 matches!(self, Self::MDX)
129 }
130
131 pub fn supports_auto_references(self) -> bool {
133 matches!(self, Self::MkDocs)
134 }
135
136 pub fn supports_kramdown_syntax(self) -> bool {
138 matches!(self, Self::Kramdown)
139 }
140
141 pub fn supports_attr_lists(self) -> bool {
143 matches!(self, Self::MkDocs | Self::Kramdown)
144 }
145
146 pub fn requires_strict_list_indent(self) -> bool {
151 matches!(self, Self::MkDocs)
152 }
153
154 pub fn is_pandoc_compatible(self) -> bool {
158 matches!(self, Self::Pandoc | Self::Quarto)
159 }
160
161 pub fn name(self) -> &'static str {
163 match self {
164 Self::Standard => "Standard",
165 Self::MkDocs => "MkDocs",
166 Self::MDX => "MDX",
167 Self::Pandoc => "Pandoc",
168 Self::Quarto => "Quarto",
169 Self::Obsidian => "Obsidian",
170 Self::Kramdown => "Kramdown",
171 }
172 }
173}
174
175pub fn normalize_key(key: &str) -> String {
177 if key.len() == 5 && key.to_ascii_lowercase().starts_with("md") && key[2..].chars().all(|c| c.is_ascii_digit()) {
179 key.to_ascii_uppercase()
180 } else {
181 key.replace('_', "-").to_ascii_lowercase()
182 }
183}
184
185pub(super) fn warn_comma_without_brace_in_pattern(pattern: &str, config_file: &str) {
189 if pattern.contains(',') && !pattern.contains('{') {
190 eprintln!("Warning: Pattern \"{pattern}\" in {config_file} contains a comma but no braces.");
191 eprintln!(" To match multiple files, use brace expansion: \"{{{pattern}}}\"");
192 eprintln!(" Or use separate entries for each file.");
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
204 fn test_display_all_variants_are_lowercase() {
205 let cases = [
206 (MarkdownFlavor::Standard, "standard"),
207 (MarkdownFlavor::MkDocs, "mkdocs"),
208 (MarkdownFlavor::MDX, "mdx"),
209 (MarkdownFlavor::Pandoc, "pandoc"),
210 (MarkdownFlavor::Quarto, "quarto"),
211 (MarkdownFlavor::Obsidian, "obsidian"),
212 (MarkdownFlavor::Kramdown, "kramdown"),
213 ];
214 for (variant, expected) in cases {
215 let displayed = variant.to_string();
216 assert_eq!(
217 displayed, expected,
218 "MarkdownFlavor::{variant:?} Display should produce \"{expected}\", got \"{displayed}\""
219 );
220 assert!(
222 displayed.chars().all(|c| !c.is_ascii_uppercase()),
223 "MarkdownFlavor::{variant:?} Display must be entirely lowercase, got \"{displayed}\""
224 );
225 }
226 }
227
228 #[test]
231 fn test_display_round_trips_through_from_str() {
232 let variants = [
233 MarkdownFlavor::Standard,
234 MarkdownFlavor::MkDocs,
235 MarkdownFlavor::MDX,
236 MarkdownFlavor::Pandoc,
237 MarkdownFlavor::Quarto,
238 MarkdownFlavor::Obsidian,
239 MarkdownFlavor::Kramdown,
240 ];
241 for variant in variants {
242 let displayed = variant.to_string();
243 let parsed: MarkdownFlavor = displayed
244 .parse()
245 .unwrap_or_else(|e| panic!("Display string \"{displayed}\" for {variant:?} failed to parse back: {e}"));
246 assert_eq!(
247 parsed, variant,
248 "Display(\"{displayed}\") for {variant:?} round-trips to a different variant: {parsed:?}"
249 );
250 }
251 }
252
253 #[test]
254 fn test_pandoc_from_str() {
255 assert_eq!("pandoc".parse::<MarkdownFlavor>().unwrap(), MarkdownFlavor::Pandoc);
256 assert_eq!("PANDOC".parse::<MarkdownFlavor>().unwrap(), MarkdownFlavor::Pandoc);
257 }
258
259 #[test]
260 fn test_pandoc_name_and_display() {
261 assert_eq!(MarkdownFlavor::Pandoc.name(), "Pandoc");
262 assert_eq!(MarkdownFlavor::Pandoc.to_string(), "pandoc");
263 }
264
265 #[test]
266 fn test_from_extension_does_not_auto_detect_pandoc() {
267 assert_eq!(MarkdownFlavor::from_extension("md"), MarkdownFlavor::Standard);
269 assert_eq!(MarkdownFlavor::from_extension("markdown"), MarkdownFlavor::Standard);
270 }
271
272 #[test]
273 fn test_is_pandoc_compatible() {
274 assert!(MarkdownFlavor::Pandoc.is_pandoc_compatible());
275 assert!(MarkdownFlavor::Quarto.is_pandoc_compatible());
276
277 assert!(!MarkdownFlavor::Standard.is_pandoc_compatible());
278 assert!(!MarkdownFlavor::MkDocs.is_pandoc_compatible());
279 assert!(!MarkdownFlavor::MDX.is_pandoc_compatible());
280 assert!(!MarkdownFlavor::Obsidian.is_pandoc_compatible());
281 assert!(!MarkdownFlavor::Kramdown.is_pandoc_compatible());
282 }
283}