work
authorBen Pfaff <blp@cs.stanford.edu>
Fri, 12 Dec 2025 01:07:37 +0000 (17:07 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Fri, 12 Dec 2025 01:07:37 +0000 (17:07 -0800)
rust/pspp/src/output/pivot.rs
rust/pspp/src/output/pivot/look.rs
rust/pspp/src/output/pivot/look_xml.rs
rust/pspp/src/output/pivot/tlo.rs
rust/pspp/src/output/render.rs
rust/pspp/src/spv/read.rs
rust/pspp/src/spv/read/css.rs
rust/pspp/src/spv/read/html.rs
rust/pspp/src/spv/read/light.rs
rust/pspp/src/spv/write.rs

index 66596e6aa6b866339ea2dd6c7f2944acc05e5983..a8ccd80fc0ada8b466aa567cfdce5e4f75a8dc80 100644 (file)
@@ -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<Look>,
 
     pub rotate_inner_column_labels: bool,
index 583eb41dfb6d9c558d6297b3ea4c47c7204b67b0..58c716138c9f0e34cd65f737e2ae28d13cf59f3e 100644 (file)
@@ -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 <http://www.gnu.org/licenses/>.
+
+//! 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<Border, BorderStyle> {
         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<Range<usize>>,
 }
 
-/// 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<String>,
 
     /// 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<Border, BorderStyle>,
 
+    /// 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<Axis2, bool>,
 
-    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<String>,
 
+    /// 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<Border, BorderStyle>) -> 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<Look> {
         static LOOK: OnceLock<Arc<Look>> = 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<Self, ParseLookError> {
         Ok(from_str::<TableProperties>(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<Self, ParseLookError> {
         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<Self, ParseLookError> {
         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<R>(mut reader: R) -> Result<Self, ParseLookError>
     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<Axis2> 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<HorzAlign>,
+
+    /// 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<Axis2, [i32; 2]>,
 }
 
@@ -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<HorzAlign>) -> 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<Axis2, [i32; 2]>) -> Self {
         Self { margins, ..self }
     }
 }
 
+/// Horizontal alignment of text.
+///
+/// "Mixed" alignment is implemented at a higher level using
+/// `Option<HorzAlign>`.
 #[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,
+
+    /// <u>Underline</u>
     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<String>) -> 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<Stroke> 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
     }
index 1c7e90d3f0e5c23f7afb43816795c87a48daf63d..429b59538c1af3fb4d76706875eed645ccc2ea78 100644 (file)
@@ -94,12 +94,12 @@ impl From<TableProperties> 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,
         };
index c1329afe18fb8a8a74e1e9c89701568a074eddd9..31a108910fe60d0b36cc361383cae321fad9314f 100644 (file)
@@ -141,8 +141,7 @@ impl From<TableLook> 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() {
index bdb30d6138b9af6df4636dcf6530bd4389ff2928..3162508801278455b042df3579570dce6201eaf5 100644 (file)
@@ -58,8 +58,10 @@ pub struct Params {
     pub line_widths: EnumMap<Stroke, isize>,
 
     /// 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<isize>,
 
     /// Minimum cell width or height before allowing the cell to be broken
index 05b7063bb990ded4b28b1fdd5f2754d39194d658..29384b9b621ea3ca00cac87e6bd1a83160f0ccbc 100644 (file)
@@ -13,7 +13,7 @@
 //
 // You should have received a copy of the GNU General Public License along with
 // this program.  If not, see <http://www.gnu.org/licenses/>.
-
+#![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<Item>, Option<page::PageSetup>) {
         (self.items, self.page_setup)
     }
index 76d14190d1eebf2bb139c6ef6bce0df14abe2d7d..a6c04997b4da14646ad6f98babd4dece6e26ea36 100644 (file)
@@ -99,8 +99,12 @@ impl<'a> Iterator for Lexer<'a> {
 }
 
 impl HorzAlign {
-    pub fn from_css(s: &str) -> Option<Self> {
-        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<Self> {
+        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<Style>, s: &str) {
-        let mut lexer = Lexer(s);
+    /// Parses the CSS found in `css` and returns the corresponding [Style]s.
+    ///
+    /// This is only good enough to parse the simple CSS found in SPV files.
+    pub fn parse_css(css: &str) -> Vec<Style> {
+        let mut lexer = Lexer(css);
+        let mut styles = Vec::new();
         while let Some(token) = lexer.next() {
             if let Token::Id(key) = token
                 && let Some(Token::Colon) = lexer.next()
@@ -142,12 +150,16 @@ impl Style {
                 }
             }
         }
+        styles
     }
 }
 
 impl FontStyle {
-    pub fn parse_css(&mut self, s: &str) {
-        let mut lexer = Lexer(s);
+    /// Parses the CSS found in `css` and modifies this `FontStyle` accordingly.
+    ///
+    /// This is only good enough to parse the simple CSS found in SPV files.
+    pub fn parse_css(&mut self, css: &str) {
+        let mut lexer = Lexer(css);
         while let Some(token) = lexer.next() {
             if let Token::Id(key) = token
                 && let Some(Token::Colon) = lexer.next()
@@ -174,12 +186,18 @@ impl FontStyle {
         }
     }
 
-    pub fn from_css(s: &str) -> Self {
+    /// Parses the CSS found in `css` and returns a corresponding `FontStyle`
+    /// (starting from [FontStyle::default]).
+    ///
+    /// This is only good enough to parse the simple CSS found in SPV files.
+    pub fn from_css(css: &str) -> Self {
         let mut style = FontStyle::default();
-        style.parse_css(s);
+        style.parse_css(css);
         style
     }
 
+    /// Returns CSS for the differences from `base` to this font style.  Returns
+    /// `None` if there are no differences.
     pub fn to_css(&self, base: &FontStyle) -> Option<String> {
         let mut settings = Vec::new();
         if self.font != base.font {
index eefeea481d5777c21c1e2bc2c96fdf8422fe2ffa..179396584222c2e0015e3c49133cc48fd15e95fa 100644 (file)
@@ -634,7 +634,7 @@ fn parse_dom(dom: &Dom) -> Vec<Block> {
     };
 
     // If there's a `head` element, parse it for CSS and then skip past it.
-    let mut css = Vec::new();
+    let mut head_styles = Vec::new();
     let mut default_horz_align = HorzAlign::Left;
     let roots = if let Some((first, rest)) = roots.split_first()
         && let Some(head) = node_as_element(first, "head")
@@ -642,7 +642,7 @@ fn parse_dom(dom: &Dom) -> Vec<Block> {
         if let Some(style) = find_element(&head.children, "style") {
             let mut text = String::new();
             get_element_text(style, &mut text);
-            Style::parse_css(&mut css, &text);
+            head_styles = Style::parse_css(&text);
             if let Some(horz_align) = HorzAlign::from_css(&text) {
                 default_horz_align = horz_align;
             }
@@ -678,7 +678,11 @@ fn parse_dom(dom: &Dom) -> Vec<Block> {
             }
             (end, default_horz_align)
         };
-        blocks.push(Block::new(parse_nodes(&body[start..end]), align, &css));
+        blocks.push(Block::new(
+            parse_nodes(&body[start..end]),
+            align,
+            &head_styles,
+        ));
         start = end;
     }
 
index f47987dbdb6491e2df8b029d22931d86a2ce8cd5..63ae143a98558d4f99d7cd1e6776f2ed1dfcdd08 100644 (file)
@@ -111,8 +111,10 @@ impl LightTable {
                 Axis2::X => self.print_settings.fit_width,
                 Axis2::Y => self.print_settings.fit_length,
             },
-            top_continuation: self.print_settings.top_continuation,
-            bottom_continuation: self.print_settings.bottom_continuation,
+            show_continuations: [
+                self.print_settings.top_continuation,
+                self.print_settings.bottom_continuation,
+            ],
             continuation: self
                 .print_settings
                 .continuation_string
index 89a36d3362f641c65dcd185f297b1591048f73d9..6e49bbf5db54c793b28ccc52ce6898af57558d25 100644 (file)
@@ -352,8 +352,8 @@ impl BinWrite for PivotTable {
             SpvBool(self.style.look.paginate_layers),
             SpvBool(self.style.look.shrink_to_fit[Axis2::X]),
             SpvBool(self.style.look.shrink_to_fit[Axis2::Y]),
-            SpvBool(self.style.look.top_continuation),
-            SpvBool(self.style.look.bottom_continuation),
+            SpvBool(self.style.look.show_continuations[0]),
+            SpvBool(self.style.look.show_continuations[1]),
             self.style.look.n_orphan_lines as u32,
             SpvString(
                 self.style
@@ -549,6 +549,7 @@ impl<W> Writer<W>
 where
     W: Write + Seek,
 {
+    /// Writes `item` to the SPV file.
     pub fn write(&mut self, item: &Item) -> Result<(), Error> {
         if item.details.is_page_break() {
             self.needs_page_break = true;