Unicode boxes are generally more attractive but they can be harder
to work with in some environments. The default is `unicode`.
-* `emphasis = <bool>`
- 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`)
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<Emphasis>,
/// Page width.
pub width: Option<usize>,
pub struct TextRenderer {
/// Enable bold and underline in output?
- emphasis: bool,
+ emphasis: Option<Emphasis>,
/// Page width.
width: isize,
}
impl TextRenderer {
+ pub fn with_emphasis(self, emphasis: Option<Emphasis>) -> 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 {
Ok(())
}
- fn render<W>(&mut self, item: &Item, writer: &mut W) -> FmtResult
+ pub fn render<W>(&mut self, item: &Item, writer: &mut W) -> FmtResult
where
W: FmtWrite,
{
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}")?;
+ }
+ }
}
}
}
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);
}
}
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<Color>,
- pub bg: Option<Color>,
+ 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) => ("", ""),
}
}
+ pub fn overstrike<'a>(&self, content: &'a str) -> Overstrike<'a> {
+ Overstrike {
+ bold: self.bold,
+ underline: self.underline,
+ content,
+ }
+ }
+
pub fn sgr<T>(&self, content: T) -> Sgr<'_, T> {
Sgr {
attribute: self,
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,
}
}
+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,
{
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();
}
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)
}
}
&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<'_> {
x: 0,
}
}
+
+ pub fn width(&self) -> usize {
+ self.width
+ }
}
impl Display for TextLine {
}
}
-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(())
}
};
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.
/// (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 `<setFormat reset="true">`.
#[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 `<setFormat>` elements within a single
/// `<setCellProperties>`.
#[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<Emphasis>) {
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<R>(spvfile: R, expected: &str, expected_filename: &Path)
+fn test_spvfile<R>(spvfile: R, expected: &str, expected_filename: &Path, emphasis: Option<Emphasis>)
where
R: BufRead + Seek + 'static,
{
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();
--- /dev/null
+\e[38;2;0;0;0;48;2;255;255;255m \e[0m\e[38;2;0;0;0;48;2;255;255;255;1mCoefficients[a]\e[0m\e[38;2;0;0;0;48;2;255;255;255m \e[0m
+\e[38;2;0;0;0;48;2;255;255;255m╭───────────────┬────────────────────────────┬─────────────────────────┬──────┬────╮\e[0m\e[38;2;0;0;0;48;2;255;255;255m\e[0m
+\e[38;2;0;0;0;48;2;255;255;255m│ │ \e[0m\e[38;2;0;0;0;48;2;255;255;255mUnstandardized Coefficients\e[0m\e[38;2;0;0;0;48;2;255;255;255m│\e[0m\e[38;2;0;0;0;48;2;255;255;255mStandardized Coefficients\e[0m\e[38;2;0;0;0;48;2;255;255;255m│ │ │\e[0m\e[38;2;0;0;0;48;2;255;255;255m\e[0m
+\e[38;2;0;0;0;48;2;255;255;255m│ ├────────────┬───────────────┼─────────────────────────┤ │ │\e[0m\e[38;2;0;0;0;48;2;255;255;255m\e[0m
+\e[38;2;0;0;0;48;2;255;255;255m│\e[0m\e[38;2;0;0;0;48;2;255;255;255mModel\e[0m\e[38;2;0;0;0;48;2;255;255;255m │ \e[0m\e[38;2;0;0;0;48;2;255;255;255mB\e[0m\e[38;2;0;0;0;48;2;255;255;255m │ \e[0m\e[38;2;0;0;0;48;2;255;255;255mStd. Error\e[0m\e[38;2;0;0;0;48;2;255;255;255m │ \e[0m\e[38;2;0;0;0;48;2;255;255;255mBeta\e[0m\e[38;2;0;0;0;48;2;255;255;255m │ \e[0m\e[38;2;0;0;0;48;2;255;255;255mt\e[0m\e[38;2;0;0;0;48;2;255;255;255m │\e[0m\e[38;2;0;0;0;48;2;255;255;255mSig.\e[0m\e[38;2;0;0;0;48;2;255;255;255m│\e[0m\e[38;2;0;0;0;48;2;255;255;255m\e[0m
+\e[38;2;0;0;0;48;2;255;255;255m├───────────────┼────────────┼───────────────┼─────────────────────────┼──────┼────┤\e[0m\e[38;2;0;0;0;48;2;255;255;255m\e[0m
+\e[38;2;0;0;0;48;2;255;255;255m│\e[0m\e[38;2;0;0;0;48;2;255;255;255m1.00\e[0m\e[38;2;0;0;0;48;2;255;255;255m \e[0m\e[38;2;0;0;0;48;2;255;255;255m(Constant)\e[0m\e[38;2;0;0;0;48;2;255;255;255m│ \e[0m\e[38;2;0;0;0;48;2;255;255;255m59.146\e[0m\e[38;2;0;0;0;48;2;255;255;255m│ \e[0m\e[38;2;0;0;0;48;2;255;255;255m18.854\e[0m\e[38;2;0;0;0;48;2;255;255;255m│ │ \e[0m\e[38;2;0;0;0;48;2;239;51;56;1;3m3.137\e[0m\e[38;2;0;0;0;48;2;255;255;255m│\e[0m\e[38;2;0;0;0;48;2;255;255;255m.016\e[0m\e[38;2;0;0;0;48;2;255;255;255m│\e[0m\e[38;2;0;0;0;48;2;255;255;255m\e[0m
+\e[38;2;0;0;0;48;2;255;255;255m│ \e[0m\e[38;2;0;0;0;48;2;255;255;255mVariable A\e[0m\e[38;2;0;0;0;48;2;255;255;255m│ \e[0m\e[38;2;0;0;0;48;2;255;255;255m-.664\e[0m\e[38;2;0;0;0;48;2;255;255;255m│ \e[0m\e[38;2;0;0;0;48;2;255;255;255m.585\e[0m\e[38;2;0;0;0;48;2;255;255;255m│ \e[0m\e[38;2;0;0;0;48;2;255;255;255m-.395\e[0m\e[38;2;0;0;0;48;2;255;255;255m│\e[0m\e[38;2;0;0;0;48;2;255;255;255m-1.136\e[0m\e[38;2;0;0;0;48;2;255;255;255m│\e[0m\e[38;2;0;0;0;48;2;255;255;255m.293\e[0m\e[38;2;0;0;0;48;2;255;255;255m│\e[0m\e[38;2;0;0;0;48;2;255;255;255m\e[0m
+\e[38;2;0;0;0;48;2;255;255;255m╰───────────────┴────────────┴───────────────┴─────────────────────────┴──────┴────╯\e[0m\e[38;2;0;0;0;48;2;255;255;255m\e[0m
+\e[38;2;0;0;0;48;2;255;255;255ma. Dependent Variable: A\e[0m\e[38;2;0;0;0;48;2;255;255;255m \e[0m