feat: control format of multi-line strings

This commit is contained in:
dougefresh 2026-01-03 08:26:58 +00:00
parent 268f3a2d00
commit 1aaa178d00
No known key found for this signature in database
GPG Key ID: 3A4FC51F203786D1
4 changed files with 173 additions and 1 deletions

View File

@ -169,6 +169,59 @@ impl KdlEntry {
} }
} }
/// Auto-formats this entry with multiline string handling.
pub(crate) fn autoformat_with_multiline(
&mut self,
expand: crate::fmt::MultilineStringExpansion,
) {
use crate::fmt::MultilineStringExpansion::*;
let should_preserve_repr = match expand {
Never => false,
Always => {
if let Some(s) = self.value.as_string() {
if s.contains('\n') {
// Convert to multiline format
let multiline = format!("\"\"\"\n{}\n\"\"\"", s);
#[cfg(feature = "v1")]
self.ensure_v2();
self.format = Some(KdlEntryFormat {
value_repr: multiline,
leading: " ".into(),
..Default::default()
});
if let Some(name) = &mut self.name {
name.autoformat();
}
return;
}
}
false
}
TripleQuotes => self
.format
.as_ref()
.is_some_and(|f| f.value_repr.starts_with("\"\"\"")),
};
if should_preserve_repr {
#[cfg(feature = "v1")]
self.ensure_v2();
self.format = self.format.take().map(|f| KdlEntryFormat {
value_repr: f.value_repr,
leading: " ".into(),
..Default::default()
});
} else {
self.autoformat();
return;
}
if let Some(name) = &mut self.name {
name.autoformat();
}
}
/// Auto-formats this entry. /// Auto-formats this entry.
pub fn autoformat(&mut self) { pub fn autoformat(&mut self) {
// TODO once MSRV allows (1.80.0): // TODO once MSRV allows (1.80.0):

View File

@ -1,5 +1,17 @@
use std::fmt::Write as _; use std::fmt::Write as _;
/// Controls how multiline strings are handled during formatting.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MultilineStringExpansion {
/// Keep strings as-is (current behavior).
#[default]
Never,
/// Convert any string with `\n` to `"""..."""` syntax.
Always,
/// Only preserve existing `"""..."""` syntax.
TripleQuotes,
}
/// Formatting configuration for use with [`KdlDocument::autoformat_config`](`crate::KdlDocument::autoformat_config`) /// Formatting configuration for use with [`KdlDocument::autoformat_config`](`crate::KdlDocument::autoformat_config`)
/// and [`KdlNode::autoformat_config`](`crate::KdlNode::autoformat_config`). /// and [`KdlNode::autoformat_config`](`crate::KdlNode::autoformat_config`).
#[non_exhaustive] #[non_exhaustive]
@ -18,6 +30,9 @@ pub struct FormatConfig<'a> {
/// Whether to keep individual entry formatting. /// Whether to keep individual entry formatting.
pub entry_autoformate_keep: bool, pub entry_autoformate_keep: bool,
/// How to handle multiline strings during formatting.
pub expand_multiline: MultilineStringExpansion,
} }
/// See field documentation for defaults. /// See field documentation for defaults.
@ -48,6 +63,7 @@ impl<'a> FormatConfigBuilder<'a> {
indent: " ", indent: " ",
no_comments: false, no_comments: false,
entry_autoformate_keep: false, entry_autoformate_keep: false,
expand_multiline: MultilineStringExpansion::Never,
}) })
} }
@ -105,6 +121,13 @@ impl<'a> FormatConfigBuilder<'a> {
self self
} }
/// How to handle multiline strings during formatting.
/// Defaults to `Never` iff not specified.
pub const fn expand_multiline(mut self, expand_multiline: MultilineStringExpansion) -> Self {
self.0.expand_multiline = expand_multiline;
self
}
/// Builds the [`FormatConfig`]. /// Builds the [`FormatConfig`].
pub const fn build(self) -> FormatConfig<'a> { pub const fn build(self) -> FormatConfig<'a> {
self.0 self.0
@ -168,6 +191,7 @@ mod test {
indent: " \t", indent: " \t",
no_comments: true, no_comments: true,
entry_autoformate_keep: false, entry_autoformate_keep: false,
expand_multiline: MultilineStringExpansion::Never,
} }
)); ));
Ok(()) Ok(())

View File

@ -311,8 +311,10 @@ impl KdlNode {
for entry in &mut self.entries { for entry in &mut self.entries {
if config.entry_autoformate_keep { if config.entry_autoformate_keep {
entry.keep_format(); entry.keep_format();
entry.autoformat();
} else {
entry.autoformat_with_multiline(config.expand_multiline);
} }
entry.autoformat();
} }
if let Some(children) = self.children.as_mut() { if let Some(children) = self.children.as_mut() {
children.autoformat_config(&FormatConfig { children.autoformat_config(&FormatConfig {

View File

@ -24,3 +24,96 @@ fn build_and_format() {
"# "#
); );
} }
#[test]
fn multiline_string_never() -> miette::Result<()> {
let input = r#"multi """
a
b
c
"""
multi-explicit "a\nb\nc""#;
let mut doc: KdlDocument = input.parse()?;
doc.autoformat();
let output = doc.to_string();
assert_eq!(
output,
"multi \"a\\nb\\nc\"\nmulti-explicit \"a\\nb\\nc\"\n"
);
Ok(())
}
#[test]
fn multiline_string_triple_quotes() -> miette::Result<()> {
let input = r#"multi """
a
b
c
"""
multi-explicit "a\nb\nc""#;
let mut doc: KdlDocument = input.parse()?;
doc.autoformat_config(
&kdl::FormatConfig::builder()
.expand_multiline(kdl::MultilineStringExpansion::TripleQuotes)
.build(),
);
let output = doc.to_string();
assert!(output.contains("multi \"\"\""));
assert!(output.contains("multi-explicit \"a\\nb\\nc\""));
Ok(())
}
#[test]
fn multiline_string_always() -> miette::Result<()> {
let input = r#"multi """
a
b
c
"""
multi-explicit "a\nb\nc""#;
let mut doc: KdlDocument = input.parse()?;
doc.autoformat_config(
&kdl::FormatConfig::builder()
.expand_multiline(kdl::MultilineStringExpansion::Always)
.build(),
);
let output = doc.to_string();
assert!(output.contains("multi \"\"\""));
assert!(output.contains("multi-explicit \"\"\""));
Ok(())
}
#[test]
fn multiline_string_nested_triple_quotes() -> miette::Result<()> {
let input = r#"some-node {
multi """
a
b
c
"""
explicit "a\nb\nc"
}"#;
let mut doc: KdlDocument = input.parse()?;
doc.autoformat_config(
&kdl::FormatConfig::builder()
.expand_multiline(kdl::MultilineStringExpansion::TripleQuotes)
.build(),
);
let output = doc.to_string();
// Multiline string should be preserved
assert!(output.contains("multi \"\"\""));
// Regular string should be normalized
assert!(output.contains("explicit \"a\\nb\\nc\""));
// Indentation should be normalized to 4 spaces
assert!(output.contains(" multi"));
assert!(output.contains(" explicit"));
Ok(())
}