From: Ben Pfaff Date: Mon, 5 Jan 2026 17:34:39 +0000 (-0800) Subject: Add tests for background colors. X-Git-Url: https://pintos-os.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8dd54a6bb49b3a651e7431431779accc67eccc88;p=pspp Add tests for background colors. --- diff --git a/rust/doc/src/invoking/output.md b/rust/doc/src/invoking/output.md index 920ffec573..3b8aaa4819 100644 --- a/rust/doc/src/invoking/output.md +++ b/rust/doc/src/invoking/output.md @@ -37,10 +37,15 @@ This driver has the following options: Unicode boxes are generally more attractive but they can be harder to work with in some environments. The default is `unicode`. -* `emphasis = ` - If this is set to true, then the output includes bold and underline - emphasis with overstriking. This is supported by only some - software, mainly on Unix. The default is `false`. +* `emphasis = "ansi"` + `emphasis = "overstrike"` + By default, text output is plain, without support for emphasis. Set + this to `ansi` to enable bold, italic, underline, and colors in + terminals that support it using [ANSI escape codes], or to + `overstrike` to enable bold and underline for emphasis with + overstriking. + + [ANSI escape codes]: https://en.wikipedia.org/wiki/ANSI_escape_code # PDF Output (`.pdf`) diff --git a/rust/pspp/src/output/drivers/text.rs b/rust/pspp/src/output/drivers/text.rs index 6f5fa1601e..855a87c940 100644 --- a/rust/pspp/src/output/drivers/text.rs +++ b/rust/pspp/src/output/drivers/text.rs @@ -74,11 +74,23 @@ pub struct TextConfig { options: TextRendererOptions, } +/// How to enable emphasis in output. +#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum Emphasis { + /// Use ANSI codes for full-color output. + #[default] + Ansi, + + /// Use backspaces and overstriking for bold and underline. + Overstrike, +} + #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(default)] pub struct TextRendererOptions { - /// Enable bold and underline in output? - pub emphasis: bool, + /// Enable emphasis in output? + pub emphasis: Option, /// Page width. pub width: Option, @@ -89,7 +101,7 @@ pub struct TextRendererOptions { pub struct TextRenderer { /// Enable bold and underline in output? - emphasis: bool, + emphasis: Option, /// Page width. width: isize, @@ -111,6 +123,10 @@ impl Default for TextRenderer { } impl TextRenderer { + pub fn with_emphasis(self, emphasis: Option) -> Self { + Self { emphasis, ..self } + } + pub fn new(config: &TextRendererOptions) -> Self { let width = config.width.unwrap_or(usize::MAX).min(isize::MAX as usize) as isize; Self { @@ -398,7 +414,7 @@ impl TextRenderer { Ok(()) } - fn render(&mut self, item: &Item, writer: &mut W) -> FmtResult + pub fn render(&mut self, item: &Item, writer: &mut W) -> FmtResult where W: FmtWrite, { @@ -437,8 +453,28 @@ impl TextRenderer { let mut pager = Pager::new(self, table, Some(layer_indexes.as_slice())); while pager.has_next(self).is_some() { pager.draw_next(self, isize::MAX); - for line in self.lines.drain(..) { - writeln!(writer, "{line}")?; + match self.emphasis { + Some(Emphasis::Ansi) => { + let width = self + .lines + .iter() + .map(|line| line.width()) + .max() + .unwrap_or_default(); + for line in self.lines.drain(..) { + writeln!(writer, "{}", line.display_sgr().with_width(width))?; + } + } + Some(Emphasis::Overstrike) => { + for line in self.lines.drain(..) { + writeln!(writer, "{}", line.display_overstrike())?; + } + } + None => { + for line in self.lines.drain(..) { + writeln!(writer, "{line}")?; + } + } } } } @@ -664,7 +700,10 @@ impl Device for TextRenderer { continue; }; - let attribute = self.emphasis.then(|| Attribute::for_style(cell.font_style)); + let attribute = self + .emphasis + .is_some() + .then(|| Attribute::for_style(cell.font_style)); self.get_line(y as usize).put(x as usize, &text, attribute); } } diff --git a/rust/pspp/src/output/drivers/text/text_line.rs b/rust/pspp/src/output/drivers/text/text_line.rs index e4f992c925..9d1019c465 100644 --- a/rust/pspp/src/output/drivers/text/text_line.rs +++ b/rust/pspp/src/output/drivers/text/text_line.rs @@ -26,20 +26,28 @@ use unicode_width::UnicodeWidthChar; use crate::output::pivot::look::{Color, FontStyle}; -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct Attribute { - pub fg: Option, - pub bg: Option, + pub fg: Color, + pub bg: Color, pub bold: bool, pub italic: bool, pub underline: bool, } -impl Attribute { - pub fn is_empty(&self) -> bool { - self.fg.is_none() && self.bg.is_none() && !self.bold && !self.italic && !self.underline +impl Default for Attribute { + fn default() -> Self { + Self { + fg: Color::BLACK, + bg: Color::WHITE, + bold: false, + italic: false, + underline: false, + } } +} +impl Attribute { pub fn affixes(&self) -> (&'static str, &'static str) { match (self.bold, self.italic, self.underline) { (false, false, false) => ("", ""), @@ -53,6 +61,14 @@ impl Attribute { } } + pub fn overstrike<'a>(&self, content: &'a str) -> Overstrike<'a> { + Overstrike { + bold: self.bold, + underline: self.underline, + content, + } + } + pub fn sgr(&self, content: T) -> Sgr<'_, T> { Sgr { attribute: self, @@ -62,8 +78,8 @@ impl Attribute { pub fn for_style(font_style: &FontStyle) -> Self { Self { - fg: Some(font_style.fg), - bg: Some(font_style.bg), + fg: font_style.fg, + bg: font_style.bg, bold: font_style.bold, italic: font_style.italic, underline: font_style.underline, @@ -71,6 +87,40 @@ impl Attribute { } } +pub struct Overstrike<'a> { + bold: bool, + underline: bool, + content: &'a str, +} + +impl<'a> Display for Overstrike<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if !self.bold && !self.underline { + return f.write_str(self.content); + } + for c in self.content.chars() { + f.write_char(c)?; + if let Some(width) = c.width() { + if self.bold { + for _ in 0..width { + f.write_char('\x08')?; + } + f.write_char(c)?; + } + if self.underline { + for _ in 0..width { + f.write_char('\x08')?; + } + for _ in 0..width { + f.write_char('_')?; + } + } + } + } + Ok(()) + } +} + pub struct Sgr<'a, T> { attribute: &'a Attribute, content: T, @@ -82,12 +132,12 @@ where { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut s = SmallString::<[u8; 32]>::new(); - if let Some(fg) = self.attribute.fg { - write!(&mut s, "38;2;{};{};{};", fg.r, fg.g, fg.b).unwrap(); - } - if let Some(bg) = self.attribute.bg { - write!(&mut s, "48;2;{};{};{};", bg.r, bg.g, bg.b).unwrap(); - } + + let (r, g, b) = self.attribute.fg.into_rgb(); + write!(&mut s, "38;2;{r};{g};{b};").unwrap(); + + let (r, g, b) = self.attribute.bg.into_rgb(); + write!(&mut s, "48;2;{r};{g};{b};").unwrap(); if self.attribute.bold { write!(&mut s, "1;").unwrap(); } @@ -97,13 +147,8 @@ where if self.attribute.underline { write!(&mut s, "4;").unwrap(); } - if !s.is_empty() { - s.pop(); - write!(f, "\x1b[{s}m{}\x1b[0m", &self.content)?; - } else { - write!(f, "{}", &self.content)?; - } - Ok(()) + s.pop(); + write!(f, "\x1b[{s}m{}\x1b[0m", &self.content) } } @@ -245,8 +290,15 @@ impl TextLine { &self.string } + pub fn display_overstrike(&self) -> DisplayOverstrike<'_> { + DisplayOverstrike(self) + } + pub fn display_sgr(&self) -> DisplaySgr<'_> { - DisplaySgr(self) + DisplaySgr { + line: self, + width: 0, + } } pub fn display_wiki(&self) -> DisplayWiki<'_> { @@ -262,6 +314,10 @@ impl TextLine { x: 0, } } + + pub fn width(&self) -> usize { + self.width + } } impl Display for TextLine { @@ -286,16 +342,50 @@ impl<'a> Display for DisplayWiki<'a> { } } -pub struct DisplaySgr<'a>(&'a TextLine); +pub struct DisplayOverstrike<'a>(&'a TextLine); -impl<'a> Display for DisplaySgr<'a> { +impl<'a> Display for DisplayOverstrike<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let default = Attribute::default(); for (s, attribute) in self.0.iter() { - if let Some(attribute) = attribute { - write!(f, "{}", attribute.sgr(s))?; - } else { - f.write_str(s)?; + write!( + f, + "{}", + attribute.as_ref().unwrap_or(&default).overstrike(s) + )?; + } + Ok(()) + } +} + +pub struct DisplaySgr<'a> { + line: &'a TextLine, + width: usize, +} + +impl<'a> DisplaySgr<'a> { + pub fn with_width(self, width: usize) -> Self { + Self { width, ..self } + } +} + +impl<'a> Display for DisplaySgr<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let default = Attribute::default(); + for (s, attribute) in self.line.iter() { + write!(f, "{}", attribute.as_ref().unwrap_or(&default).sgr(s))?; + } + if let Some(pad) = self.width.checked_sub(self.line.width) { + struct Spaces(usize); + impl Display for Spaces { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for _ in 0..self.0 { + f.write_char(' ')?; + } + Ok(()) + } } + write!(f, "{}", default.sgr(Spaces(pad)))?; } Ok(()) } diff --git a/rust/pspp/src/spv/read/tests.rs b/rust/pspp/src/spv/read/tests.rs index 65de04f68a..a8ef1ac775 100644 --- a/rust/pspp/src/spv/read/tests.rs +++ b/rust/pspp/src/spv/read/tests.rs @@ -5,28 +5,32 @@ use std::{ }; use crate::{ - output::{Text, pivot::tests::assert_lines_eq}, + output::{ + Text, + drivers::text::{Emphasis, TextRenderer}, + pivot::tests::assert_lines_eq, + }, spv::SpvArchive, }; #[test] fn legacy1() { - test_raw_spvfile("legacy1"); + test_raw_spvfile("legacy1", None); } #[test] fn legacy2() { - test_raw_spvfile("legacy2"); + test_raw_spvfile("legacy2", None); } #[test] fn legacy3() { - test_raw_spvfile("legacy3"); + test_raw_spvfile("legacy3", None); } #[test] fn legacy4() { - test_raw_spvfile("legacy4"); + test_raw_spvfile("legacy4", None); } /// Layer. @@ -34,91 +38,97 @@ fn legacy4() { /// (But we need to support selecting a layer value, too.) #[test] fn legacy5() { - test_raw_spvfile("legacy5"); + test_raw_spvfile("legacy5", None); } /// Layer, with a particular layer selected. #[test] fn legacy6() { - test_raw_spvfile("legacy6"); + test_raw_spvfile("legacy6", None); } /// Regression test for ``. #[test] fn legacy7() { - test_raw_spvfile("legacy7"); + test_raw_spvfile("legacy7", None); } /// Checks for `Dimension::hide_all_labels`. #[test] fn legacy8() { - test_raw_spvfile("legacy8"); + test_raw_spvfile("legacy8", None); } /// Checks for caption defined as a footnote label, and for footnotes in layer /// values. #[test] fn legacy9() { - test_raw_spvfile("legacy9"); + test_raw_spvfile("legacy9", None); } /// Checks for footnotes in dimension labels. #[test] fn legacy10() { - test_raw_spvfile("legacy10"); + test_raw_spvfile("legacy10", None); } /// Checks for footnotes on data cells added via XML rather than `cellFootnotes` /// series. #[test] fn legacy11() { - test_raw_spvfile("legacy11"); + test_raw_spvfile("legacy11", None); } /// Checks that footnotes on data cells added via the `cellFootnotes` series /// can't be deleted via `setFormat`. #[test] fn legacy12() { - test_raw_spvfile("legacy12"); + test_raw_spvfile("legacy12", None); } /// Checks that we support multiple `` elements within a single /// ``. #[test] fn legacy13() { - test_raw_spvfile("legacy13"); + test_raw_spvfile("legacy13", None); } /// Check for correct defaults in XML looks. #[test] fn legacy14() { - test_raw_spvfile("legacy14"); + test_raw_spvfile("legacy14", None); } /// Checks that categories are ordered correctly when the first row has some /// missing cells (in this case, "Beta" lacks a value in the first row). #[test] fn legacy15() { - test_raw_spvfile("legacy15"); + test_raw_spvfile("legacy15", None); } /// Subscript support. #[test] fn legacy16() { - test_raw_spvfile("legacy16"); + test_raw_spvfile("legacy16", None); } -fn test_raw_spvfile(name: &str) { +/// Support styling cells with colored backgrounds. +#[test] +fn legacy17() { + test_raw_spvfile("legacy17", Some(Emphasis::Ansi)); +} + +fn test_raw_spvfile(name: &str, emphasis: Option) { let input_filename = Path::new("src/spv/testdata") .join(name) .with_extension("spv"); let spvfile = BufReader::new(File::open(&input_filename).unwrap()); let expected_filename = input_filename.with_extension("expected"); let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap(); - test_spvfile(spvfile, &expected, &expected_filename); + test_spvfile(spvfile, &expected, &expected_filename, emphasis); } -fn test_spvfile(spvfile: R, expected: &str, expected_filename: &Path) +fn test_spvfile(spvfile: R, expected: &str, expected_filename: &Path, emphasis: Option) where R: BufRead + Seek + 'static, { @@ -145,7 +155,9 @@ where Err(error) => Text::new_log(error.to_string()).into_item(), }; - let actual = output.to_string(); + let mut renderer = TextRenderer::default().with_emphasis(emphasis); + let mut actual = String::new(); + renderer.render(&output, &mut actual).unwrap(); if expected != actual { if std::env::var("PSPP_REFRESH_EXPECTED").is_ok() { std::fs::write(expected_filename, actual).unwrap(); diff --git a/rust/pspp/src/spv/testdata/legacy17.expected b/rust/pspp/src/spv/testdata/legacy17.expected new file mode 100644 index 0000000000..3aa1e77162 --- /dev/null +++ b/rust/pspp/src/spv/testdata/legacy17.expected @@ -0,0 +1,10 @@ + Coefficients[a]  +╭───────────────┬────────────────────────────┬─────────────────────────┬──────┬────╮ +│ │ Unstandardized Coefficients│Standardized Coefficients│ │ │ +│ ├────────────┬───────────────┼─────────────────────────┤ │ │ +│Model │ B │ Std. Error │ Beta │ t │Sig.│ +├───────────────┼────────────┼───────────────┼─────────────────────────┼──────┼────┤ +│1.00 (Constant)│ 59.146│ 18.854│ │ 3.137│.016│ +│ Variable A│ -.664│ .585│ -.395│-1.136│.293│ +╰───────────────┴────────────┴───────────────┴─────────────────────────┴──────┴────╯ +a. Dependent Variable: A  diff --git a/rust/pspp/src/spv/testdata/legacy17.spv b/rust/pspp/src/spv/testdata/legacy17.spv new file mode 100644 index 0000000000..b9af58643d Binary files /dev/null and b/rust/pspp/src/spv/testdata/legacy17.spv differ