Skip to main content

panache_formatter/
config.rs

1use std::collections::HashMap;
2
3pub use panache_parser::Dialect;
4pub use panache_parser::Extensions;
5pub use panache_parser::Extensions as ParserExtensions;
6pub use panache_parser::Flavor;
7pub use panache_parser::PandocCompat;
8pub use panache_parser::ParserOptions;
9
10fn default_external_max_parallel() -> usize {
11    std::thread::available_parallelism()
12        .map(|n| n.get())
13        .unwrap_or(1)
14        .clamp(1, 8)
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum MathDelimiterStyle {
19    /// Preserve original delimiter style (\(...\) stays \(...\), $...$ stays $...$)
20    #[default]
21    Preserve,
22    /// Normalize all to dollar syntax ($...$ and $$...$$)
23    Dollars,
24    /// Normalize all to backslash syntax (\(...\) and \[...\])
25    Backslash,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum TabStopMode {
30    /// Normalize tabs to spaces (4-column tab stop).
31    #[default]
32    Normalize,
33    /// Preserve tabs in literal code spans/blocks.
34    Preserve,
35}
36
37#[derive(Debug, Clone, PartialEq)]
38pub struct FormatterConfig {
39    pub cmd: String,
40    pub args: Vec<String>,
41    pub enabled: bool,
42    pub stdin: bool,
43}
44
45#[derive(Debug, Clone, PartialEq)]
46pub enum WrapMode {
47    Preserve,
48    Reflow,
49    Sentence,
50}
51
52#[derive(Debug, Clone, PartialEq)]
53pub enum LineEnding {
54    Auto,
55    Lf,
56    Crlf,
57}
58
59#[derive(Debug, Clone, PartialEq)]
60pub enum BlankLines {
61    /// Preserve original blank lines (any number)
62    Preserve,
63    /// Collapse multiple consecutive blank lines to a single blank line
64    Collapse,
65}
66
67#[derive(Debug, Clone, PartialEq)]
68pub struct FormatterExtensions {
69    pub blank_before_header: bool,
70    pub bookdown_references: bool,
71    pub escaped_line_breaks: bool,
72    pub gfm_auto_identifiers: bool,
73    pub quarto_crossrefs: bool,
74    pub smart: bool,
75    pub smart_quotes: bool,
76}
77
78impl Default for FormatterExtensions {
79    fn default() -> Self {
80        Self::for_flavor(Flavor::default())
81    }
82}
83
84impl FormatterExtensions {
85    pub fn for_flavor(flavor: Flavor) -> Self {
86        let parser_defaults = ParserExtensions::for_flavor(flavor);
87        let smart_default = matches!(flavor, Flavor::Pandoc | Flavor::Quarto | Flavor::RMarkdown);
88
89        Self {
90            blank_before_header: parser_defaults.blank_before_header,
91            bookdown_references: parser_defaults.bookdown_references,
92            escaped_line_breaks: parser_defaults.escaped_line_breaks,
93            gfm_auto_identifiers: parser_defaults.gfm_auto_identifiers,
94            quarto_crossrefs: parser_defaults.quarto_crossrefs,
95            smart: smart_default,
96            smart_quotes: false,
97        }
98    }
99
100    pub fn merge_with_flavor(overrides: HashMap<String, bool>, flavor: Flavor) -> Self {
101        let mut base = Self::for_flavor(flavor);
102        for (key, value) in overrides {
103            match key.replace('_', "-").to_ascii_lowercase().as_str() {
104                "blank-before-header" => base.blank_before_header = value,
105                "bookdown-references" => base.bookdown_references = value,
106                "escaped-line-breaks" => base.escaped_line_breaks = value,
107                "gfm-auto-identifiers" => base.gfm_auto_identifiers = value,
108                "quarto-crossrefs" => base.quarto_crossrefs = value,
109                "smart" => base.smart = value,
110                "smart-quotes" => base.smart_quotes = value,
111                _ => {}
112            }
113        }
114        base
115    }
116}
117
118#[derive(Debug, Clone)]
119pub struct Config {
120    pub flavor: Flavor,
121    pub parser_extensions: ParserExtensions,
122    pub formatter_extensions: FormatterExtensions,
123    pub line_ending: Option<LineEnding>,
124    pub line_width: usize,
125    pub math_indent: usize,
126    pub math_delimiter_style: MathDelimiterStyle,
127    pub tab_stops: TabStopMode,
128    pub tab_width: usize,
129    pub wrap: Option<WrapMode>,
130    pub blank_lines: BlankLines,
131    /// Language → Formatter(s) mapping (supports multiple formatters per language)
132    pub formatters: HashMap<String, Vec<FormatterConfig>>,
133    /// Max parallel external tool invocations (formatters/linters) per document.
134    pub external_max_parallel: usize,
135    /// Compatibility target for ambiguous Pandoc behavior.
136    pub parser: PandocCompat,
137}
138
139impl Default for Config {
140    fn default() -> Self {
141        let flavor = Flavor::default();
142        Self {
143            flavor,
144            parser_extensions: ParserExtensions::for_flavor(flavor),
145            formatter_extensions: FormatterExtensions::for_flavor(flavor),
146            line_ending: Some(LineEnding::Auto),
147            line_width: 80,
148            math_indent: 0,
149            math_delimiter_style: MathDelimiterStyle::default(),
150            tab_stops: TabStopMode::Normalize,
151            tab_width: 4,
152            wrap: Some(WrapMode::Reflow),
153            blank_lines: BlankLines::Collapse,
154            formatters: HashMap::new(), // Opt-in: empty by default
155            external_max_parallel: default_external_max_parallel(),
156            parser: PandocCompat::default(),
157        }
158    }
159}
160
161impl Config {
162    pub fn parser_options(&self) -> ParserOptions {
163        ParserOptions {
164            flavor: self.flavor,
165            dialect: Dialect::for_flavor(self.flavor),
166            extensions: self.parser_extensions.clone(),
167            pandoc_compat: self.parser,
168            refdef_labels: None,
169        }
170    }
171}
172
173#[derive(Default, Clone)]
174pub struct ConfigBuilder {
175    config: Config,
176}
177
178impl ConfigBuilder {
179    pub fn math_indent(mut self, indent: usize) -> Self {
180        self.config.math_indent = indent;
181        self
182    }
183
184    pub fn tab_stops(mut self, mode: TabStopMode) -> Self {
185        self.config.tab_stops = mode;
186        self
187    }
188
189    pub fn tab_width(mut self, width: usize) -> Self {
190        self.config.tab_width = width;
191        self
192    }
193
194    pub fn line_width(mut self, width: usize) -> Self {
195        self.config.line_width = width;
196        self
197    }
198
199    pub fn line_ending(mut self, ending: LineEnding) -> Self {
200        self.config.line_ending = Some(ending);
201        self
202    }
203
204    pub fn blank_lines(mut self, mode: BlankLines) -> Self {
205        self.config.blank_lines = mode;
206        self
207    }
208
209    pub fn build(self) -> Config {
210        self.config
211    }
212}