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.
This commit is contained in:
Yuya Nishihara 2024-10-16 18:40:37 +09:00
parent e00f6f121e
commit ca5d119c10
2 changed files with 177 additions and 0 deletions

View file

@ -594,6 +594,14 @@ impl FormatRecorder {
FormatRecorder::default()
}
/// Creates new buffer containing the given `data`.
pub fn with_data(data: impl Into<Vec<u8>>) -> Self {
FormatRecorder {
data: data.into(),
ops: vec![],
}
}
pub fn data(&self) -> &[u8] {
&self.data
}

View file

@ -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![]);