From c2f219898a1219da66f66f0c090f56bdd6a3bd85 Mon Sep 17 00:00:00 2001 From: Pavel Ivanov Date: Mon, 5 Jan 2026 13:43:42 +0100 Subject: [PATCH] fix: fix formatting of whitespace-only values --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/formatting.rs | 36 ++++++++++++++++++++++++++---------- src/formatting/tests.rs | 24 +++++++++++++++++++----- 4 files changed, 47 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0768c80d..766a554d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1129,7 +1129,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hl" -version = "0.34.1-alpha.2" +version = "0.34.1-alpha.3" dependencies = [ "anstream", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 5242ab2d..eeaee031 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ members = [ ] [workspace.package] -version = "0.34.1-alpha.2" +version = "0.34.1-alpha.3" edition = "2024" repository = "https://github.com/pamburus/hl" license = "MIT" diff --git a/src/formatting.rs b/src/formatting.rs index eefbe132..424d9532 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -1,6 +1,9 @@ // std imports use std::sync::Arc; +// third-party imports +use enumset::{EnumSet, EnumSetType}; + // workspace imports use encstr::EncodedString; @@ -745,29 +748,37 @@ impl<'a> FieldFormatter<'a> { // --- -pub trait WithAutoTrim { - fn with_auto_trim(&mut self, f: F) -> R +trait WithAutoTrim { + fn with_auto_trim(&mut self, f: F, flags: impl Into) -> R where F: FnOnce(&mut Self) -> R; } impl WithAutoTrim for Vec { #[inline(always)] - fn with_auto_trim(&mut self, f: F) -> R + fn with_auto_trim(&mut self, f: F, flags: impl Into) -> R where F: FnOnce(&mut Self) -> R, { + let flags = flags.into(); let begin = self.len(); let result = f(self); if let Some(end) = self[begin..].iter().rposition(|&b| !b.is_ascii_whitespace()) { self.truncate(begin + end + 1); - } else { + } else if !flags.contains(AutoTrimFlag::PreserveWhiteSpaceOnly) { self.truncate(begin); } result } } +#[derive(EnumSetType, Debug)] +enum AutoTrimFlag { + PreserveWhiteSpaceOnly, +} + +type AutoTrimFlags = EnumSet; + // --- trait KeyPrettify { @@ -810,7 +821,7 @@ pub mod string { // local imports use crate::{ - formatting::WithAutoTrim, + formatting::{AutoTrimFlag, AutoTrimFlags, WithAutoTrim}, model::{MAX_NUMBER_LEN, looks_like_number}, settings::MessageFormat, }; @@ -907,7 +918,10 @@ pub mod string { } let begin = buf.len(); - buf.with_auto_trim(|buf| ValueFormatRaw.format(input, buf))?; + buf.with_auto_trim( + |buf| ValueFormatRaw.format(input, buf), + AutoTrimFlag::PreserveWhiteSpaceOnly, + )?; let mut mask = Mask::empty(); @@ -952,7 +966,9 @@ pub mod string { return Ok(()); } - if !mask.intersects(Flag::Backtick | Flag::Control) { + const WS: Mask = mask!(Flag::NewLine | Flag::Tab | Flag::Space); + + if !mask.intersects(Flag::Backtick | Flag::Control) && mask.intersects(!WS) { buf.push(b'`'); buf.push(b'`'); buf[begin..].rotate_right(1); @@ -998,7 +1014,7 @@ pub mod string { } let begin = buf.len(); - buf.with_auto_trim(|buf| MessageFormatRaw.format(input, buf))?; + buf.with_auto_trim(|buf| MessageFormatRaw.format(input, buf), AutoTrimFlags::empty())?; let mut mask = Mask::empty(); @@ -1051,7 +1067,7 @@ pub mod string { let begin = buf.len(); buf.push(b'"'); - buf.with_auto_trim(|buf| MessageFormatRaw.format(input, buf))?; + buf.with_auto_trim(|buf| MessageFormatRaw.format(input, buf), AutoTrimFlags::empty())?; let mut mask = Mask::empty(); @@ -1100,7 +1116,7 @@ pub mod string { } let begin = buf.len(); - buf.with_auto_trim(|buf| MessageFormatRaw.format(input, buf))?; + buf.with_auto_trim(|buf| MessageFormatRaw.format(input, buf), AutoTrimFlags::empty())?; let mut mask = Mask::empty(); diff --git a/src/formatting/tests.rs b/src/formatting/tests.rs index 01927e58..9b912cca 100644 --- a/src/formatting/tests.rs +++ b/src/formatting/tests.rs @@ -1178,9 +1178,9 @@ mod string { #[case::trailing_newline("hello\n", "hello")] #[case::trailing_crlf("hello\r\n", "hello")] #[case::multiple_trailing("text \t\n", "text")] - // Only whitespace after trimming still produces output, just empty after trim - #[case::only_spaces(" ", "")] - #[case::only_tabs("\t\t", "")] + // Only whitespace requires quoting + #[case::only_spaces(" ", r#"" ""#)] + #[case::only_tabs("\t\t", r#""\t\t""#)] // Leading whitespace is preserved and triggers quoting #[case::leading_space(" hello", r#"" hello""#)] // Leading tab uses backticks @@ -1205,7 +1205,21 @@ mod string { } // --- - // Test 8: ValueFormatAuto special characters and edge cases + // Test 8: ValueFormatAuto error handling + // --- + + #[test] + fn test_value_format_auto_invalid_json() { + use encstr::json::JsonEncodedString; + + let invalid_json = JsonEncodedString::new(r#""invalid\xZZ""#); + let mut buf = Vec::new(); + let result = ValueFormatAuto.format(EncodedString::Json(invalid_json), &mut buf); + assert!(result.is_err()); + } + + // --- + // Test 9: ValueFormatAuto special characters and edge cases // --- #[rstest] @@ -1593,7 +1607,7 @@ mod string { #[case::trailing_space("message ", " | ", "message | ")] #[case::trailing_tab("message\t", " | ", "message | ")] #[case::trailing_newline("message\n", " | ", "message | ")] - // Whitespace-only: empty check is on input (before trim), so delimiter is appended + // Whitespace-only: trimmed to empty, so only delimiter appears #[case::only_spaces(" ", " | ", " | ")] fn test_message_format_delimited_whitespace(#[case] input: &str, #[case] delim: &str, #[case] expected: &str) { let formatter = MessageFormatDelimited::new(delim.to_string());