From: Ben Pfaff Date: Fri, 12 Dec 2025 01:07:37 +0000 (-0800) Subject: work X-Git-Url: https://pintos-os.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=47c13c24b10128ab742b655e081da4167457d659;p=pspp work --- diff --git a/rust/pspp/src/output/pivot.rs b/rust/pspp/src/output/pivot.rs index 66596e6aa6..a8ccd80fc0 100644 --- a/rust/pspp/src/output/pivot.rs +++ b/rust/pspp/src/output/pivot.rs @@ -900,8 +900,14 @@ pub enum FootnoteMarkerPosition { Superscript, } +/// A [Look] and other styling for a [PivotTable]. +/// +/// The division between [Look] and the rest of the styling in this structure is +/// fairly arbitrary. The ultimate reason for the division is simply because +/// that's how SPSS documentation and file formats do it. #[derive(Clone, Debug, Serialize)] pub struct PivotTableStyle { + /// The [Look]. pub look: Arc, pub rotate_inner_column_labels: bool, diff --git a/rust/pspp/src/output/pivot/look.rs b/rust/pspp/src/output/pivot/look.rs index 583eb41dfb..58c716138c 100644 --- a/rust/pspp/src/output/pivot/look.rs +++ b/rust/pspp/src/output/pivot/look.rs @@ -1,3 +1,30 @@ +// PSPP - a program for statistical analysis. +// Copyright (C) 2025 Free Software Foundation, Inc. +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . + +//! Pivot table styles (called "TableLooks" by SPSS). +//! +//! Each [PivotTable] is styled with a [PivotTableStyle], which contains a +//! [Look]. This module contains [Look] and its contents. +//! +//! [PivotTable]: super::PivotTable +//! [PivotTableStyle]: super::PivotTableStyle + +// Warn about missing docs, but not for items declared with `#[cfg(test)]`. +#![cfg_attr(not(test), warn(missing_docs))] + use std::{ fmt::{Debug, Display}, io::Read, @@ -24,15 +51,24 @@ use crate::{ #[derive(Copy, Clone, Debug, Enum, PartialEq, Eq)] pub enum Area { /// Title. + /// + /// Displayed above the table. If a table is split across multiple pages + /// for printing, the title appears on each page. Title, /// Caption. + /// + /// Displayed below the table. Caption, - /// Footnotes, + /// Footnotes. + /// + /// Displayed below the table. Footer, - // Top-left corner. + /// Top-left corner. + /// + /// To the left of the column labels, and above the row labels. Corner, /// Labels. @@ -118,16 +154,37 @@ impl Display for RowParity { /// Table borders for styling purposes. #[derive(Copy, Clone, Debug, Enum, PartialEq, Eq)] pub enum Border { + /// Title. Title, - OuterFrame(BoxBorder), - InnerFrame(BoxBorder), - Dimension(RowColBorder), - Category(RowColBorder), + + /// Outer frame. + OuterFrame( + /// Which border of the outer frame. + BoxBorder, + ), + /// Inner frame. + InnerFrame( + /// Which border of the inner frame. + BoxBorder, + ), + /// Between dimensions. + Dimension( + /// Which part between dimensions. + RowColBorder, + ), + /// Between categories. + Category( + /// Which part between categories. + RowColBorder, + ), + /// Between the row borders and the data. DataLeft, + /// Between the column borders and the data. DataTop, } impl Border { + /// Returns the default [Stroke] for this border. pub fn default_stroke(self) -> Stroke { match self { Self::InnerFrame(_) | Self::DataLeft | Self::DataTop => Stroke::Thick, @@ -138,6 +195,7 @@ impl Border { _ => Stroke::None, } } + /// Returns the default [BorderStyle] for this border. pub fn default_border_style(self) -> BorderStyle { BorderStyle { stroke: self.default_stroke(), @@ -145,6 +203,7 @@ impl Border { } } + /// Returns an alternative border for this one. pub fn fallback(self) -> Self { match self { Self::Title @@ -157,6 +216,7 @@ impl Border { } } + /// Returns all the default borders. pub fn default_borders() -> EnumMap { EnumMap::from_fn(Border::default_border_style) } @@ -189,9 +249,13 @@ impl Serialize for Border { #[derive(Copy, Clone, Debug, Enum, PartialEq, Eq, Serialize)] #[serde(rename_all = "snake_case")] pub enum BoxBorder { + /// Left side. Left, + /// Top. Top, + /// Right side. Right, + /// Bottom. Bottom, } @@ -245,18 +309,34 @@ pub struct Sizing { pub keeps: Vec>, } -/// Styling for a pivot table. +/// Core styling for a pivot table. +/// +/// The division between `Look` and [PivotTableStyle] is fairly arbitrary. The +/// ultimate reason for the division is simply because that's how SPSS +/// documentation and file formats do it. +/// +/// A `Look` can be read from standalone files in [XML] and [binary] formats and +/// extracted from [PivotTable]s, which in turn can be read from [SPV files]. /// -/// The division between this and the style information in [PivotTable] seems -/// fairly arbitrary. The ultimate reason for the division is simply because -/// that's how SPSS documentation and file formats do it. +/// [XML]: Self::from_xml +/// [binary]: Self::from_binary +/// [PivotTable]: super::PivotTable +/// [PivotTableStyle]: super::PivotTableStyle +/// [SPV files]: crate::spv #[derive(Clone, Debug, PartialEq, Serialize)] pub struct Look { + /// Optional name for this `Look`. + /// + /// This allows building a catalog of `Look`s based on more than their + /// filenames. pub name: Option, /// Whether to hide rows or columns whose cells are all empty. pub hide_empty: bool, + /// Where to place [Group] labels. + /// + /// [Group]: super::Group pub row_label_position: LabelPosition, /// Ranges of column widths in the two heading regions, in 1/96" units. @@ -274,30 +354,55 @@ pub struct Look { /// Styles for borders in the pivot table. pub borders: EnumMap, + /// Whether to print all layers. + /// + /// If true, all table layers are printed sequentially, + /// If false, only the current layer is printed. + /// + /// This affects only printing. On-screen display shows just one layer at a + /// time. pub print_all_layers: bool, + /// If true, print each layer on its own page. pub paginate_layers: bool, + /// If `shrink_to_fit[Axis2::X]`, then tables wider than the page are scaled + /// to fit horizontally. + /// + /// If `shrink_to_fit[Axis2::Y]`, then tables longer than the page are + /// scaled to fit vertically. pub shrink_to_fit: EnumMap, - pub top_continuation: bool, - - pub bottom_continuation: bool, + /// When to show `continuation`: + /// + /// - `show_continuations[0]`: Whether to show `continuation` at the top of + /// a table that is continued from the previous page. + /// + /// - `show_continuations[1]`: Whether to show `continuation` at the bottom + /// of a table that continues onto the next page. + pub show_continuations: [bool; 2], + /// Text that can be shown at the top or bottom of a table that continues + /// across multiple pages. pub continuation: Option, + /// Minimum number of rows or columns to put in one part of a table that is + /// broken across pages. pub n_orphan_lines: usize, } impl Look { + /// Returns this look with `omit_empty` set as provided. pub fn with_omit_empty(mut self, omit_empty: bool) -> Self { self.hide_empty = omit_empty; self } + /// Returns this look with `row_label_position` set as provided. pub fn with_row_label_position(mut self, row_label_position: LabelPosition) -> Self { self.row_label_position = row_label_position; self } + /// Returns this look with `borders` set as provided. pub fn with_borders(mut self, borders: EnumMap) -> Self { self.borders = borders; self @@ -321,45 +426,79 @@ impl Default for Look { print_all_layers: false, paginate_layers: false, shrink_to_fit: EnumMap::from_fn(|_| false), - top_continuation: false, - bottom_continuation: false, + show_continuations: [false, false], continuation: None, n_orphan_lines: 0, } } } +/// Error type returned by [Look] methods that parse a file. #[derive(thiserror::Error, Debug)] pub enum ParseLookError { + /// [quick_xml] deserialization errors. #[error(transparent)] - XmlError(#[from] DeError), + XmlError( + /// Inner error. + #[from] + DeError, + ), + /// UTF-8 decoding error. #[error(transparent)] - Utf8Error(#[from] Utf8Error), + Utf8Error( + /// Inner error. + #[from] + Utf8Error, + ), + /// [binrw] deserialization error. #[error(transparent)] - BinError(#[from] binrw::Error), + BinError( + /// Inner error. + #[from] + binrw::Error, + ), + /// I/O error. #[error(transparent)] - IoError(#[from] std::io::Error), + IoError( + /// Inner error. + #[from] + std::io::Error, + ), } impl Look { + /// Returns a globally shared copy of [Look::default]. pub fn shared_default() -> Arc { static LOOK: OnceLock> = OnceLock::new(); LOOK.get_or_init(|| Arc::new(Look::default())).clone() } + /// Parses `xml` as an XML-formatted `Look`, as found in [`.stt` files]. + /// + /// [`.stt` files]: https://pspp.benpfaff.org/manual/tablelook.html#the-stt-format pub fn from_xml(xml: &str) -> Result { Ok(from_str::(xml) .map_err(ParseLookError::from)? .into()) } + /// Parses `xml` as a binary-formatted `Look`, as found in [`.tlo` files]. + /// + /// # Obsolescence + /// + /// The `.tlo` format is obsolete. PSPP only supports it as an input + /// format. + /// + /// [`.tlo` files]: https://pspp.benpfaff.org/manual/tablelook.html#the-tlo-format pub fn from_binary(tlo: &[u8]) -> Result { parse_tlo(tlo).map_err(ParseLookError::from) } + /// Parses `data` as the binary or XML format of a `Look`, automatically + /// detecting which format. pub fn from_data(data: &[u8]) -> Result { if data.starts_with(b"\xff\xff\0\0") { Self::from_binary(data) @@ -368,19 +507,20 @@ impl Look { } } + /// Reads a [Look] in binary or XML format from `reader`. pub fn from_reader(mut reader: R) -> Result where R: Read, { let mut buffer = Vec::new(); - reader - .read_to_end(&mut buffer) - .map_err(ParseLookError::from)?; + reader.read_to_end(&mut buffer)?; Self::from_data(&buffer) } } -/// Position for group labels. +/// Position for [Group] labels. +/// +/// [Group]: super::Group #[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] pub enum LabelPosition { /// Hierarchically enclosing the categories. @@ -435,11 +575,21 @@ pub enum LabelPosition { #[derive(Copy, Clone, Debug, PartialEq, Eq, Enum, Serialize)] #[serde(rename_all = "snake_case")] pub enum HeadingRegion { + /// Headings for labeling rows. + /// + /// These appear on the left side of the pivot table, including the top-left + /// corner area. Rows, + + /// Headings for labeling columns. + /// + /// These appear along the top of the pivot table, excluding the top-left + /// corner area. Columns, } impl HeadingRegion { + /// Returns "rows" or "columns". pub fn as_str(&self) -> &'static str { match self { HeadingRegion::Rows => "rows", @@ -463,13 +613,17 @@ impl From for HeadingRegion { } } +/// Default style for cells in an [Area]. #[derive(Clone, Debug, PartialEq, Serialize)] pub struct AreaStyle { + /// Cell style for the area. pub cell_style: CellStyle, + /// Font style for the area. pub font_style: FontStyle, } impl AreaStyle { + /// Returns the default style for `area`. pub fn default_for_area(area: Area) -> Self { Self { cell_style: CellStyle::default_for_area(area), @@ -478,19 +632,29 @@ impl AreaStyle { } } +/// Style for the cells that contain a [Value]. +/// +/// The division between [CellStyle] and [FontStyle] isn't particularly +/// meaningful but it matches SPSS file formats. +/// +/// [Value]: super::value::Value #[derive(Clone, Debug, Serialize, PartialEq)] pub struct CellStyle { + /// Horizontal alignment. + /// /// `None` means "mixed" alignment: align strings to the left, numbers to /// the right. pub horz_align: Option, + + /// Vertical alignment. pub vert_align: VertAlign, - /// Margins in 1/96" units. + /// Margins in 1/96" units: /// - /// `margins[Axis2::X][0]` is the left margin. - /// `margins[Axis2::X][1]` is the right margin. - /// `margins[Axis2::Y][0]` is the top margin. - /// `margins[Axis2::Y][1]` is the bottom margin. + /// - `margins[Axis2::X][0]` is the left margin. + /// - `margins[Axis2::X][1]` is the right margin. + /// - `margins[Axis2::Y][0]` is the top margin. + /// - `margins[Axis2::Y][1]` is the bottom margin. pub margins: EnumMap, } @@ -501,6 +665,7 @@ impl Default for CellStyle { } impl CellStyle { + /// Returns the default cell style for `area`. pub fn default_for_area(area: Area) -> Self { use HorzAlign::*; use VertAlign::*; @@ -520,17 +685,24 @@ impl CellStyle { margins: enum_map! { Axis2::X => hmargins, Axis2::Y => vmargins }, } } + /// Returns the cell style with `horz_align` set as specified. pub fn with_horz_align(self, horz_align: Option) -> Self { Self { horz_align, ..self } } + /// Returns the cell style with `vert_align` set as specified. pub fn with_vert_align(self, vert_align: VertAlign) -> Self { Self { vert_align, ..self } } + /// Returns the cell style with `margins` set as specified. pub fn with_margins(self, margins: EnumMap) -> Self { Self { margins, ..self } } } +/// Horizontal alignment of text. +/// +/// "Mixed" alignment is implemented at a higher level using +/// `Option`. #[derive(Copy, Clone, Debug, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum HorzAlign { @@ -554,6 +726,8 @@ pub enum HorzAlign { } impl HorzAlign { + /// Returns the [HorzAlign] to use for "mixed alignment" based on the + /// variable type. pub fn for_mixed(var_type: VarType) -> Self { match var_type { VarType::Numeric => Self::Right, @@ -561,6 +735,9 @@ impl HorzAlign { } } + /// Returns this alignment as a static string. + /// + /// Decimal alignment doesn't have a static string representation. pub fn as_str(&self) -> Option<&'static str> { match self { HorzAlign::Right => Some("right"), @@ -591,6 +768,7 @@ impl FromStr for HorzAlign { } } +/// Vertical alignment. #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)] #[serde(rename_all = "snake_case")] pub enum VertAlign { @@ -604,13 +782,30 @@ pub enum VertAlign { Bottom, } +/// Style of the font used in a [Value]. +/// +/// The division between [CellStyle] and [FontStyle] isn't particularly +/// meaningful but it matches SPSS file formats. +/// +/// [Value]: super::value::Value #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct FontStyle { + /// **Bold** pub bold: bool, + + /// *Italic* pub italic: bool, + + /// Underline pub underline: bool, + + /// Typeface. pub font: String, + + /// Foreground color. pub fg: Color, + + /// Background color. pub bg: Color, /// In 1/72" units. @@ -632,50 +827,76 @@ impl Default for FontStyle { } impl FontStyle { + /// Returns the default font style for `area`. pub fn default_for_area(area: Area) -> Self { Self::default().with_bold(area == Area::Title) } + /// Returns the font with its `size` set as specified. pub fn with_size(self, size: i32) -> Self { Self { size, ..self } } + /// Returns the font with `bold` set as specified. pub fn with_bold(self, bold: bool) -> Self { Self { bold, ..self } } + /// Returns the font with `italic` set as specified. pub fn with_italic(self, italic: bool) -> Self { Self { italic, ..self } } + /// Returns the font with `underline` set as specified. pub fn with_underline(self, underline: bool) -> Self { Self { underline, ..self } } + /// Returns the font with `font` set as specified. pub fn with_font(self, font: impl Into) -> Self { Self { font: font.into(), ..self } } + /// Returns the font with `fg` set as specified. pub fn with_fg(self, fg: Color) -> Self { Self { fg, ..self } } + /// Returns the font with `bg` set as specified. pub fn with_bg(self, fg: Color) -> Self { Self { fg, ..self } } } +/// Color used in [FontStyle]. #[derive(Copy, Clone, PartialEq, Eq)] pub struct Color { + /// Alpha channel. + /// + /// 255 is opaque, 0 is transparent. pub alpha: u8, + + /// Red. pub r: u8, + + /// Green. pub g: u8, + + /// Blue. pub b: u8, } impl Color { + /// Black. pub const BLACK: Color = Color::new(0, 0, 0); + /// White. pub const WHITE: Color = Color::new(255, 255, 255); + /// Red. pub const RED: Color = Color::new(255, 0, 0); + /// Green. + pub const GREEN: Color = Color::new(0, 255, 0); + /// Blue. pub const BLUE: Color = Color::new(0, 0, 255); + /// Transparent. pub const TRANSPARENT: Color = Color::new(0, 0, 0).with_alpha(0); + /// Returns an opaque color with the given red, green, and blue values. pub const fn new(r: u8, g: u8, b: u8) -> Self { Self { alpha: 255, @@ -685,23 +906,28 @@ impl Color { } } + /// Returns this color with the alpha channel set as specified. pub const fn with_alpha(self, alpha: u8) -> Self { Self { alpha, ..self } } + /// Returns an opaque version of this color. pub const fn without_alpha(self) -> Self { self.with_alpha(255) } /// Displays opaque colors as `#rrggbb` and others as `rgb(r, g, b, alpha)`. - pub fn display_css(&self) -> DisplayCss { - DisplayCss(*self) + pub fn display_css(&self) -> ColorDisplayCss { + ColorDisplayCss(*self) } + /// Returns the red, green, and blue channels of this color. pub fn into_rgb(&self) -> (u8, u8, u8) { (self.r, self.g, self.b) } + /// Returns 16-bit versions of the red, green, and blue channels of this + /// color. pub fn into_rgb16(&self) -> (u16, u16, u16) { ( self.r as u16 * 257, @@ -775,9 +1001,12 @@ impl<'de> Deserialize<'de> for Color { } } -pub struct DisplayCss(Color); +/// A structure for formatting a [Color] in a CSS-compatible format. +/// +/// See [Color::display_css]. +pub struct ColorDisplayCss(Color); -impl Display for DisplayCss { +impl Display for ColorDisplayCss { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let Color { alpha, r, g, b } = self.0; match alpha { @@ -787,11 +1016,14 @@ impl Display for DisplayCss { } } +/// Style for drawing a border in a pivot table. #[derive(Copy, Clone, Debug, PartialEq, Deserialize)] pub struct BorderStyle { + /// The kind of line to draw. #[serde(rename = "@borderStyleType")] pub stroke: Stroke, + /// Line color. #[serde(rename = "@color")] pub color: Color, } @@ -815,6 +1047,7 @@ impl From for BorderStyle { } impl BorderStyle { + /// Returns a black border style with the given `stroke`. pub const fn new(stroke: Stroke) -> Self { Self { stroke, @@ -822,10 +1055,12 @@ impl BorderStyle { } } + /// Returns a border style with no line. pub const fn none() -> Self { Self::new(Stroke::None) } + /// Returns whether this border style has no line. pub fn is_none(&self) -> bool { self.stroke.is_none() } @@ -841,18 +1076,26 @@ impl BorderStyle { } } +/// A line style for borders in a pivot table. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Enum, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub enum Stroke { + /// No line. None, + /// Solid line. Solid, + /// Dashed line. Dashed, + /// Thick solid line. Thick, + /// Thin solid line. Thin, + /// Two lines. Double, } impl Stroke { + /// Return whether this stroke is [Stroke::None]. pub fn is_none(&self) -> bool { self == &Self::None } diff --git a/rust/pspp/src/output/pivot/look_xml.rs b/rust/pspp/src/output/pivot/look_xml.rs index 1c7e90d3f0..429b59538c 100644 --- a/rust/pspp/src/output/pivot/look_xml.rs +++ b/rust/pspp/src/output/pivot/look_xml.rs @@ -94,12 +94,12 @@ impl From for Look { Axis2::X => table_properties.printing_properties.rescale_wide_table_to_fit_page, Axis2::Y => table_properties.printing_properties.rescale_long_table_to_fit_page, }, - top_continuation: table_properties + show_continuations: [ table_properties .printing_properties .continuation_text_at_top, - bottom_continuation: table_properties + table_properties .printing_properties - .continuation_text_at_bottom, + .continuation_text_at_bottom], continuation: { let text = table_properties.printing_properties.continuation_text; if text.is_empty() { @@ -955,8 +955,7 @@ mod tests { print_all_layers: true, paginate_layers: false, shrink_to_fit: EnumMap::from_fn(|_| false), - top_continuation: false, - bottom_continuation: false, + show_continuations: [false, false], continuation: None, n_orphan_lines: 5, }; diff --git a/rust/pspp/src/output/pivot/tlo.rs b/rust/pspp/src/output/pivot/tlo.rs index c1329afe18..31a108910f 100644 --- a/rust/pspp/src/output/pivot/tlo.rs +++ b/rust/pspp/src/output/pivot/tlo.rs @@ -141,8 +141,7 @@ impl From for Look { Axis2::X => (flags & 0x10) != 0, Axis2::Y => (flags & 0x20) != 0 }, - top_continuation: (flags & 0x80) != 0, - bottom_continuation: (flags & 0x100) != 0, + show_continuations: [(flags & 0x80) != 0, (flags & 0x100) != 0], continuation: { let s = &look.v2_styles.continuation; if s.is_empty() { diff --git a/rust/pspp/src/output/render.rs b/rust/pspp/src/output/render.rs index bdb30d6138..3162508801 100644 --- a/rust/pspp/src/output/render.rs +++ b/rust/pspp/src/output/render.rs @@ -58,8 +58,10 @@ pub struct Params { pub line_widths: EnumMap, /// 1/96" of an inch (1px) in the rendering unit. Currently used only for - /// column width ranges, as in `width_ranges` in - /// [crate::output::pivot::Look]. Set to `None` to disable this feature. + /// column width ranges, as in `width_ranges` in [Look]. Set to `None` to + /// disable this feature. + /// + /// [Look]: crate::output::pivot::look::Look pub px_size: Option, /// Minimum cell width or height before allowing the cell to be broken diff --git a/rust/pspp/src/spv/read.rs b/rust/pspp/src/spv/read.rs index 05b7063bb9..29384b9b62 100644 --- a/rust/pspp/src/spv/read.rs +++ b/rust/pspp/src/spv/read.rs @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU General Public License along with // this program. If not, see . - +#![allow(dead_code)] use std::{ fs::File, io::{BufReader, Cursor, Read, Seek}, @@ -164,7 +164,7 @@ pub struct SpvFile { } impl SpvFile { - // Returns the individual parts of the `SpvFile`. + /// Returns the individual parts of the `SpvFile`. pub fn into_parts(self) -> (Vec, Option) { (self.items, self.page_setup) } diff --git a/rust/pspp/src/spv/read/css.rs b/rust/pspp/src/spv/read/css.rs index 76d14190d1..a6c04997b4 100644 --- a/rust/pspp/src/spv/read/css.rs +++ b/rust/pspp/src/spv/read/css.rs @@ -99,8 +99,12 @@ impl<'a> Iterator for Lexer<'a> { } impl HorzAlign { - pub fn from_css(s: &str) -> Option { - let mut lexer = Lexer(s); + /// Parses `s` as CSS and returns the value of `text-align` found in it, if + /// any. + /// + /// This is only good enough to handle the simple CSS found in SPV files. + pub fn from_css(css: &str) -> Option { + let mut lexer = Lexer(css); while let Some(token) = lexer.next() { if let Token::Id(key) = token && let Some(Token::Colon) = lexer.next() @@ -116,8 +120,12 @@ impl HorzAlign { } impl Style { - pub fn parse_css(styles: &mut Vec