From ca5d119c10299a195f6942259cf630af4dde2cbf Mon Sep 17 00:00:00 2001 From: Yuya Nishihara Date: Wed, 16 Oct 2024 18:40:37 +0900 Subject: [PATCH] text_util: add function to pad labeled text This could be inlined in templater implementation, but I decided to add text_util functions for ease of testing. --- cli/src/formatter.rs | 8 ++ cli/src/text_util.rs | 169 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) diff --git a/cli/src/formatter.rs b/cli/src/formatter.rs index 20133f07d..f6d27ce49 100644 --- a/cli/src/formatter.rs +++ b/cli/src/formatter.rs @@ -594,6 +594,14 @@ impl FormatRecorder { FormatRecorder::default() } + /// Creates new buffer containing the given `data`. + pub fn with_data(data: impl Into>) -> Self { + FormatRecorder { + data: data.into(), + ops: vec![], + } + } + pub fn data(&self) -> &[u8] { &self.data } diff --git a/cli/src/text_util.rs b/cli/src/text_util.rs index e44ee19ce..a6e7e5f44 100644 --- a/cli/src/text_util.rs +++ b/cli/src/text_util.rs @@ -18,6 +18,7 @@ use std::io; use bstr::ByteSlice as _; use unicode_width::UnicodeWidthChar as _; +use unicode_width::UnicodeWidthStr as _; use crate::formatter::FormatRecorder; use crate::formatter::Formatter; @@ -265,6 +266,62 @@ pub fn write_truncated_end( Ok(truncated_width) } +/// Writes text padded to `min_width` by adding leading fill characters. +/// +/// The input `recorded_content` should be a single-line text. The +/// `recorded_fill_char` should be bytes of 1-width character. +pub fn write_padded_start( + formatter: &mut dyn Formatter, + recorded_content: &FormatRecorder, + recorded_fill_char: &FormatRecorder, + min_width: usize, +) -> io::Result<()> { + // We don't care about the width of non-UTF-8 bytes, but should not panic. + let width = String::from_utf8_lossy(recorded_content.data()).width(); + let fill_width = min_width.saturating_sub(width); + write_padding(formatter, recorded_fill_char, fill_width)?; + recorded_content.replay(formatter)?; + Ok(()) +} + +/// Writes text padded to `min_width` by adding leading fill characters. +/// +/// The input `recorded_content` should be a single-line text. The +/// `recorded_fill_char` should be bytes of 1-width character. +pub fn write_padded_end( + formatter: &mut dyn Formatter, + recorded_content: &FormatRecorder, + recorded_fill_char: &FormatRecorder, + min_width: usize, +) -> io::Result<()> { + // We don't care about the width of non-UTF-8 bytes, but should not panic. + let width = String::from_utf8_lossy(recorded_content.data()).width(); + let fill_width = min_width.saturating_sub(width); + recorded_content.replay(formatter)?; + write_padding(formatter, recorded_fill_char, fill_width)?; + Ok(()) +} + +fn write_padding( + formatter: &mut dyn Formatter, + recorded_fill_char: &FormatRecorder, + fill_width: usize, +) -> io::Result<()> { + if fill_width == 0 { + return Ok(()); + } + let data = recorded_fill_char.data(); + recorded_fill_char.replay_with(formatter, |formatter, range| { + // Don't emit labels repeatedly, just repeat content. Suppose fill char + // is a single character, the byte sequence shouldn't be broken up to + // multiple labeled regions. + for _ in 0..fill_width { + formatter.write_all(&data[range.clone()])?; + } + Ok(()) + }) +} + /// Indents each line by the given prefix preserving labels. pub fn write_indented( formatter: &mut dyn Formatter, @@ -740,6 +797,118 @@ mod tests { ); } + #[test] + fn test_write_padded_labeled_content() { + let mut recorder = FormatRecorder::new(); + for (label, word) in [("red", "foo"), ("cyan", "bar")] { + recorder.push_label(label).unwrap(); + write!(recorder, "{word}").unwrap(); + recorder.pop_label().unwrap(); + } + let fill = FormatRecorder::with_data("="); + + // Pad start + insta::assert_snapshot!( + format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 6)), + @"foobar" + ); + insta::assert_snapshot!( + format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 7)), + @"=foobar" + ); + insta::assert_snapshot!( + format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 8)), + @"==foobar" + ); + + // Pad end + insta::assert_snapshot!( + format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 6)), + @"foobar" + ); + insta::assert_snapshot!( + format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 7)), + @"foobar=" + ); + insta::assert_snapshot!( + format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 8)), + @"foobar==" + ); + } + + #[test] + fn test_write_padded_labeled_fill_char() { + let recorder = FormatRecorder::with_data("foo"); + let mut fill = FormatRecorder::new(); + fill.push_label("red").unwrap(); + write!(fill, "=").unwrap(); + fill.pop_label().unwrap(); + + // Pad start + insta::assert_snapshot!( + format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 5)), + @"==foo" + ); + + // Pad end + insta::assert_snapshot!( + format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 6)), + @"foo===" + ); + } + + #[test] + fn test_write_padded_non_ascii_chars() { + let recorder = FormatRecorder::with_data("a\u{300}bc\u{300}一二三"); + let fill = FormatRecorder::with_data("="); + + // Pad start + insta::assert_snapshot!( + format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 9)), + @"àbc̀一二三" + ); + insta::assert_snapshot!( + format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 10)), + @"=àbc̀一二三" + ); + + // Pad end + insta::assert_snapshot!( + format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 9)), + @"àbc̀一二三" + ); + insta::assert_snapshot!( + format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 10)), + @"àbc̀一二三=" + ); + } + + #[test] + fn test_write_padded_empty_content() { + let recorder = FormatRecorder::new(); + let fill = FormatRecorder::with_data("="); + + // Pad start + insta::assert_snapshot!( + format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 0)), + @"" + ); + insta::assert_snapshot!( + format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 1)), + @"=" + ); + + // Pad end + insta::assert_snapshot!( + format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 0)), + @"" + ); + insta::assert_snapshot!( + format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 1)), + @"=" + ); + } + #[test] fn test_split_byte_line_to_words() { assert_eq!(split_byte_line_to_words(b""), vec![]);