kdl/
fmt.rs

1use std::fmt::Write as _;
2
3/// Formatting configuration for use with [`KdlDocument::autoformat_config`](`crate::KdlDocument::autoformat_config`)
4/// and [`KdlNode::autoformat_config`](`crate::KdlNode::autoformat_config`).
5#[non_exhaustive]
6#[derive(Debug)]
7pub struct FormatConfig<'a> {
8    /// How deeply to indent the overall node or document,
9    /// in repetitions of [`indent`](`FormatConfig::indent`).
10    /// Defaults to `0`.
11    pub indent_level: usize,
12
13    /// The indentation to use at each level. Defaults to four spaces.
14    pub indent: &'a str,
15
16    /// Whether to remove comments. Defaults to `false`.
17    pub no_comments: bool,
18
19    /// Whether to keep individual entry formatting.
20    pub entry_autoformate_keep: bool,
21}
22
23/// See field documentation for defaults.
24impl Default for FormatConfig<'_> {
25    fn default() -> Self {
26        Self::builder().build()
27    }
28}
29
30impl FormatConfig<'_> {
31    /// Creates a new [`FormatConfigBuilder`] with default configuration.
32    pub const fn builder() -> FormatConfigBuilder<'static> {
33        FormatConfigBuilder::new()
34    }
35}
36
37/// A [`FormatConfig`] builder.
38///
39/// Note that setters can be repeated.
40#[derive(Debug, Default)]
41pub struct FormatConfigBuilder<'a>(FormatConfig<'a>);
42
43impl<'a> FormatConfigBuilder<'a> {
44    /// Creates a new [`FormatConfig`] builder with default configuration.
45    pub const fn new() -> Self {
46        Self(FormatConfig {
47            indent_level: 0,
48            indent: "    ",
49            no_comments: false,
50            entry_autoformate_keep: false,
51        })
52    }
53
54    /// How deeply to indent the overall node or document,
55    /// in repetitions of [`indent`](`FormatConfig::indent`).
56    /// Defaults to `0` iff not specified.
57    pub const fn maybe_indent_level(mut self, indent_level: Option<usize>) -> Self {
58        if let Some(indent_level) = indent_level {
59            self.0.indent_level = indent_level;
60        }
61        self
62    }
63
64    /// How deeply to indent the overall node or document,
65    /// in repetitions of [`indent`](`FormatConfig::indent`).
66    /// Defaults to `0` iff not specified.
67    pub const fn indent_level(mut self, indent_level: usize) -> Self {
68        self.0.indent_level = indent_level;
69        self
70    }
71
72    /// The indentation to use at each level.
73    /// Defaults to four spaces iff not specified.
74    pub const fn maybe_indent<'b, 'c>(self, indent: Option<&'b str>) -> FormatConfigBuilder<'c>
75    where
76        'a: 'b,
77        'b: 'c,
78    {
79        if let Some(indent) = indent {
80            self.indent(indent)
81        } else {
82            self
83        }
84    }
85
86    /// The indentation to use at each level.
87    /// Defaults to four spaces if not specified.
88    pub const fn indent(self, indent: &str) -> FormatConfigBuilder<'_> {
89        FormatConfigBuilder(FormatConfig { indent, ..self.0 })
90    }
91
92    /// Whether to remove comments.
93    /// Defaults to `false` iff not specified.
94    pub const fn maybe_no_comments(mut self, no_comments: Option<bool>) -> Self {
95        if let Some(no_comments) = no_comments {
96            self.0.no_comments = no_comments;
97        }
98        self
99    }
100
101    /// Whether to remove comments.
102    /// Defaults to `false` iff not specified.
103    pub const fn no_comments(mut self, no_comments: bool) -> Self {
104        self.0.no_comments = no_comments;
105        self
106    }
107
108    /// Builds the [`FormatConfig`].
109    pub const fn build(self) -> FormatConfig<'a> {
110        self.0
111    }
112}
113
114pub(crate) fn autoformat_leading(leading: &mut String, config: &FormatConfig<'_>) {
115    let mut result = String::new();
116    if !config.no_comments {
117        let input = leading.trim();
118        if !input.is_empty() {
119            for line in input.lines() {
120                let trimmed = line.trim();
121                if !trimmed.is_empty() {
122                    for _ in 0..config.indent_level {
123                        result.push_str(config.indent);
124                    }
125                    writeln!(result, "{trimmed}").unwrap();
126                }
127            }
128        }
129    }
130    for _ in 0..config.indent_level {
131        result.push_str(config.indent);
132    }
133    *leading = result;
134}
135
136pub(crate) fn autoformat_trailing(decor: &mut String, no_comments: bool) {
137    if decor.is_empty() {
138        return;
139    }
140    *decor = decor.trim().to_string();
141    let mut result = String::new();
142    if !decor.is_empty() && !no_comments {
143        if decor.trim_start() == &decor[..] {
144            write!(result, " ").unwrap();
145        }
146        for comment in decor.lines() {
147            writeln!(result, "{comment}").unwrap();
148        }
149    }
150    *decor = result;
151}
152
153#[cfg(test)]
154mod test {
155    use super::*;
156
157    #[test]
158    fn builder() -> miette::Result<()> {
159        let built = FormatConfig::builder()
160            .indent_level(12)
161            .indent(" \t")
162            .no_comments(true)
163            .build();
164        assert!(matches!(
165            built,
166            FormatConfig {
167                indent_level: 12,
168                indent: " \t",
169                no_comments: true,
170                entry_autoformate_keep: false,
171            }
172        ));
173        Ok(())
174    }
175}