diff --git a/src/entry.rs b/src/entry.rs index 94f87b8..42398b7 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -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. pub fn autoformat(&mut self) { // TODO once MSRV allows (1.80.0): diff --git a/src/fmt.rs b/src/fmt.rs index 238fb75..4abff1d 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -1,5 +1,17 @@ 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`) /// and [`KdlNode::autoformat_config`](`crate::KdlNode::autoformat_config`). #[non_exhaustive] @@ -18,6 +30,9 @@ pub struct FormatConfig<'a> { /// Whether to keep individual entry formatting. pub entry_autoformate_keep: bool, + + /// How to handle multiline strings during formatting. + pub expand_multiline: MultilineStringExpansion, } /// See field documentation for defaults. @@ -48,6 +63,7 @@ impl<'a> FormatConfigBuilder<'a> { indent: " ", no_comments: false, entry_autoformate_keep: false, + expand_multiline: MultilineStringExpansion::Never, }) } @@ -105,6 +121,13 @@ impl<'a> FormatConfigBuilder<'a> { 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`]. pub const fn build(self) -> FormatConfig<'a> { self.0 @@ -168,6 +191,7 @@ mod test { indent: " \t", no_comments: true, entry_autoformate_keep: false, + expand_multiline: MultilineStringExpansion::Never, } )); Ok(()) diff --git a/src/node.rs b/src/node.rs index af9914b..a0cb56a 100644 --- a/src/node.rs +++ b/src/node.rs @@ -311,8 +311,10 @@ impl KdlNode { for entry in &mut self.entries { if config.entry_autoformate_keep { entry.keep_format(); + entry.autoformat(); + } else { + entry.autoformat_with_multiline(config.expand_multiline); } - entry.autoformat(); } if let Some(children) = self.children.as_mut() { children.autoformat_config(&FormatConfig { diff --git a/tests/formatting.rs b/tests/formatting.rs index a554aae..f9fd007 100644 --- a/tests/formatting.rs +++ b/tests/formatting.rs @@ -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(()) +}