data::{ByteString, Case, Datum},
dictionary::Dictionary,
file::FileType,
- output::{Criteria, drivers::Driver, spv},
+ output::{Criteria, drivers::Driver},
pc::PcFile,
por::PortableFile,
sys::ReadOptions,
self.write_data(dictionary, cases)
}
Some(FileType::Viewer { .. }) => {
- let (items, page_setup) = spv::ReadOptions::new()
+ let (items, page_setup) = pspp::spv::read::ReadOptions::new()
.with_password(self.password.clone())
.open_file(&self.input)?
.into_parts();
use anyhow::Result;
use clap::{Args, ValueEnum};
-use pspp::output::{Criteria, Item, spv};
+use pspp::output::{Criteria, Item};
use std::{fmt::Display, path::PathBuf};
/// Show information about SPSS viewer files (SPV files).
pub fn run(self) -> Result<()> {
match self.mode {
Mode::Directory => {
- let item = spv::ReadOptions::new()
+ let item = pspp::spv::read::ReadOptions::new()
.with_password(self.password)
.open_file(&self.input)?
.into_items();
Ok(())
}
Mode::View => {
- let item = spv::ReadOptions::new()
+ let item = pspp::spv::read::ReadOptions::new()
.with_password(self.password)
.open_file(&self.input)?
.into_items();
pub mod page;
pub mod pivot;
pub mod render;
-pub mod spv;
pub mod table;
/// A single output item.
use paper_sizes::Unit;
use serde::{Deserialize, Serialize};
-use crate::output::{
- Details, Item, ItemCursor, TextType,
- drivers::{
- Driver,
- cairo::{
- fsm::{CairoFsmStyle, parse_font_style},
- pager::{CairoPageStyle, CairoPager},
+use crate::{
+ output::{
+ Details, Item, ItemCursor, TextType,
+ drivers::{
+ Driver,
+ cairo::{
+ fsm::{CairoFsmStyle, parse_font_style},
+ pager::{CairoPageStyle, CairoPager},
+ },
},
+ page::PageSetup,
+ pivot::{Color, Coord2, FontStyle},
},
- page::PageSetup,
- pivot::{Color, Coord2, FontStyle},
- spv::html::Variable,
+ spv::read::html::Variable,
};
use crate::output::pivot::Axis2;
use crate::output::drivers::cairo::{px_to_xr, xr_to_pt};
use crate::output::pivot::{Axis2, BorderStyle, Coord2, FontStyle, HorzAlign, Rect2, Stroke};
use crate::output::render::{Device, Extreme, Pager, Params};
-use crate::output::spv::html::Markup;
use crate::output::table::DrawCell;
use crate::output::{Details, Item};
use crate::output::{pivot::Color, table::Content};
+use crate::spv::read::html::Markup;
/// Width of an ordinary line.
const LINE_WIDTH: isize = LINE_SPACE / 2;
use enum_map::EnumMap;
use pango::Layout;
-use crate::output::{
- Item,
+use crate::{output::{
drivers::cairo::{
fsm::{CairoFsm, CairoFsmStyle},
xr_to_pt,
- },
- pivot::{Axis2, CellStyle, FontStyle, Rect2, ValueOptions},
- spv::html::{Document, Variable},
- table::DrawCell,
-};
+ }, pivot::{Axis2, CellStyle, FontStyle, Rect2, ValueOptions}, table::DrawCell, Item
+}, spv::read::html::{Document, Variable}};
#[derive(Clone, Debug)]
pub struct CairoPageStyle {
use paper_sizes::{Catalog, Length, PaperSize, Unit};
use serde::{Deserialize, Deserializer, Serialize, de::Error};
-use crate::output::spv::html::Document;
+use crate::spv::read::html::Document;
use super::pivot::Axis2;
use tlo::parse_tlo;
use crate::{
- calendar::date_time_to_pspp,
- data::{ByteString, Datum, EncodedString},
- format::{
- DATETIME40_0, Decimal, F8_2, F40, F40_2, F40_3, Format, PCT40_1,
- Settings as FormatSettings, Type, UncheckedFormat,
- },
- output::spv::html::Markup,
- settings::{Settings, Show},
- util::ToSmallString,
- variable::{VarType, Variable},
+ calendar::date_time_to_pspp, data::{ByteString, Datum, EncodedString}, format::{
+ Decimal, Format, Settings as FormatSettings, Type, UncheckedFormat, DATETIME40_0, F40, F40_2, F40_3, F8_2, PCT40_1
+ }, settings::{Settings, Show}, spv::read::html::Markup, util::ToSmallString, variable::{VarType, Variable}
};
pub mod output;
+++ /dev/null
-// 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/>.
-
-use std::{
- fs::File,
- io::{BufReader, Cursor, Read, Seek},
- path::Path,
-};
-
-use anyhow::{Context, anyhow};
-use binrw::{BinRead, error::ContextExt};
-use cairo::ImageSurface;
-use displaydoc::Display;
-use paper_sizes::PaperSize;
-use serde::Deserialize;
-use zip::{ZipArchive, result::ZipError};
-
-use crate::{
- crypto::EncryptedFile,
- output::{
- Details, Item, SpvInfo, SpvMembers, Text,
- page::{self},
- pivot::{Axis2, Length, Look, TableProperties, Value},
- spv::{
- html::Document,
- legacy_bin::LegacyBin,
- legacy_xml::Visualization,
- light::{LightError, LightTable},
- },
- },
-};
-
-mod css;
-pub mod html;
-mod legacy_bin;
-mod legacy_xml;
-mod light;
-
-/// Options for reading an SPV file.
-#[derive(Clone, Debug, Default)]
-pub struct ReadOptions {
- /// Password to use to unlock an encrypted SPV file.
- ///
- /// For an encrypted SPV file, this must be set to the (encoded or
- /// unencoded) password.
- ///
- /// For a plaintext SPV file, this must be None.
- pub password: Option<String>,
-}
-
-impl ReadOptions {
- /// Construct a new [ReadOptions] without a password.
- pub fn new() -> Self {
- Self::default()
- }
-
- /// Causes the file to be read by decrypting it with the given `password` or
- /// without decrypting if `password` is None.
- pub fn with_password(self, password: Option<String>) -> Self {
- Self { password }
- }
-
- /// Opens the file at `path`.
- pub fn open_file<P>(mut self, path: P) -> Result<SpvFile, anyhow::Error>
- where
- P: AsRef<Path>,
- {
- let file = File::open(path)?;
- if let Some(password) = self.password.take() {
- self.open_reader_encrypted(file, password)
- } else {
- Self::open_reader_inner(file)
- }
- }
-
- /// Opens the file read from `reader`.
- fn open_reader_encrypted<R>(self, reader: R, password: String) -> Result<SpvFile, anyhow::Error>
- where
- R: Read + Seek + 'static,
- {
- Self::open_reader_inner(
- EncryptedFile::new(reader)?
- .unlock(password.as_bytes())
- .map_err(|_| anyhow!("Incorrect password."))?,
- )
- }
-
- /// Opens the file read from `reader`.
- pub fn open_reader<R>(mut self, reader: R) -> Result<SpvFile, anyhow::Error>
- where
- R: Read + Seek + 'static,
- {
- if let Some(password) = self.password.take() {
- self.open_reader_encrypted(reader, password)
- } else {
- Self::open_reader_inner(reader)
- }
- }
-
- fn open_reader_inner<R>(reader: R) -> Result<SpvFile, anyhow::Error>
- where
- R: Read + Seek + 'static,
- {
- // Open archive.
- let mut archive = ZipArchive::new(reader).map_err(|error| match error {
- ZipError::InvalidArchive(_) => Error::NotSpv,
- other => other.into(),
- })?;
- Ok(Self::from_spv_zip_archive(&mut archive)?)
- }
-
- fn from_spv_zip_archive<R>(archive: &mut ZipArchive<R>) -> Result<SpvFile, Error>
- where
- R: Read + Seek,
- {
- // Check manifest.
- let mut file = archive
- .by_name("META-INF/MANIFEST.MF")
- .map_err(|_| Error::NotSpv)?;
- let mut string = String::new();
- file.read_to_string(&mut string)?;
- if string.trim() != "allowPivoting=true" {
- return Err(Error::NotSpv);
- }
- drop(file);
-
- let mut items = Vec::new();
- let mut page_setup = None;
- for i in 0..archive.len() {
- let name = String::from(archive.name_for_index(i).unwrap());
- if name.starts_with("outputViewer") && name.ends_with(".xml") {
- let (mut new_items, ps) = read_heading(archive, i, &name)?;
- items.append(&mut new_items);
- page_setup = page_setup.or(ps);
- }
- }
-
- Ok(SpvFile {
- item: items.into_iter().collect(),
- page_setup,
- })
- }
-}
-
-pub struct SpvFile {
- /// SPV file contents.
- pub item: Vec<Item>,
-
- /// The page setup in the SPV file, if any.
- pub page_setup: Option<page::PageSetup>,
-}
-
-impl SpvFile {
- pub fn into_parts(self) -> (Vec<Item>, Option<page::PageSetup>) {
- (self.item, self.page_setup)
- }
-
- pub fn into_items(self) -> Vec<Item> {
- self.item
- }
-}
-
-#[derive(Debug, Display, thiserror::Error)]
-pub enum Error {
- /// Not an SPV file.
- NotSpv,
-
- /// {0}
- ZipError(#[from] ZipError),
-
- /// {0}
- IoError(#[from] std::io::Error),
-
- /// {0}
- DeError(#[from] quick_xml::DeError),
-
- /// {0}
- BinrwError(#[from] binrw::Error),
-
- /// {0}
- LightError(#[from] LightError),
-
- /// {0}
- CairoError(#[from] cairo::IoError),
-}
-
-fn new_error_item(message: impl Into<Value>) -> Item {
- Text::new_log(message).into_item().with_label("Error")
-}
-
-fn read_heading<R>(
- archive: &mut ZipArchive<R>,
- file_number: usize,
- structure_member: &str,
-) -> Result<(Vec<Item>, Option<page::PageSetup>), Error>
-where
- R: Read + Seek,
-{
- let member = BufReader::new(archive.by_index(file_number)?);
- let mut heading: Heading = match serde_path_to_error::deserialize(
- &mut quick_xml::de::Deserializer::from_reader(member),
- )
- .with_context(|| format!("Failed to parse {structure_member}"))
- {
- Ok(result) => result,
- Err(error) => panic!("{error:?}"),
- };
- let _page_setup = heading.page_setup.take();
- // XXX convert page_setup to the internal format
- Ok((heading.decode(archive, structure_member)?, None))
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Heading {
- #[serde(rename = "@visibility")]
- visibility: Option<String>,
- #[serde(rename = "@commandName")]
- command_name: Option<String>,
- label: Label,
- page_setup: Option<PageSetup>,
-
- #[serde(rename = "$value")]
- #[serde(default)]
- children: Vec<HeadingContent>,
-}
-
-impl Heading {
- fn decode<R>(
- self,
- archive: &mut ZipArchive<R>,
- structure_member: &str,
- ) -> Result<Vec<Item>, Error>
- where
- R: Read + Seek,
- {
- let mut items = Vec::new();
- for child in self.children {
- match child {
- HeadingContent::Container(container) => {
- if container.page_break_before == PageBreakBefore::Always {
- items.push(
- Details::PageBreak
- .into_item()
- .with_spv_info(SpvInfo::new(structure_member)),
- );
- }
- let item = match container.content {
- ContainerContent::Table(table) => {
- table.decode(archive, structure_member).unwrap() /* XXX*/
- }
- ContainerContent::Graph(graph) => graph.decode(structure_member),
- ContainerContent::Text(container_text) => Text::new(
- match container_text.text_type {
- TextType::Title => crate::output::TextType::Title,
- TextType::Log | TextType::Text => crate::output::TextType::Log,
- TextType::PageTitle => crate::output::TextType::PageTitle,
- },
- container_text.decode(),
- )
- .into_item()
- .with_command_name(container_text.command_name)
- .with_spv_info(SpvInfo::new(structure_member)),
- ContainerContent::Image(image) => {
- image.decode(archive, structure_member).unwrap()
- } /*XXX*/,
- ContainerContent::Object(object) => {
- object.decode(archive, structure_member).unwrap()
- } /*XXX*/,
- ContainerContent::Model => new_error_item("models not yet implemented")
- .with_spv_info(SpvInfo::new(structure_member).with_error()),
- ContainerContent::Tree => new_error_item("trees not yet implemented")
- .with_spv_info(SpvInfo::new(structure_member).with_error()),
- };
- items.push(item.with_show(container.visibility == Visibility::Visible));
- }
- HeadingContent::Heading(mut heading) => {
- let show = !heading.visibility.is_some();
- let label = std::mem::take(&mut heading.label.text);
- let command_name = heading.command_name.take();
- items.push(
- heading
- .decode(archive, structure_member)?
- .into_iter()
- .collect::<Item>()
- .with_show(show)
- .with_label(label)
- .with_command_name(command_name)
- .with_spv_info(SpvInfo::new(structure_member)),
- );
- }
- }
- }
- Ok(items)
- }
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct PageSetup {
- #[serde(rename = "@initial-page-number")]
- pub initial_page_number: Option<i32>,
- #[serde(rename = "@chart-size")]
- pub chart_size: Option<ChartSize>,
- #[serde(rename = "@margin-left")]
- pub margin_left: Option<Length>,
- #[serde(rename = "@margin-right")]
- pub margin_right: Option<Length>,
- #[serde(rename = "@margin-top")]
- pub margin_top: Option<Length>,
- #[serde(rename = "@margin-bottom")]
- pub margin_bottom: Option<Length>,
- #[serde(rename = "@paper-height")]
- pub paper_height: Option<Length>,
- #[serde(rename = "@paper-width")]
- pub paper_width: Option<Length>,
- #[serde(rename = "@reference-orientation")]
- pub reference_orientation: Option<ReferenceOrientation>,
- #[serde(rename = "@space-after")]
- pub space_after: Option<Length>,
- pub page_header: PageHeader,
- pub page_footer: PageFooter,
-}
-
-impl PageSetup {
- fn decode(&self) -> page::PageSetup {
- let mut setup = page::PageSetup::default();
- if let Some(initial_page_number) = self.initial_page_number {
- setup.initial_page_number = initial_page_number;
- }
- if let Some(chart_size) = self.chart_size {
- setup.chart_size = chart_size.into();
- }
- if let Some(margin_left) = self.margin_left {
- setup.margins.0[Axis2::X][0] = margin_left.into();
- }
- if let Some(margin_right) = self.margin_right {
- setup.margins.0[Axis2::X][1] = margin_right.into();
- }
- if let Some(margin_top) = self.margin_top {
- setup.margins.0[Axis2::Y][0] = margin_top.into();
- }
- if let Some(margin_bottom) = self.margin_bottom {
- setup.margins.0[Axis2::Y][1] = margin_bottom.into();
- }
- match (self.paper_width, self.paper_height) {
- (Some(width), Some(height)) => {
- setup.paper = PaperSize::new(width.0, height.0, paper_sizes::Unit::Inch)
- }
- (Some(length), None) | (None, Some(length)) => {
- setup.paper = PaperSize::new(length.0, length.0, paper_sizes::Unit::Inch)
- }
- (None, None) => (),
- }
- if let Some(reference_orientation) = self.reference_orientation {
- setup.orientation = reference_orientation.into();
- }
- if let Some(space_after) = self.space_after {
- setup.object_spacing = space_after.into();
- }
- if let Some(PageParagraph { text }) = &self.page_header.page_paragraph {
- setup.header = text.decode();
- }
- if let Some(PageParagraph { text }) = &self.page_footer.page_paragraph {
- setup.footer = text.decode();
- }
- setup
- }
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct PageHeader {
- page_paragraph: Option<PageParagraph>,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct PageFooter {
- page_paragraph: Option<PageParagraph>,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct PageParagraph {
- text: PageParagraphText,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct PageParagraphText {
- #[serde(default, rename = "$text")]
- text: String,
-}
-
-impl PageParagraphText {
- fn decode(&self) -> Document {
- Document::from_html(&self.text)
- }
-}
-
-#[derive(Copy, Clone, Debug, Default, Deserialize)]
-#[serde(rename = "snake_case")]
-pub enum ReferenceOrientation {
- #[serde(alias = "0")]
- #[serde(alias = "0deg")]
- #[serde(alias = "inherit")]
- #[default]
- Portrait,
-
- #[serde(alias = "90")]
- #[serde(alias = "90deg")]
- #[serde(alias = "-270")]
- #[serde(alias = "-270deg")]
- Landscape,
-
- #[serde(alias = "180")]
- #[serde(alias = "180deg")]
- #[serde(alias = "-1280")]
- #[serde(alias = "-180deg")]
- ReversePortrait,
-
- #[serde(alias = "270")]
- #[serde(alias = "270deg")]
- #[serde(alias = "-90")]
- #[serde(alias = "-90deg")]
- Seascape,
-}
-
-impl From<ReferenceOrientation> for page::Orientation {
- fn from(value: ReferenceOrientation) -> Self {
- match value {
- ReferenceOrientation::Portrait | ReferenceOrientation::ReversePortrait => {
- page::Orientation::Portrait
- }
- ReferenceOrientation::Landscape | ReferenceOrientation::Seascape => {
- page::Orientation::Landscape
- }
- }
- }
-}
-
-/// Chart size.
-#[derive(Copy, Clone, Debug, Default, Deserialize)]
-pub enum ChartSize {
- #[default]
- #[serde(rename = "as-is")]
- AsIs,
-
- #[serde(rename = "full-height")]
- FullHeight,
-
- #[serde(rename = "half-height")]
- HalfHeight,
-
- #[serde(rename = "quarter-height")]
- QuarterHeight,
-}
-
-impl From<ChartSize> for page::ChartSize {
- fn from(value: ChartSize) -> Self {
- match value {
- ChartSize::AsIs => page::ChartSize::AsIs,
- ChartSize::FullHeight => page::ChartSize::FullHeight,
- ChartSize::HalfHeight => page::ChartSize::HalfHeight,
- ChartSize::QuarterHeight => page::ChartSize::QuarterHeight,
- }
- }
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-enum HeadingContent {
- Container(Container),
- Heading(Box<Heading>),
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Label {
- #[serde(default, rename = "$text")]
- text: String,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Container {
- #[serde(default, rename = "@visibility")]
- visibility: Visibility,
- #[serde(rename = "@page-break-before")]
- #[serde(default)]
- page_break_before: PageBreakBefore,
- #[serde(rename = "@text-align")]
- text_align: Option<TextAlign>,
- #[serde(rename = "@width")]
- width: Option<String>,
- label: Label,
-
- #[serde(rename = "$value")]
- content: ContainerContent,
-}
-
-#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum PageBreakBefore {
- #[default]
- Auto,
- Always,
- Avoid,
- Left,
- Right,
- Inherit,
-}
-
-#[derive(Deserialize, Debug, Default, PartialEq, Eq)]
-#[serde(rename_all = "camelCase")]
-enum Visibility {
- #[default]
- Visible,
- Hidden,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-enum TextAlign {
- Left,
- Center,
- Right,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-enum ContainerContent {
- Table(Table),
- Text(ContainerText),
- Graph(Graph),
- Model,
- Object(Object),
- Image(Image),
- Tree,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Graph {
- #[serde(rename = "@commandName")]
- command_name: String,
- data_path: Option<String>,
- path: String,
- csv_path: Option<String>,
-}
-
-impl Graph {
- fn decode(&self, structure_member: &str) -> Item {
- crate::output::Chart
- .into_item()
- .with_spv_info(
- SpvInfo::new(structure_member).with_members(SpvMembers::Graph {
- data: self.data_path.clone(),
- xml: self.path.clone(),
- csv: self.csv_path.clone(),
- }),
- )
- }
-}
-
-fn decode_image<R>(
- archive: &mut ZipArchive<R>,
- structure_member: &str,
- command_name: &Option<String>,
- image_name: &str,
-) -> Result<Item, Error>
-where
- R: Read + Seek,
-{
- let mut png = archive.by_name(image_name)?;
- let image = ImageSurface::create_from_png(&mut png)?;
- Ok(Details::Image(image)
- .into_item()
- .with_command_name(command_name.clone())
- .with_spv_info(
- SpvInfo::new(structure_member).with_members(SpvMembers::Image(image_name.into())),
- ))
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Image {
- #[serde(rename = "@commandName")]
- command_name: Option<String>,
- data_path: String,
-}
-
-impl Image {
- fn decode<R>(&self, archive: &mut ZipArchive<R>, structure_member: &str) -> Result<Item, Error>
- where
- R: Read + Seek,
- {
- decode_image(
- archive,
- structure_member,
- &self.command_name,
- &self.data_path,
- )
- }
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Object {
- #[serde(rename = "@commandName")]
- command_name: Option<String>,
- #[serde(rename = "@uri")]
- uri: String,
-}
-
-impl Object {
- fn decode<R>(&self, archive: &mut ZipArchive<R>, structure_member: &str) -> Result<Item, Error>
- where
- R: Read + Seek,
- {
- decode_image(archive, structure_member, &self.command_name, &self.uri)
- }
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Table {
- #[serde(rename = "@commandName")]
- command_name: String,
- #[serde(rename = "@subType")]
- sub_type: String,
- #[serde(rename = "@tableId")]
- table_id: Option<i64>,
- #[serde(rename = "@type")]
- table_type: TableType,
- properties: Option<TableProperties>,
- table_structure: TableStructure,
-}
-
-impl Table {
- fn decode<R>(&self, archive: &mut ZipArchive<R>, structure_member: &str) -> Result<Item, Error>
- where
- R: Read + Seek,
- {
- match &self.table_structure.path {
- None => {
- let member_name = &self.table_structure.data_path;
- let mut light = archive.by_name(member_name)?;
- let mut data = Vec::with_capacity(light.size() as usize);
- light.read_to_end(&mut data)?;
- let mut cursor = Cursor::new(data);
- let table = LightTable::read(&mut cursor).map_err(|e| {
- e.with_message(format!(
- "While parsing {member_name:?} as light binary SPV member"
- ))
- })?;
- let pivot_table = table.decode()?;
- Ok(pivot_table.into_item().with_spv_info(
- SpvInfo::new(structure_member)
- .with_members(SpvMembers::Light(self.table_structure.data_path.clone())),
- ))
- }
- Some(xml_member_name) => {
- let bin_member_name = &self.table_structure.data_path;
- let mut bin_member = archive.by_name(bin_member_name)?;
- let mut bin_data = Vec::with_capacity(bin_member.size() as usize);
- bin_member.read_to_end(&mut bin_data)?;
- let mut cursor = Cursor::new(bin_data);
- let legacy_bin = LegacyBin::read(&mut cursor).map_err(|e| {
- e.with_message(format!(
- "While parsing {bin_member_name:?} as legacy binary SPV member"
- ))
- })?;
- let data = legacy_bin.decode();
- drop(bin_member);
-
- let member = BufReader::new(archive.by_name(&xml_member_name)?);
- let visualization: Visualization = match serde_path_to_error::deserialize(
- &mut quick_xml::de::Deserializer::from_reader(member),
- )
- .with_context(|| format!("Failed to parse {xml_member_name}"))
- {
- Ok(result) => result,
- Err(error) => panic!("{error:?}"),
- };
- let pivot_table = visualization.decode(
- data,
- self.properties
- .as_ref()
- .map_or_else(Look::default, |properties| properties.clone().into()),
- )?;
-
- Ok(pivot_table.into_item().with_spv_info(
- SpvInfo::new(structure_member).with_members(SpvMembers::Legacy {
- xml: xml_member_name.clone(),
- binary: bin_member_name.clone(),
- }),
- ))
- }
- }
- }
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-enum TableType {
- Table,
- Note,
- Warning,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct ContainerText {
- #[serde(rename = "@type")]
- text_type: TextType,
- #[serde(rename = "@commandName")]
- command_name: Option<String>,
- html: String,
-}
-
-impl ContainerText {
- fn decode(&self) -> Value {
- html::Document::from_html(&self.html).into_value()
- }
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-enum TextType {
- Title,
- Log,
- Text,
- #[serde(rename = "page-title")]
- PageTitle,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct TableStructure {
- /// The `.xml` member name, for legacy members only.
- path: Option<String>,
- /// The `.bin` member name.
- data_path: String,
- /// Rarely used, not understood.
- csv_path: Option<String>,
-}
-
-#[cfg(test)]
-#[test]
-fn test_spv() {
- let items = ReadOptions::new()
- .open_file("/home/blp/pspp/rust/tests/utilities/regress.spv")
- .unwrap()
- .into_items();
- for item in items {
- println!("{item}");
- }
- todo!()
-}
+++ /dev/null
-use std::{
- borrow::Cow,
- fmt::{Display, Write},
- mem::discriminant,
- ops::Not,
-};
-
-use itertools::Itertools;
-
-use crate::output::{
- pivot::{FontStyle, HorzAlign},
- spv::html::Style,
-};
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-enum Token<'a> {
- Id(Cow<'a, str>),
- LeftCurly,
- RightCurly,
- Colon,
- Semicolon,
- Error,
-}
-
-struct Lexer<'a>(&'a str);
-
-impl<'a> Iterator for Lexer<'a> {
- type Item = Token<'a>;
-
- fn next(&mut self) -> Option<Self::Item> {
- let mut s = self.0;
- loop {
- s = s.trim_start();
- if let Some(rest) = s.strip_prefix("<!--") {
- s = rest;
- } else if let Some(rest) = s.strip_prefix("-->") {
- s = rest;
- } else {
- break;
- }
- }
- let mut iter = s.chars();
- let (c, mut rest) = (iter.next()?, iter.as_str());
- let (token, rest) = match c {
- '{' => (Token::LeftCurly, rest),
- '}' => (Token::RightCurly, rest),
- ':' => (Token::Colon, rest),
- ';' => (Token::Semicolon, rest),
- '\'' | '"' => {
- let quote = c;
- let mut s = String::new();
- while let Some(c) = iter.next() {
- if c == quote {
- break;
- } else if c != '\\' {
- s.push(c);
- } else {
- let start = iter.as_str();
- match iter.next() {
- None => break,
- Some(a) if a.is_ascii_alphanumeric() => {
- let n = start
- .chars()
- .take_while(|c| c.is_ascii_alphanumeric())
- .take(6)
- .count();
- iter = start[n..].chars();
- if let Ok(code_point) = u32::from_str_radix(&start[..n], 16)
- && let Ok(c) = char::try_from(code_point)
- {
- s.push(c);
- }
- }
- Some('\n') => (),
- Some(other) => s.push(other),
- }
- }
- }
- (Token::Id(Cow::from(s)), iter.as_str())
- }
- _ => {
- while !iter.as_str().starts_with("-->")
- && let Some(c) = iter.next()
- && !c.is_whitespace()
- && c != '{'
- && c != '}'
- && c != ':'
- && c != ';'
- {
- rest = iter.as_str();
- }
- let id_len = s.len() - rest.len();
- let (id, rest) = s.split_at(id_len);
- (Token::Id(Cow::from(id)), rest)
- }
- };
- self.0 = rest;
- Some(token)
- }
-}
-
-impl HorzAlign {
- pub fn from_css(s: &str) -> Option<Self> {
- let mut lexer = Lexer(s);
- while let Some(token) = lexer.next() {
- if let Token::Id(key) = token
- && let Some(Token::Colon) = lexer.next()
- && let Some(Token::Id(value)) = lexer.next()
- && key.as_ref() == "text-align"
- && let Ok(align) = value.parse()
- {
- return Some(align);
- }
- }
- None
- }
-}
-
-impl Style {
- pub fn parse_css(styles: &mut Vec<Style>, s: &str) {
- let mut lexer = Lexer(s);
- while let Some(token) = lexer.next() {
- if let Token::Id(key) = token
- && let Some(Token::Colon) = lexer.next()
- && let Some(Token::Id(value)) = lexer.next()
- && let Some((style, add)) = match key.as_ref() {
- "color" => value.parse().ok().map(|color| (Style::Color(color), true)),
- "font-weight" => Some((Style::Bold, value == "bold")),
- "font-style" => Some((Style::Italic, value == "italic")),
- "text-decoration" => Some((Style::Underline, value == "underline")),
- "font-family" => Some((Style::Face(value.into()), true)),
- "font-size" => value
- .parse::<i32>()
- .ok()
- .map(|size| (Style::Size(size as f64 * 0.75), true)),
- _ => None,
- }
- {
- // Remove from `styles` any style of the same kind as `style`.
- styles.retain(|s| discriminant(s) != discriminant(&style));
- if add {
- styles.push(style);
- }
- }
- }
- }
-}
-
-impl FontStyle {
- pub fn parse_css(&mut self, s: &str) {
- let mut lexer = Lexer(s);
- while let Some(token) = lexer.next() {
- if let Token::Id(key) = token
- && let Some(Token::Colon) = lexer.next()
- && let Some(Token::Id(value)) = lexer.next()
- {
- match key.as_ref() {
- "color" => {
- if let Ok(color) = value.parse() {
- self.fg = color;
- }
- }
- "font-weight" => self.bold = value == "bold",
- "font-style" => self.italic = value == "italic",
- "text-decoration" => self.underline = value == "underline",
- "font-family" => self.font = value.into(),
- "font-size" => {
- if let Ok(size) = value.parse::<i32>() {
- self.size = (size as i64 * 3 / 4) as i32;
- }
- }
- _ => (),
- }
- }
- }
- }
-
- pub fn from_css(s: &str) -> Self {
- let mut style = FontStyle::default();
- style.parse_css(s);
- style
- }
-
- pub fn to_css(&self, base: &FontStyle) -> Option<String> {
- let mut settings = Vec::new();
- if self.font != base.font {
- if is_css_ident(&self.font) {
- settings.push(format!("font-family: {}", &self.font));
- } else {
- settings.push(format!("font-family: {}", CssString(&self.font)));
- }
- }
- if self.bold != base.bold {
- settings.push(format!(
- "font-weight: {}",
- if self.bold { "bold" } else { "normal" }
- ));
- }
- if self.italic != base.italic {
- settings.push(format!(
- "font-style: {}",
- if self.bold { "italic" } else { "normal" }
- ));
- }
- if self.underline != base.underline {
- settings.push(format!(
- "text-decoration: {}",
- if self.bold { "underline" } else { "none" }
- ));
- }
- if self.size != base.size {
- settings.push(format!("font-size: {}", self.size as i64 * 4 / 3));
- }
- if self.fg != base.fg {
- settings.push(format!("color: {}", self.fg.display_css()));
- }
- settings
- .is_empty()
- .not()
- .then(|| format!("<!-- p {{ {} }} -->", settings.into_iter().join("; ")))
- }
-}
-
-fn is_css_ident(s: &str) -> bool {
- fn is_nmstart(c: char) -> bool {
- c.is_ascii_alphabetic() || c == '_'
- }
- s.chars().next().is_some_and(is_nmstart) && s.chars().all(|c| is_nmstart(c) || c as u32 > 159)
-}
-
-struct CssString<'a>(&'a str);
-
-impl<'a> Display for CssString<'a> {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let quote = if self.0.contains('"') && !self.0.contains('\'') {
- '\''
- } else {
- '"'
- };
- f.write_char(quote)?;
- for c in self.0.chars() {
- match c {
- _ if c == quote || c == '\\' => {
- f.write_char('\\')?;
- f.write_char(c)?;
- }
- '\n' => f.write_str("\\00000a")?,
- c => f.write_char(c)?,
- }
- }
- f.write_char(quote)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use std::borrow::Cow;
-
- use crate::output::{
- pivot::{Color, FontStyle, HorzAlign},
- spv::css::{Lexer, Token},
- };
-
- #[test]
- fn css_horz_align() {
- assert_eq!(
- HorzAlign::from_css("text-align: left"),
- Some(HorzAlign::Left)
- );
- assert_eq!(
- HorzAlign::from_css("margin-top: 0; text-align:center"),
- Some(HorzAlign::Center)
- );
- assert_eq!(
- HorzAlign::from_css("text-align: Right; margin-top:0"),
- Some(HorzAlign::Right)
- );
- assert_eq!(HorzAlign::from_css("text-align: other"), None);
- assert_eq!(HorzAlign::from_css("margin-top: 0"), None);
- }
-
- #[test]
- fn css_strings() {
- #[track_caller]
- fn test_string(css: &str, value: &str) {
- let mut lexer = Lexer(css);
- assert_eq!(lexer.next(), Some(Token::Id(Cow::from(value))));
- assert_eq!(lexer.next(), None);
- }
-
- test_string(r#""abc""#, "abc");
- test_string(r#""a\"'\'bc""#, "a\"''bc");
- test_string(r#""a\22 bc""#, "a\" bc");
- test_string(r#""a\000022bc""#, "a\"bc");
- test_string(r#""a'bc""#, "a'bc");
- test_string(
- r#""\\\
-xyzzy""#,
- "\\xyzzy",
- );
-
- test_string(r#"'abc'"#, "abc");
- test_string(r#"'a"\"\'bc'"#, "a\"\"'bc");
- test_string(r#"'a\22 bc'"#, "a\" bc");
- test_string(r#"'a\000022bc'"#, "a\"bc");
- test_string(r#"'a\'bc'"#, "a'bc");
- test_string(
- r#"'a\'bc\
-xyz'"#,
- "a'bcxyz",
- );
- test_string(r#"'\\'"#, "\\");
- }
-
- #[test]
- fn style_from_css() {
- assert_eq!(FontStyle::from_css(""), FontStyle::default());
- assert_eq!(
- FontStyle::from_css(r#"p{color:ff0000}"#),
- FontStyle::default().with_fg(Color::RED)
- );
- assert_eq!(
- FontStyle::from_css("p {font-weight: bold; text-decoration: underline}"),
- FontStyle::default().with_bold(true).with_underline(true)
- );
- assert_eq!(
- FontStyle::from_css("p {font-family: Monospace}"),
- FontStyle::default().with_font("Monospace")
- );
- assert_eq!(
- FontStyle::from_css("p {font-size: 24}"),
- FontStyle::default().with_size(18)
- );
- assert_eq!(
- FontStyle::from_css(
- "<!--color: red; font-weight: bold; font-style: italic; text-decoration: underline; font-family: Serif-->"
- ),
- FontStyle::default()
- .with_fg(Color::RED)
- .with_bold(true)
- .with_italic(true)
- .with_underline(true)
- .with_font("Serif")
- );
- }
-
- #[test]
- fn style_to_css() {
- let base = FontStyle::default();
- assert_eq!(base.to_css(&base), None);
- assert_eq!(
- FontStyle::default().with_size(18).to_css(&base),
- Some("<!-- p { font-size: 24 } -->".into())
- );
- assert_eq!(
- FontStyle::default()
- .with_bold(true)
- .with_underline(true)
- .to_css(&base),
- Some("<!-- p { font-weight: bold; text-decoration: underline } -->".into())
- );
- assert_eq!(
- FontStyle::default().with_fg(Color::RED).to_css(&base),
- Some("<!-- p { color: #ff0000 } -->".into())
- );
- assert_eq!(
- FontStyle::default().with_font("Monospace").to_css(&base),
- Some("<!-- p { font-family: Monospace } -->".into())
- );
- assert_eq!(
- FontStyle::default()
- .with_font("Times New Roman")
- .to_css(&base),
- Some(r#"<!-- p { font-family: "Times New Roman" } -->"#.into())
- );
- }
-}
+++ /dev/null
-#![warn(dead_code)]
-use std::{
- borrow::{Borrow, Cow},
- fmt::{Display, Write as _},
- io::{Cursor, Write},
- mem::{discriminant, take},
- str::FromStr,
-};
-
-use hashbrown::HashMap;
-use html_parser::{Dom, Element, Node};
-use pango::{AttrColor, AttrInt, AttrList, AttrSize, AttrString, IsAttribute};
-use quick_xml::{
- Writer as XmlWriter,
- escape::unescape,
- events::{BytesText, Event},
-};
-use serde::{Deserialize, Deserializer, Serialize, ser::SerializeMap};
-
-use crate::output::pivot::{CellStyle, Color, FontStyle, HorzAlign, Value};
-
-fn lowercase<'a>(s: &'a str) -> Cow<'a, str> {
- if s.chars().any(|c| c.is_ascii_uppercase()) {
- Cow::from(s.to_ascii_lowercase())
- } else {
- Cow::from(s)
- }
-}
-
-#[derive(Clone, Debug, PartialEq)]
-pub enum Markup {
- Seq(Vec<Markup>),
- Text(String),
- Variable(Variable),
- Style { style: Style, child: Box<Markup> },
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize)]
-pub enum Variable {
- Date,
- Time,
- Head(u8),
- PageTitle,
- Page,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
-#[error("Unknown variable")]
-pub struct UnknownVariable;
-
-impl FromStr for Variable {
- type Err = UnknownVariable;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- match s {
- "Date" => Ok(Self::Date),
- "Time" => Ok(Self::Time),
- "PageTitle" => Ok(Self::PageTitle),
- "Page" => Ok(Self::Page),
- _ => {
- if let Some(suffix) = s.strip_prefix("Head")
- && let Ok(number) = suffix.parse()
- && number >= 1
- {
- Ok(Self::Head(number))
- } else {
- Err(UnknownVariable)
- }
- }
- }
- }
-}
-
-impl Variable {
- fn as_str(&self) -> Cow<'static, str> {
- match self {
- Variable::Date => Cow::from("Date"),
- Variable::Time => Cow::from("Time"),
- Variable::Head(index) => Cow::from(format!("Head{index}")),
- Variable::PageTitle => Cow::from("PageTitle"),
- Variable::Page => Cow::from("Page"),
- }
- }
-}
-
-impl Display for Variable {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}", self.as_str())
- }
-}
-
-impl Default for Markup {
- fn default() -> Self {
- Self::Seq(Vec::new())
- }
-}
-
-impl Serialize for Markup {
- fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- where
- S: serde::Serializer,
- {
- match self {
- Markup::Seq(inner) => serializer.collect_seq(inner),
- Markup::Text(string) => serializer.serialize_str(string.as_str()),
- Markup::Variable(name) => serializer.serialize_newtype_struct("Variable", name),
- Markup::Style { style, child } => {
- let (mut style, mut child) = (style, child);
- let mut styles = HashMap::new();
- loop {
- styles.insert(discriminant(style), style);
- match &**child {
- Markup::Style {
- style: inner,
- child: inner_child,
- } => {
- style = inner;
- child = inner_child;
- }
- _ => break,
- }
- }
- let mut map = serializer.serialize_map(Some(styles.len() + 1))?;
- for style in styles.into_values() {
- match style {
- Style::Bold => map.serialize_entry("bool", &true),
- Style::Italic => map.serialize_entry("italic", &true),
- Style::Underline => map.serialize_entry("underline", &true),
- Style::Strike => map.serialize_entry("strike", &true),
- Style::Emphasis => map.serialize_entry("em", &true),
- Style::Strong => map.serialize_entry("strong", &true),
- Style::Face(name) => map.serialize_entry("font", name),
- Style::Color(color) => map.serialize_entry("color", color),
- Style::Size(size) => map.serialize_entry("size", size),
- }?;
- }
- map.serialize_entry("content", child)?;
- map.end()
- }
- }
- }
-}
-
-impl Display for Markup {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- fn inner(this: &Markup, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match this {
- Markup::Seq(seq) => {
- for markup in seq {
- inner(markup, f)?;
- }
- Ok(())
- }
- Markup::Text(string) => f.write_str(string.as_str()),
- Markup::Variable(name) => write!(f, "&[{name}]"),
- Markup::Style { child, .. } => inner(child, f),
- }
- }
- inner(self, f)
- }
-}
-
-impl Markup {
- fn is_empty(&self) -> bool {
- match self {
- Markup::Seq(seq) => seq.is_empty(),
- _ => false,
- }
- }
- fn is_style(&self) -> bool {
- matches!(self, Markup::Style { .. })
- }
- fn into_style(self) -> Option<(Style, Markup)> {
- match self {
- Markup::Style { style, child } => Some((style, *child)),
- _ => None,
- }
- }
- fn is_text(&self) -> bool {
- matches!(self, Markup::Text(_))
- }
- fn as_text(&self) -> Option<&str> {
- match self {
- Markup::Text(text) => Some(text.as_str()),
- _ => None,
- }
- }
- fn into_text(self) -> Option<String> {
- match self {
- Markup::Text(text) => Some(text),
- _ => None,
- }
- }
- fn write_html<X>(&self, writer: &mut XmlWriter<X>) -> std::io::Result<()>
- where
- X: Write,
- {
- match self {
- Markup::Seq(children) => {
- for child in children {
- child.write_html(writer)?;
- }
- }
- Markup::Text(text) => writer.write_event(Event::Text(BytesText::new(text.as_str())))?,
- Markup::Variable(name) => {
- writer.write_event(Event::Text(BytesText::new(&format!("&[{name}]"))))?
- }
- Markup::Style { style, child } => {
- match style {
- Style::Bold => writer.create_element("b"),
- Style::Italic => writer.create_element("i"),
- Style::Underline => writer.create_element("u"),
- Style::Strike => writer.create_element("strike"),
- Style::Emphasis => writer.create_element("em"),
- Style::Strong => writer.create_element("strong"),
- Style::Face(face) => writer
- .create_element("font")
- .with_attribute(("face", face.as_str())),
- Style::Color(color) => writer
- .create_element("font")
- .with_attribute(("color", color.display_css().to_string().as_str())),
- Style::Size(points) => writer
- .create_element("font")
- .with_attribute(("size", format!("{points}pt").as_str())),
- }
- .write_inner_content(|w| child.write_html(w))?;
- }
- }
- Ok(())
- }
-
- pub fn to_html(&self) -> String {
- let mut writer = XmlWriter::new(Cursor::new(Vec::new()));
- writer
- .create_element("html")
- .write_inner_content(|w| self.write_html(w))
- .unwrap();
- String::from_utf8(writer.into_inner().into_inner()).unwrap()
- }
-
- pub fn to_pango<'a, F>(&self, substitutions: F) -> (String, AttrList)
- where
- F: Fn(Variable) -> Option<Cow<'a, str>>,
- {
- let mut s = String::new();
- let mut attrs = AttrList::new();
- self.to_pango_inner(&substitutions, &mut s, &mut attrs);
- (s, attrs)
- }
-
- fn to_pango_inner<'a, F>(&self, substitutions: &F, s: &mut String, attrs: &mut AttrList)
- where
- F: Fn(Variable) -> Option<Cow<'a, str>>,
- {
- match self {
- Markup::Seq(seq) => {
- for child in seq {
- child.to_pango_inner(substitutions, s, attrs);
- }
- }
- Markup::Text(string) => s.push_str(&string),
- Markup::Variable(variable) => match substitutions(*variable) {
- Some(value) => s.push_str(&*value),
- None => write!(s, "&[{variable}]").unwrap(),
- },
- Markup::Style { style, child } => {
- let start_index = s.len();
- child.to_pango_inner(substitutions, s, attrs);
- let end_index = s.len();
-
- let mut attr = match style {
- Style::Bold | Style::Strong => {
- AttrInt::new_weight(pango::Weight::Bold).upcast()
- }
- Style::Italic | Style::Emphasis => {
- AttrInt::new_style(pango::Style::Italic).upcast()
- }
- Style::Underline => AttrInt::new_underline(pango::Underline::Single).upcast(),
- Style::Strike => AttrInt::new_strikethrough(true).upcast(),
- Style::Face(face) => AttrString::new_family(&face).upcast(),
- Style::Color(color) => {
- let (r, g, b) = color.into_rgb16();
- AttrColor::new_foreground(r, g, b).upcast()
- }
- Style::Size(points) => AttrSize::new((points * 1024.0) as i32).upcast(),
- };
- attr.set_start_index(start_index as u32);
- attr.set_end_index(end_index as u32);
- attrs.insert(attr);
- }
- }
- }
-
- fn parse_variables(&self) -> Option<Vec<Markup>> {
- let Some(mut s) = self.as_text() else {
- return None;
- };
- let mut results = Vec::new();
- let mut offset = 0;
- while let Some(start) = s[offset..].find("&[").map(|pos| pos + offset)
- && let Some(end) = s[start..].find("]").map(|pos| pos + start)
- {
- if let Ok(variable) = Variable::from_str(&s[start + 2..end]) {
- if start > 0 {
- results.push(Markup::Text(s[..start].into()));
- }
- results.push(Markup::Variable(variable));
- s = &s[end + 1..];
- offset = 0;
- } else {
- offset = end + 1;
- }
- }
- if results.is_empty() {
- None
- } else {
- if !s.is_empty() {
- results.push(Markup::Text(s.into()));
- }
- Some(results)
- }
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Serialize)]
-pub struct Paragraph {
- pub markup: Markup,
- pub horz_align: HorzAlign,
-}
-
-impl Default for Paragraph {
- fn default() -> Self {
- Self {
- markup: Markup::default(),
- horz_align: HorzAlign::Left,
- }
- }
-}
-
-impl Paragraph {
- fn new(mut markup: Markup, horz_align: HorzAlign, css: &[Style]) -> Self {
- for style in css {
- apply_style(&mut markup, style.clone());
- }
- Self { markup, horz_align }
- }
-
- fn into_value(self) -> Value {
- let mut font_style = FontStyle::default().with_size(10);
- let cell_style = CellStyle::default().with_horz_align(Some(self.horz_align));
- let mut markup = self.markup;
- let mut strike = false;
- while markup.is_style() {
- let (style, child) = markup.into_style().unwrap();
- match style {
- Style::Bold => font_style.bold = true,
- Style::Italic => font_style.italic = true,
- Style::Underline => font_style.underline = true,
- Style::Strike => strike = true,
- Style::Emphasis => font_style.italic = true,
- Style::Strong => font_style.bold = true,
- Style::Face(face) => font_style.font = face,
- Style::Color(color) => font_style.fg = color,
- Style::Size(points) => font_style.size = points as i32,
- };
- markup = child;
- }
- if strike {
- apply_style(&mut markup, Style::Strike);
- }
- if markup.is_text() {
- Value::new_user_text(markup.into_text().unwrap())
- } else {
- Value::new_markup(markup)
- }
- .with_font_style(font_style)
- .with_cell_style(cell_style)
- }
-}
-
-#[derive(Clone, Debug, Default, PartialEq)]
-pub struct Document(pub Vec<Paragraph>);
-
-impl<'de> Deserialize<'de> for Document {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: Deserializer<'de>,
- {
- Ok(Document::from_html(&String::deserialize(deserializer)?))
- }
-}
-
-impl Serialize for Document {
- fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- where
- S: serde::Serializer,
- {
- self.to_html().serialize(serializer)
- }
-}
-
-impl Document {
- pub fn is_empty(&self) -> bool {
- self.0.is_empty()
- }
-
- pub fn from_html(input: &str) -> Self {
- match Dom::parse(&format!("<!doctype html>{input}")) {
- Ok(dom) => Self(parse_dom(&dom)),
- Err(_) if !input.is_empty() => Self(vec![Paragraph {
- markup: Markup::Text(input.into()),
- horz_align: HorzAlign::Left,
- }]),
- Err(_) => Self::default(),
- }
- }
-
- pub fn into_value(self) -> Value {
- self.0.into_iter().next().unwrap_or_default().into_value()
- }
-
- pub fn to_html(&self) -> String {
- let mut writer = XmlWriter::new(Cursor::new(Vec::new()));
- writer
- .create_element("html")
- .write_inner_content(|w| {
- for paragraph in &self.0 {
- w.create_element("p")
- .with_attribute(("align", paragraph.horz_align.as_str().unwrap()))
- .write_inner_content(|w| paragraph.markup.write_html(w))?;
- }
- Ok(())
- })
- .unwrap();
-
- // Return the result with `<html>` and `</html>` stripped off.
- str::from_utf8(&writer.into_inner().into_inner())
- .unwrap()
- .strip_prefix("<html>")
- .unwrap()
- .strip_suffix("</html>")
- .unwrap()
- .into()
- }
-
- pub fn to_values(&self) -> Vec<Value> {
- self.0
- .iter()
- .map(|paragraph| paragraph.clone().into_value())
- .collect()
- }
-}
-
-#[derive(Clone, Debug, PartialEq)]
-pub enum Style {
- Bold,
- Italic,
- Underline,
- Strike,
- Emphasis,
- Strong,
- Face(String),
- Color(Color),
- Size(f64),
-}
-
-fn node_as_element<'a>(node: &'a Node, name: &str) -> Option<&'a Element> {
- if let Node::Element(element) = node
- && element.name.eq_ignore_ascii_case(name)
- {
- Some(element)
- } else {
- None
- }
-}
-
-fn node_is_element(node: &Node, name: &str) -> bool {
- node_as_element(node, name).is_some()
-}
-
-/// Returns the horizontal alignment for the `<p>` element in `p`.
-fn horz_align_from_p(p: &Element) -> Option<HorzAlign> {
- if let Some(Some(s)) = p.attributes.get("align")
- && let Ok(align) = HorzAlign::from_str(s)
- {
- Some(align)
- } else if let Some(Some(s)) = p.attributes.get("style")
- && let Some(align) = HorzAlign::from_css(s)
- {
- Some(align)
- } else {
- None
- }
-}
-
-fn apply_style(markup: &mut Markup, style: Style) {
- let child = take(markup);
- *markup = Markup::Style {
- style,
- child: Box::new(child),
- };
-}
-
-pub fn parse_dom(dom: &Dom) -> Vec<Paragraph> {
- // Get the top-level elements, descending into an `html` element if
- // there is one.
- let roots = if dom.children.len() == 1
- && let Some(first) = dom.children.first()
- && let Some(html) = node_as_element(first, "html")
- {
- &html.children
- } else {
- &dom.children
- };
-
- // If there's a `head` element, parse it for CSS and then skip past it.
- let mut css = 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")
- {
- 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);
- if let Some(horz_align) = HorzAlign::from_css(&text) {
- default_horz_align = horz_align;
- }
- }
- rest
- } else {
- roots
- };
-
- // If only a `body` element is left, descend into it.
- let body = if roots.len() == 1
- && let Some(first) = roots.first()
- && let Some(body) = node_as_element(first, "body")
- {
- &body.children
- } else {
- roots
- };
-
- let mut paragraphs = Vec::new();
-
- let mut start = 0;
- while start < body.len() {
- let (end, align) = if let Some(p) = node_as_element(&body[start], "p") {
- (
- start + 1,
- horz_align_from_p(p).unwrap_or(default_horz_align),
- )
- } else {
- let mut end = start + 1;
- while end < body.len() && !node_is_element(&body[end], "p") {
- end += 1;
- }
- (end, default_horz_align)
- };
- paragraphs.push(Paragraph::new(parse_nodes(&body[start..end]), align, &css));
- start = end;
- }
-
- paragraphs
-}
-
-fn parse_nodes(nodes: &[Node]) -> Markup {
- // Appends `markup` to `dst`, merging text at the end of `dst` with text
- // in `markup`.
- fn add_markup(dst: &mut Vec<Markup>, markup: Markup) {
- if let Markup::Text(suffix) = &markup
- && let Some(Markup::Text(last)) = dst.last_mut()
- {
- last.push_str(&suffix);
- } else {
- dst.push(markup);
- }
-
- if let Some(mut expansion) = dst.last().unwrap().parse_variables() {
- dst.pop();
- dst.append(&mut expansion);
- }
- }
-
- let mut retval = Vec::new();
- for (i, node) in nodes.iter().enumerate() {
- match node {
- Node::Comment(_) => (),
- Node::Text(text) => {
- let text = if i == 0 {
- text.trim_start()
- } else {
- text.as_str()
- };
- let text = if i == nodes.len() - 1 {
- text.trim_end()
- } else {
- text
- };
- add_markup(
- &mut retval,
- Markup::Text(unescape(&text).unwrap_or(Cow::from(text)).into_owned()),
- );
- }
- Node::Element(br) if br.name.eq_ignore_ascii_case("br") => {
- add_markup(&mut retval, Markup::Text('\n'.into()));
- }
- Node::Element(element) => {
- let mut inner = parse_nodes(&element.children);
- if inner.is_empty() {
- continue;
- }
-
- let style = match lowercase(&element.name).borrow() {
- "b" => Some(Style::Bold),
- "i" => Some(Style::Italic),
- "u" => Some(Style::Underline),
- "s" | "strike" => Some(Style::Strike),
- "strong" => Some(Style::Strong),
- "em" => Some(Style::Emphasis),
- "font" => {
- if let Some(Some(face)) = element.attributes.get("face") {
- apply_style(&mut inner, Style::Face(face.clone()));
- }
- if let Some(Some(color)) = element.attributes.get("color")
- && let Ok(color) = Color::from_str(&color)
- {
- apply_style(&mut inner, Style::Color(color));
- }
- if let Some(Some(html_size)) = element.attributes.get("size")
- && let Ok(html_size) = usize::from_str(&html_size)
- && let Some(index) = html_size.checked_sub(1)
- && let Some(points) =
- [6.0, 7.5, 9.0, 10.5, 13.5, 18.0, 27.0].get(index).copied()
- {
- apply_style(&mut inner, Style::Size(points));
- }
- None
- }
- _ => None,
- };
- match style {
- None => match inner {
- Markup::Seq(seq) => {
- for markup in seq {
- add_markup(&mut retval, markup);
- }
- }
- _ => add_markup(&mut retval, inner),
- },
- Some(style) => retval.push(Markup::Style {
- style,
- child: Box::new(inner),
- }),
- }
- }
- }
- }
- if retval.len() == 1 {
- retval.into_iter().next().unwrap()
- } else {
- Markup::Seq(retval)
- }
-}
-
-fn find_element<'a>(elements: &'a [Node], name: &str) -> Option<&'a Element> {
- for element in elements {
- if let Node::Element(element) = element
- && element.name == name
- {
- return Some(element);
- }
- }
- None
-}
-
-fn parse_entity(s: &str) -> (char, &str) {
- static ENTITIES: [(&str, char); 6] = [
- ("amp;", '&'),
- ("lt;", '<'),
- ("gt;", '>'),
- ("apos;", '\''),
- ("quot;", '"'),
- ("nbsp;", '\u{00a0}'),
- ];
- for (name, ch) in ENTITIES {
- if let Some(rest) = s.strip_prefix(name) {
- return (ch, rest);
- }
- }
- ('&', s)
-}
-
-fn get_node_text(node: &Node, text: &mut String) {
- match node {
- Node::Text(string) => {
- let mut s = string.as_str();
- while !s.is_empty() {
- let amp = s.find('&').unwrap_or(s.len());
- let (head, rest) = s.split_at(amp);
- text.push_str(head);
- if rest.is_empty() {
- break;
- }
- let ch;
- (ch, s) = parse_entity(&s[1..]);
- text.push(ch);
- }
- }
- Node::Element(element) => get_element_text(element, text),
- Node::Comment(_) => (),
- }
-}
-
-fn get_element_text(element: &Element, text: &mut String) {
- for child in &element.children {
- get_node_text(child, text);
- }
-}
-
-#[cfg(test)]
-mod tests {
- use std::{borrow::Cow, str::FromStr};
-
- use crate::output::spv::html::{self, Document, Markup, Variable};
-
- #[test]
- fn variable() {
- assert_eq!(Variable::from_str("Head1").unwrap(), Variable::Head(1));
- assert_eq!(Variable::from_str("Page").unwrap(), Variable::Page);
- assert_eq!(Variable::from_str("Date").unwrap(), Variable::Date);
- assert_eq!(Variable::Head(1).to_string(), "Head1");
- assert_eq!(Variable::Page.to_string(), "Page");
- assert_eq!(Variable::Date.to_string(), "Date");
- }
-
- #[test]
- fn parse_variables() {
- assert_eq!(Markup::Text("asdf".into()).parse_variables(), None);
- assert_eq!(Markup::Text("&[asdf]".into()).parse_variables(), None);
- assert_eq!(
- Markup::Text("&[Page]".into()).parse_variables(),
- Some(vec![Markup::Variable(Variable::Page)])
- );
- assert_eq!(
- Markup::Text("xyzzy &[Invalid] &[Page] &[Invalid2] quux".into()).parse_variables(),
- Some(vec![
- Markup::Text("xyzzy &[Invalid] ".into()),
- Markup::Variable(Variable::Page),
- Markup::Text(" &[Invalid2] quux".into()),
- ])
- );
- }
-
- /// Example from the documentation.
- #[test]
- fn example1() {
- let text = r##"<xml><html xmlns="http://www.w3.org/1999/xhtml" lang="en">
- <head>
-
- </head>
- <body>
- <p>
- plain&#160;<font color="#000000" size="3" face="Monospaced"><b>bold</b></font>&#160;<font color="#000000" size="3" face="Monospaced"><i>italic</i>&#160;<strike>strikeout</strike></font>
- </p>
- </body>
-</html>
-</xml>"##;
- let content = quick_xml::de::from_str::<String>(text).unwrap();
- assert_eq!(
- Document::from_html(&content).to_html(),
- r##"<p align="left">plain <font size="9pt"><font color="#000000"><font face="Monospaced"><b>bold</b></font></font></font> <font size="9pt"><font color="#000000"><font face="Monospaced"><i>italic</i> <strike>strikeout</strike></font></font></font></p>"##
- );
- }
-
- /// Another example from the documentation.
- #[test]
- fn example2() {
- let text = r##"<xml><html xmlns="http://www.w3.org/1999/xhtml" lang="en">
- <head>
-
- </head>
- <body>
- <p>left</p>
- <p align="center"><font color="#000000" size="5" face="Monospaced">center&#160;large</font></p>
- <p align="right"><font color="#000000" size="3" face="Monospaced"><b><i>right</i></b></font></p>
- </body>
-</html></xml>
-"##;
- let content = quick_xml::de::from_str::<String>(text).unwrap();
- assert_eq!(
- Document::from_html(&content).to_html(),
- r##"<p align="left">left</p><p align="center"><font size="13.5pt"><font color="#000000"><font face="Monospaced">center large</font></font></font></p><p align="right"><font size="9pt"><font color="#000000"><font face="Monospaced"><b><i>right</i></b></font></font></font></p>"##
- );
- }
-
- /*
- #[test]
- fn value() {
- let value = parse_value(
- r#"<b>bold</b><br><i>italic</i><BR><b><i>bold italic</i></b><br><font color="red" face="Serif">red serif</font><br><font size="7">big</font><br>"#,
- );
- assert_eq!(
- value,
- Value::new_markup(
- r##"<b>bold</b>
-<i>italic</i>
-<b><i>bold italic</i></b>
-<span face="Serif" color="#ff0000">red serif</span>
-<span size="20480">big</span>
-"##
- )
- .with_font_style(FontStyle::default().with_size(10))
- );
- }*/
-
- /// From the corpus (also included in the documentation).
- #[test]
- fn header1() {
- let text = r##"<xml><html xmlns="http://xml.spss.com/spss/viewer/viewer-tree">
- <head>
-
- </head>
- <body>
- <p style="text-align:center; margin-top: 0">
- &[PageTitle]
- </p>
- </body>
-</html></xml>"##;
- let content = quick_xml::de::from_str::<String>(text).unwrap();
- assert_eq!(
- Document::from_html(&content).to_html(),
- r##"<p align="center">&[PageTitle]</p>"##
- );
- }
-
- /// From the corpus (also included in the documentation).
- #[test]
- fn footer1() {
- let text = r##"<xml><html xmlns="http://xml.spss.com/spss/viewer/viewer-tree">
- <head>
-
- </head>
- <body>
- <p style="text-align:right; margin-top: 0">
- Page &[Page]
- </p>
- </body>
-</html></xml>"##;
- let content = quick_xml::de::from_str::<String>(text).unwrap();
- assert_eq!(
- Document::from_html(&content).to_html(),
- r##"<p align="right">Page &[Page]</p>"##
- );
- }
-
- /// From the corpus (also included in the documentation).
- #[test]
- fn header2() {
- let text = r##"<xml><html xmlns="http://www.w3.org/1999/xhtml" lang="en">
- <head>
- <style type="text/css">
- p { font-family: sans-serif;
- font-size: 10pt; text-align: center;
- font-weight: normal;
- color: #000000;
- }
- </style>
- </head>
- <body>
- <p>&amp;[PageTitle]</p>
- </body>
-</html></xml>"##;
- let content = quick_xml::de::from_str::<String>(text).unwrap();
- let document = Document::from_html(&content);
- assert_eq!(
- document.to_html(),
- r##"<p align="center"><font color="#000000"><font face="sans-serif">&[PageTitle]</font></font></p>"##
- );
- assert_eq!(
- document.0[0]
- .markup
- .to_pango(
- |name| (name == html::Variable::PageTitle).then_some(Cow::from("The title"))
- )
- .0,
- "The title"
- );
- }
-
- /// From the corpus (also included in the documentation).
- #[test]
- fn footer2() {
- let text = r##"<xml><html xmlns="http://www.w3.org/1999/xhtml" lang="en">
- <head>
- <style type="text/css">
- p { font-family: sans-serif;
- font-size: 10pt; text-align: right;
- font-weight: normal;
- color: #000000;
- }
- </style>
- </head>
- <body>
- <p>Page &amp;[Page]</p>
- </body>
-</html>
-</xml>"##;
- let content = quick_xml::de::from_str::<String>(text).unwrap();
- let html = Document::from_html(&content);
- assert_eq!(
- html.to_html(),
- r##"<p align="right"><font color="#000000"><font face="sans-serif">Page &[Page]</font></font></p>"##
- );
- }
-
- /// Checks that the `escape-html` feature is enabled in [quick_xml], since
- /// we need that to resolve ` ` and other HTML entities.
- #[test]
- fn html_escapes() {
- let html = Document::from_html(" ");
- assert_eq!(html.to_html(), "<p align=\"left\">\u{a0}</p>")
- }
-}
+++ /dev/null
-use std::{
- collections::HashMap,
- io::{Read, Seek, SeekFrom},
-};
-
-use binrw::{BinRead, BinResult, binread};
-use chrono::{NaiveDateTime, NaiveTime};
-use encoding_rs::UTF_8;
-
-use crate::{
- calendar::{date_time_to_pspp, time_to_pspp},
- data::Datum,
- format::{Category, Format},
- output::{
- pivot::Value,
- spv::light::{U32String, decode_format, parse_vec},
- },
-};
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-pub struct LegacyBin {
- #[br(magic(0u8))]
- version: Version,
- #[br(temp)]
- n_sources: u16,
- member_size: u32,
- #[br(count(n_sources), args { inner: (version,) })]
- metadata: Vec<Metadata>,
- #[br(parse_with(parse_data), args(metadata.as_slice()))]
- data: Vec<Data>,
- #[br(parse_with(parse_strings))]
- strings: Option<Strings>,
-}
-
-impl LegacyBin {
- pub fn decode(&self) -> HashMap<String, HashMap<String, Vec<DataValue>>> {
- fn decode_asciiz(name: &[u8]) -> String {
- let len = name.iter().position(|b| *b == 0).unwrap_or(name.len());
- std::str::from_utf8(&name[..len]).unwrap().into() // XXX unwrap
- }
-
- let mut sources = HashMap::new();
- for (metadata, data) in self.metadata.iter().zip(&self.data) {
- let mut variables = HashMap::new();
- for variable in &data.variables {
- variables.insert(
- variable.variable_name.clone(),
- variable
- .values
- .iter()
- .map(|value| DataValue {
- index: None,
- value: Datum::Number((*value != f64::MIN).then_some(*value)),
- })
- .collect::<Vec<_>>(),
- );
- }
- sources.insert(metadata.source_name.clone(), variables);
- }
- if let Some(strings) = &self.strings {
- for map in &strings.source_maps {
- let source = sources.get_mut(&map.source_name).unwrap(); // XXX unwrap
- for var_map in &map.variable_maps {
- let variable = source.get_mut(&var_map.variable_name).unwrap(); // XXX unwrap
- for datum_map in &var_map.datum_maps {
- // XXX two possibly out-of-range indexes below
- variable[datum_map.value_idx].value =
- Datum::String(strings.labels[datum_map.label_idx].label.clone());
- }
- }
- }
- }
- sources
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct DataValue {
- pub index: Option<f64>,
- pub value: Datum<String>,
-}
-
-impl DataValue {
- pub fn category(&self) -> Option<usize> {
- match &self.value {
- Datum::Number(number) => *number,
- _ => self.index,
- }
- .and_then(|v| (v >= 0.0 && v < usize::MAX as f64).then_some(v as usize))
- }
-
- // This should probably be a method on some hypothetical FormatMap.
- pub fn as_format(&self, format_map: &HashMap<i64, Format>) -> Format {
- let f = match &self.value {
- Datum::Number(Some(number)) => *number as i64,
- Datum::Number(None) => 0,
- Datum::String(s) => s.parse().unwrap_or_default(),
- };
- match format_map.get(&f) {
- Some(format) => *format,
- None => decode_format(f as u32),
- }
- }
-
- pub fn as_pivot_value(&self, format: Format) -> Value {
- if format.type_().category() == Category::Date
- && let Some(s) = self.value.as_string()
- && let Ok(date_time) =
- NaiveDateTime::parse_from_str(s.as_str(), "%Y-%m-%dT%H:%M:%S%.3f")
- {
- Value::new_number_with_format(Some(date_time_to_pspp(date_time)), format)
- } else if format.type_().category() == Category::Time
- && let Some(s) = self.value.as_string()
- && let Ok(time) = NaiveTime::parse_from_str(s.as_str(), "%H:%M:%S%.3f")
- {
- Value::new_number_with_format(Some(time_to_pspp(time)), format)
- } else {
- Value::new_datum_with_format(&self.value, format)
- }
- }
-}
-
-#[binread]
-#[br(little)]
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
-enum Version {
- #[br(magic = 0xafu8)]
- Vaf,
- #[br(magic = 0xb0u8)]
- Vb0,
-}
-
-#[binread]
-#[br(little, import(version: Version))]
-#[derive(Debug)]
-struct Metadata {
- n_values: u32,
- n_variables: u32,
- data_offset: u32,
- #[br(parse_with(parse_fixed_utf8_string), args(if version == Version::Vaf { 28 } else { 64 }))]
- source_name: String,
- #[br(if(version == Version::Vb0), temp)]
- _x: u32,
-}
-
-#[derive(Debug)]
-struct Data {
- variables: Vec<Variable>,
-}
-
-#[binrw::parser(reader, endian)]
-fn parse_data(metadata: &[Metadata]) -> BinResult<Vec<Data>> {
- let mut data = Vec::with_capacity(metadata.len());
- for metadata in metadata {
- reader.seek(SeekFrom::Start(metadata.data_offset as u64))?;
- let mut variables = Vec::with_capacity(metadata.n_variables as usize);
- for _ in 0..metadata.n_variables {
- variables.push(Variable::read_options(
- reader,
- endian,
- (metadata.n_values,),
- )?);
- }
- data.push(Data { variables });
- }
- Ok(data)
-}
-
-impl BinRead for Data {
- type Args<'a> = &'a [Metadata];
-
- fn read_options<R: Read + Seek>(
- reader: &mut R,
- endian: binrw::Endian,
- metadata: Self::Args<'_>,
- ) -> binrw::BinResult<Self> {
- let mut variables = Vec::with_capacity(metadata.len());
- for metadata in metadata {
- reader.seek(SeekFrom::Start(metadata.data_offset as u64))?;
- variables.push(Variable::read_options(
- reader,
- endian,
- (metadata.n_values,),
- )?);
- }
- Ok(Self { variables })
- }
-}
-
-#[binread]
-#[br(little, import(n_values: u32))]
-#[derive(Debug)]
-struct Variable {
- #[br(parse_with(parse_fixed_utf8_string), args(288))]
- variable_name: String,
- #[br(count(n_values))]
- values: Vec<f64>,
-}
-
-#[binrw::parser(reader, endian)]
-fn parse_strings() -> BinResult<Option<Strings>> {
- let position = reader.stream_position()?;
- let length = reader.seek(SeekFrom::End(0))?;
- if position != length {
- reader.seek(SeekFrom::Start(position))?;
- Ok(Some(Strings::read_options(reader, endian, ())?))
- } else {
- Ok(None)
- }
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct Strings {
- #[br(parse_with(parse_vec))]
- source_maps: Vec<SourceMap>,
- #[br(parse_with(parse_vec))]
- labels: Vec<Label>,
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct SourceMap {
- #[br(parse_with(parse_utf8_string))]
- source_name: String,
- #[br(parse_with(parse_vec))]
- variable_maps: Vec<VariableMap>,
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct VariableMap {
- #[br(parse_with(parse_utf8_string))]
- variable_name: String,
- #[br(parse_with(parse_vec))]
- datum_maps: Vec<DatumMap>,
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct DatumMap {
- #[br(map(|x: u32| x as usize))]
- value_idx: usize,
- #[br(map(|x: u32| x as usize))]
- label_idx: usize,
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct Label {
- #[br(temp)]
- _frequency: u32,
- #[br(parse_with(parse_utf8_string))]
- label: String,
-}
-
-/// Parses a UTF-8 string preceded by a 32-bit length.
-#[binrw::parser(reader, endian)]
-pub(super) fn parse_utf8_string() -> BinResult<String> {
- Ok(U32String::read_options(reader, endian, ())?.decode(UTF_8))
-}
-
-/// Parses a UTF-8 string that is exactly `n` bytes long and whose contents end
-/// at the first null byte.
-#[binrw::parser(reader)]
-pub(super) fn parse_fixed_utf8_string(n: usize) -> BinResult<String> {
- let mut buf = vec![0; n];
- reader.read_exact(&mut buf)?;
- let len = buf.iter().take_while(|b| **b != 0).count();
- Ok(
- std::str::from_utf8(&buf[..len]).unwrap().into(), // XXX unwrap
- )
-}
+++ /dev/null
-// 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/>.
-
-use std::{
- cell::{Cell, RefCell},
- collections::{BTreeMap, HashMap},
- marker::PhantomData,
- mem::take,
- num::NonZeroUsize,
- ops::Range,
- sync::Arc,
-};
-
-use chrono::{NaiveDateTime, NaiveTime};
-use enum_map::{Enum, EnumMap};
-use hashbrown::HashSet;
-use ordered_float::OrderedFloat;
-use serde::Deserialize;
-
-use crate::{
- calendar::{date_time_to_pspp, time_to_pspp},
- data::Datum,
- format::{self, Decimal::Dot, F8_0, F40_2, Type, UncheckedFormat},
- output::{
- pivot::{
- self, Area, AreaStyle, Axis2, Axis3, Category, CategoryLocator, CellStyle, Color,
- Dimension, Group, HeadingRegion, HorzAlign, Leaf, Length, Look, NumberValue,
- PivotTable, RowParity, Value, ValueInner, VertAlign,
- },
- spv::legacy_bin::DataValue,
- },
-};
-
-#[derive(Debug)]
-struct Ref<T> {
- references: String,
- _phantom: PhantomData<T>,
-}
-
-impl<T> Ref<T> {
- fn get<'a>(&self, table: &HashMap<&str, &'a T>) -> Option<&'a T> {
- table.get(self.references.as_str()).map(|v| &**v)
- }
-}
-
-impl<'de, T> Deserialize<'de> for Ref<T> {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: serde::Deserializer<'de>,
- {
- Ok(Self {
- references: String::deserialize(deserializer)?,
- _phantom: PhantomData,
- })
- }
-}
-
-#[derive(Clone, Debug, Default)]
-struct Map(HashMap<OrderedFloat<f64>, Datum<String>>);
-
-impl Map {
- fn new() -> Self {
- Self::default()
- }
-
- fn remap_formats(
- &mut self,
- format: &Option<Format>,
- string_format: &Option<StringFormat>,
- ) -> (crate::format::Format, Vec<Affix>) {
- let (format, affixes, relabels, try_strings_as_numbers) = if let Some(format) = &format {
- (
- Some(format.decode()),
- format.affixes.clone(),
- format.relabels.as_slice(),
- format.try_strings_as_numbers.unwrap_or_default(),
- )
- } else if let Some(string_format) = &string_format {
- (
- None,
- string_format.affixes.clone(),
- string_format.relabels.as_slice(),
- false,
- )
- } else {
- (None, Vec::new(), [].as_slice(), false)
- };
- for relabel in relabels {
- let value = if try_strings_as_numbers && let Ok(to) = relabel.to.trim().parse::<f64>() {
- Datum::Number(Some(to))
- } else if let Some(format) = format
- && let Ok(to) = relabel.to.trim().parse::<f64>()
- {
- Datum::String(
- Datum::<String>::Number(Some(to))
- .display(format)
- .with_stretch()
- .to_string(),
- )
- } else {
- Datum::String(relabel.to.clone())
- };
- self.0.insert(OrderedFloat(relabel.from), value);
- // XXX warn on duplicate
- }
- (format.unwrap_or(F8_0), affixes)
- }
-
- fn apply(&self, data: &mut Vec<DataValue>) {
- for value in data {
- let Datum::Number(Some(number)) = value.value else {
- continue;
- };
- if let Some(to) = self.0.get(&OrderedFloat(number)) {
- value.index = Some(number);
- value.value = to.clone();
- }
- }
- }
-
- fn insert_labels(
- &mut self,
- data: &[DataValue],
- label_series: &Series,
- format: crate::format::Format,
- ) {
- for (value, label) in data.iter().zip(label_series.values.iter()) {
- if let Some(Some(number)) = value.value.as_number() {
- let dest = match &label.value {
- Datum::Number(_) => label.value.display(format).with_stretch().to_string(),
- Datum::String(s) => s.clone(),
- };
- self.0.insert(OrderedFloat(number), Datum::String(dest));
- }
- }
- }
-
- fn remap_vmes(&mut self, value_map: &[ValueMapEntry]) {
- for vme in value_map {
- for from in vme.from.split(';') {
- let from = from.trim().parse::<f64>().unwrap(); // XXX
- let to = if let Ok(to) = vme.to.trim().parse::<f64>() {
- Datum::Number(Some(to))
- } else {
- Datum::String(vme.to.clone())
- };
- self.0.insert(OrderedFloat(from), to);
- }
- }
- }
-
- fn lookup<'a>(&'a self, dv: &'a DataValue) -> &'a Datum<String> {
- if let Datum::Number(Some(number)) = &dv.value
- && let Some(value) = self.0.get(&OrderedFloat(*number))
- {
- value
- } else {
- &dv.value
- }
- }
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-pub struct Visualization {
- /// In format `YYYY-MM-DD`.
- #[serde(rename = "@date")]
- date: String,
- // Locale used for output, e.g. `en-US`.
- #[serde(rename = "@lang")]
- lang: String,
- /// Localized title of the pivot table.
- #[serde(rename = "@name")]
- name: String,
- /// Base style for the pivot table.
- #[serde(rename = "@style")]
- style: Ref<Style>,
-
- #[serde(rename = "$value")]
- children: Vec<VisChild>,
-}
-
-impl Visualization {
- pub fn decode(
- &self,
- data: HashMap<String, HashMap<String, Vec<DataValue>>>,
- mut look: Look,
- ) -> Result<PivotTable, super::Error> {
- let mut extension = None;
- let mut user_source = None;
- let mut source_variables = Vec::new();
- let mut derived_variables = Vec::new();
- let mut graph = None;
- let mut labels = EnumMap::from_fn(|_| Vec::new());
- let mut styles = HashMap::new();
- let mut _layer_controller = None;
- for child in &self.children {
- match child {
- VisChild::Extension(e) => extension = Some(e),
- VisChild::UserSource(us) => user_source = Some(us),
- VisChild::SourceVariable(source_variable) => source_variables.push(source_variable),
- VisChild::DerivedVariable(derived_variable) => {
- derived_variables.push(derived_variable)
- }
- VisChild::CategoricalDomain(_) => (),
- VisChild::Graph(g) => graph = Some(g),
- VisChild::LabelFrame(label_frame) => {
- if let Some(label) = &label_frame.label
- && let Some(purpose) = label.purpose
- {
- labels[purpose].push(label);
- }
- }
- VisChild::Container(c) => {
- for label_frame in &c.label_frames {
- if let Some(label) = &label_frame.label
- && let Some(purpose) = label.purpose
- {
- labels[purpose].push(label);
- }
- }
- }
- VisChild::Style(style) => {
- if let Some(id) = &style.id {
- styles.insert(id.as_str(), style);
- }
- }
- VisChild::LayerController(lc) => _layer_controller = Some(lc),
- }
- }
- let Some(graph) = graph else { todo!() };
- let Some(_user_source) = user_source else {
- todo!()
- };
-
- let mut axes = HashMap::new();
- let mut major_ticks = HashMap::new();
- for child in &graph.facet_layout.children {
- if let FacetLayoutChild::FacetLevel(facet_level) = child {
- axes.insert(facet_level.level, &facet_level.axis);
- major_ticks.insert(
- facet_level.axis.major_ticks.id.as_str(),
- &facet_level.axis.major_ticks,
- );
- }
- }
-
- // Footnotes.
- //
- // Any [Value] might refer to footnotes, so it's important to process
- // the footnotes early to ensure that those references can be resolved.
- // There is a possible problem that a footnote might itself reference an
- // as-yet-unprocessed footnote, but that's OK because footnote
- // references don't actually look at the footnote contents but only
- // resolve a pointer to where the footnote will go later.
- //
- // Before we really start, create all the footnotes we'll fill in. This
- // is because sometimes footnotes refer to themselves or to each other
- // and we don't want to reject those references.
- let mut footnote_builder = BTreeMap::<usize, Footnote>::new();
- if let Some(f) = &graph.interval.footnotes {
- f.decode(&mut footnote_builder);
- }
- for child in &graph.interval.labeling.children {
- if let LabelingChild::Footnotes(f) = child {
- f.decode(&mut footnote_builder);
- }
- }
- for label in &labels[Purpose::Footnote] {
- for (index, text) in label.text().iter().enumerate() {
- if let Some(uses_reference) = text.uses_reference {
- let entry = footnote_builder
- .entry(uses_reference.get() - 1)
- .or_default();
- if index % 2 == 0 {
- entry.content = text.text.strip_suffix('\n').unwrap_or(&text.text).into();
- } else {
- entry.marker =
- Some(text.text.strip_suffix('.').unwrap_or(&text.text).into());
- }
- }
- }
- }
- let mut footnotes = Vec::new();
- for (index, footnote) in footnote_builder {
- while footnotes.len() < index {
- footnotes.push(pivot::Footnote::default());
- }
- footnotes.push(
- pivot::Footnote::new(footnote.content)
- .with_marker(footnote.marker.map(|s| Value::new_user_text(s))),
- );
- }
- let footnotes = pivot::Footnotes::from_iter(footnotes);
-
- for (purpose, area) in [
- (Purpose::Title, Area::Title),
- (Purpose::SubTitle, Area::Caption),
- (Purpose::Layer, Area::Layers),
- (Purpose::Footnote, Area::Footer),
- ] {
- for label in &labels[purpose] {
- label.decode_style(&mut look.areas[area], &styles);
- }
- }
- let title = LabelFrame::decode_label(&labels[Purpose::Title]);
- let caption = LabelFrame::decode_label(&labels[Purpose::SubTitle]);
- if let Some(style) = &graph.interval.labeling.style
- && let Some(style) = styles.get(style.references.as_str())
- {
- Style::decode_area(
- Some(*style),
- graph.cell_style.get(&styles),
- &mut look.areas[Area::Data(RowParity::Even)],
- );
- look.areas[Area::Data(RowParity::Odd)] =
- look.areas[Area::Data(RowParity::Even)].clone();
- }
-
- let _show_grid_lines = extension
- .as_ref()
- .and_then(|extension| extension.show_gridline);
- if let Some(style) = styles.get(graph.cell_style.references.as_str())
- && let Some(width) = &style.width
- {
- let mut parts = width.split(';');
- parts.next();
- if let Some(min_width) = parts.next()
- && let Some(max_width) = parts.next()
- && let Ok(min_width) = min_width.parse::<Length>()
- && let Ok(max_width) = max_width.parse::<Length>()
- {
- look.heading_widths[HeadingRegion::Columns] =
- min_width.as_pt_f64() as isize..=max_width.as_pt_f64() as isize;
- }
- }
-
- let mut series = HashMap::<&str, Series>::new();
- while let n_source = source_variables.len()
- && let n_derived = derived_variables.len()
- && (n_source > 0 || n_derived > 0)
- {
- for sv in take(&mut source_variables) {
- match sv.decode(&data, &series) {
- Ok(s) => {
- series.insert(&sv.id, s);
- }
- Err(()) => source_variables.push(sv),
- }
- }
-
- for dv in take(&mut derived_variables) {
- match dv.decode(&series) {
- Ok(s) => {
- series.insert(&dv.id, s);
- }
- Err(()) => derived_variables.push(dv),
- }
- }
-
- if n_source == source_variables.len() && n_derived == derived_variables.len() {
- unreachable!();
- }
- }
-
- fn decode_dimension<'a>(
- variables: &[(&'a Series, usize)],
- axes: &HashMap<usize, &Axis>,
- styles: &HashMap<&str, &Style>,
- a: Axis3,
- look: &mut Look,
- rotate_inner_column_labels: &mut bool,
- rotate_outer_row_labels: &mut bool,
- footnotes: &pivot::Footnotes,
- dims: &mut Vec<Dim<'a>>,
- ) {
- let base_level = variables[0].1;
- let show_label = if let Ok(a) = Axis2::try_from(a)
- && let Some(axis) = axes.get(&(base_level + variables.len()))
- && let Some(label) = &axis.label
- {
- let out = &mut look.areas[Area::Labels(a)];
- *out = AreaStyle::default_for_area(Area::Labels(a));
- let style = label.style.get(&styles);
- Style::decode_area(
- style,
- label.text_frame_style.as_ref().and_then(|r| r.get(styles)),
- out,
- );
- style.is_some_and(|s| s.visible.unwrap_or_default())
- } else {
- false
- };
- if a == Axis3::Y
- && let Some(axis) = axes.get(&(base_level + variables.len() - 1))
- {
- Style::decode_area(
- axis.major_ticks.style.get(&styles),
- axis.major_ticks.tick_frame_style.get(&styles),
- &mut look.areas[Area::Labels(Axis2::Y)],
- );
- }
-
- if let Some(axis) = axes.get(&base_level)
- && axis.major_ticks.label_angle == -90.0
- {
- if a == Axis3::X {
- *rotate_inner_column_labels = true;
- } else {
- *rotate_outer_row_labels = true;
- }
- }
-
- let variables = variables
- .into_iter()
- .map(|(series, _level)| *series)
- .collect::<Vec<_>>();
-
- #[derive(Clone)]
- struct CatBuilder {
- /// The category we've built so far.
- category: Category,
-
- /// The range of leaf indexes covered by `category`.
- ///
- /// If `category` is a leaf, the range has a length of 1.
- /// If `category` is a group, the length is at least 1.
- leaves: Range<usize>,
-
- /// How to find this category in its dimension.
- location: CategoryLocator,
- }
-
- // Make leaf categories.
- let mut coordinate_to_index = HashMap::new();
- let mut cats = Vec::new();
- for (index, value) in variables[0].values.iter().enumerate() {
- let Some(row) = value.category() else {
- continue;
- };
- coordinate_to_index.insert(row, CategoryLocator::new_leaf(index));
- let name = variables[0].new_name(value, footnotes);
- cats.push(CatBuilder {
- category: Category::from(Leaf::new(name)),
- leaves: cats.len()..cats.len() + 1,
- location: CategoryLocator::new_leaf(cats.len()),
- });
- }
- *variables[0].coordinate_to_index.borrow_mut() = coordinate_to_index;
-
- // Now group them, in one pass per grouping variable, innermost first.
- for j in 1..variables.len() {
- let mut coordinate_to_index = HashMap::new();
- let mut next_cats = Vec::with_capacity(cats.len());
- let mut start = 0;
- for end in 1..=cats.len() {
- let dv1 = &variables[j].values[cats[start].leaves.start];
- if end < cats.len()
- && variables[j].values[cats[end].leaves.clone()]
- .iter()
- .all(|dv| &dv.value == &dv1.value)
- {
- } else {
- let name = variables[j].map.lookup(dv1);
- let next_cat = if end - start > 1 || name.is_number_or(|s| s.is_empty()) {
- let name = variables[j].new_name(dv1, footnotes);
- let mut group = Group::new(name);
- for i in start..end {
- group.push(cats[i].category.clone());
- }
- CatBuilder {
- category: Category::from(group),
- leaves: cats[start].leaves.start..cats[end - 1].leaves.end,
- location: cats[start].location.parent(),
- }
- } else {
- cats[start].clone()
- };
- coordinate_to_index
- .insert(dv1.category().unwrap() /*XXX?*/, next_cat.location);
- next_cats.push(next_cat);
- start = end;
- }
- }
- *variables[j].coordinate_to_index.borrow_mut() = coordinate_to_index;
- cats = next_cats;
- }
-
- let dimension = Dimension::new(
- Group::new(
- variables[0]
- .label
- .as_ref()
- .map_or_else(|| Value::empty(), |label| Value::new_user_text(label)),
- )
- .with_multiple(cats.into_iter().map(|cb| cb.category))
- .with_show_label(show_label),
- );
-
- for variable in &variables {
- variable.dimension_index.set(Some(dims.len()));
- }
- dims.push(Dim {
- axis: a,
- dimension,
- coordinate: variables[0],
- });
- }
-
- fn decode_dimensions<'a, 'b>(
- variables: impl IntoIterator<Item = &'a str>,
- series: &'b HashMap<&str, Series>,
- axes: &HashMap<usize, &Axis>,
- styles: &HashMap<&str, &Style>,
- a: Axis3,
- look: &mut Look,
- rotate_inner_column_labels: &mut bool,
- rotate_outer_row_labels: &mut bool,
- footnotes: &pivot::Footnotes,
- level_ofs: usize,
- dims: &mut Vec<Dim<'b>>,
- ) {
- let variables = variables
- .into_iter()
- .zip(level_ofs..)
- .map(|(variable_name, level)| {
- series
- .get(variable_name)
- .filter(|s| !s.values.is_empty())
- .map(|s| (s, level))
- })
- .collect::<Vec<_>>();
- let mut dim_vars = Vec::new();
- for var in variables {
- if let Some((var, level)) = var {
- dim_vars.push((var, level));
- } else if !dim_vars.is_empty() {
- decode_dimension(
- &dim_vars,
- axes,
- styles,
- a,
- look,
- rotate_inner_column_labels,
- rotate_outer_row_labels,
- footnotes,
- dims,
- );
- dim_vars.clear();
- }
- }
- if !dim_vars.is_empty() {
- decode_dimension(
- &dim_vars,
- axes,
- styles,
- a,
- look,
- rotate_inner_column_labels,
- rotate_outer_row_labels,
- footnotes,
- dims,
- );
- }
- }
-
- struct Dim<'a> {
- axis: Axis3,
- dimension: pivot::Dimension,
- coordinate: &'a Series,
- }
-
- let mut rotate_inner_column_labels = false;
- let mut rotate_outer_row_labels = false;
- let cross = &graph.faceting.cross.children;
- let columns = cross
- .first()
- .map(|child| child.variables())
- .unwrap_or_default();
- let mut dims = Vec::new();
- decode_dimensions(
- columns.into_iter().map(|vr| vr.reference.as_str()),
- &series,
- &axes,
- &styles,
- Axis3::X,
- &mut look,
- &mut rotate_inner_column_labels,
- &mut rotate_outer_row_labels,
- &footnotes,
- 1,
- &mut dims,
- );
- let rows = cross
- .get(1)
- .map(|child| child.variables())
- .unwrap_or_default();
- decode_dimensions(
- rows.into_iter().map(|vr| vr.reference.as_str()),
- &series,
- &axes,
- &styles,
- Axis3::Y,
- &mut look,
- &mut rotate_inner_column_labels,
- &mut rotate_outer_row_labels,
- &footnotes,
- 1 + columns.len(),
- &mut dims,
- );
-
- let mut level_ofs = columns.len() + rows.len() + 1;
- for layers in [&graph.faceting.layers1, &graph.faceting.layers2] {
- decode_dimensions(
- layers.iter().map(|layer| layer.variable.as_str()),
- &series,
- &axes,
- &styles,
- Axis3::Y,
- &mut look,
- &mut rotate_inner_column_labels,
- &mut rotate_outer_row_labels,
- &footnotes,
- level_ofs,
- &mut dims,
- );
- level_ofs += layers.len();
- }
-
- let cell = series.get("cell").unwrap()/*XXX*/;
- let mut coords = Vec::with_capacity(dims.len());
- let (cell_formats, format_map) = graph.interval.labeling.decode_format_map(&series);
- let cell_footnotes =
- graph
- .interval
- .labeling
- .children
- .iter()
- .find_map(|child| match child {
- LabelingChild::Footnotes(footnotes) => series.get(footnotes.variable.as_str()),
- _ => None,
- });
- let mut data = HashMap::new();
- for (i, cell) in cell.values.iter().enumerate() {
- coords.clear();
- for dim in &dims {
- // XXX indexing of values, and unwrap
- let coordinate = dim.coordinate.values[i].category().unwrap();
- let Some(index) = dim
- .coordinate
- .coordinate_to_index
- .borrow()
- .get(&coordinate)
- .and_then(CategoryLocator::as_leaf)
- else {
- panic!("can't find {coordinate}") // XXX
- };
- coords.push(index);
- }
-
- let format = if let Some(cell_formats) = &cell_formats {
- // XXX indexing of values
- cell_formats.values[i].as_format(&format_map)
- } else {
- F40_2
- };
- let mut value = cell.as_pivot_value(format);
-
- if let Some(cell_footnotes) = &cell_footnotes {
- // XXX indexing
- let dv = &cell_footnotes.values[i];
- if let Some(s) = dv.value.as_string() {
- for part in s.split(',') {
- if let Ok(index) = part.parse::<usize>()
- && let Some(index) = index.checked_sub(1)
- && let Some(footnote) = footnotes.get(index)
- {
- value = value.with_footnote(footnote);
- }
- }
- }
- }
- if let Value {
- inner: ValueInner::Number(NumberValue { value: None, .. }),
- styling: None,
- } = &value
- {
- // A system-missing value without a footnote represents an empty cell.
- } else {
- // XXX cell_index might be invalid?
- data.insert(coords.clone(), value);
- }
- }
-
- for child in &graph.facet_layout.children {
- let FacetLayoutChild::SetCellProperties(scp) = child else {
- continue;
- };
-
- #[derive(Copy, Clone, Debug, PartialEq)]
- enum TargetType {
- Graph,
- Labeling,
- Interval,
- MajorTicks,
- }
-
- impl TargetType {
- fn from_id(
- target: &str,
- graph: &Graph,
- major_ticks: &HashMap<&str, &MajorTicks>,
- ) -> Option<Self> {
- if let Some(id) = &graph.id
- && id == target
- {
- Some(Self::Graph)
- } else if let Some(id) = &graph.interval.labeling.id
- && id == target
- {
- Some(Self::Labeling)
- } else if let Some(id) = &graph.interval.id
- && id == target
- {
- Some(Self::Interval)
- } else if major_ticks.contains_key(target) {
- Some(Self::MajorTicks)
- } else {
- None
- }
- }
- }
-
- #[derive(Default)]
- struct Target<'a> {
- graph: Option<&'a Style>,
- labeling: Option<&'a Style>,
- interval: Option<&'a Style>,
- major_ticks: Option<&'a Style>,
- frame: Option<&'a Style>,
- format: Option<(&'a SetFormat, Option<TargetType>)>,
- }
- impl<'a> Target<'a> {
- fn decode(
- &self,
- intersect: &Intersect,
- look: &mut Look,
- series: &HashMap<&str, Series>,
- dims: &mut [Dim],
- data: &mut HashMap<Vec<usize>, Value>,
- ) {
- let mut wheres = Vec::new();
- let mut alternating = false;
- for child in &intersect.children {
- match child {
- IntersectChild::Where(w) => wheres.push(w),
- IntersectChild::IntersectWhere(_) => {
- // Presumably we should do something (but we don't).
- }
- IntersectChild::Alternating => alternating = true,
- IntersectChild::Empty => (),
- }
- }
-
- match self {
- Self {
- graph: Some(_),
- labeling: Some(_),
- interval: None,
- major_ticks: None,
- frame: None,
- format: None,
- } if alternating => {
- let mut style = AreaStyle::default_for_area(Area::Data(RowParity::Odd));
- Style::decode_area(self.labeling, self.graph, &mut style);
- let font_style = &mut look.areas[Area::Data(RowParity::Odd)].font_style;
- font_style.fg = style.font_style.fg;
- font_style.bg = style.font_style.bg;
- }
- Self {
- graph: Some(_),
- labeling: None,
- interval: None,
- major_ticks: None,
- frame: None,
- format: None,
- } => {
- // `graph.width` likely just sets the width of the table as a whole.
- }
- Self {
- graph: None,
- labeling: None,
- interval: None,
- major_ticks: None,
- frame: None,
- format: None,
- } => {
- // No-op. (Presumably there's a setMetaData we don't care about.)
- }
- Self {
- format: Some((_, Some(TargetType::MajorTicks))),
- ..
- }
- | Self {
- major_ticks: Some(_),
- ..
- }
- | Self { frame: Some(_), .. }
- if !wheres.is_empty() =>
- {
- // Formatting for individual row or column labels.
- for w in &wheres {
- let Some(s) = series.get(w.variable.as_str()) else {
- continue;
- };
- let Some(dim_index) = s.dimension_index.get() else {
- continue;
- };
- let dimension = &mut dims[dim_index].dimension;
- let Ok(axis) = Axis2::try_from(dims[dim_index].axis) else {
- continue;
- };
- for index in
- w.include.split(';').filter_map(|s| s.parse::<usize>().ok())
- {
- if let Some(locator) =
- s.coordinate_to_index.borrow().get(&index).copied()
- && let Some(category) = dimension.root.category_mut(locator)
- {
- Style::apply_to_value(
- category.name_mut(),
- self.format.map(|(sf, _)| sf),
- self.major_ticks,
- self.frame,
- &look.areas[Area::Labels(axis)],
- );
- }
- }
- }
- }
- Self {
- format: Some((_, Some(TargetType::Labeling))),
- ..
- }
- | Self {
- labeling: Some(_), ..
- }
- | Self {
- interval: Some(_), ..
- } => {
- // Formatting for individual cells or groups of them
- // with some dimensions in common.
- let mut include = vec![HashSet::new(); dims.len()];
- for w in &wheres {
- let Some(s) = series.get(w.variable.as_str()) else {
- continue;
- };
- let Some(dim_index) = s.dimension_index.get() else {
- // Group indexes may be included even though
- // they are redundant. Ignore them.
- continue;
- };
- for index in
- w.include.split(';').filter_map(|s| s.parse::<usize>().ok())
- {
- if let Some(locator) =
- s.coordinate_to_index.borrow().get(&index).copied()
- && let Some(leaf_index) = locator.as_leaf()
- {
- include[dim_index].insert(leaf_index);
- }
- }
- }
-
- // XXX This is inefficient in the common case where
- // all of the dimensions are matched. We should use
- // a heuristic where if all of the dimensions are
- // matched and the product of n[*] is less than the
- // number of cells then iterate through all the
- // possibilities rather than all the cells. Or even
- // only do it if there is just one possibility.
- for (indexes, value) in data {
- let mut skip = false;
- for (dimension, index) in indexes.iter().enumerate() {
- if !include[dimension].is_empty()
- && !include[dimension].contains(index)
- {
- skip = true;
- break;
- }
- }
- if !skip {
- Style::apply_to_value(
- value,
- self.format.map(|(sf, _)| sf),
- self.major_ticks,
- self.frame,
- &look.areas[Area::Data(RowParity::Even)],
- );
- }
- }
- }
- _ => (),
- }
- }
- }
- let mut target = Target::default();
- for set in &scp.sets {
- match set {
- Set::SetStyle(set_style) => {
- if let Some(style) = set_style.style.get(&styles) {
- match TargetType::from_id(&set_style.target, graph, &major_ticks) {
- Some(TargetType::Graph) => target.graph = Some(style),
- Some(TargetType::Interval) => target.interval = Some(style),
- Some(TargetType::Labeling) => target.labeling = Some(style),
- Some(TargetType::MajorTicks) => target.major_ticks = Some(style),
- None => (),
- }
- }
- }
- Set::SetFrameStyle(set_frame_style) => {
- target.frame = set_frame_style.style.get(&styles)
- }
- Set::SetFormat(sf) => {
- let target_type = TargetType::from_id(&sf.target, graph, &major_ticks);
- target.format = Some((sf, target_type))
- }
- Set::SetMetaData(_) => (),
- }
- }
-
- match (
- scp.union_.as_ref(),
- scp.apply_to_converse.unwrap_or_default(),
- ) {
- (Some(union_), false) => {
- for intersect in &union_.intersects {
- target.decode(
- intersect,
- &mut look,
- &series,
- dims.as_mut_slice(),
- &mut data,
- );
- }
- }
- (Some(_), true) => {
- // Not implemented, not seen in the corpus.
- }
- (None, true) => {
- if target
- .format
- .is_some_and(|(_sf, target_type)| target_type == Some(TargetType::Labeling))
- || target.labeling.is_some()
- || target.interval.is_some()
- {
- for value in data.values_mut() {
- Style::apply_to_value(
- value,
- target.format.map(|(sf, _target_type)| sf),
- None,
- None,
- &look.areas[Area::Data(RowParity::Even)],
- );
- }
- }
- }
- (None, false) => {
- // Appears to be used to set the font for something—but what?
- }
- }
- }
-
- let dimensions = dims
- .into_iter()
- .map(|dim| (dim.axis, dim.dimension))
- .collect::<Vec<_>>();
- let mut pivot_table = PivotTable::new(dimensions)
- .with_look(Arc::new(look))
- .with_data(data);
- if let Some(title) = title {
- pivot_table = pivot_table.with_title(title);
- }
- if let Some(caption) = caption {
- pivot_table = pivot_table.with_caption(caption);
- }
- Ok(pivot_table)
- }
-}
-
-struct Series {
- name: String,
- label: Option<String>,
- format: crate::format::Format,
- remapped: bool,
- values: Vec<DataValue>,
- map: Map,
- affixes: Vec<Affix>,
- coordinate_to_index: RefCell<HashMap<usize, CategoryLocator>>,
- dimension_index: Cell<Option<usize>>,
-}
-
-impl Series {
- fn new(name: String, values: Vec<DataValue>, map: Map) -> Self {
- Self {
- name,
- label: None,
- format: F8_0,
- remapped: false,
- values,
- map,
- affixes: Vec::new(),
- coordinate_to_index: Default::default(),
- dimension_index: Default::default(),
- }
- }
- fn with_format(self, format: crate::format::Format) -> Self {
- Self { format, ..self }
- }
- fn with_label(self, label: Option<String>) -> Self {
- Self { label, ..self }
- }
- fn with_affixes(self, affixes: Vec<Affix>) -> Self {
- Self { affixes, ..self }
- }
- fn add_affixes(&self, mut value: Value, footnotes: &pivot::Footnotes) -> Value {
- for affix in &self.affixes {
- if let Some(index) = affix.defines_reference.checked_sub(1)
- && let Ok(index) = usize::try_from(index)
- && let Some(footnote) = footnotes.get(index)
- {
- value = value.with_footnote(footnote);
- }
- }
- value
- }
-
- fn max_category(&self) -> Option<usize> {
- self.values
- .iter()
- .filter_map(|value| value.category())
- .max()
- }
-
- fn new_name(&self, dv: &DataValue, footnotes: &pivot::Footnotes) -> Value {
- let dv = self.map.lookup(dv);
- let name = Value::new_datum(dv);
- self.add_affixes(name, &footnotes)
- }
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-enum VisChild {
- Extension(VisualizationExtension),
- UserSource(UserSource),
- SourceVariable(SourceVariable),
- DerivedVariable(DerivedVariable),
- CategoricalDomain(CategoricalDomain),
- Graph(Graph),
- LabelFrame(LabelFrame),
- Container(Container),
- Style(Style),
- LayerController(LayerController),
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename = "extension", rename_all = "camelCase")]
-struct VisualizationExtension {
- #[serde(rename = "@showGridline")]
- show_gridline: Option<bool>,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-enum Variable {
- SourceVariable(SourceVariable),
- DerivedVariable(DerivedVariable),
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct SourceVariable {
- #[serde(rename = "@id")]
- id: String,
-
- /// The `source-name` in the `tableData.bin` member.
- #[serde(rename = "@source")]
- source: String,
-
- /// The name of a variable within the source, corresponding to the
- /// `variable-name` in the `tableData.bin` member.
- #[serde(rename = "@sourceName")]
- source_name: String,
-
- /// Variable label, if any.
- #[serde(rename = "@label")]
- label: Option<String>,
-
- /// A variable whose string values correspond one-to-one with the values of
- /// this variable and are suitable as value labels.
- #[serde(rename = "@labelVariable")]
- label_variable: Option<Ref<SourceVariable>>,
-
- #[serde(default, rename = "extension")]
- extensions: Vec<VariableExtension>,
- format: Option<Format>,
- string_format: Option<StringFormat>,
-}
-
-impl SourceVariable {
- fn decode(
- &self,
- data: &HashMap<String, HashMap<String, Vec<DataValue>>>,
- series: &HashMap<&str, Series>,
- ) -> Result<Series, ()> {
- let label_series = if let Some(label_variable) = &self.label_variable {
- let Some(label_series) = series.get(label_variable.references.as_str()) else {
- return Err(());
- };
- Some(label_series)
- } else {
- None
- };
-
- let Some(data) = data
- .get(&self.source)
- .and_then(|source| source.get(&self.source_name))
- else {
- todo!()
- };
- let mut map = Map::new();
- let (format, affixes) = map.remap_formats(&self.format, &self.string_format);
- let mut data = data.clone();
- if !map.0.is_empty() {
- map.apply(&mut data);
- } else if let Some(label_series) = label_series {
- map.insert_labels(&data, label_series, format);
- }
- Ok(Series::new(self.id.clone(), data, map)
- .with_format(format)
- .with_affixes(affixes)
- .with_label(self.label.clone()))
- }
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct DerivedVariable {
- #[serde(rename = "@id")]
- id: String,
-
- /// An expression that defines the variable's value.
- #[serde(rename = "@value")]
- value: String,
- #[serde(default, rename = "extension")]
- extensions: Vec<VariableExtension>,
- format: Option<Format>,
- string_format: Option<StringFormat>,
- #[serde(default, rename = "valueMapEntry")]
- value_map: Vec<ValueMapEntry>,
-}
-
-impl DerivedVariable {
- fn decode(&self, series: &HashMap<&str, Series>) -> Result<Series, ()> {
- let mut values = if self.value == "constant(0)" {
- let n_values = if let Some(series) = series.values().next() {
- series.values.len()
- } else {
- return Err(());
- };
- (0..n_values)
- .map(|_| DataValue {
- index: Some(0.0),
- value: Datum::Number(Some(0.0)),
- })
- .collect()
- } else if self.value.starts_with("constant") {
- vec![]
- } else if let Some(rest) = self.value.strip_prefix("map(")
- && let Some(var_name) = rest.strip_suffix(")")
- {
- let Some(dependency) = series.get(var_name) else {
- return Err(());
- };
- dependency.values.clone()
- } else {
- unreachable!()
- };
- let mut map = Map::new();
- map.remap_vmes(&self.value_map);
- map.apply(&mut values);
- map.remap_formats(&self.format, &self.string_format);
- if values
- .iter()
- .all(|value| value.value.is_string_and(|s| s.is_empty()))
- {
- values.clear();
- }
- Ok(Series::new(self.id.clone(), values, map))
- }
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename = "extension", rename_all = "camelCase")]
-struct VariableExtension {
- #[serde(rename = "@from")]
- from: String,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct UserSource {
- #[serde(rename = "@id")]
- id: String,
-
- #[serde(rename = "@missing")]
- missing: Option<Missing>,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct CategoricalDomain {
- variable_reference: VariableReference,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct VariableReference {
- #[serde(rename = "@ref")]
- reference: String,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum Missing {
- Listwise,
- Pairwise,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct StringFormat {
- #[serde(default, rename = "relabel")]
- relabels: Vec<Relabel>,
- #[serde(default, rename = "affix")]
- affixes: Vec<Affix>,
-}
-
-#[derive(Deserialize, Debug, Default)]
-#[serde(rename_all = "camelCase")]
-struct Format {
- #[serde(rename = "@baseFormat")]
- base_format: Option<BaseFormat>,
- #[serde(rename = "@errorCharacter")]
- error_character: Option<char>,
- #[serde(rename = "@separatorChars")]
- separator_chars: Option<String>,
- #[serde(rename = "@mdyOrder")]
- mdy_order: Option<MdyOrder>,
- #[serde(rename = "@showYear")]
- show_year: Option<bool>,
- #[serde(rename = "@showQuarter")]
- show_quarter: Option<bool>,
- #[serde(rename = "@quarterPrefix")]
- quarter_prefix: Option<String>,
- #[serde(rename = "@quarterSuffix")]
- quarter_suffix: Option<String>,
- #[serde(rename = "@yearAbbreviation")]
- year_abbreviation: Option<bool>,
- #[serde(rename = "@showMonth")]
- show_month: Option<bool>,
- #[serde(rename = "@monthFormat")]
- month_format: Option<MonthFormat>,
- #[serde(rename = "@dayPadding")]
- day_padding: Option<bool>,
- #[serde(rename = "@dayOfMonthPadding")]
- day_of_month_padding: Option<bool>,
- #[serde(rename = "@showWeek")]
- show_week: Option<bool>,
- #[serde(rename = "@weekPadding")]
- week_padding: Option<bool>,
- #[serde(rename = "@weekSuffix")]
- week_suffix: Option<String>,
- #[serde(rename = "@showDayOfWeek")]
- show_day_of_week: Option<bool>,
- #[serde(rename = "@dayOfWeekAbbreviation")]
- day_of_week_abbreviation: Option<bool>,
- #[serde(rename = "hourPadding")]
- hour_padding: Option<bool>,
- #[serde(rename = "minutePadding")]
- minute_padding: Option<bool>,
- #[serde(rename = "secondPadding")]
- second_padding: Option<bool>,
- #[serde(rename = "@showDay")]
- show_day: Option<bool>,
- #[serde(rename = "@showHour")]
- show_hour: Option<bool>,
- #[serde(rename = "@showMinute")]
- show_minute: Option<bool>,
- #[serde(rename = "@showSecond")]
- show_second: Option<bool>,
- #[serde(rename = "@showMillis")]
- show_millis: Option<bool>,
- #[serde(rename = "@dayType")]
- day_type: Option<DayType>,
- #[serde(rename = "@hourFormat")]
- hour_format: Option<HourFormat>,
- #[serde(rename = "@minimumIntegerDigits")]
- minimum_integer_digits: Option<usize>,
- #[serde(rename = "@maximumFractionDigits")]
- maximum_fraction_digits: Option<i64>,
- #[serde(rename = "@minimumFractionDigits")]
- minimum_fraction_digits: Option<usize>,
- #[serde(rename = "@useGrouping")]
- use_grouping: Option<bool>,
- #[serde(rename = "@scientific")]
- scientific: Option<Scientific>,
- #[serde(rename = "@small")]
- small: Option<f64>,
- #[serde(default, rename = "@prefix")]
- prefix: String,
- #[serde(default, rename = "@suffix")]
- suffix: String,
- #[serde(rename = "@tryStringsAsNumbers")]
- try_strings_as_numbers: Option<bool>,
- #[serde(rename = "@negativesOutside")]
- negatives_outside: Option<bool>,
- #[serde(default, rename = "relabel")]
- relabels: Vec<Relabel>,
- #[serde(default, rename = "affix")]
- affixes: Vec<Affix>,
-}
-
-impl Format {
- fn decode(&self) -> crate::format::Format {
- if self.base_format.is_some() {
- SignificantDateTimeFormat::from(self).decode()
- } else {
- SignificantNumberFormat::from(self).decode()
- }
- }
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct NumberFormat {
- #[serde(rename = "@minimumIntegerDigits")]
- minimum_integer_digits: Option<i64>,
- #[serde(rename = "@maximumFractionDigits")]
- maximum_fraction_digits: Option<i64>,
- #[serde(rename = "@minimumFractionDigits")]
- minimum_fraction_digits: Option<i64>,
- #[serde(rename = "@useGrouping")]
- use_grouping: Option<bool>,
- #[serde(rename = "@scientific")]
- scientific: Option<Scientific>,
- #[serde(rename = "@small")]
- small: Option<f64>,
- #[serde(default, rename = "@prefix")]
- prefix: String,
- #[serde(default, rename = "@suffix")]
- suffix: String,
- #[serde(default, rename = "affix")]
- affixes: Vec<Affix>,
-}
-
-struct SignificantNumberFormat<'a> {
- scientific: Option<Scientific>,
- prefix: &'a str,
- suffix: &'a str,
- use_grouping: Option<bool>,
- maximum_fraction_digits: Option<i64>,
-}
-
-impl<'a> From<&'a NumberFormat> for SignificantNumberFormat<'a> {
- fn from(value: &'a NumberFormat) -> Self {
- Self {
- scientific: value.scientific,
- prefix: &value.prefix,
- suffix: &value.suffix,
- use_grouping: value.use_grouping,
- maximum_fraction_digits: value.maximum_fraction_digits,
- }
- }
-}
-
-impl<'a> From<&'a Format> for SignificantNumberFormat<'a> {
- fn from(value: &'a Format) -> Self {
- Self {
- scientific: value.scientific,
- prefix: &value.prefix,
- suffix: &value.suffix,
- use_grouping: value.use_grouping,
- maximum_fraction_digits: value.maximum_fraction_digits,
- }
- }
-}
-
-impl<'a> SignificantNumberFormat<'a> {
- fn decode(&self) -> crate::format::Format {
- let type_ = if self.scientific == Some(Scientific::True) {
- Type::E
- } else if self.prefix == "$" {
- Type::Dollar
- } else if self.suffix == "%" {
- Type::Pct
- } else if self.use_grouping == Some(true) {
- Type::Comma
- } else {
- Type::F
- };
- let d = match self.maximum_fraction_digits {
- Some(d) if (0..=15).contains(&d) => d,
- _ => 2,
- };
- UncheckedFormat {
- type_,
- w: 40,
- d: d as u8,
- }
- .fix()
- }
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct DateTimeFormat {
- #[serde(rename = "@baseFormat")]
- base_format: BaseFormat,
- #[serde(rename = "@separatorChars")]
- separator_chars: Option<String>,
- #[serde(rename = "@mdyOrder")]
- mdy_order: Option<MdyOrder>,
- #[serde(rename = "@showYear")]
- show_year: Option<bool>,
- #[serde(rename = "@showQuarter")]
- show_quarter: Option<bool>,
- #[serde(rename = "@quarterPrefix")]
- quarter_prefix: Option<String>,
- #[serde(rename = "@quarterSuffix")]
- quarter_suffix: Option<String>,
- #[serde(rename = "@yearAbbreviation")]
- year_abbreviation: Option<bool>,
- #[serde(rename = "@showMonth")]
- show_month: Option<bool>,
- #[serde(rename = "@monthFormat")]
- month_format: Option<MonthFormat>,
- #[serde(rename = "@dayPadding")]
- day_padding: Option<bool>,
- #[serde(rename = "@dayOfMonthPadding")]
- day_of_month_padding: Option<bool>,
- #[serde(rename = "@showWeek")]
- show_week: Option<bool>,
- #[serde(rename = "@weekPadding")]
- week_padding: Option<bool>,
- #[serde(rename = "@weekSuffix")]
- week_suffix: Option<String>,
- #[serde(rename = "@showDayOfWeek")]
- show_day_of_week: Option<bool>,
- #[serde(rename = "@dayOfWeekAbbreviation")]
- day_of_week_abbreviation: Option<bool>,
- #[serde(rename = "hourPadding")]
- hour_padding: Option<bool>,
- #[serde(rename = "minutePadding")]
- minute_padding: Option<bool>,
- #[serde(rename = "secondPadding")]
- second_padding: Option<bool>,
- #[serde(rename = "@showDay")]
- show_day: Option<bool>,
- #[serde(rename = "@showHour")]
- show_hour: Option<bool>,
- #[serde(rename = "@showMinute")]
- show_minute: Option<bool>,
- #[serde(rename = "@showSecond")]
- show_second: Option<bool>,
- #[serde(rename = "@showMillis")]
- show_millis: Option<bool>,
- #[serde(rename = "@dayType")]
- day_type: Option<DayType>,
- #[serde(rename = "@hourFormat")]
- hour_format: Option<HourFormat>,
- #[serde(default, rename = "affix")]
- affixes: Vec<Affix>,
-}
-
-struct SignificantDateTimeFormat {
- base_format: Option<BaseFormat>,
- show_quarter: Option<bool>,
- show_week: Option<bool>,
- show_day: Option<bool>,
- show_hour: Option<bool>,
- show_second: Option<bool>,
- show_millis: Option<bool>,
- mdy_order: Option<MdyOrder>,
- month_format: Option<MonthFormat>,
- year_abbreviation: Option<bool>,
-}
-
-impl From<&Format> for SignificantDateTimeFormat {
- fn from(value: &Format) -> Self {
- Self {
- base_format: value.base_format,
- show_quarter: value.show_quarter,
- show_week: value.show_week,
- show_day: value.show_day,
- show_hour: value.show_hour,
- show_second: value.show_second,
- show_millis: value.show_millis,
- mdy_order: value.mdy_order,
- month_format: value.month_format,
- year_abbreviation: value.year_abbreviation,
- }
- }
-}
-impl From<&DateTimeFormat> for SignificantDateTimeFormat {
- fn from(value: &DateTimeFormat) -> Self {
- Self {
- base_format: Some(value.base_format),
- show_quarter: value.show_quarter,
- show_week: value.show_week,
- show_day: value.show_day,
- show_hour: value.show_hour,
- show_second: value.show_second,
- show_millis: value.show_millis,
- mdy_order: value.mdy_order,
- month_format: value.month_format,
- year_abbreviation: value.year_abbreviation,
- }
- }
-}
-impl SignificantDateTimeFormat {
- fn decode(&self) -> crate::format::Format {
- let type_ = match self.base_format {
- Some(BaseFormat::Date) => {
- let type_ = if self.show_quarter == Some(true) {
- Type::QYr
- } else if self.show_week == Some(true) {
- Type::WkYr
- } else {
- match (self.mdy_order, self.month_format) {
- (Some(MdyOrder::DayMonthYear), Some(MonthFormat::Number)) => Type::EDate,
- (Some(MdyOrder::DayMonthYear), Some(MonthFormat::PaddedNumber)) => {
- Type::EDate
- }
- (Some(MdyOrder::DayMonthYear), _) => Type::Date,
- (Some(MdyOrder::YearMonthDay), _) => Type::SDate,
- _ => Type::ADate,
- }
- };
- let mut w = type_.min_width();
- if self.year_abbreviation != Some(true) {
- w += 2;
- };
- return UncheckedFormat { type_, w, d: 0 }.try_into().unwrap();
- }
- Some(BaseFormat::DateTime) => {
- if self.mdy_order == Some(MdyOrder::YearMonthDay) {
- Type::YmdHms
- } else {
- Type::DateTime
- }
- }
- _ => {
- if self.show_day == Some(true) {
- Type::DTime
- } else if self.show_hour == Some(true) {
- Type::Time
- } else {
- Type::MTime
- }
- }
- };
- date_time_format(type_, self.show_second, self.show_millis)
- }
-}
-
-impl DateTimeFormat {
- fn decode(&self) -> crate::format::Format {
- SignificantDateTimeFormat::from(self).decode()
- }
-}
-
-fn date_time_format(
- type_: Type,
- show_second: Option<bool>,
- show_millis: Option<bool>,
-) -> crate::format::Format {
- let mut w = type_.min_width();
- let mut d = 0;
- if show_second == Some(true) {
- w += 3;
- if show_millis == Some(true) {
- d = 3;
- w += d as u16 + 1;
- }
- }
- UncheckedFormat { type_, w, d }.try_into().unwrap()
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct ElapsedTimeFormat {
- #[serde(rename = "@dayPadding")]
- day_padding: Option<bool>,
- #[serde(rename = "hourPadding")]
- hour_padding: Option<bool>,
- #[serde(rename = "minutePadding")]
- minute_padding: Option<bool>,
- #[serde(rename = "secondPadding")]
- second_padding: Option<bool>,
- #[serde(rename = "@showDay")]
- show_day: Option<bool>,
- #[serde(rename = "@showHour")]
- show_hour: Option<bool>,
- #[serde(rename = "@showMinute")]
- show_minute: Option<bool>,
- #[serde(rename = "@showSecond")]
- show_second: Option<bool>,
- #[serde(rename = "@showMillis")]
- show_millis: Option<bool>,
- #[serde(rename = "@showYear")]
- show_year: Option<bool>,
- #[serde(default, rename = "affix")]
- affixes: Vec<Affix>,
-}
-
-impl ElapsedTimeFormat {
- fn decode(&self) -> crate::format::Format {
- let type_ = if self.show_day == Some(true) {
- Type::DTime
- } else if self.show_hour == Some(true) {
- Type::Time
- } else {
- Type::MTime
- };
- date_time_format(type_, self.show_second, self.show_millis)
- }
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum BaseFormat {
- Date,
- Time,
- DateTime,
- ElapsedTime,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum MdyOrder {
- DayMonthYear,
- MonthDayYear,
- YearMonthDay,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum MonthFormat {
- Long,
- Short,
- Number,
- PaddedNumber,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum DayType {
- Month,
- Year,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum HourFormat {
- #[serde(rename = "AMPM")]
- AmPm,
- #[serde(rename = "AS_24")]
- As24,
- #[serde(rename = "AS_12")]
- As12,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum Scientific {
- OnlyForSmall,
- WhenNeeded,
- True,
- False,
-}
-
-#[derive(Clone, Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct Affix {
- /// The footnote number as a natural number: 1 for the first footnote, 2 for
- /// the second, and so on.
- #[serde(rename = "@definesReference")]
- defines_reference: u64,
-
- /// Position for the footnote label.
- #[serde(rename = "@position")]
- position: Position,
-
- /// Whether the affix is a suffix (true) or a prefix (false).
- #[serde(rename = "@suffix")]
- suffix: bool,
-
- /// The text of the suffix or prefix. Typically a letter, e.g. `a` for
- /// footnote 1, `b` for footnote 2, ...
- #[serde(rename = "@value")]
- value: String,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum Position {
- Subscript,
- Superscript,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Relabel {
- #[serde(rename = "@from")]
- from: f64,
- #[serde(rename = "@to")]
- to: String,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct ValueMapEntry {
- #[serde(rename = "@from")]
- from: String,
- #[serde(rename = "@to")]
- to: String,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Style {
- #[serde(rename = "@id")]
- id: Option<String>,
-
- /// The text color or, in some cases, background color.
- #[serde(rename = "@color")]
- color: Option<Color>,
-
- /// Not used.
- #[serde(rename = "@color2")]
- color2: Option<Color>,
-
- /// Normally 0. The value -90 causes inner column or outer row labels to be
- /// rotated vertically.
- #[serde(rename = "@labelAngle")]
- label_angle: Option<f64>,
-
- #[serde(rename = "@border-bottom")]
- border_bottom: Option<Border>,
-
- #[serde(rename = "@border-top")]
- border_top: Option<Border>,
-
- #[serde(rename = "@border-left")]
- border_left: Option<Border>,
-
- #[serde(rename = "@border-right")]
- border_right: Option<Border>,
-
- #[serde(rename = "@border-bottom-color")]
- border_bottom_color: Option<Color>,
-
- #[serde(rename = "@border-top-color")]
- border_top_color: Option<Color>,
-
- #[serde(rename = "@border-left-color")]
- border_left_color: Option<Color>,
-
- #[serde(rename = "@border-right-color")]
- border_right_color: Option<Color>,
-
- #[serde(rename = "@font-family")]
- font_family: Option<String>,
-
- #[serde(rename = "@font-size")]
- font_size: Option<String>,
-
- #[serde(rename = "@font-weight")]
- font_weight: Option<FontWeight>,
-
- #[serde(rename = "@font-style")]
- font_style: Option<FontStyle>,
-
- #[serde(rename = "@font-underline")]
- font_underline: Option<FontUnderline>,
-
- #[serde(rename = "@margin-bottom")]
- margin_bottom: Option<Length>,
-
- #[serde(rename = "@margin-top")]
- margin_top: Option<Length>,
-
- #[serde(rename = "@margin-left")]
- margin_left: Option<Length>,
-
- #[serde(rename = "@margin-right")]
- margin_right: Option<Length>,
-
- #[serde(rename = "@textAlignment")]
- text_alignment: Option<TextAlignment>,
-
- #[serde(rename = "@labelLocationHorizontal")]
- label_location_horizontal: Option<LabelLocation>,
-
- #[serde(rename = "@labelLocationVertical")]
- label_location_vertical: Option<LabelLocation>,
-
- #[serde(rename = "@size")]
- size: Option<String>,
-
- #[serde(rename = "@width")]
- width: Option<String>,
-
- #[serde(rename = "@visible")]
- visible: Option<bool>,
-
- #[serde(rename = "@decimal-offset")]
- decimal_offset: Option<Length>,
-}
-
-impl Style {
- fn apply_to_value(
- value: &mut Value,
- sf: Option<&SetFormat>,
- fg: Option<&Style>,
- bg: Option<&Style>,
- base_style: &AreaStyle,
- ) {
- if let Some(sf) = sf {
- if sf.reset == Some(true) {
- value.styling_mut().footnotes.clear();
- }
-
- let format = match &sf.child {
- Some(SetFormatChild::Format(format)) => Some(format.decode()),
- Some(SetFormatChild::NumberFormat(format)) => {
- Some(SignificantNumberFormat::from(format).decode())
- }
- Some(SetFormatChild::StringFormat(_)) => None,
- Some(SetFormatChild::DateTimeFormat(format)) => Some(format.decode()),
- Some(SetFormatChild::ElapsedTimeFormat(format)) => Some(format.decode()),
- None => None,
- };
- if let Some(format) = format {
- match &mut value.inner {
- ValueInner::Number(number) => {
- number.format = format;
- }
- ValueInner::String(string) => {
- if format.type_().category() == format::Category::Date
- && let Ok(date_time) =
- NaiveDateTime::parse_from_str(&string.s, "%Y-%m-%dT%H:%M:%S%.3f")
- {
- value.inner = ValueInner::Number(NumberValue {
- show: None,
- format,
- honor_small: false,
- value: Some(date_time_to_pspp(date_time)),
- variable: None,
- value_label: None,
- })
- } else if format.type_().category() == format::Category::Time
- && let Ok(time) = NaiveTime::parse_from_str(&string.s, "%H:%M:%S%.3f")
- {
- value.inner = ValueInner::Number(NumberValue {
- show: None,
- format,
- honor_small: false,
- value: Some(time_to_pspp(time)),
- variable: None,
- value_label: None,
- })
- } else if let Ok(number) = string.s.parse::<f64>() {
- value.inner = ValueInner::Number(NumberValue {
- show: None,
- format,
- honor_small: false,
- value: Some(number),
- variable: None,
- value_label: None,
- })
- }
- }
- _ => (),
- }
- }
- }
-
- if fg.is_some() || bg.is_some() {
- let styling = value.styling_mut();
- let font_style = styling
- .font_style
- .get_or_insert_with(|| base_style.font_style.clone());
- let cell_style = styling
- .cell_style
- .get_or_insert_with(|| base_style.cell_style.clone());
- Self::decode(fg, bg, cell_style, font_style);
- }
- }
-
- fn decode(
- fg: Option<&Style>,
- bg: Option<&Style>,
- cell_style: &mut CellStyle,
- font_style: &mut pivot::FontStyle,
- ) {
- if let Some(fg) = fg {
- if let Some(weight) = fg.font_weight {
- font_style.bold = weight.is_bold();
- }
- if let Some(style) = fg.font_style {
- font_style.italic = style.is_italic();
- }
- if let Some(underline) = fg.font_underline {
- font_style.underline = underline.is_underline();
- }
- if let Some(color) = fg.color {
- font_style.fg = color;
- }
- if let Some(font_size) = &fg.font_size {
- if let Ok(size) = font_size
- .trim_end_matches(|c: char| c.is_alphabetic())
- .parse()
- {
- font_style.size = size;
- } else {
- // XXX warn?
- }
- }
- if let Some(alignment) = fg.text_alignment {
- cell_style.horz_align = alignment.as_horz_align(fg.decimal_offset);
- }
- if let Some(label_local_vertical) = fg.label_location_vertical {
- cell_style.vert_align = label_local_vertical.into();
- }
- }
- if let Some(bg) = bg {
- if let Some(color) = bg.color {
- font_style.bg = color;
- }
- }
- }
-
- fn decode_area(fg: Option<&Style>, bg: Option<&Style>, out: &mut AreaStyle) {
- Self::decode(fg, bg, &mut out.cell_style, &mut out.font_style);
- }
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum Border {
- Solid,
- Thick,
- Thin,
- Double,
- None,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum FontWeight {
- Regular,
- Bold,
-}
-
-impl FontWeight {
- fn is_bold(&self) -> bool {
- *self == Self::Bold
- }
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum FontStyle {
- Regular,
- Italic,
-}
-
-impl FontStyle {
- fn is_italic(&self) -> bool {
- *self == Self::Italic
- }
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum FontUnderline {
- None,
- Underline,
-}
-
-impl FontUnderline {
- fn is_underline(&self) -> bool {
- *self == Self::Underline
- }
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum TextAlignment {
- Left,
- Right,
- Center,
- Decimal,
- Mixed,
-}
-
-impl TextAlignment {
- fn as_horz_align(&self, decimal_offset: Option<Length>) -> Option<HorzAlign> {
- match self {
- TextAlignment::Left => Some(HorzAlign::Left),
- TextAlignment::Right => Some(HorzAlign::Right),
- TextAlignment::Center => Some(HorzAlign::Center),
- TextAlignment::Decimal => Some(HorzAlign::Decimal {
- offset: decimal_offset.unwrap_or_default().as_px_f64(),
- decimal: Dot,
- }),
- TextAlignment::Mixed => None,
- }
- }
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum LabelLocation {
- Positive,
- Negative,
- Center,
-}
-
-impl From<LabelLocation> for VertAlign {
- fn from(value: LabelLocation) -> Self {
- match value {
- LabelLocation::Positive => VertAlign::Top,
- LabelLocation::Negative => VertAlign::Bottom,
- LabelLocation::Center => VertAlign::Middle,
- }
- }
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Graph {
- #[serde(rename = "@id")]
- id: Option<String>,
-
- #[serde(rename = "@cellStyle")]
- cell_style: Ref<Style>,
-
- #[serde(rename = "@style")]
- style: Ref<Style>,
-
- #[serde(rename = "location")]
- locations: Vec<Location>,
- coordinates: Coordinates,
- faceting: Faceting,
- facet_layout: FacetLayout,
- interval: Interval,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Coordinates;
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Location {
- /// The part of the table being located.
- #[serde(rename = "@part")]
- part: Part,
-
- /// How the location is determined.
- #[serde(rename = "@method")]
- method: Method,
-
- /// Minimum size.
- #[serde(rename = "@min")]
- min: Option<Length>,
-
- /// Maximum size.
- #[serde(rename = "@max")]
- max: Option<Length>,
-
- /// An element to attach to. Required when method is attach or same, not
- /// observed otherwise.
- #[serde(rename = "@target")]
- target: Option<String>,
-
- #[serde(rename = "@value")]
- value: Option<String>,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum Part {
- Height,
- Width,
- Top,
- Bottom,
- Left,
- Right,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum Method {
- SizeToContent,
- Attach,
- Fixed,
- Same,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Faceting {
- #[serde(rename = "@id")]
- id: Option<String>,
-
- #[serde(default)]
- layers1: Vec<Layer>,
- cross: Cross,
- #[serde(default)]
- layers2: Vec<Layer>,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Cross {
- #[serde(rename = "$value")]
- children: Vec<CrossChild>,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-enum CrossChild {
- /// No dimensions along this axis.
- Unity,
- /// Dimensions along this axis.
- Nest(Nest),
-}
-
-impl CrossChild {
- fn variables(&self) -> &[VariableReference] {
- match self {
- CrossChild::Unity => &[],
- CrossChild::Nest(nest) => &nest.variable_references,
- }
- }
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Nest {
- #[serde(rename = "variableReference")]
- variable_references: Vec<VariableReference>,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Layer {
- #[serde(rename = "@variable")]
- variable: String,
-
- #[serde(rename = "@value")]
- value: String,
-
- #[serde(rename = "@visible")]
- visible: Option<bool>,
-
- #[serde(rename = "@titleVisible")]
- title_visible: Option<bool>,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct FacetLayout {
- table_layout: TableLayout,
- #[serde(rename = "$value")]
- children: Vec<FacetLayoutChild>,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-enum FacetLayoutChild {
- SetCellProperties(SetCellProperties),
- FacetLevel(FacetLevel),
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct TableLayout {
- #[serde(rename = "@verticalTitlesInCorner")]
- vertical_titles_in_corner: bool,
-
- #[serde(rename = "@style")]
- style: Option<Ref<Style>>,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct SetCellProperties {
- #[serde(rename = "@id")]
- id: Option<String>,
-
- #[serde(rename = "@applyToConverse")]
- apply_to_converse: Option<bool>,
-
- #[serde(rename = "$value")]
- sets: Vec<Set>,
-
- #[serde(rename = "union")]
- union_: Option<Union>,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Union {
- #[serde(default, rename = "intersect")]
- intersects: Vec<Intersect>,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Intersect {
- #[serde(default, rename = "$value")]
- children: Vec<IntersectChild>,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-enum IntersectChild {
- Where(Where),
- IntersectWhere(IntersectWhere),
- Alternating,
- #[serde(other)]
- Empty,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Where {
- #[serde(rename = "@variable")]
- variable: String,
- #[serde(rename = "@include")]
- include: String,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct IntersectWhere {
- #[serde(rename = "@variable")]
- variable: String,
-
- #[serde(rename = "@variable2")]
- variable2: String,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-enum Set {
- SetStyle(SetStyle),
- SetFrameStyle(SetFrameStyle),
- SetFormat(SetFormat),
- SetMetaData(SetMetaData),
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct SetStyle {
- #[serde(rename = "@target")]
- target: String,
-
- #[serde(rename = "@style")]
- style: Ref<Style>,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct SetMetaData {
- #[serde(rename = "@target")]
- target: Ref<Graph>,
-
- #[serde(rename = "@key")]
- key: String,
-
- #[serde(rename = "@value")]
- value: String,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct SetFormat {
- #[serde(rename = "@target")]
- target: String,
-
- #[serde(rename = "@reset")]
- reset: Option<bool>,
-
- #[serde(rename = "$value")]
- child: Option<SetFormatChild>,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-enum SetFormatChild {
- Format(Format),
- NumberFormat(NumberFormat),
- StringFormat(Vec<StringFormat>),
- DateTimeFormat(DateTimeFormat),
- ElapsedTimeFormat(ElapsedTimeFormat),
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct SetFrameStyle {
- #[serde(rename = "@style")]
- style: Ref<Style>,
-
- #[serde(rename = "@target")]
- target: Ref<MajorTicks>,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Interval {
- #[serde(rename = "@id")]
- id: Option<String>,
-
- #[serde(rename = "@style")]
- style: Ref<Style>,
-
- labeling: Labeling,
- footnotes: Option<Footnotes>,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Labeling {
- #[serde(rename = "@id")]
- id: Option<String>,
-
- #[serde(rename = "@style")]
- style: Option<Ref<Style>>,
-
- #[serde(rename = "@variable")]
- variable: String,
-
- #[serde(default)]
- children: Vec<LabelingChild>,
-}
-
-impl Labeling {
- fn decode_format_map<'a>(
- &self,
- series: &'a HashMap<&str, Series>,
- ) -> (Option<&'a Series>, HashMap<i64, crate::format::Format>) {
- let mut map = HashMap::new();
- let mut cell_format = None;
- for child in &self.children {
- if let LabelingChild::Formatting(formatting) = child {
- cell_format = series.get(formatting.variable.as_str());
- for mapping in &formatting.mappings {
- if let Some(format) = &mapping.format {
- map.insert(mapping.from, format.decode());
- }
- }
- }
- }
- (cell_format, map)
- }
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-enum LabelingChild {
- Formatting(Formatting),
- Format(Format),
- Footnotes(Footnotes),
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Formatting {
- #[serde(rename = "@variable")]
- variable: String,
-
- mappings: Vec<FormatMapping>,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct FormatMapping {
- #[serde(rename = "@from")]
- from: i64,
-
- format: Option<Format>,
-}
-
-#[derive(Clone, Debug, Default)]
-struct Footnote {
- content: String,
- marker: Option<String>,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Footnotes {
- #[serde(rename = "@superscript")]
- superscript: Option<bool>,
-
- #[serde(rename = "@variable")]
- variable: String,
-
- #[serde(default, rename = "footnoteMapping")]
- mappings: Vec<FootnoteMapping>,
-}
-
-impl Footnotes {
- fn decode(&self, dst: &mut BTreeMap<usize, Footnote>) {
- for f in &self.mappings {
- dst.entry(f.defines_reference.get() - 1)
- .or_default()
- .content = f.to.clone();
- }
- }
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct FootnoteMapping {
- #[serde(rename = "@definesReference")]
- defines_reference: NonZeroUsize,
-
- #[serde(rename = "@from")]
- from: i64,
-
- #[serde(rename = "@to")]
- to: String,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct FacetLevel {
- #[serde(rename = "@id")]
- id: Option<String>,
-
- #[serde(rename = "@level")]
- level: usize,
-
- #[serde(rename = "@gap")]
- gap: Option<Length>,
- axis: Axis,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Axis {
- #[serde(rename = "@style")]
- style: Ref<Style>,
-
- label: Option<Label>,
- major_ticks: MajorTicks,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct MajorTicks {
- #[serde(rename = "@id")]
- id: String,
-
- #[serde(rename = "@labelAngle")]
- label_angle: f64,
-
- #[serde(rename = "@length")]
- length: Length,
-
- #[serde(rename = "@style")]
- style: Ref<Style>,
-
- #[serde(rename = "@tickFrameStyle")]
- tick_frame_style: Ref<Style>,
-
- #[serde(rename = "@labelFrequency")]
- label_frequency: Option<i64>,
-
- #[serde(rename = "@stagger")]
- stagger: Option<bool>,
-
- gridline: Option<Gridline>,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Gridline {
- #[serde(rename = "@style")]
- style: Ref<Style>,
-
- #[serde(rename = "@zOrder")]
- z_order: i64,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Label {
- #[serde(rename = "@style")]
- style: Ref<Style>,
-
- #[serde(rename = "@textFrameStyle")]
- text_frame_style: Option<Ref<Style>>,
-
- #[serde(rename = "@purpose")]
- purpose: Option<Purpose>,
-
- #[serde(rename = "$value")]
- child: LabelChild,
-}
-
-impl Label {
- fn text(&self) -> &[Text] {
- match &self.child {
- LabelChild::Text(texts) => texts.as_slice(),
- LabelChild::DescriptionGroup(_) => &[],
- }
- }
-
- fn decode_style(&self, area_style: &mut AreaStyle, styles: &HashMap<&str, &Style>) {
- let fg = self.style.get(styles);
- let bg = self.text_frame_style.as_ref().and_then(|r| r.get(styles));
- Style::decode_area(fg, bg, area_style);
- }
-}
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Enum)]
-#[serde(rename_all = "camelCase")]
-enum Purpose {
- Title,
- SubTitle,
- SubSubTitle,
- Layer,
- Footnote,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-enum LabelChild {
- Text(Vec<Text>),
- DescriptionGroup(DescriptionGroup),
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Text {
- #[serde(rename = "@usesReference")]
- uses_reference: Option<NonZeroUsize>,
-
- #[serde(rename = "@definesReference")]
- defines_reference: Option<NonZeroUsize>,
-
- #[serde(rename = "@position")]
- position: Option<Position>,
-
- #[serde(rename = "@style")]
- style: Option<Ref<Style>>,
-
- #[serde(default, rename = "$text")]
- text: String,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct DescriptionGroup {
- #[serde(rename = "@target")]
- target: Ref<Faceting>,
-
- #[serde(rename = "@separator")]
- separator: Option<String>,
-
- #[serde(rename = "$value")]
- children: Vec<DescriptionGroupChild>,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-enum DescriptionGroupChild {
- Description(Description),
- Text(Text),
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Description {
- #[serde(rename = "@name")]
- name: Name,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum Name {
- Variable,
- Value,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct LabelFrame {
- #[serde(rename = "@id")]
- id: Option<String>,
-
- #[serde(rename = "@style")]
- style: Option<Ref<Style>>,
-
- #[serde(rename = "location")]
- locations: Vec<Location>,
-
- label: Option<Label>,
- paragraph: Option<Paragraph>,
-}
-
-impl LabelFrame {
- fn decode_label(labels: &[&Label]) -> Option<Value> {
- if !labels.is_empty() {
- let mut s = String::new();
- for t in labels {
- if let LabelChild::Text(text) = &t.child {
- for t in text {
- if let Some(_defines_reference) = t.defines_reference {
- // XXX footnote
- }
- s += &t.text;
- }
- }
- }
- Some(Value::new_user_text(s))
- } else {
- None
- }
- }
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Paragraph;
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Container {
- #[serde(rename = "@style")]
- style: Ref<Style>,
-
- #[serde(default, rename = "extension")]
- extensions: Option<ContainerExtension>,
- #[serde(default)]
- locations: Vec<Location>,
- #[serde(default)]
- label_frames: Vec<LabelFrame>,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename = "extension", rename_all = "camelCase")]
-struct ContainerExtension {
- #[serde(rename = "@combinedFootnotes")]
- combined_footnotes: Option<bool>,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct LayerController {
- #[serde(rename = "@target")]
- target: Option<Ref<Label>>,
-}
+++ /dev/null
-use std::{
- fmt::Debug,
- io::{Cursor, Read, Seek},
- ops::Deref,
- str::FromStr,
- sync::Arc,
-};
-
-use binrw::{
- BinRead, BinResult, Endian, Error as BinError, VecArgs, binread, error::ContextExt,
- io::TakeSeekExt,
-};
-use chrono::DateTime;
-use displaydoc::Display;
-use encoding_rs::{Encoding, WINDOWS_1252};
-use enum_map::{EnumMap, enum_map};
-
-use crate::{
- format::{
- CC, Decimal, Decimals, Epoch, F40, Format, NumberStyle, Settings, Type, UncheckedFormat,
- Width,
- },
- output::pivot::{
- self, AreaStyle, Axis2, Axis3, BoxBorder, Color, FootnoteMarkerPosition,
- FootnoteMarkerType, Footnotes, Group, HeadingRegion, HorzAlign, LabelPosition, Look,
- PivotTable, PivotTableMetadata, PivotTableStyle, PrecomputedIndex, RowColBorder, RowParity,
- StringValue, Stroke, TemplateValue, ValueStyle, VariableValue, VertAlign, parse_bool,
- },
- settings::Show,
-};
-
-#[derive(Debug, Display, thiserror::Error)]
-pub enum LightError {
- /// Expected {expected} dimensions along axes, found {actual} dimensions ({n_layers} layers + {n_rows} rows + {n_columns} columns).
- WrongAxisCount {
- expected: usize,
- actual: usize,
- n_layers: usize,
- n_rows: usize,
- n_columns: usize,
- },
-
- /// Invalid dimension index {index} in table with {n} dimensions.
- InvalidDimensionIndex { index: usize, n: usize },
-
- /// Dimension with index {0} appears twice in table axes.
- DuplicateDimensionIndex(usize),
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-pub struct LightTable {
- header: Header,
- #[br(args(header.version))]
- titles: Titles,
- #[br(parse_with(parse_vec), args(header.version))]
- footnotes: Vec<Footnote>,
- #[br(args(header.version))]
- areas: Areas,
- #[br(parse_with(parse_counted))]
- borders: Borders,
- #[br(parse_with(parse_counted))]
- print_settings: PrintSettings,
- #[br(if(header.version == Version::V3), parse_with(parse_counted))]
- table_settings: TableSettings,
- #[br(if(header.version == Version::V1), temp)]
- _ts: Option<Counted<Sponge>>,
- #[br(args(header.version))]
- formats: Formats,
- #[br(parse_with(parse_vec), args(header.version))]
- dimensions: Vec<Dimension>,
- axes: Axes,
- #[br(parse_with(parse_vec), args(header.version))]
- cells: Vec<Cell>,
-}
-
-impl LightTable {
- fn decode_look(&self, encoding: &'static Encoding) -> Look {
- Look {
- name: self.table_settings.table_look.decode_optional(encoding),
- hide_empty: self.table_settings.omit_empty,
- row_label_position: if self.table_settings.show_row_labels_in_corner {
- LabelPosition::Corner
- } else {
- LabelPosition::Nested
- },
- heading_widths: enum_map! {
- HeadingRegion::Rows => self.header.min_row_heading_width as isize..=self.header.max_row_heading_width as isize,
- HeadingRegion::Columns => self.header.min_column_heading_width as isize..=self.header.max_column_heading_width as isize,
- },
- footnote_marker_type: if self.table_settings.show_alphabetic_markers {
- FootnoteMarkerType::Alphabetic
- } else {
- FootnoteMarkerType::Numeric
- },
- footnote_marker_position: if self.table_settings.footnote_marker_subscripts {
- FootnoteMarkerPosition::Subscript
- } else {
- FootnoteMarkerPosition::Superscript
- },
- areas: self.areas.decode(encoding),
- borders: self.borders.decode(),
- print_all_layers: self.print_settings.alll_layers,
- paginate_layers: self.print_settings.paginate_layers,
- shrink_to_fit: enum_map! {
- 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,
- continuation: self
- .print_settings
- .continuation_string
- .decode_optional(encoding),
- n_orphan_lines: self.print_settings.n_orphan_lines,
- }
- }
-
- pub fn decode(&self) -> Result<PivotTable, LightError> {
- let encoding = self.formats.encoding();
-
- let n1 = self.formats.n1();
- let n2 = self.formats.n2();
- let n3 = self.formats.n3();
- let n3_inner = n3.and_then(|n3| n3.inner.as_ref());
- let y1 = self.formats.y1();
- let footnotes = self
- .footnotes
- .iter()
- .map(|f| f.decode(encoding, &Footnotes::new()))
- .collect();
- let cells = self
- .cells
- .iter()
- .map(|cell| {
- (
- PrecomputedIndex(cell.index as usize),
- cell.value.decode(encoding, &footnotes),
- )
- })
- .collect::<Vec<_>>();
- let dimensions = self
- .dimensions
- .iter()
- .map(|d| {
- let mut root = Group::new(d.name.decode(encoding, &footnotes))
- .with_show_label(!d.hide_dim_label);
- for category in &d.categories {
- category.decode(encoding, &footnotes, &mut root);
- }
- pivot::Dimension {
- presentation_order: (0..root.len()).collect(), /*XXX*/
- root,
- hide_all_labels: d.hide_all_labels,
- }
- })
- .collect::<Vec<_>>();
- let pivot_table = PivotTable::new(self.axes.decode(dimensions)?)
- .with_style(PivotTableStyle {
- look: Arc::new(self.decode_look(encoding)),
- rotate_inner_column_labels: self.header.rotate_inner_column_labels,
- rotate_outer_row_labels: self.header.rotate_outer_row_labels,
- show_grid_lines: self.borders.show_grid_lines,
- show_title: n1.map_or(true, |x1| x1.show_title != 10),
- show_caption: n1.map_or(true, |x1| x1.show_caption),
- show_values: n1.map_or(None, |x1| x1.show_values),
- show_variables: n1.map_or(None, |x1| x1.show_variables),
- sizing: self.table_settings.sizing.decode(
- &self.formats.column_widths,
- n2.map_or(&[], |x2| &x2.row_heights),
- ),
- settings: Settings {
- epoch: self.formats.y0.epoch(),
- decimal: self.formats.y0.decimal(),
- leading_zero: y1.map_or(false, |y1| y1.include_leading_zero),
- ccs: self.formats.custom_currency.decode(encoding),
- },
- grouping: {
- let grouping = self.formats.y0.grouping;
- b",.' ".contains(&grouping).then_some(grouping as char)
- },
- small: n3.map_or(0.0, |n3| n3.small),
- weight_format: F40,
- })
- .with_metadata(PivotTableMetadata {
- command_local: y1.map(|y1| y1.command_local.decode(encoding)),
- command_c: y1.map(|y1| y1.command.decode(encoding)),
- language: y1.map(|y1| y1.language.decode(encoding)),
- locale: y1.map(|y1| y1.locale.decode(encoding)),
- dataset: n3_inner.and_then(|strings| strings.dataset.decode_optional(encoding)),
- datafile: n3_inner.and_then(|strings| strings.datafile.decode_optional(encoding)),
- date: n3_inner.and_then(|inner| {
- if inner.date != 0 {
- DateTime::from_timestamp(inner.date as i64, 0).map(|dt| dt.naive_utc())
- } else {
- None
- }
- }),
- title: Some(Box::new(self.titles.title.decode(encoding, &footnotes))),
- subtype: Some(Box::new(self.titles.subtype.decode(encoding, &footnotes))),
- corner_text: self
- .titles
- .corner_text
- .as_ref()
- .map(|corner| Box::new(corner.decode(encoding, &footnotes))),
- caption: self
- .titles
- .caption
- .as_ref()
- .map(|caption| Box::new(caption.decode(encoding, &footnotes))),
- notes: self.table_settings.notes.decode_optional(encoding),
- })
- .with_footnotes(footnotes)
- .with_data(cells);
- Ok(pivot_table)
- }
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct Header {
- #[br(magic = b"\x01\0")]
- version: Version,
- #[br(parse_with(parse_bool), temp)]
- _x0: bool,
- #[br(parse_with(parse_bool), temp)]
- _x1: bool,
- #[br(parse_with(parse_bool))]
- rotate_inner_column_labels: bool,
- #[br(parse_with(parse_bool))]
- rotate_outer_row_labels: bool,
- #[br(parse_with(parse_bool), temp)]
- _x2: bool,
- #[br(temp)]
- _x3: i32,
- min_column_heading_width: u32,
- max_column_heading_width: u32,
- min_row_heading_width: u32,
- max_row_heading_width: u32,
- table_id: i64,
-}
-
-#[binread]
-#[br(little)]
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
-enum Version {
- #[br(magic = 1u32)]
- V1,
- #[br(magic = 3u32)]
- V3,
-}
-
-#[binread]
-#[br(little, import(version: Version))]
-#[derive(Debug)]
-struct Titles {
- #[br(args(version))]
- title: Value,
- #[br(temp)]
- _1: Optional<One>,
- #[br(args(version))]
- subtype: Value,
- #[br(temp)]
- _2: Optional<One>,
- #[br(magic = b'1')]
- #[br(args(version))]
- user_title: Value,
- #[br(temp)]
- _3: Optional<One>,
- #[br(parse_with(parse_explicit_optional), args(version))]
- corner_text: Option<Value>,
- #[br(parse_with(parse_explicit_optional), args(version))]
- caption: Option<Value>,
-}
-
-#[binread]
-#[br(little, magic = 1u8)]
-#[derive(Debug)]
-struct One;
-
-#[binread]
-#[br(little, magic = 0u8)]
-#[derive(Debug)]
-struct Zero;
-
-#[binrw::parser(reader, endian)]
-pub fn parse_explicit_optional<'a, T, A>(args: A, ...) -> BinResult<Option<T>>
-where
- T: BinRead<Args<'a> = A>,
-{
- let byte = <u8>::read_options(reader, endian, ())?;
- match byte {
- b'1' => Ok(Some(T::read_options(reader, endian, args)?)),
- b'X' => Ok(None),
- _ => Err(BinError::NoVariantMatch {
- pos: reader.stream_position()? - 1,
- }),
- }
-}
-
-#[binrw::parser(reader, endian)]
-pub(super) fn parse_vec<T, A>(inner: A, ...) -> BinResult<Vec<T>>
-where
- for<'a> T: BinRead<Args<'a> = A>,
- A: Clone,
- T: 'static,
-{
- let count = u32::read_options(reader, endian, ())? as usize;
- <Vec<T>>::read_options(reader, endian, VecArgs { count, inner })
-}
-
-#[binread]
-#[br(little, import(version: Version))]
-#[derive(Debug)]
-struct Footnote {
- #[br(args(version))]
- text: Value,
- #[br(parse_with(parse_explicit_optional))]
- #[br(args(version))]
- marker: Option<Value>,
- show: i32,
-}
-
-impl Footnote {
- fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> pivot::Footnote {
- pivot::Footnote::new(self.text.decode(encoding, footnotes))
- .with_marker(self.marker.as_ref().map(|m| m.decode(encoding, footnotes)))
- .with_show(self.show > 0)
- }
-}
-
-#[binread]
-#[br(little, import(version: Version))]
-#[derive(Debug)]
-struct Areas {
- #[br(temp)]
- _1: Optional<Zero>,
- #[br(args(version))]
- areas: [Area; 8],
-}
-
-impl Areas {
- fn decode(&self, encoding: &'static Encoding) -> EnumMap<pivot::Area, AreaStyle> {
- EnumMap::from_fn(|area| {
- let index = match area {
- pivot::Area::Title => 0,
- pivot::Area::Caption => 1,
- pivot::Area::Footer => 2,
- pivot::Area::Corner => 3,
- pivot::Area::Labels(Axis2::X) => 4,
- pivot::Area::Labels(Axis2::Y) => 5,
- pivot::Area::Data(_) => 6,
- pivot::Area::Layers => 7,
- };
- let data_row = match area {
- pivot::Area::Data(row) => row,
- _ => RowParity::default(),
- };
- self.areas[index].decode(encoding, data_row)
- })
- }
-}
-
-#[binrw::parser(reader, endian)]
-fn parse_color() -> BinResult<Color> {
- let pos = reader.stream_position()?;
- let string = U32String::read_options(reader, endian, ())?;
- let string = string.decode(WINDOWS_1252);
- if string.is_empty() {
- Ok(Color::BLACK)
- } else {
- Color::from_str(&string).map_err(|error| binrw::Error::Custom {
- pos,
- err: Box::new(error),
- })
- }
-}
-
-#[binread]
-#[br(little, import(version: Version))]
-#[derive(Debug)]
-struct Area {
- #[br(temp)]
- _index: u8,
- #[br(magic = b'1')]
- typeface: U32String,
- size: f32,
- style: i32,
- #[br(parse_with(parse_bool))]
- underline: bool,
- halign: i32,
- valign: i32,
- #[br(parse_with(parse_color))]
- fg: Color,
- #[br(parse_with(parse_color))]
- bg: Color,
- #[br(parse_with(parse_bool))]
- alternate: bool,
- #[br(parse_with(parse_color))]
- alt_fg: Color,
- #[br(parse_with(parse_color))]
- alt_bg: Color,
- #[br(if(version == Version::V3))]
- margins: Margins,
-}
-
-impl Area {
- fn decode(&self, encoding: &'static Encoding, data_row: RowParity) -> AreaStyle {
- AreaStyle {
- cell_style: pivot::CellStyle {
- horz_align: match self.halign {
- 0 => Some(HorzAlign::Center),
- 2 => Some(HorzAlign::Left),
- 4 => Some(HorzAlign::Right),
- _ => None,
- },
- vert_align: match self.valign {
- 0 => VertAlign::Middle,
- 3 => VertAlign::Bottom,
- _ => VertAlign::Top,
- },
- margins: enum_map! {
- Axis2::X => [self.margins.left_margin, self.margins.right_margin],
- Axis2::Y => [self.margins.top_margin, self.margins.bottom_margin]
- },
- },
- font_style: pivot::FontStyle {
- bold: (self.style & 1) != 0,
- italic: (self.style & 2) != 0,
- underline: self.underline,
- font: self.typeface.decode(encoding),
- fg: if data_row == RowParity::Odd && self.alternate {
- self.alt_fg
- } else {
- self.fg
- },
- bg: if data_row == RowParity::Odd && self.alternate {
- self.alt_bg
- } else {
- self.bg
- },
- size: (self.size / 1.33) as i32,
- },
- }
- }
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug, Default)]
-struct Margins {
- left_margin: i32,
- right_margin: i32,
- top_margin: i32,
- bottom_margin: i32,
-}
-
-#[binread]
-#[br(big)]
-#[derive(Debug)]
-struct Borders {
- #[br(magic(1u32), parse_with(parse_vec))]
- borders: Vec<Border>,
-
- #[br(parse_with(parse_bool))]
- show_grid_lines: bool,
-
- #[br(temp, magic(b"\0\0\0"))]
- _1: (),
-}
-
-impl Borders {
- fn decode(&self) -> EnumMap<pivot::Border, pivot::BorderStyle> {
- let mut borders = pivot::Border::default_borders();
- for border in &self.borders {
- if let Some((border, style)) = border.decode() {
- borders[border] = style;
- } else {
- // warning
- }
- }
- borders
- }
-}
-
-#[binread]
-#[br(big)]
-#[derive(Debug)]
-struct Border {
- #[br(map(|index: u32| index as usize))]
- index: usize,
- stroke: i32,
- color: u32,
-}
-
-impl Border {
- fn decode(&self) -> Option<(pivot::Border, pivot::BorderStyle)> {
- let border = match self.index {
- 0 => pivot::Border::Title,
- 1 => pivot::Border::OuterFrame(BoxBorder::Left),
- 2 => pivot::Border::OuterFrame(BoxBorder::Top),
- 3 => pivot::Border::OuterFrame(BoxBorder::Right),
- 4 => pivot::Border::OuterFrame(BoxBorder::Bottom),
- 5 => pivot::Border::InnerFrame(BoxBorder::Left),
- 6 => pivot::Border::InnerFrame(BoxBorder::Top),
- 7 => pivot::Border::InnerFrame(BoxBorder::Right),
- 8 => pivot::Border::InnerFrame(BoxBorder::Bottom),
- 9 => pivot::Border::DataLeft,
- 10 => pivot::Border::DataLeft,
- 11 => pivot::Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::X)),
- 12 => pivot::Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::X)),
- 13 => pivot::Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::X)),
- 14 => pivot::Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::X)),
- 15 => pivot::Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::X)),
- 16 => pivot::Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::X)),
- 17 => pivot::Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::X)),
- 18 => pivot::Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::X)),
- _ => return None,
- };
-
- let stroke = match self.stroke {
- 0 => Stroke::None,
- 2 => Stroke::Dashed,
- 3 => Stroke::Thick,
- 4 => Stroke::Thin,
- 6 => Stroke::Double,
- _ => Stroke::Solid,
- };
-
- let color = Color::new(
- (self.color >> 16) as u8,
- (self.color >> 8) as u8,
- self.color as u8,
- )
- .with_alpha((self.color >> 24) as u8);
-
- Some((border, pivot::BorderStyle { stroke, color }))
- }
-}
-
-#[binread]
-#[br(big)]
-#[derive(Debug)]
-struct PrintSettings {
- #[br(magic = b"\0\0\0\x01")]
- #[br(parse_with(parse_bool))]
- alll_layers: bool,
- #[br(parse_with(parse_bool))]
- paginate_layers: bool,
- #[br(parse_with(parse_bool))]
- fit_width: bool,
- #[br(parse_with(parse_bool))]
- fit_length: bool,
- #[br(parse_with(parse_bool))]
- top_continuation: bool,
- #[br(parse_with(parse_bool))]
- bottom_continuation: bool,
- #[br(map(|n: u32| n as usize))]
- n_orphan_lines: usize,
- continuation_string: U32String,
-}
-
-#[binread]
-#[br(big)]
-#[derive(Debug, Default)]
-struct TableSettings {
- #[br(temp, magic = 1u32)]
- _x5: i32,
- current_layer: i32,
- #[br(parse_with(parse_bool))]
- omit_empty: bool,
- #[br(parse_with(parse_bool))]
- show_row_labels_in_corner: bool,
- #[br(parse_with(parse_bool))]
- show_alphabetic_markers: bool,
- #[br(parse_with(parse_bool))]
- footnote_marker_subscripts: bool,
- #[br(temp)]
- _x6: u8,
- #[br(big, parse_with(parse_counted))]
- sizing: Sizing,
- notes: U32String,
- table_look: U32String,
- #[br(temp)]
- _sponge: Sponge,
-}
-
-#[binread]
-#[br(big)]
-#[derive(Debug, Default)]
-struct Sizing {
- #[br(parse_with(parse_vec))]
- row_breaks: Vec<u32>,
- #[br(parse_with(parse_vec))]
- column_breaks: Vec<u32>,
- #[br(parse_with(parse_vec))]
- row_keeps: Vec<(i32, i32)>,
- #[br(parse_with(parse_vec))]
- column_keeps: Vec<(i32, i32)>,
- #[br(parse_with(parse_vec))]
- row_point_keeps: Vec<[i32; 3]>,
- #[br(parse_with(parse_vec))]
- column_point_keeps: Vec<[i32; 3]>,
-}
-
-impl Sizing {
- fn decode(
- &self,
- column_widths: &[i32],
- row_heights: &[i32],
- ) -> EnumMap<Axis2, Option<Box<pivot::Sizing>>> {
- fn decode_axis(
- widths: &[i32],
- breaks: &[u32],
- keeps: &[(i32, i32)],
- ) -> Option<Box<pivot::Sizing>> {
- if widths.is_empty() && breaks.is_empty() && keeps.is_empty() {
- None
- } else {
- Some(Box::new(pivot::Sizing {
- widths: widths.into(),
- breaks: breaks.into_iter().map(|b| *b as usize).collect(),
- keeps: keeps
- .into_iter()
- .map(|(low, high)| *low as usize..*high as usize)
- .collect(),
- }))
- }
- }
-
- enum_map! {
- Axis2::X => decode_axis(column_widths, &self.column_breaks, &self.column_keeps),
- Axis2::Y => decode_axis(row_heights, &self.row_breaks, &self.row_keeps),
- }
- }
-}
-
-#[binread]
-#[derive(Default)]
-pub(super) struct U32String {
- #[br(parse_with(parse_vec))]
- string: Vec<u8>,
-}
-
-impl U32String {
- pub(super) fn decode(&self, encoding: &'static Encoding) -> String {
- if let Ok(string) = str::from_utf8(&self.string) {
- string.into()
- } else {
- encoding
- .decode_without_bom_handling(&self.string)
- .0
- .into_owned()
- }
- }
- pub(super) fn decode_optional(&self, encoding: &'static Encoding) -> Option<String> {
- let string = self.decode(encoding);
- if !string.is_empty() {
- Some(string)
- } else {
- None
- }
- }
-}
-
-impl Debug for U32String {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let s = self.string.iter().map(|c| *c as char).collect::<String>();
- write!(f, "{s:?}")
- }
-}
-
-#[binread]
-struct CountedInner {
- #[br(parse_with(parse_vec))]
- data: Vec<u8>,
-}
-
-impl CountedInner {
- fn cursor(self) -> Cursor<Vec<u8>> {
- Cursor::new(self.data)
- }
-}
-
-impl Debug for CountedInner {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{:?}", &self.data)
- }
-}
-
-#[derive(Clone, Debug, Default)]
-struct Counted<T>(T);
-
-impl<T> Deref for Counted<T> {
- type Target = T;
-
- fn deref(&self) -> &Self::Target {
- &self.0
- }
-}
-
-impl<T> BinRead for Counted<T>
-where
- T: BinRead,
-{
- type Args<'a> = T::Args<'a>;
-
- fn read_options<R: Read + Seek>(
- reader: &mut R,
- endian: binrw::Endian,
- args: Self::Args<'_>,
- ) -> BinResult<Self> {
- let count = u32::read_options(reader, endian, ())? as u64;
- let start = reader.stream_position()?;
- let end = start + count;
- let mut inner = reader.take_seek(count);
- let result = <T>::read_options(&mut inner, Endian::Little, args)?;
- let pos = inner.stream_position()?;
- if pos != end {
- let consumed = pos - start;
- return Err(binrw::Error::Custom {
- pos,
- err: Box::new(format!(
- "counted data not exhausted (consumed {consumed} bytes out of {count})"
- )),
- });
- }
- Ok(Self(result))
- }
-}
-
-#[binrw::parser(reader, endian)]
-fn parse_counted<T, A>(args: A, ...) -> BinResult<T>
-where
- for<'a> T: BinRead<Args<'a> = A>,
- A: Clone,
- T: 'static,
-{
- Ok(<Counted<T>>::read_options(reader, endian, args)?.0)
-}
-
-/// `BinRead` for `Option<T>` always requires the value to be there. This
-/// instead tries to read it and falls back to None if there's no match.
-#[derive(Clone, Debug)]
-struct Optional<T>(Option<T>);
-
-impl<T> Default for Optional<T> {
- fn default() -> Self {
- Self(None)
- }
-}
-
-impl<T> Deref for Optional<T> {
- type Target = Option<T>;
-
- fn deref(&self) -> &Self::Target {
- &self.0
- }
-}
-
-impl<T> BinRead for Optional<T>
-where
- T: BinRead,
-{
- type Args<'a> = T::Args<'a>;
-
- fn read_options<R: Read + Seek>(
- reader: &mut R,
- endian: binrw::Endian,
- args: Self::Args<'_>,
- ) -> BinResult<Self> {
- let start = reader.stream_position()?;
- let result = <T>::read_options(reader, endian, args).ok();
- if result.is_none() {
- reader.seek(std::io::SeekFrom::Start(start))?;
- }
- Ok(Self(result))
- }
-}
-
-#[binread]
-#[br(little)]
-#[br(import(version: Version))]
-#[derive(Debug)]
-struct Formats {
- #[br(parse_with(parse_vec))]
- column_widths: Vec<i32>,
- locale: U32String,
- current_layer: i32,
- #[br(temp, parse_with(parse_bool))]
- _x7: bool,
- #[br(temp, parse_with(parse_bool))]
- _x8: bool,
- #[br(temp, parse_with(parse_bool))]
- _x9: bool,
- y0: Y0,
- custom_currency: CustomCurrency,
- #[br(if(version == Version::V1))]
- v1: Counted<Optional<N0>>,
- #[br(if(version == Version::V3))]
- v3: Option<Counted<FormatsV3>>,
-}
-
-impl Formats {
- fn y1(&self) -> Option<&Y1> {
- self.v1
- .as_ref()
- .map(|n0| &n0.y1)
- .or_else(|| self.v3.as_ref().map(|v3| &v3.n3.y1))
- }
-
- fn n1(&self) -> Option<&N1> {
- self.v3.as_ref().map(|v3| &v3.n1_n2.x1)
- }
-
- fn n2(&self) -> Option<&N2> {
- self.v3.as_ref().map(|v3| &v3.n1_n2.x2)
- }
-
- fn n3(&self) -> Option<&N3> {
- self.v3.as_ref().map(|v3| &v3.n3)
- }
-
- fn charset(&self) -> Option<&U32String> {
- self.y1().map(|y1| &y1.charset)
- }
-
- fn encoding(&self) -> &'static Encoding {
- // XXX We should probably warn for unknown encodings below
- if let Some(charset) = self.charset()
- && let Some(encoding) = Encoding::for_label(&charset.string)
- {
- encoding
- } else if let Ok(locale) = str::from_utf8(&self.locale.string)
- && let Some(dot) = locale.find('.')
- && let Some(encoding) = Encoding::for_label(locale[dot + 1..].as_bytes())
- {
- encoding
- } else {
- WINDOWS_1252
- }
- }
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct FormatsV3 {
- #[br(parse_with(parse_counted))]
- n1_n2: N1N2,
- #[br(parse_with(parse_counted))]
- n3: N3,
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct N1N2 {
- x1: N1,
- #[br(parse_with(parse_counted))]
- x2: N2,
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct N0 {
- #[br(temp)]
- _bytes: [u8; 14],
- y1: Y1,
- y2: Y2,
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct Y1 {
- command: U32String,
- command_local: U32String,
- language: U32String,
- charset: U32String,
- locale: U32String,
- #[br(temp, parse_with(parse_bool))]
- _x10: bool,
- #[br(parse_with(parse_bool))]
- include_leading_zero: bool,
- #[br(temp, parse_with(parse_bool))]
- _x12: bool,
- #[br(temp, parse_with(parse_bool))]
- _x13: bool,
- y0: Y0,
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct Y2 {
- custom_currency: CustomCurrency,
- missing: u8,
- #[br(temp, parse_with(parse_bool))]
- _x17: bool,
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct N1 {
- #[br(temp, parse_with(parse_bool))]
- _x14: bool,
- show_title: u8,
- #[br(temp, parse_with(parse_bool))]
- _x16: bool,
- lang: u8,
- #[br(parse_with(parse_show))]
- show_variables: Option<Show>,
- #[br(parse_with(parse_show))]
- show_values: Option<Show>,
- #[br(temp)]
- _x18: i32,
- #[br(temp)]
- _x19: i32,
- #[br(temp)]
- _zeros: [u8; 17],
- #[br(temp, parse_with(parse_bool))]
- _x20: bool,
- #[br(parse_with(parse_bool))]
- show_caption: bool,
-}
-
-#[binrw::parser(reader, endian)]
-fn parse_show() -> BinResult<Option<Show>> {
- match <u8>::read_options(reader, endian, ())? {
- 0 => Ok(None),
- 1 => Ok(Some(Show::Value)),
- 2 => Ok(Some(Show::Label)),
- 3 => Ok(Some(Show::Both)),
- _ => {
- // XXX warn about invalid value
- Ok(None)
- }
- }
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct N2 {
- #[br(parse_with(parse_vec))]
- row_heights: Vec<i32>,
- #[br(parse_with(parse_vec))]
- style_map: Vec<(i64, i16)>,
- #[br(parse_with(parse_vec))]
- styles: Vec<StylePair>,
- #[br(parse_with(parse_counted))]
- tail: Optional<[u8; 8]>,
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct N3 {
- #[br(temp, magic = b"\x01\0")]
- _x21: u8,
- #[br(magic = b"\0\0\0")]
- y1: Y1,
- small: f64,
- #[br(magic = 1u8, temp)]
- _one: (),
- inner: Optional<N3Inner>,
- y2: Y2,
- #[br(temp)]
- _tail: Optional<N3Tail>,
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct N3Inner {
- dataset: U32String,
- datafile: U32String,
- notes_unexpanded: U32String,
- date: i32,
- #[br(magic = 0u32, temp)]
- _tail: (),
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct N3Tail {
- #[br(temp)]
- _x22: i32,
- #[br(temp, assert(_zero == 0))]
- _zero: i32,
- #[br(temp, assert(_x25.is_none_or(|x25| x25 == 0 || x25 == 1)))]
- _x25: Optional<u8>,
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct Y0 {
- epoch: i32,
- decimal: u8,
- grouping: u8,
-}
-
-impl Y0 {
- fn epoch(&self) -> Epoch {
- if (1000..=9999).contains(&self.epoch) {
- Epoch(self.epoch)
- } else {
- Epoch::default()
- }
- }
-
- fn decimal(&self) -> Decimal {
- // XXX warn about bad decimal point?
- Decimal::try_from(self.decimal as char).unwrap_or_default()
- }
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct CustomCurrency {
- #[br(parse_with(parse_vec))]
- ccs: Vec<U32String>,
-}
-
-impl CustomCurrency {
- fn decode(&self, encoding: &'static Encoding) -> EnumMap<CC, Option<Box<NumberStyle>>> {
- let mut ccs = EnumMap::default();
- for (cc, string) in enum_iterator::all().zip(&self.ccs) {
- if let Ok(style) = NumberStyle::from_str(&string.decode(encoding)) {
- ccs[cc] = Some(Box::new(style));
- } else {
- // XXX warning
- }
- }
- ccs
- }
-}
-
-#[binread]
-#[br(little, import(version: Version))]
-#[derive(Debug)]
-struct ValueNumber {
- #[br(parse_with(parse_explicit_optional), args(version))]
- mods: Option<ValueMods>,
- #[br(parse_with(parse_format))]
- format: Format,
- x: f64,
-}
-
-#[binread]
-#[br(little, import(version: Version))]
-#[derive(Debug)]
-struct ValueVarNumber {
- #[br(parse_with(parse_explicit_optional), args(version))]
- mods: Option<ValueMods>,
- #[br(parse_with(parse_format))]
- format: Format,
- x: f64,
- var_name: U32String,
- value_label: U32String,
- #[br(parse_with(parse_show))]
- show: Option<Show>,
-}
-
-#[binread]
-#[br(little, import(version: Version))]
-#[derive(Debug)]
-struct ValueText {
- local: U32String,
- #[br(parse_with(parse_explicit_optional), args(version))]
- mods: Option<ValueMods>,
- id: U32String,
- c: U32String,
- #[br(parse_with(parse_bool))]
- fixed: bool,
-}
-
-#[binread]
-#[br(little, import(version: Version))]
-#[derive(Debug)]
-struct ValueString {
- #[br(parse_with(parse_explicit_optional), args(version))]
- mods: Option<ValueMods>,
- #[br(parse_with(parse_format))]
- format: Format,
- value_label: U32String,
- var_name: U32String,
- #[br(parse_with(parse_show))]
- show: Option<Show>,
- s: U32String,
-}
-
-#[binread]
-#[br(little, import(version: Version))]
-#[derive(Debug)]
-struct ValueVarName {
- #[br(parse_with(parse_explicit_optional), args(version))]
- mods: Option<ValueMods>,
- var_name: U32String,
- var_label: U32String,
- #[br(parse_with(parse_show))]
- show: Option<Show>,
-}
-
-#[binread]
-#[br(little, import(version: Version))]
-#[derive(Debug)]
-struct ValueFixedText {
- local: U32String,
- #[br(parse_with(parse_explicit_optional), args(version))]
- mods: Option<ValueMods>,
- id: U32String,
- c: U32String,
-}
-
-#[binread]
-#[br(little, import(version: Version))]
-#[derive(Debug)]
-struct ValueTemplate {
- #[br(parse_with(parse_explicit_optional), args(version))]
- mods: Option<ValueMods>,
- template: U32String,
- #[br(parse_with(parse_vec), args(version))]
- args: Vec<Argument>,
-}
-
-#[derive(Debug)]
-enum Value {
- Number(ValueNumber),
- VarNumber(ValueVarNumber),
- Text(ValueText),
- String(ValueString),
- VarName(ValueVarName),
- FixedText(ValueFixedText),
- Template(ValueTemplate),
-}
-
-impl BinRead for Value {
- type Args<'a> = (Version,);
-
- fn read_options<R: Read + Seek>(
- reader: &mut R,
- endian: Endian,
- args: Self::Args<'_>,
- ) -> BinResult<Self> {
- let start = reader.stream_position()?;
- let kind = loop {
- let x = <u8>::read_options(reader, endian, ())?;
- if x != 0 {
- break x;
- }
- };
- match kind {
- 1 => ValueNumber::read_options(reader, endian, args).map(Self::Number),
- 2 => Ok(Self::VarNumber(ValueVarNumber::read_options(
- reader, endian, args,
- )?)),
- 3 => Ok(Self::Text(ValueText::read_options(reader, endian, args)?)),
- 4 => Ok(Self::String(ValueString::read_options(
- reader, endian, args,
- )?)),
- 5 => Ok(Self::VarName(ValueVarName::read_options(
- reader, endian, args,
- )?)),
- 6 => Ok(Self::FixedText(ValueFixedText::read_options(
- reader, endian, args,
- )?)),
- b'1' | b'X' => {
- reader.seek(std::io::SeekFrom::Current(-1))?;
- Ok(Self::Template(ValueTemplate::read_options(
- reader, endian, args,
- )?))
- }
- _ => Err(BinError::NoVariantMatch { pos: start }),
- }
- .map_err(|e| e.with_message(format!("while parsing Value starting at offset {start:#x}")))
- }
-}
-
-pub(super) fn decode_format(raw: u32) -> Format {
- if raw == 0 || raw == 0x10000 || raw == 1 {
- return Format::new(Type::F, 40, 2).unwrap();
- }
-
- let raw_type = (raw >> 16) as u16;
- let type_ = if raw_type >= 40 {
- Type::F
- } else if let Ok(type_) = Type::try_from(raw_type) {
- type_
- } else {
- // XXX warn
- Type::F
- };
- let w = ((raw >> 8) & 0xff) as Width;
- let d = raw as Decimals;
-
- UncheckedFormat::new(type_, w, d).fix()
-}
-
-#[binrw::parser(reader, endian)]
-fn parse_format() -> BinResult<Format> {
- Ok(decode_format(u32::read_options(reader, endian, ())?))
-}
-
-impl ValueNumber {
- fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> pivot::Value {
- pivot::Value::new_number_with_format((self.x != -f64::MAX).then_some(self.x), self.format)
- .with_styling(ValueMods::decode_optional(&self.mods, encoding, footnotes))
- }
-}
-
-impl ValueVarNumber {
- fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> pivot::Value {
- pivot::Value::new_number_with_format((self.x != -f64::MAX).then_some(self.x), self.format)
- .with_styling(ValueMods::decode_optional(&self.mods, encoding, footnotes))
- .with_value_label(self.value_label.decode_optional(encoding))
- .with_variable_name(Some(self.var_name.decode(encoding)))
- .with_show_value_label(self.show)
- }
-}
-
-impl ValueText {
- fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> pivot::Value {
- pivot::Value::new_general_text(
- self.local.decode(encoding),
- self.c.decode(encoding),
- self.id.decode(encoding),
- !self.fixed,
- )
- .with_styling(ValueMods::decode_optional(&self.mods, encoding, footnotes))
- }
-}
-
-impl ValueString {
- fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> pivot::Value {
- pivot::Value::new(pivot::ValueInner::String(StringValue {
- s: self.s.decode(encoding),
- hex: self.format.type_() == Type::AHex,
- show: self.show,
- var_name: self.var_name.decode_optional(encoding),
- value_label: self.value_label.decode_optional(encoding),
- }))
- .with_styling(ValueMods::decode_optional(&self.mods, encoding, footnotes))
- }
-}
-
-impl ValueVarName {
- fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> pivot::Value {
- pivot::Value::new(pivot::ValueInner::Variable(VariableValue {
- show: self.show,
- var_name: self.var_name.decode(encoding),
- variable_label: self.var_label.decode_optional(encoding),
- }))
- .with_styling(ValueMods::decode_optional(&self.mods, encoding, footnotes))
- }
-}
-impl ValueFixedText {
- fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> pivot::Value {
- pivot::Value::new_general_text(
- self.local.decode(encoding),
- self.c.decode(encoding),
- self.id.decode(encoding),
- false,
- )
- .with_styling(ValueMods::decode_optional(&self.mods, encoding, footnotes))
- }
-}
-
-impl ValueTemplate {
- fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> pivot::Value {
- pivot::Value::new(pivot::ValueInner::Template(TemplateValue {
- args: self
- .args
- .iter()
- .map(|argument| argument.decode(encoding, footnotes))
- .collect(),
- localized: self.template.decode(encoding),
- id: self
- .mods
- .as_ref()
- .and_then(|mods| mods.template_id(encoding)),
- }))
- .with_styling(ValueMods::decode_optional(&self.mods, encoding, footnotes))
- }
-}
-
-impl Value {
- fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> pivot::Value {
- match self {
- Value::Number(number) => number.decode(encoding, footnotes),
- Value::VarNumber(var_number) => var_number.decode(encoding, footnotes),
- Value::Text(text) => text.decode(encoding, footnotes),
- Value::String(string) => string.decode(encoding, footnotes),
- Value::VarName(var_name) => var_name.decode(encoding, footnotes),
- Value::FixedText(fixed_text) => fixed_text.decode(encoding, footnotes),
- Value::Template(template) => template.decode(encoding, footnotes),
- }
- }
-}
-
-#[derive(Debug)]
-struct Argument(Vec<Value>);
-
-impl BinRead for Argument {
- type Args<'a> = (Version,);
-
- fn read_options<R: Read + Seek>(
- reader: &mut R,
- endian: Endian,
- (version,): (Version,),
- ) -> BinResult<Self> {
- let count = u32::read_options(reader, endian, ())? as usize;
- if count == 0 {
- Ok(Self(vec![Value::read_options(reader, endian, (version,))?]))
- } else {
- let zero = u32::read_options(reader, endian, ())?;
- assert_eq!(zero, 0);
- let values = <Vec<_>>::read_options(
- reader,
- endian,
- VecArgs {
- count,
- inner: (version,),
- },
- )?;
- Ok(Self(values))
- }
- }
-}
-
-impl Argument {
- fn decode(
- &self,
- encoding: &'static Encoding,
- footnotes: &pivot::Footnotes,
- ) -> Vec<pivot::Value> {
- self.0
- .iter()
- .map(|value| value.decode(encoding, footnotes))
- .collect()
- }
-}
-
-#[binread]
-#[br(little, import(version: Version))]
-#[derive(Debug)]
-struct ValueMods {
- #[br(parse_with(parse_vec))]
- refs: Vec<i16>,
- #[br(parse_with(parse_vec))]
- subscripts: Vec<U32String>,
- #[br(if(version == Version::V1))]
- v1: Option<ValueModsV1>,
- #[br(if(version == Version::V3), parse_with(parse_counted))]
- v3: ValueModsV3,
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug, Default)]
-struct ValueModsV1 {
- #[br(temp, magic(0u8), assert(_1 == 1 || _1 == 2))]
- _1: i32,
- #[br(temp)]
- _0: Optional<Zero>,
- #[br(temp)]
- _1: Optional<Zero>,
- #[br(temp)]
- _2: i32,
- #[br(temp)]
- _3: Optional<Zero>,
- #[br(temp)]
- _4: Optional<Zero>,
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug, Default)]
-struct ValueModsV3 {
- #[br(parse_with(parse_counted))]
- template_string: Optional<TemplateString>,
- style_pair: StylePair,
-}
-
-impl ValueMods {
- fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> ValueStyle {
- let font_style =
- self.v3
- .style_pair
- .font_style
- .as_ref()
- .map(|font_style| pivot::FontStyle {
- bold: font_style.bold,
- italic: font_style.italic,
- underline: font_style.underline,
- font: font_style.typeface.decode(encoding),
- fg: font_style.fg,
- bg: font_style.bg,
- size: (font_style.size as i32) * 4 / 3,
- });
- let cell_style = self.v3.style_pair.cell_style.as_ref().map(|cell_style| {
- pivot::CellStyle {
- horz_align: match cell_style.halign {
- 0 => Some(HorzAlign::Center),
- 2 => Some(HorzAlign::Left),
- 4 => Some(HorzAlign::Right),
- 6 => Some(HorzAlign::Decimal {
- offset: cell_style.decimal_offset,
- decimal: Decimal::Dot, /*XXX*/
- }),
- _ => None,
- },
- vert_align: match cell_style.valign {
- 0 => VertAlign::Middle,
- 3 => VertAlign::Bottom,
- _ => VertAlign::Top,
- },
- margins: enum_map! {
- Axis2::X => [cell_style.left_margin as i32, cell_style.right_margin as i32],
- Axis2::Y => [cell_style.top_margin as i32, cell_style.bottom_margin as i32],
- },
- }
- });
- ValueStyle {
- cell_style,
- font_style,
- subscripts: self.subscripts.iter().map(|s| s.decode(encoding)).collect(),
- footnotes: self
- .refs
- .iter()
- .flat_map(|index| footnotes.get(*index as usize))
- .cloned()
- .collect(),
- }
- }
- fn decode_optional(
- mods: &Option<Self>,
- encoding: &'static Encoding,
- footnotes: &pivot::Footnotes,
- ) -> Option<Box<pivot::ValueStyle>> {
- mods.as_ref()
- .map(|mods| Box::new(mods.decode(encoding, footnotes)))
- }
- fn template_id(&self, encoding: &'static Encoding) -> Option<String> {
- self.v3
- .template_string
- .as_ref()
- .and_then(|template_string| template_string.id.as_ref())
- .map(|s| s.decode(encoding))
- }
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct TemplateString {
- #[br(parse_with(parse_counted), temp)]
- _sponge: Sponge,
- #[br(parse_with(parse_explicit_optional))]
- id: Option<U32String>,
-}
-
-#[derive(Debug, Default)]
-struct Sponge;
-
-impl BinRead for Sponge {
- type Args<'a> = ();
-
- fn read_options<R: Read + Seek>(reader: &mut R, _endian: Endian, _args: ()) -> BinResult<Self> {
- let mut buf = [0; 32];
- while reader.read(&mut buf)? > 0 {}
- Ok(Self)
- }
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug, Default)]
-struct StylePair {
- #[br(parse_with(parse_explicit_optional))]
- font_style: Option<FontStyle>,
- #[br(parse_with(parse_explicit_optional))]
- cell_style: Option<CellStyle>,
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct FontStyle {
- #[br(parse_with(parse_bool))]
- bold: bool,
- #[br(parse_with(parse_bool))]
- italic: bool,
- #[br(parse_with(parse_bool))]
- underline: bool,
- #[br(parse_with(parse_bool))]
- show: bool,
- #[br(parse_with(parse_color))]
- fg: Color,
- #[br(parse_with(parse_color))]
- bg: Color,
- typeface: U32String,
- size: u8,
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct CellStyle {
- halign: i32,
- valign: i32,
- decimal_offset: f64,
- left_margin: i16,
- right_margin: i16,
- top_margin: i16,
- bottom_margin: i16,
-}
-
-#[binread]
-#[br(little)]
-#[br(import(version: Version))]
-#[derive(Debug)]
-struct Dimension {
- #[br(args(version))]
- name: Value,
- #[br(temp)]
- _x1: u8,
- #[br(temp)]
- _x2: u8,
- #[br(temp)]
- _x3: u32,
- #[br(parse_with(parse_bool))]
- hide_dim_label: bool,
- #[br(parse_with(parse_bool))]
- hide_all_labels: bool,
- #[br(magic(1u8), temp)]
- _dim_index: i32,
- #[br(parse_with(parse_vec), args(version))]
- categories: Vec<Category>,
-}
-
-#[binread]
-#[br(little, import(version: Version))]
-#[derive(Debug)]
-struct Category {
- #[br(args(version))]
- name: Value,
- #[br(args(version))]
- child: Child,
-}
-
-impl Category {
- fn decode(&self, encoding: &'static Encoding, footnotes: &Footnotes, group: &mut pivot::Group) {
- let name = self.name.decode(encoding, footnotes);
- match &self.child {
- Child::Leaf { leaf_index: _ } => {
- group.push(pivot::Leaf::new(name));
- }
- Child::Group {
- merge: true,
- subcategories,
- } => {
- for subcategory in subcategories {
- subcategory.decode(encoding, footnotes, group);
- }
- }
- Child::Group {
- merge: false,
- subcategories,
- } => {
- let mut subgroup = Group::new(name).with_label_shown();
- for subcategory in subcategories {
- subcategory.decode(encoding, footnotes, &mut subgroup);
- }
- group.push(subgroup);
- }
- }
- }
-}
-
-#[binread]
-#[br(little, import(version: Version))]
-#[derive(Debug)]
-enum Child {
- Leaf {
- #[br(magic(0u16), parse_with(parse_bool), temp)]
- _x24: bool,
- #[br(magic(b"\x02\0\0\0"))]
- leaf_index: u32,
- #[br(magic(0u32), temp)]
- _tail: (),
- },
- Group {
- #[br(parse_with(parse_bool))]
- merge: bool,
- #[br(temp, magic(b"\0\x01"))]
- _x23: i32,
- #[br(magic(-1i32), parse_with(parse_vec), args(version))]
- subcategories: Vec<Box<Category>>,
- },
-}
-
-#[binread]
-#[br(little)]
-#[derive(Debug)]
-struct Axes {
- #[br(temp)]
- n_layers: u32,
- #[br(temp)]
- n_rows: u32,
- #[br(temp)]
- n_columns: u32,
- #[br(count(n_layers))]
- layers: Vec<u32>,
- #[br(count(n_rows))]
- rows: Vec<u32>,
- #[br(count(n_columns))]
- columns: Vec<u32>,
-}
-
-impl Axes {
- fn decode(
- &self,
- dimensions: Vec<pivot::Dimension>,
- ) -> Result<Vec<(Axis3, pivot::Dimension)>, LightError> {
- let n = self.layers.len() + self.rows.len() + self.columns.len();
- if n != dimensions.len() {
- /* XXX warn
- return Err(LightError::WrongAxisCount {
- expected: dimensions.len(),
- actual: n,
- n_layers: self.layers.len(),
- n_rows: self.rows.len(),
- n_columns: self.columns.len(),
- });*/
- return Ok(dimensions
- .into_iter()
- .map(|dimension| (Axis3::Y, dimension))
- .collect());
- }
-
- fn axis_dims(axis: Axis3, dimensions: &[u32]) -> impl Iterator<Item = (Axis3, usize)> {
- dimensions.iter().map(move |d| (axis, *d as usize))
- }
-
- let mut axes = vec![None; n];
- for (axis, index) in axis_dims(Axis3::Z, &self.layers)
- .chain(axis_dims(Axis3::Y, &self.rows))
- .chain(axis_dims(Axis3::X, &self.columns))
- {
- if index >= n {
- return Err(LightError::InvalidDimensionIndex { index, n });
- } else if axes[index].is_some() {
- return Err(LightError::DuplicateDimensionIndex(index));
- }
- axes[index] = Some(axis);
- }
- Ok(axes
- .into_iter()
- .map(|axis| axis.unwrap())
- .zip(dimensions)
- .collect())
- }
-}
-
-#[binread]
-#[br(little, import(version: Version))]
-#[derive(Debug)]
-struct Cell {
- index: u64,
- #[br(if(version == Version::V1), temp)]
- _zero: Optional<Zero>,
- #[br(args(version))]
- value: Value,
-}
use enum_map::{EnumMap, enum_map};
use ndarray::{Array, Array2};
-use crate::output::{
- pivot::{CellStyle, DisplayValue, FontStyle, Footnote, HorzAlign, ValueInner},
- spv::html,
-};
+use crate::{output::pivot::{CellStyle, DisplayValue, FontStyle, Footnote, HorzAlign, ValueInner}, spv::read::html};
use super::pivot::{
Area, AreaStyle, Axis2, Border, BorderStyle, HeadingRegion, Value, ValueOptions,
pub use write::Writer;
+pub mod read;
mod write;
--- /dev/null
+// 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/>.
+
+use std::{
+ fs::File,
+ io::{BufReader, Cursor, Read, Seek},
+ path::Path,
+};
+
+use anyhow::{Context, anyhow};
+use binrw::{BinRead, error::ContextExt};
+use cairo::ImageSurface;
+use displaydoc::Display;
+use paper_sizes::PaperSize;
+use serde::Deserialize;
+use zip::{ZipArchive, result::ZipError};
+
+use crate::{
+ crypto::EncryptedFile,
+ output::{
+ Details, Item, SpvInfo, SpvMembers, Text,
+ page::{self},
+ pivot::{Axis2, Length, Look, TableProperties, Value},
+ },
+ spv::read::{
+ html::Document,
+ legacy_bin::LegacyBin,
+ legacy_xml::Visualization,
+ light::{LightError, LightTable},
+ },
+};
+
+mod css;
+pub mod html;
+mod legacy_bin;
+mod legacy_xml;
+mod light;
+
+/// Options for reading an SPV file.
+#[derive(Clone, Debug, Default)]
+pub struct ReadOptions {
+ /// Password to use to unlock an encrypted SPV file.
+ ///
+ /// For an encrypted SPV file, this must be set to the (encoded or
+ /// unencoded) password.
+ ///
+ /// For a plaintext SPV file, this must be None.
+ pub password: Option<String>,
+}
+
+impl ReadOptions {
+ /// Construct a new [ReadOptions] without a password.
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Causes the file to be read by decrypting it with the given `password` or
+ /// without decrypting if `password` is None.
+ pub fn with_password(self, password: Option<String>) -> Self {
+ Self { password }
+ }
+
+ /// Opens the file at `path`.
+ pub fn open_file<P>(mut self, path: P) -> Result<SpvFile, anyhow::Error>
+ where
+ P: AsRef<Path>,
+ {
+ let file = File::open(path)?;
+ if let Some(password) = self.password.take() {
+ self.open_reader_encrypted(file, password)
+ } else {
+ Self::open_reader_inner(file)
+ }
+ }
+
+ /// Opens the file read from `reader`.
+ fn open_reader_encrypted<R>(self, reader: R, password: String) -> Result<SpvFile, anyhow::Error>
+ where
+ R: Read + Seek + 'static,
+ {
+ Self::open_reader_inner(
+ EncryptedFile::new(reader)?
+ .unlock(password.as_bytes())
+ .map_err(|_| anyhow!("Incorrect password."))?,
+ )
+ }
+
+ /// Opens the file read from `reader`.
+ pub fn open_reader<R>(mut self, reader: R) -> Result<SpvFile, anyhow::Error>
+ where
+ R: Read + Seek + 'static,
+ {
+ if let Some(password) = self.password.take() {
+ self.open_reader_encrypted(reader, password)
+ } else {
+ Self::open_reader_inner(reader)
+ }
+ }
+
+ fn open_reader_inner<R>(reader: R) -> Result<SpvFile, anyhow::Error>
+ where
+ R: Read + Seek + 'static,
+ {
+ // Open archive.
+ let mut archive = ZipArchive::new(reader).map_err(|error| match error {
+ ZipError::InvalidArchive(_) => Error::NotSpv,
+ other => other.into(),
+ })?;
+ Ok(Self::from_spv_zip_archive(&mut archive)?)
+ }
+
+ fn from_spv_zip_archive<R>(archive: &mut ZipArchive<R>) -> Result<SpvFile, Error>
+ where
+ R: Read + Seek,
+ {
+ // Check manifest.
+ let mut file = archive
+ .by_name("META-INF/MANIFEST.MF")
+ .map_err(|_| Error::NotSpv)?;
+ let mut string = String::new();
+ file.read_to_string(&mut string)?;
+ if string.trim() != "allowPivoting=true" {
+ return Err(Error::NotSpv);
+ }
+ drop(file);
+
+ let mut items = Vec::new();
+ let mut page_setup = None;
+ for i in 0..archive.len() {
+ let name = String::from(archive.name_for_index(i).unwrap());
+ if name.starts_with("outputViewer") && name.ends_with(".xml") {
+ let (mut new_items, ps) = read_heading(archive, i, &name)?;
+ items.append(&mut new_items);
+ page_setup = page_setup.or(ps);
+ }
+ }
+
+ Ok(SpvFile {
+ items: items.into_iter().collect(),
+ page_setup,
+ })
+ }
+}
+
+/// A SPSS viewer (SPV) file read with [ReadOptions].
+pub struct SpvFile {
+ /// SPV file contents.
+ pub items: Vec<Item>,
+
+ /// The page setup in the SPV file, if any.
+ pub page_setup: Option<page::PageSetup>,
+}
+
+impl SpvFile {
+ // Returns the individual parts of the `SpvFile`.
+ pub fn into_parts(self) -> (Vec<Item>, Option<page::PageSetup>) {
+ (self.items, self.page_setup)
+ }
+
+ /// Returns just the [Item]s.
+ pub fn into_items(self) -> Vec<Item> {
+ self.items
+ }
+}
+
+/// An error reading an SPV file.
+///
+/// Returned by [ReadOptions::open_file] and [ReadOptions::open_reader].
+#[derive(Debug, Display, thiserror::Error)]
+pub enum Error {
+ /// Not an SPV file.
+ NotSpv,
+
+ /// {0}
+ ZipError(#[from] ZipError),
+
+ /// {0}
+ IoError(#[from] std::io::Error),
+
+ /// {0}
+ DeError(#[from] quick_xml::DeError),
+
+ /// {0}
+ BinrwError(#[from] binrw::Error),
+
+ /// {0}
+ LightError(#[from] LightError),
+
+ /// {0}
+ CairoError(#[from] cairo::IoError),
+}
+
+fn new_error_item(message: impl Into<Value>) -> Item {
+ Text::new_log(message).into_item().with_label("Error")
+}
+
+fn read_heading<R>(
+ archive: &mut ZipArchive<R>,
+ file_number: usize,
+ structure_member: &str,
+) -> Result<(Vec<Item>, Option<page::PageSetup>), Error>
+where
+ R: Read + Seek,
+{
+ let member = BufReader::new(archive.by_index(file_number)?);
+ let mut heading: Heading = match serde_path_to_error::deserialize(
+ &mut quick_xml::de::Deserializer::from_reader(member),
+ )
+ .with_context(|| format!("Failed to parse {structure_member}"))
+ {
+ Ok(result) => result,
+ Err(error) => panic!("{error:?}"),
+ };
+ let page_setup = heading.page_setup.take().map(|ps| ps.decode());
+ Ok((heading.decode(archive, structure_member)?, page_setup))
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Heading {
+ #[serde(rename = "@visibility")]
+ visibility: Option<String>,
+ #[serde(rename = "@commandName")]
+ command_name: Option<String>,
+ label: Label,
+ page_setup: Option<PageSetup>,
+
+ #[serde(rename = "$value")]
+ #[serde(default)]
+ children: Vec<HeadingContent>,
+}
+
+impl Heading {
+ fn decode<R>(
+ self,
+ archive: &mut ZipArchive<R>,
+ structure_member: &str,
+ ) -> Result<Vec<Item>, Error>
+ where
+ R: Read + Seek,
+ {
+ let mut items = Vec::new();
+ for child in self.children {
+ match child {
+ HeadingContent::Container(container) => {
+ if container.page_break_before == PageBreakBefore::Always {
+ items.push(
+ Details::PageBreak
+ .into_item()
+ .with_spv_info(SpvInfo::new(structure_member)),
+ );
+ }
+ let item = match container.content {
+ ContainerContent::Table(table) => {
+ table.decode(archive, structure_member).unwrap() /* XXX*/
+ }
+ ContainerContent::Graph(graph) => graph.decode(structure_member),
+ ContainerContent::Text(container_text) => Text::new(
+ match container_text.text_type {
+ TextType::Title => crate::output::TextType::Title,
+ TextType::Log | TextType::Text => crate::output::TextType::Log,
+ TextType::PageTitle => crate::output::TextType::PageTitle,
+ },
+ container_text.decode(),
+ )
+ .into_item()
+ .with_command_name(container_text.command_name)
+ .with_spv_info(SpvInfo::new(structure_member)),
+ ContainerContent::Image(image) => {
+ image.decode(archive, structure_member).unwrap()
+ } /*XXX*/,
+ ContainerContent::Object(object) => {
+ object.decode(archive, structure_member).unwrap()
+ } /*XXX*/,
+ ContainerContent::Model => new_error_item("models not yet implemented")
+ .with_spv_info(SpvInfo::new(structure_member).with_error()),
+ ContainerContent::Tree => new_error_item("trees not yet implemented")
+ .with_spv_info(SpvInfo::new(structure_member).with_error()),
+ };
+ items.push(item.with_show(container.visibility == Visibility::Visible));
+ }
+ HeadingContent::Heading(mut heading) => {
+ let show = !heading.visibility.is_some();
+ let label = std::mem::take(&mut heading.label.text);
+ let command_name = heading.command_name.take();
+ items.push(
+ heading
+ .decode(archive, structure_member)?
+ .into_iter()
+ .collect::<Item>()
+ .with_show(show)
+ .with_label(label)
+ .with_command_name(command_name)
+ .with_spv_info(SpvInfo::new(structure_member)),
+ );
+ }
+ }
+ }
+ Ok(items)
+ }
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct PageSetup {
+ #[serde(rename = "@initial-page-number")]
+ pub initial_page_number: Option<i32>,
+ #[serde(rename = "@chart-size")]
+ pub chart_size: Option<ChartSize>,
+ #[serde(rename = "@margin-left")]
+ pub margin_left: Option<Length>,
+ #[serde(rename = "@margin-right")]
+ pub margin_right: Option<Length>,
+ #[serde(rename = "@margin-top")]
+ pub margin_top: Option<Length>,
+ #[serde(rename = "@margin-bottom")]
+ pub margin_bottom: Option<Length>,
+ #[serde(rename = "@paper-height")]
+ pub paper_height: Option<Length>,
+ #[serde(rename = "@paper-width")]
+ pub paper_width: Option<Length>,
+ #[serde(rename = "@reference-orientation")]
+ pub reference_orientation: Option<ReferenceOrientation>,
+ #[serde(rename = "@space-after")]
+ pub space_after: Option<Length>,
+ pub page_header: PageHeader,
+ pub page_footer: PageFooter,
+}
+
+impl PageSetup {
+ fn decode(&self) -> page::PageSetup {
+ let mut setup = page::PageSetup::default();
+ if let Some(initial_page_number) = self.initial_page_number {
+ setup.initial_page_number = initial_page_number;
+ }
+ if let Some(chart_size) = self.chart_size {
+ setup.chart_size = chart_size.into();
+ }
+ if let Some(margin_left) = self.margin_left {
+ setup.margins.0[Axis2::X][0] = margin_left.into();
+ }
+ if let Some(margin_right) = self.margin_right {
+ setup.margins.0[Axis2::X][1] = margin_right.into();
+ }
+ if let Some(margin_top) = self.margin_top {
+ setup.margins.0[Axis2::Y][0] = margin_top.into();
+ }
+ if let Some(margin_bottom) = self.margin_bottom {
+ setup.margins.0[Axis2::Y][1] = margin_bottom.into();
+ }
+ match (self.paper_width, self.paper_height) {
+ (Some(width), Some(height)) => {
+ setup.paper = PaperSize::new(width.0, height.0, paper_sizes::Unit::Inch)
+ }
+ (Some(length), None) | (None, Some(length)) => {
+ setup.paper = PaperSize::new(length.0, length.0, paper_sizes::Unit::Inch)
+ }
+ (None, None) => (),
+ }
+ if let Some(reference_orientation) = self.reference_orientation {
+ setup.orientation = reference_orientation.into();
+ }
+ if let Some(space_after) = self.space_after {
+ setup.object_spacing = space_after.into();
+ }
+ if let Some(PageParagraph { text }) = &self.page_header.page_paragraph {
+ setup.header = text.decode();
+ }
+ if let Some(PageParagraph { text }) = &self.page_footer.page_paragraph {
+ setup.footer = text.decode();
+ }
+ setup
+ }
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct PageHeader {
+ page_paragraph: Option<PageParagraph>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct PageFooter {
+ page_paragraph: Option<PageParagraph>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct PageParagraph {
+ text: PageParagraphText,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct PageParagraphText {
+ #[serde(default, rename = "$text")]
+ text: String,
+}
+
+impl PageParagraphText {
+ fn decode(&self) -> Document {
+ Document::from_html(&self.text)
+ }
+}
+
+#[derive(Copy, Clone, Debug, Default, Deserialize)]
+#[serde(rename = "snake_case")]
+enum ReferenceOrientation {
+ #[serde(alias = "0")]
+ #[serde(alias = "0deg")]
+ #[serde(alias = "inherit")]
+ #[default]
+ Portrait,
+
+ #[serde(alias = "90")]
+ #[serde(alias = "90deg")]
+ #[serde(alias = "-270")]
+ #[serde(alias = "-270deg")]
+ Landscape,
+
+ #[serde(alias = "180")]
+ #[serde(alias = "180deg")]
+ #[serde(alias = "-1280")]
+ #[serde(alias = "-180deg")]
+ ReversePortrait,
+
+ #[serde(alias = "270")]
+ #[serde(alias = "270deg")]
+ #[serde(alias = "-90")]
+ #[serde(alias = "-90deg")]
+ Seascape,
+}
+
+impl From<ReferenceOrientation> for page::Orientation {
+ fn from(value: ReferenceOrientation) -> Self {
+ match value {
+ ReferenceOrientation::Portrait | ReferenceOrientation::ReversePortrait => {
+ page::Orientation::Portrait
+ }
+ ReferenceOrientation::Landscape | ReferenceOrientation::Seascape => {
+ page::Orientation::Landscape
+ }
+ }
+ }
+}
+
+/// Chart size.
+#[derive(Copy, Clone, Debug, Default, Deserialize)]
+enum ChartSize {
+ #[default]
+ #[serde(rename = "as-is")]
+ AsIs,
+
+ #[serde(rename = "full-height")]
+ FullHeight,
+
+ #[serde(rename = "half-height")]
+ HalfHeight,
+
+ #[serde(rename = "quarter-height")]
+ QuarterHeight,
+}
+
+impl From<ChartSize> for page::ChartSize {
+ fn from(value: ChartSize) -> Self {
+ match value {
+ ChartSize::AsIs => page::ChartSize::AsIs,
+ ChartSize::FullHeight => page::ChartSize::FullHeight,
+ ChartSize::HalfHeight => page::ChartSize::HalfHeight,
+ ChartSize::QuarterHeight => page::ChartSize::QuarterHeight,
+ }
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum HeadingContent {
+ Container(Container),
+ Heading(Box<Heading>),
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Label {
+ #[serde(default, rename = "$text")]
+ text: String,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Container {
+ #[serde(default, rename = "@visibility")]
+ visibility: Visibility,
+ #[serde(rename = "@page-break-before")]
+ #[serde(default)]
+ page_break_before: PageBreakBefore,
+ #[serde(rename = "@text-align")]
+ text_align: Option<TextAlign>,
+ #[serde(rename = "@width")]
+ width: Option<String>,
+ label: Label,
+
+ #[serde(rename = "$value")]
+ content: ContainerContent,
+}
+
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum PageBreakBefore {
+ #[default]
+ Auto,
+ Always,
+ Avoid,
+ Left,
+ Right,
+ Inherit,
+}
+
+#[derive(Deserialize, Debug, Default, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+enum Visibility {
+ #[default]
+ Visible,
+ Hidden,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum TextAlign {
+ Left,
+ Center,
+ Right,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum ContainerContent {
+ Table(Table),
+ Text(ContainerText),
+ Graph(Graph),
+ Model,
+ Object(Object),
+ Image(Image),
+ Tree,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Graph {
+ #[serde(rename = "@commandName")]
+ command_name: String,
+ data_path: Option<String>,
+ path: String,
+ csv_path: Option<String>,
+}
+
+impl Graph {
+ fn decode(&self, structure_member: &str) -> Item {
+ crate::output::Chart
+ .into_item()
+ .with_spv_info(
+ SpvInfo::new(structure_member).with_members(SpvMembers::Graph {
+ data: self.data_path.clone(),
+ xml: self.path.clone(),
+ csv: self.csv_path.clone(),
+ }),
+ )
+ }
+}
+
+fn decode_image<R>(
+ archive: &mut ZipArchive<R>,
+ structure_member: &str,
+ command_name: &Option<String>,
+ image_name: &str,
+) -> Result<Item, Error>
+where
+ R: Read + Seek,
+{
+ let mut png = archive.by_name(image_name)?;
+ let image = ImageSurface::create_from_png(&mut png)?;
+ Ok(Details::Image(image)
+ .into_item()
+ .with_command_name(command_name.clone())
+ .with_spv_info(
+ SpvInfo::new(structure_member).with_members(SpvMembers::Image(image_name.into())),
+ ))
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Image {
+ #[serde(rename = "@commandName")]
+ command_name: Option<String>,
+ data_path: String,
+}
+
+impl Image {
+ fn decode<R>(&self, archive: &mut ZipArchive<R>, structure_member: &str) -> Result<Item, Error>
+ where
+ R: Read + Seek,
+ {
+ decode_image(
+ archive,
+ structure_member,
+ &self.command_name,
+ &self.data_path,
+ )
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Object {
+ #[serde(rename = "@commandName")]
+ command_name: Option<String>,
+ #[serde(rename = "@uri")]
+ uri: String,
+}
+
+impl Object {
+ fn decode<R>(&self, archive: &mut ZipArchive<R>, structure_member: &str) -> Result<Item, Error>
+ where
+ R: Read + Seek,
+ {
+ decode_image(archive, structure_member, &self.command_name, &self.uri)
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Table {
+ #[serde(rename = "@commandName")]
+ command_name: String,
+ #[serde(rename = "@subType")]
+ sub_type: String,
+ #[serde(rename = "@tableId")]
+ table_id: Option<i64>,
+ #[serde(rename = "@type")]
+ table_type: TableType,
+ properties: Option<TableProperties>,
+ table_structure: TableStructure,
+}
+
+impl Table {
+ fn decode<R>(&self, archive: &mut ZipArchive<R>, structure_member: &str) -> Result<Item, Error>
+ where
+ R: Read + Seek,
+ {
+ match &self.table_structure.path {
+ None => {
+ let member_name = &self.table_structure.data_path;
+ let mut light = archive.by_name(member_name)?;
+ let mut data = Vec::with_capacity(light.size() as usize);
+ light.read_to_end(&mut data)?;
+ let mut cursor = Cursor::new(data);
+ let table = LightTable::read(&mut cursor).map_err(|e| {
+ e.with_message(format!(
+ "While parsing {member_name:?} as light binary SPV member"
+ ))
+ })?;
+ let pivot_table = table.decode()?;
+ Ok(pivot_table.into_item().with_spv_info(
+ SpvInfo::new(structure_member)
+ .with_members(SpvMembers::Light(self.table_structure.data_path.clone())),
+ ))
+ }
+ Some(xml_member_name) => {
+ let bin_member_name = &self.table_structure.data_path;
+ let mut bin_member = archive.by_name(bin_member_name)?;
+ let mut bin_data = Vec::with_capacity(bin_member.size() as usize);
+ bin_member.read_to_end(&mut bin_data)?;
+ let mut cursor = Cursor::new(bin_data);
+ let legacy_bin = LegacyBin::read(&mut cursor).map_err(|e| {
+ e.with_message(format!(
+ "While parsing {bin_member_name:?} as legacy binary SPV member"
+ ))
+ })?;
+ let data = legacy_bin.decode();
+ drop(bin_member);
+
+ let member = BufReader::new(archive.by_name(&xml_member_name)?);
+ let visualization: Visualization = match serde_path_to_error::deserialize(
+ &mut quick_xml::de::Deserializer::from_reader(member),
+ )
+ .with_context(|| format!("Failed to parse {xml_member_name}"))
+ {
+ Ok(result) => result,
+ Err(error) => panic!("{error:?}"),
+ };
+ let pivot_table = visualization.decode(
+ data,
+ self.properties
+ .as_ref()
+ .map_or_else(Look::default, |properties| properties.clone().into()),
+ )?;
+
+ Ok(pivot_table.into_item().with_spv_info(
+ SpvInfo::new(structure_member).with_members(SpvMembers::Legacy {
+ xml: xml_member_name.clone(),
+ binary: bin_member_name.clone(),
+ }),
+ ))
+ }
+ }
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum TableType {
+ Table,
+ Note,
+ Warning,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct ContainerText {
+ #[serde(rename = "@type")]
+ text_type: TextType,
+ #[serde(rename = "@commandName")]
+ command_name: Option<String>,
+ html: String,
+}
+
+impl ContainerText {
+ fn decode(&self) -> Value {
+ html::Document::from_html(&self.html).into_value()
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum TextType {
+ Title,
+ Log,
+ Text,
+ #[serde(rename = "page-title")]
+ PageTitle,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct TableStructure {
+ /// The `.xml` member name, for legacy members only.
+ path: Option<String>,
+ /// The `.bin` member name.
+ data_path: String,
+ /// Rarely used, not understood.
+ #[serde(rename = "csvPath")]
+ _csv_path: Option<String>,
+}
+
+#[cfg(test)]
+#[test]
+fn test_spv() {
+ let items = ReadOptions::new()
+ .open_file("/home/blp/pspp/rust/tests/utilities/regress.spv")
+ .unwrap()
+ .into_items();
+ for item in items {
+ println!("{item}");
+ }
+ todo!()
+}
--- /dev/null
+use std::{
+ borrow::Cow,
+ fmt::{Display, Write},
+ mem::discriminant,
+ ops::Not,
+};
+
+use itertools::Itertools;
+
+use crate::{
+ output::pivot::{FontStyle, HorzAlign},
+ spv::read::html::Style,
+};
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+enum Token<'a> {
+ Id(Cow<'a, str>),
+ LeftCurly,
+ RightCurly,
+ Colon,
+ Semicolon,
+}
+
+struct Lexer<'a>(&'a str);
+
+impl<'a> Iterator for Lexer<'a> {
+ type Item = Token<'a>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let mut s = self.0;
+ loop {
+ s = s.trim_start();
+ if let Some(rest) = s.strip_prefix("<!--") {
+ s = rest;
+ } else if let Some(rest) = s.strip_prefix("-->") {
+ s = rest;
+ } else {
+ break;
+ }
+ }
+ let mut iter = s.chars();
+ let (c, mut rest) = (iter.next()?, iter.as_str());
+ let (token, rest) = match c {
+ '{' => (Token::LeftCurly, rest),
+ '}' => (Token::RightCurly, rest),
+ ':' => (Token::Colon, rest),
+ ';' => (Token::Semicolon, rest),
+ '\'' | '"' => {
+ let quote = c;
+ let mut s = String::new();
+ while let Some(c) = iter.next() {
+ if c == quote {
+ break;
+ } else if c != '\\' {
+ s.push(c);
+ } else {
+ let start = iter.as_str();
+ match iter.next() {
+ None => break,
+ Some(a) if a.is_ascii_alphanumeric() => {
+ let n = start
+ .chars()
+ .take_while(|c| c.is_ascii_alphanumeric())
+ .take(6)
+ .count();
+ iter = start[n..].chars();
+ if let Ok(code_point) = u32::from_str_radix(&start[..n], 16)
+ && let Ok(c) = char::try_from(code_point)
+ {
+ s.push(c);
+ }
+ }
+ Some('\n') => (),
+ Some(other) => s.push(other),
+ }
+ }
+ }
+ (Token::Id(Cow::from(s)), iter.as_str())
+ }
+ _ => {
+ while !iter.as_str().starts_with("-->")
+ && let Some(c) = iter.next()
+ && !c.is_whitespace()
+ && c != '{'
+ && c != '}'
+ && c != ':'
+ && c != ';'
+ {
+ rest = iter.as_str();
+ }
+ let id_len = s.len() - rest.len();
+ let (id, rest) = s.split_at(id_len);
+ (Token::Id(Cow::from(id)), rest)
+ }
+ };
+ self.0 = rest;
+ Some(token)
+ }
+}
+
+impl HorzAlign {
+ pub fn from_css(s: &str) -> Option<Self> {
+ let mut lexer = Lexer(s);
+ while let Some(token) = lexer.next() {
+ if let Token::Id(key) = token
+ && let Some(Token::Colon) = lexer.next()
+ && let Some(Token::Id(value)) = lexer.next()
+ && key.as_ref() == "text-align"
+ && let Ok(align) = value.parse()
+ {
+ return Some(align);
+ }
+ }
+ None
+ }
+}
+
+impl Style {
+ pub fn parse_css(styles: &mut Vec<Style>, s: &str) {
+ let mut lexer = Lexer(s);
+ while let Some(token) = lexer.next() {
+ if let Token::Id(key) = token
+ && let Some(Token::Colon) = lexer.next()
+ && let Some(Token::Id(value)) = lexer.next()
+ && let Some((style, add)) = match key.as_ref() {
+ "color" => value.parse().ok().map(|color| (Style::Color(color), true)),
+ "font-weight" => Some((Style::Bold, value == "bold")),
+ "font-style" => Some((Style::Italic, value == "italic")),
+ "text-decoration" => Some((Style::Underline, value == "underline")),
+ "font-family" => Some((Style::Face(value.into()), true)),
+ "font-size" => value
+ .parse::<i32>()
+ .ok()
+ .map(|size| (Style::Size(size as f64 * 0.75), true)),
+ _ => None,
+ }
+ {
+ // Remove from `styles` any style of the same kind as `style`.
+ styles.retain(|s| discriminant(s) != discriminant(&style));
+ if add {
+ styles.push(style);
+ }
+ }
+ }
+ }
+}
+
+impl FontStyle {
+ pub fn parse_css(&mut self, s: &str) {
+ let mut lexer = Lexer(s);
+ while let Some(token) = lexer.next() {
+ if let Token::Id(key) = token
+ && let Some(Token::Colon) = lexer.next()
+ && let Some(Token::Id(value)) = lexer.next()
+ {
+ match key.as_ref() {
+ "color" => {
+ if let Ok(color) = value.parse() {
+ self.fg = color;
+ }
+ }
+ "font-weight" => self.bold = value == "bold",
+ "font-style" => self.italic = value == "italic",
+ "text-decoration" => self.underline = value == "underline",
+ "font-family" => self.font = value.into(),
+ "font-size" => {
+ if let Ok(size) = value.parse::<i32>() {
+ self.size = (size as i64 * 3 / 4) as i32;
+ }
+ }
+ _ => (),
+ }
+ }
+ }
+ }
+
+ pub fn from_css(s: &str) -> Self {
+ let mut style = FontStyle::default();
+ style.parse_css(s);
+ style
+ }
+
+ pub fn to_css(&self, base: &FontStyle) -> Option<String> {
+ let mut settings = Vec::new();
+ if self.font != base.font {
+ if is_css_ident(&self.font) {
+ settings.push(format!("font-family: {}", &self.font));
+ } else {
+ settings.push(format!("font-family: {}", CssString(&self.font)));
+ }
+ }
+ if self.bold != base.bold {
+ settings.push(format!(
+ "font-weight: {}",
+ if self.bold { "bold" } else { "normal" }
+ ));
+ }
+ if self.italic != base.italic {
+ settings.push(format!(
+ "font-style: {}",
+ if self.bold { "italic" } else { "normal" }
+ ));
+ }
+ if self.underline != base.underline {
+ settings.push(format!(
+ "text-decoration: {}",
+ if self.bold { "underline" } else { "none" }
+ ));
+ }
+ if self.size != base.size {
+ settings.push(format!("font-size: {}", self.size as i64 * 4 / 3));
+ }
+ if self.fg != base.fg {
+ settings.push(format!("color: {}", self.fg.display_css()));
+ }
+ settings
+ .is_empty()
+ .not()
+ .then(|| format!("<!-- p {{ {} }} -->", settings.into_iter().join("; ")))
+ }
+}
+
+fn is_css_ident(s: &str) -> bool {
+ fn is_nmstart(c: char) -> bool {
+ c.is_ascii_alphabetic() || c == '_'
+ }
+ s.chars().next().is_some_and(is_nmstart) && s.chars().all(|c| is_nmstart(c) || c as u32 > 159)
+}
+
+struct CssString<'a>(&'a str);
+
+impl<'a> Display for CssString<'a> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let quote = if self.0.contains('"') && !self.0.contains('\'') {
+ '\''
+ } else {
+ '"'
+ };
+ f.write_char(quote)?;
+ for c in self.0.chars() {
+ match c {
+ _ if c == quote || c == '\\' => {
+ f.write_char('\\')?;
+ f.write_char(c)?;
+ }
+ '\n' => f.write_str("\\00000a")?,
+ c => f.write_char(c)?,
+ }
+ }
+ f.write_char(quote)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::borrow::Cow;
+
+ use crate::{
+ output::pivot::{Color, FontStyle, HorzAlign},
+ spv::read::css::{Lexer, Token},
+ };
+
+ #[test]
+ fn css_horz_align() {
+ assert_eq!(
+ HorzAlign::from_css("text-align: left"),
+ Some(HorzAlign::Left)
+ );
+ assert_eq!(
+ HorzAlign::from_css("margin-top: 0; text-align:center"),
+ Some(HorzAlign::Center)
+ );
+ assert_eq!(
+ HorzAlign::from_css("text-align: Right; margin-top:0"),
+ Some(HorzAlign::Right)
+ );
+ assert_eq!(HorzAlign::from_css("text-align: other"), None);
+ assert_eq!(HorzAlign::from_css("margin-top: 0"), None);
+ }
+
+ #[test]
+ fn css_strings() {
+ #[track_caller]
+ fn test_string(css: &str, value: &str) {
+ let mut lexer = Lexer(css);
+ assert_eq!(lexer.next(), Some(Token::Id(Cow::from(value))));
+ assert_eq!(lexer.next(), None);
+ }
+
+ test_string(r#""abc""#, "abc");
+ test_string(r#""a\"'\'bc""#, "a\"''bc");
+ test_string(r#""a\22 bc""#, "a\" bc");
+ test_string(r#""a\000022bc""#, "a\"bc");
+ test_string(r#""a'bc""#, "a'bc");
+ test_string(
+ r#""\\\
+xyzzy""#,
+ "\\xyzzy",
+ );
+
+ test_string(r#"'abc'"#, "abc");
+ test_string(r#"'a"\"\'bc'"#, "a\"\"'bc");
+ test_string(r#"'a\22 bc'"#, "a\" bc");
+ test_string(r#"'a\000022bc'"#, "a\"bc");
+ test_string(r#"'a\'bc'"#, "a'bc");
+ test_string(
+ r#"'a\'bc\
+xyz'"#,
+ "a'bcxyz",
+ );
+ test_string(r#"'\\'"#, "\\");
+ }
+
+ #[test]
+ fn style_from_css() {
+ assert_eq!(FontStyle::from_css(""), FontStyle::default());
+ assert_eq!(
+ FontStyle::from_css(r#"p{color:ff0000}"#),
+ FontStyle::default().with_fg(Color::RED)
+ );
+ assert_eq!(
+ FontStyle::from_css("p {font-weight: bold; text-decoration: underline}"),
+ FontStyle::default().with_bold(true).with_underline(true)
+ );
+ assert_eq!(
+ FontStyle::from_css("p {font-family: Monospace}"),
+ FontStyle::default().with_font("Monospace")
+ );
+ assert_eq!(
+ FontStyle::from_css("p {font-size: 24}"),
+ FontStyle::default().with_size(18)
+ );
+ assert_eq!(
+ FontStyle::from_css(
+ "<!--color: red; font-weight: bold; font-style: italic; text-decoration: underline; font-family: Serif-->"
+ ),
+ FontStyle::default()
+ .with_fg(Color::RED)
+ .with_bold(true)
+ .with_italic(true)
+ .with_underline(true)
+ .with_font("Serif")
+ );
+ }
+
+ #[test]
+ fn style_to_css() {
+ let base = FontStyle::default();
+ assert_eq!(base.to_css(&base), None);
+ assert_eq!(
+ FontStyle::default().with_size(18).to_css(&base),
+ Some("<!-- p { font-size: 24 } -->".into())
+ );
+ assert_eq!(
+ FontStyle::default()
+ .with_bold(true)
+ .with_underline(true)
+ .to_css(&base),
+ Some("<!-- p { font-weight: bold; text-decoration: underline } -->".into())
+ );
+ assert_eq!(
+ FontStyle::default().with_fg(Color::RED).to_css(&base),
+ Some("<!-- p { color: #ff0000 } -->".into())
+ );
+ assert_eq!(
+ FontStyle::default().with_font("Monospace").to_css(&base),
+ Some("<!-- p { font-family: Monospace } -->".into())
+ );
+ assert_eq!(
+ FontStyle::default()
+ .with_font("Times New Roman")
+ .to_css(&base),
+ Some(r#"<!-- p { font-family: "Times New Roman" } -->"#.into())
+ );
+ }
+}
--- /dev/null
+#![warn(dead_code)]
+use std::{
+ borrow::{Borrow, Cow},
+ fmt::{Display, Write as _},
+ io::{Cursor, Write},
+ mem::{discriminant, take},
+ str::FromStr,
+};
+
+use hashbrown::HashMap;
+use html_parser::{Dom, Element, Node};
+use pango::{AttrColor, AttrInt, AttrList, AttrSize, AttrString, IsAttribute};
+use quick_xml::{
+ Writer as XmlWriter,
+ escape::unescape,
+ events::{BytesText, Event},
+};
+use serde::{Deserialize, Deserializer, Serialize, ser::SerializeMap};
+
+use crate::output::pivot::{CellStyle, Color, FontStyle, HorzAlign, Value};
+
+fn lowercase<'a>(s: &'a str) -> Cow<'a, str> {
+ if s.chars().any(|c| c.is_ascii_uppercase()) {
+ Cow::from(s.to_ascii_lowercase())
+ } else {
+ Cow::from(s)
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum Markup {
+ Seq(Vec<Markup>),
+ Text(String),
+ Variable(Variable),
+ Style { style: Style, child: Box<Markup> },
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize)]
+pub enum Variable {
+ Date,
+ Time,
+ Head(u8),
+ PageTitle,
+ Page,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
+#[error("Unknown variable")]
+pub struct UnknownVariable;
+
+impl FromStr for Variable {
+ type Err = UnknownVariable;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "Date" => Ok(Self::Date),
+ "Time" => Ok(Self::Time),
+ "PageTitle" => Ok(Self::PageTitle),
+ "Page" => Ok(Self::Page),
+ _ => {
+ if let Some(suffix) = s.strip_prefix("Head")
+ && let Ok(number) = suffix.parse()
+ && number >= 1
+ {
+ Ok(Self::Head(number))
+ } else {
+ Err(UnknownVariable)
+ }
+ }
+ }
+ }
+}
+
+impl Variable {
+ fn as_str(&self) -> Cow<'static, str> {
+ match self {
+ Variable::Date => Cow::from("Date"),
+ Variable::Time => Cow::from("Time"),
+ Variable::Head(index) => Cow::from(format!("Head{index}")),
+ Variable::PageTitle => Cow::from("PageTitle"),
+ Variable::Page => Cow::from("Page"),
+ }
+ }
+}
+
+impl Display for Variable {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.as_str())
+ }
+}
+
+impl Default for Markup {
+ fn default() -> Self {
+ Self::Seq(Vec::new())
+ }
+}
+
+impl Serialize for Markup {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ match self {
+ Markup::Seq(inner) => serializer.collect_seq(inner),
+ Markup::Text(string) => serializer.serialize_str(string.as_str()),
+ Markup::Variable(name) => serializer.serialize_newtype_struct("Variable", name),
+ Markup::Style { style, child } => {
+ let (mut style, mut child) = (style, child);
+ let mut styles = HashMap::new();
+ loop {
+ styles.insert(discriminant(style), style);
+ match &**child {
+ Markup::Style {
+ style: inner,
+ child: inner_child,
+ } => {
+ style = inner;
+ child = inner_child;
+ }
+ _ => break,
+ }
+ }
+ let mut map = serializer.serialize_map(Some(styles.len() + 1))?;
+ for style in styles.into_values() {
+ match style {
+ Style::Bold => map.serialize_entry("bool", &true),
+ Style::Italic => map.serialize_entry("italic", &true),
+ Style::Underline => map.serialize_entry("underline", &true),
+ Style::Strike => map.serialize_entry("strike", &true),
+ Style::Emphasis => map.serialize_entry("em", &true),
+ Style::Strong => map.serialize_entry("strong", &true),
+ Style::Face(name) => map.serialize_entry("font", name),
+ Style::Color(color) => map.serialize_entry("color", color),
+ Style::Size(size) => map.serialize_entry("size", size),
+ }?;
+ }
+ map.serialize_entry("content", child)?;
+ map.end()
+ }
+ }
+ }
+}
+
+impl Display for Markup {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ fn inner(this: &Markup, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match this {
+ Markup::Seq(seq) => {
+ for markup in seq {
+ inner(markup, f)?;
+ }
+ Ok(())
+ }
+ Markup::Text(string) => f.write_str(string.as_str()),
+ Markup::Variable(name) => write!(f, "&[{name}]"),
+ Markup::Style { child, .. } => inner(child, f),
+ }
+ }
+ inner(self, f)
+ }
+}
+
+impl Markup {
+ fn is_empty(&self) -> bool {
+ match self {
+ Markup::Seq(seq) => seq.is_empty(),
+ _ => false,
+ }
+ }
+ fn is_style(&self) -> bool {
+ matches!(self, Markup::Style { .. })
+ }
+ fn into_style(self) -> Option<(Style, Markup)> {
+ match self {
+ Markup::Style { style, child } => Some((style, *child)),
+ _ => None,
+ }
+ }
+ fn is_text(&self) -> bool {
+ matches!(self, Markup::Text(_))
+ }
+ fn as_text(&self) -> Option<&str> {
+ match self {
+ Markup::Text(text) => Some(text.as_str()),
+ _ => None,
+ }
+ }
+ fn into_text(self) -> Option<String> {
+ match self {
+ Markup::Text(text) => Some(text),
+ _ => None,
+ }
+ }
+ fn write_html<X>(&self, writer: &mut XmlWriter<X>) -> std::io::Result<()>
+ where
+ X: Write,
+ {
+ match self {
+ Markup::Seq(children) => {
+ for child in children {
+ child.write_html(writer)?;
+ }
+ }
+ Markup::Text(text) => writer.write_event(Event::Text(BytesText::new(text.as_str())))?,
+ Markup::Variable(name) => {
+ writer.write_event(Event::Text(BytesText::new(&format!("&[{name}]"))))?
+ }
+ Markup::Style { style, child } => {
+ match style {
+ Style::Bold => writer.create_element("b"),
+ Style::Italic => writer.create_element("i"),
+ Style::Underline => writer.create_element("u"),
+ Style::Strike => writer.create_element("strike"),
+ Style::Emphasis => writer.create_element("em"),
+ Style::Strong => writer.create_element("strong"),
+ Style::Face(face) => writer
+ .create_element("font")
+ .with_attribute(("face", face.as_str())),
+ Style::Color(color) => writer
+ .create_element("font")
+ .with_attribute(("color", color.display_css().to_string().as_str())),
+ Style::Size(points) => writer
+ .create_element("font")
+ .with_attribute(("size", format!("{points}pt").as_str())),
+ }
+ .write_inner_content(|w| child.write_html(w))?;
+ }
+ }
+ Ok(())
+ }
+
+ pub fn to_html(&self) -> String {
+ let mut writer = XmlWriter::new(Cursor::new(Vec::new()));
+ writer
+ .create_element("html")
+ .write_inner_content(|w| self.write_html(w))
+ .unwrap();
+ String::from_utf8(writer.into_inner().into_inner()).unwrap()
+ }
+
+ pub fn to_pango<'a, F>(&self, substitutions: F) -> (String, AttrList)
+ where
+ F: Fn(Variable) -> Option<Cow<'a, str>>,
+ {
+ let mut s = String::new();
+ let mut attrs = AttrList::new();
+ self.to_pango_inner(&substitutions, &mut s, &mut attrs);
+ (s, attrs)
+ }
+
+ fn to_pango_inner<'a, F>(&self, substitutions: &F, s: &mut String, attrs: &mut AttrList)
+ where
+ F: Fn(Variable) -> Option<Cow<'a, str>>,
+ {
+ match self {
+ Markup::Seq(seq) => {
+ for child in seq {
+ child.to_pango_inner(substitutions, s, attrs);
+ }
+ }
+ Markup::Text(string) => s.push_str(&string),
+ Markup::Variable(variable) => match substitutions(*variable) {
+ Some(value) => s.push_str(&*value),
+ None => write!(s, "&[{variable}]").unwrap(),
+ },
+ Markup::Style { style, child } => {
+ let start_index = s.len();
+ child.to_pango_inner(substitutions, s, attrs);
+ let end_index = s.len();
+
+ let mut attr = match style {
+ Style::Bold | Style::Strong => {
+ AttrInt::new_weight(pango::Weight::Bold).upcast()
+ }
+ Style::Italic | Style::Emphasis => {
+ AttrInt::new_style(pango::Style::Italic).upcast()
+ }
+ Style::Underline => AttrInt::new_underline(pango::Underline::Single).upcast(),
+ Style::Strike => AttrInt::new_strikethrough(true).upcast(),
+ Style::Face(face) => AttrString::new_family(&face).upcast(),
+ Style::Color(color) => {
+ let (r, g, b) = color.into_rgb16();
+ AttrColor::new_foreground(r, g, b).upcast()
+ }
+ Style::Size(points) => AttrSize::new((points * 1024.0) as i32).upcast(),
+ };
+ attr.set_start_index(start_index as u32);
+ attr.set_end_index(end_index as u32);
+ attrs.insert(attr);
+ }
+ }
+ }
+
+ fn parse_variables(&self) -> Option<Vec<Markup>> {
+ let Some(mut s) = self.as_text() else {
+ return None;
+ };
+ let mut results = Vec::new();
+ let mut offset = 0;
+ while let Some(start) = s[offset..].find("&[").map(|pos| pos + offset)
+ && let Some(end) = s[start..].find("]").map(|pos| pos + start)
+ {
+ if let Ok(variable) = Variable::from_str(&s[start + 2..end]) {
+ if start > 0 {
+ results.push(Markup::Text(s[..start].into()));
+ }
+ results.push(Markup::Variable(variable));
+ s = &s[end + 1..];
+ offset = 0;
+ } else {
+ offset = end + 1;
+ }
+ }
+ if results.is_empty() {
+ None
+ } else {
+ if !s.is_empty() {
+ results.push(Markup::Text(s.into()));
+ }
+ Some(results)
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize)]
+pub struct Paragraph {
+ pub markup: Markup,
+ pub horz_align: HorzAlign,
+}
+
+impl Default for Paragraph {
+ fn default() -> Self {
+ Self {
+ markup: Markup::default(),
+ horz_align: HorzAlign::Left,
+ }
+ }
+}
+
+impl Paragraph {
+ fn new(mut markup: Markup, horz_align: HorzAlign, css: &[Style]) -> Self {
+ for style in css {
+ apply_style(&mut markup, style.clone());
+ }
+ Self { markup, horz_align }
+ }
+
+ fn into_value(self) -> Value {
+ let mut font_style = FontStyle::default().with_size(10);
+ let cell_style = CellStyle::default().with_horz_align(Some(self.horz_align));
+ let mut markup = self.markup;
+ let mut strike = false;
+ while markup.is_style() {
+ let (style, child) = markup.into_style().unwrap();
+ match style {
+ Style::Bold => font_style.bold = true,
+ Style::Italic => font_style.italic = true,
+ Style::Underline => font_style.underline = true,
+ Style::Strike => strike = true,
+ Style::Emphasis => font_style.italic = true,
+ Style::Strong => font_style.bold = true,
+ Style::Face(face) => font_style.font = face,
+ Style::Color(color) => font_style.fg = color,
+ Style::Size(points) => font_style.size = points as i32,
+ };
+ markup = child;
+ }
+ if strike {
+ apply_style(&mut markup, Style::Strike);
+ }
+ if markup.is_text() {
+ Value::new_user_text(markup.into_text().unwrap())
+ } else {
+ Value::new_markup(markup)
+ }
+ .with_font_style(font_style)
+ .with_cell_style(cell_style)
+ }
+}
+
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct Document(pub Vec<Paragraph>);
+
+impl<'de> Deserialize<'de> for Document {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ Ok(Document::from_html(&String::deserialize(deserializer)?))
+ }
+}
+
+impl Serialize for Document {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ self.to_html().serialize(serializer)
+ }
+}
+
+impl Document {
+ pub fn is_empty(&self) -> bool {
+ self.0.is_empty()
+ }
+
+ pub fn from_html(input: &str) -> Self {
+ match Dom::parse(&format!("<!doctype html>{input}")) {
+ Ok(dom) => Self(parse_dom(&dom)),
+ Err(_) if !input.is_empty() => Self(vec![Paragraph {
+ markup: Markup::Text(input.into()),
+ horz_align: HorzAlign::Left,
+ }]),
+ Err(_) => Self::default(),
+ }
+ }
+
+ pub fn into_value(self) -> Value {
+ self.0.into_iter().next().unwrap_or_default().into_value()
+ }
+
+ pub fn to_html(&self) -> String {
+ let mut writer = XmlWriter::new(Cursor::new(Vec::new()));
+ writer
+ .create_element("html")
+ .write_inner_content(|w| {
+ for paragraph in &self.0 {
+ w.create_element("p")
+ .with_attribute(("align", paragraph.horz_align.as_str().unwrap()))
+ .write_inner_content(|w| paragraph.markup.write_html(w))?;
+ }
+ Ok(())
+ })
+ .unwrap();
+
+ // Return the result with `<html>` and `</html>` stripped off.
+ str::from_utf8(&writer.into_inner().into_inner())
+ .unwrap()
+ .strip_prefix("<html>")
+ .unwrap()
+ .strip_suffix("</html>")
+ .unwrap()
+ .into()
+ }
+
+ pub fn to_values(&self) -> Vec<Value> {
+ self.0
+ .iter()
+ .map(|paragraph| paragraph.clone().into_value())
+ .collect()
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum Style {
+ Bold,
+ Italic,
+ Underline,
+ Strike,
+ Emphasis,
+ Strong,
+ Face(String),
+ Color(Color),
+ Size(f64),
+}
+
+fn node_as_element<'a>(node: &'a Node, name: &str) -> Option<&'a Element> {
+ if let Node::Element(element) = node
+ && element.name.eq_ignore_ascii_case(name)
+ {
+ Some(element)
+ } else {
+ None
+ }
+}
+
+fn node_is_element(node: &Node, name: &str) -> bool {
+ node_as_element(node, name).is_some()
+}
+
+/// Returns the horizontal alignment for the `<p>` element in `p`.
+fn horz_align_from_p(p: &Element) -> Option<HorzAlign> {
+ if let Some(Some(s)) = p.attributes.get("align")
+ && let Ok(align) = HorzAlign::from_str(s)
+ {
+ Some(align)
+ } else if let Some(Some(s)) = p.attributes.get("style")
+ && let Some(align) = HorzAlign::from_css(s)
+ {
+ Some(align)
+ } else {
+ None
+ }
+}
+
+fn apply_style(markup: &mut Markup, style: Style) {
+ let child = take(markup);
+ *markup = Markup::Style {
+ style,
+ child: Box::new(child),
+ };
+}
+
+pub fn parse_dom(dom: &Dom) -> Vec<Paragraph> {
+ // Get the top-level elements, descending into an `html` element if
+ // there is one.
+ let roots = if dom.children.len() == 1
+ && let Some(first) = dom.children.first()
+ && let Some(html) = node_as_element(first, "html")
+ {
+ &html.children
+ } else {
+ &dom.children
+ };
+
+ // If there's a `head` element, parse it for CSS and then skip past it.
+ let mut css = 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")
+ {
+ 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);
+ if let Some(horz_align) = HorzAlign::from_css(&text) {
+ default_horz_align = horz_align;
+ }
+ }
+ rest
+ } else {
+ roots
+ };
+
+ // If only a `body` element is left, descend into it.
+ let body = if roots.len() == 1
+ && let Some(first) = roots.first()
+ && let Some(body) = node_as_element(first, "body")
+ {
+ &body.children
+ } else {
+ roots
+ };
+
+ let mut paragraphs = Vec::new();
+
+ let mut start = 0;
+ while start < body.len() {
+ let (end, align) = if let Some(p) = node_as_element(&body[start], "p") {
+ (
+ start + 1,
+ horz_align_from_p(p).unwrap_or(default_horz_align),
+ )
+ } else {
+ let mut end = start + 1;
+ while end < body.len() && !node_is_element(&body[end], "p") {
+ end += 1;
+ }
+ (end, default_horz_align)
+ };
+ paragraphs.push(Paragraph::new(parse_nodes(&body[start..end]), align, &css));
+ start = end;
+ }
+
+ paragraphs
+}
+
+fn parse_nodes(nodes: &[Node]) -> Markup {
+ // Appends `markup` to `dst`, merging text at the end of `dst` with text
+ // in `markup`.
+ fn add_markup(dst: &mut Vec<Markup>, markup: Markup) {
+ if let Markup::Text(suffix) = &markup
+ && let Some(Markup::Text(last)) = dst.last_mut()
+ {
+ last.push_str(&suffix);
+ } else {
+ dst.push(markup);
+ }
+
+ if let Some(mut expansion) = dst.last().unwrap().parse_variables() {
+ dst.pop();
+ dst.append(&mut expansion);
+ }
+ }
+
+ let mut retval = Vec::new();
+ for (i, node) in nodes.iter().enumerate() {
+ match node {
+ Node::Comment(_) => (),
+ Node::Text(text) => {
+ let text = if i == 0 {
+ text.trim_start()
+ } else {
+ text.as_str()
+ };
+ let text = if i == nodes.len() - 1 {
+ text.trim_end()
+ } else {
+ text
+ };
+ add_markup(
+ &mut retval,
+ Markup::Text(unescape(&text).unwrap_or(Cow::from(text)).into_owned()),
+ );
+ }
+ Node::Element(br) if br.name.eq_ignore_ascii_case("br") => {
+ add_markup(&mut retval, Markup::Text('\n'.into()));
+ }
+ Node::Element(element) => {
+ let mut inner = parse_nodes(&element.children);
+ if inner.is_empty() {
+ continue;
+ }
+
+ let style = match lowercase(&element.name).borrow() {
+ "b" => Some(Style::Bold),
+ "i" => Some(Style::Italic),
+ "u" => Some(Style::Underline),
+ "s" | "strike" => Some(Style::Strike),
+ "strong" => Some(Style::Strong),
+ "em" => Some(Style::Emphasis),
+ "font" => {
+ if let Some(Some(face)) = element.attributes.get("face") {
+ apply_style(&mut inner, Style::Face(face.clone()));
+ }
+ if let Some(Some(color)) = element.attributes.get("color")
+ && let Ok(color) = Color::from_str(&color)
+ {
+ apply_style(&mut inner, Style::Color(color));
+ }
+ if let Some(Some(html_size)) = element.attributes.get("size")
+ && let Ok(html_size) = usize::from_str(&html_size)
+ && let Some(index) = html_size.checked_sub(1)
+ && let Some(points) =
+ [6.0, 7.5, 9.0, 10.5, 13.5, 18.0, 27.0].get(index).copied()
+ {
+ apply_style(&mut inner, Style::Size(points));
+ }
+ None
+ }
+ _ => None,
+ };
+ match style {
+ None => match inner {
+ Markup::Seq(seq) => {
+ for markup in seq {
+ add_markup(&mut retval, markup);
+ }
+ }
+ _ => add_markup(&mut retval, inner),
+ },
+ Some(style) => retval.push(Markup::Style {
+ style,
+ child: Box::new(inner),
+ }),
+ }
+ }
+ }
+ }
+ if retval.len() == 1 {
+ retval.into_iter().next().unwrap()
+ } else {
+ Markup::Seq(retval)
+ }
+}
+
+fn find_element<'a>(elements: &'a [Node], name: &str) -> Option<&'a Element> {
+ for element in elements {
+ if let Node::Element(element) = element
+ && element.name == name
+ {
+ return Some(element);
+ }
+ }
+ None
+}
+
+fn parse_entity(s: &str) -> (char, &str) {
+ static ENTITIES: [(&str, char); 6] = [
+ ("amp;", '&'),
+ ("lt;", '<'),
+ ("gt;", '>'),
+ ("apos;", '\''),
+ ("quot;", '"'),
+ ("nbsp;", '\u{00a0}'),
+ ];
+ for (name, ch) in ENTITIES {
+ if let Some(rest) = s.strip_prefix(name) {
+ return (ch, rest);
+ }
+ }
+ ('&', s)
+}
+
+fn get_node_text(node: &Node, text: &mut String) {
+ match node {
+ Node::Text(string) => {
+ let mut s = string.as_str();
+ while !s.is_empty() {
+ let amp = s.find('&').unwrap_or(s.len());
+ let (head, rest) = s.split_at(amp);
+ text.push_str(head);
+ if rest.is_empty() {
+ break;
+ }
+ let ch;
+ (ch, s) = parse_entity(&s[1..]);
+ text.push(ch);
+ }
+ }
+ Node::Element(element) => get_element_text(element, text),
+ Node::Comment(_) => (),
+ }
+}
+
+fn get_element_text(element: &Element, text: &mut String) {
+ for child in &element.children {
+ get_node_text(child, text);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::{borrow::Cow, str::FromStr};
+
+ use crate::spv::read::html::{self, Document, Markup, Variable};
+
+ #[test]
+ fn variable() {
+ assert_eq!(Variable::from_str("Head1").unwrap(), Variable::Head(1));
+ assert_eq!(Variable::from_str("Page").unwrap(), Variable::Page);
+ assert_eq!(Variable::from_str("Date").unwrap(), Variable::Date);
+ assert_eq!(Variable::Head(1).to_string(), "Head1");
+ assert_eq!(Variable::Page.to_string(), "Page");
+ assert_eq!(Variable::Date.to_string(), "Date");
+ }
+
+ #[test]
+ fn parse_variables() {
+ assert_eq!(Markup::Text("asdf".into()).parse_variables(), None);
+ assert_eq!(Markup::Text("&[asdf]".into()).parse_variables(), None);
+ assert_eq!(
+ Markup::Text("&[Page]".into()).parse_variables(),
+ Some(vec![Markup::Variable(Variable::Page)])
+ );
+ assert_eq!(
+ Markup::Text("xyzzy &[Invalid] &[Page] &[Invalid2] quux".into()).parse_variables(),
+ Some(vec![
+ Markup::Text("xyzzy &[Invalid] ".into()),
+ Markup::Variable(Variable::Page),
+ Markup::Text(" &[Invalid2] quux".into()),
+ ])
+ );
+ }
+
+ /// Example from the documentation.
+ #[test]
+ fn example1() {
+ let text = r##"<xml><html xmlns="http://www.w3.org/1999/xhtml" lang="en">
+ <head>
+
+ </head>
+ <body>
+ <p>
+ plain&#160;<font color="#000000" size="3" face="Monospaced"><b>bold</b></font>&#160;<font color="#000000" size="3" face="Monospaced"><i>italic</i>&#160;<strike>strikeout</strike></font>
+ </p>
+ </body>
+</html>
+</xml>"##;
+ let content = quick_xml::de::from_str::<String>(text).unwrap();
+ assert_eq!(
+ Document::from_html(&content).to_html(),
+ r##"<p align="left">plain <font size="9pt"><font color="#000000"><font face="Monospaced"><b>bold</b></font></font></font> <font size="9pt"><font color="#000000"><font face="Monospaced"><i>italic</i> <strike>strikeout</strike></font></font></font></p>"##
+ );
+ }
+
+ /// Another example from the documentation.
+ #[test]
+ fn example2() {
+ let text = r##"<xml><html xmlns="http://www.w3.org/1999/xhtml" lang="en">
+ <head>
+
+ </head>
+ <body>
+ <p>left</p>
+ <p align="center"><font color="#000000" size="5" face="Monospaced">center&#160;large</font></p>
+ <p align="right"><font color="#000000" size="3" face="Monospaced"><b><i>right</i></b></font></p>
+ </body>
+</html></xml>
+"##;
+ let content = quick_xml::de::from_str::<String>(text).unwrap();
+ assert_eq!(
+ Document::from_html(&content).to_html(),
+ r##"<p align="left">left</p><p align="center"><font size="13.5pt"><font color="#000000"><font face="Monospaced">center large</font></font></font></p><p align="right"><font size="9pt"><font color="#000000"><font face="Monospaced"><b><i>right</i></b></font></font></font></p>"##
+ );
+ }
+
+ /*
+ #[test]
+ fn value() {
+ let value = parse_value(
+ r#"<b>bold</b><br><i>italic</i><BR><b><i>bold italic</i></b><br><font color="red" face="Serif">red serif</font><br><font size="7">big</font><br>"#,
+ );
+ assert_eq!(
+ value,
+ Value::new_markup(
+ r##"<b>bold</b>
+<i>italic</i>
+<b><i>bold italic</i></b>
+<span face="Serif" color="#ff0000">red serif</span>
+<span size="20480">big</span>
+"##
+ )
+ .with_font_style(FontStyle::default().with_size(10))
+ );
+ }*/
+
+ /// From the corpus (also included in the documentation).
+ #[test]
+ fn header1() {
+ let text = r##"<xml><html xmlns="http://xml.spss.com/spss/viewer/viewer-tree">
+ <head>
+
+ </head>
+ <body>
+ <p style="text-align:center; margin-top: 0">
+ &[PageTitle]
+ </p>
+ </body>
+</html></xml>"##;
+ let content = quick_xml::de::from_str::<String>(text).unwrap();
+ assert_eq!(
+ Document::from_html(&content).to_html(),
+ r##"<p align="center">&[PageTitle]</p>"##
+ );
+ }
+
+ /// From the corpus (also included in the documentation).
+ #[test]
+ fn footer1() {
+ let text = r##"<xml><html xmlns="http://xml.spss.com/spss/viewer/viewer-tree">
+ <head>
+
+ </head>
+ <body>
+ <p style="text-align:right; margin-top: 0">
+ Page &[Page]
+ </p>
+ </body>
+</html></xml>"##;
+ let content = quick_xml::de::from_str::<String>(text).unwrap();
+ assert_eq!(
+ Document::from_html(&content).to_html(),
+ r##"<p align="right">Page &[Page]</p>"##
+ );
+ }
+
+ /// From the corpus (also included in the documentation).
+ #[test]
+ fn header2() {
+ let text = r##"<xml><html xmlns="http://www.w3.org/1999/xhtml" lang="en">
+ <head>
+ <style type="text/css">
+ p { font-family: sans-serif;
+ font-size: 10pt; text-align: center;
+ font-weight: normal;
+ color: #000000;
+ }
+ </style>
+ </head>
+ <body>
+ <p>&amp;[PageTitle]</p>
+ </body>
+</html></xml>"##;
+ let content = quick_xml::de::from_str::<String>(text).unwrap();
+ let document = Document::from_html(&content);
+ assert_eq!(
+ document.to_html(),
+ r##"<p align="center"><font color="#000000"><font face="sans-serif">&[PageTitle]</font></font></p>"##
+ );
+ assert_eq!(
+ document.0[0]
+ .markup
+ .to_pango(
+ |name| (name == html::Variable::PageTitle).then_some(Cow::from("The title"))
+ )
+ .0,
+ "The title"
+ );
+ }
+
+ /// From the corpus (also included in the documentation).
+ #[test]
+ fn footer2() {
+ let text = r##"<xml><html xmlns="http://www.w3.org/1999/xhtml" lang="en">
+ <head>
+ <style type="text/css">
+ p { font-family: sans-serif;
+ font-size: 10pt; text-align: right;
+ font-weight: normal;
+ color: #000000;
+ }
+ </style>
+ </head>
+ <body>
+ <p>Page &amp;[Page]</p>
+ </body>
+</html>
+</xml>"##;
+ let content = quick_xml::de::from_str::<String>(text).unwrap();
+ let html = Document::from_html(&content);
+ assert_eq!(
+ html.to_html(),
+ r##"<p align="right"><font color="#000000"><font face="sans-serif">Page &[Page]</font></font></p>"##
+ );
+ }
+
+ /// Checks that the `escape-html` feature is enabled in [quick_xml], since
+ /// we need that to resolve ` ` and other HTML entities.
+ #[test]
+ fn html_escapes() {
+ let html = Document::from_html(" ");
+ assert_eq!(html.to_html(), "<p align=\"left\">\u{a0}</p>")
+ }
+}
--- /dev/null
+use std::{
+ collections::HashMap,
+ io::{Read, Seek, SeekFrom},
+};
+
+use binrw::{BinRead, BinResult, binread};
+use chrono::{NaiveDateTime, NaiveTime};
+use encoding_rs::UTF_8;
+
+use crate::{
+ calendar::{date_time_to_pspp, time_to_pspp},
+ data::Datum,
+ format::{Category, Format},
+ output::pivot::Value,
+ spv::read::light::{U32String, decode_format, parse_vec},
+};
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+pub struct LegacyBin {
+ #[br(magic(0u8), temp)]
+ version: Version,
+ #[br(temp)]
+ n_sources: u16,
+ #[br(temp)]
+ _member_size: u32,
+ #[br(count(n_sources), args { inner: (version,) })]
+ metadata: Vec<Metadata>,
+ #[br(parse_with(parse_data), args(metadata.as_slice()))]
+ data: Vec<Data>,
+ #[br(parse_with(parse_strings))]
+ strings: Option<Strings>,
+}
+
+impl LegacyBin {
+ pub fn decode(&self) -> HashMap<String, HashMap<String, Vec<DataValue>>> {
+ let mut sources = HashMap::new();
+ for (metadata, data) in self.metadata.iter().zip(&self.data) {
+ let mut variables = HashMap::new();
+ for variable in &data.variables {
+ variables.insert(
+ variable.variable_name.clone(),
+ variable
+ .values
+ .iter()
+ .map(|value| DataValue {
+ index: None,
+ value: Datum::Number((*value != f64::MIN).then_some(*value)),
+ })
+ .collect::<Vec<_>>(),
+ );
+ }
+ sources.insert(metadata.source_name.clone(), variables);
+ }
+ if let Some(strings) = &self.strings {
+ for map in &strings.source_maps {
+ let source = sources.get_mut(&map.source_name).unwrap(); // XXX unwrap
+ for var_map in &map.variable_maps {
+ let variable = source.get_mut(&var_map.variable_name).unwrap(); // XXX unwrap
+ for datum_map in &var_map.datum_maps {
+ // XXX two possibly out-of-range indexes below
+ variable[datum_map.value_idx].value =
+ Datum::String(strings.labels[datum_map.label_idx].label.clone());
+ }
+ }
+ }
+ }
+ sources
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct DataValue {
+ pub index: Option<f64>,
+ pub value: Datum<String>,
+}
+
+impl DataValue {
+ pub fn category(&self) -> Option<usize> {
+ match &self.value {
+ Datum::Number(number) => *number,
+ _ => self.index,
+ }
+ .and_then(|v| (v >= 0.0 && v < usize::MAX as f64).then_some(v as usize))
+ }
+
+ // This should probably be a method on some hypothetical FormatMap.
+ pub fn as_format(&self, format_map: &HashMap<i64, Format>) -> Format {
+ let f = match &self.value {
+ Datum::Number(Some(number)) => *number as i64,
+ Datum::Number(None) => 0,
+ Datum::String(s) => s.parse().unwrap_or_default(),
+ };
+ match format_map.get(&f) {
+ Some(format) => *format,
+ None => decode_format(f as u32),
+ }
+ }
+
+ pub fn as_pivot_value(&self, format: Format) -> Value {
+ if format.type_().category() == Category::Date
+ && let Some(s) = self.value.as_string()
+ && let Ok(date_time) =
+ NaiveDateTime::parse_from_str(s.as_str(), "%Y-%m-%dT%H:%M:%S%.3f")
+ {
+ Value::new_number_with_format(Some(date_time_to_pspp(date_time)), format)
+ } else if format.type_().category() == Category::Time
+ && let Some(s) = self.value.as_string()
+ && let Ok(time) = NaiveTime::parse_from_str(s.as_str(), "%H:%M:%S%.3f")
+ {
+ Value::new_number_with_format(Some(time_to_pspp(time)), format)
+ } else {
+ Value::new_datum_with_format(&self.value, format)
+ }
+ }
+}
+
+#[binread]
+#[br(little)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+enum Version {
+ #[br(magic = 0xafu8)]
+ Vaf,
+ #[br(magic = 0xb0u8)]
+ Vb0,
+}
+
+#[binread]
+#[br(little, import(version: Version))]
+#[derive(Debug)]
+struct Metadata {
+ n_values: u32,
+ n_variables: u32,
+ data_offset: u32,
+ #[br(parse_with(parse_fixed_utf8_string), args(if version == Version::Vaf { 28 } else { 64 }))]
+ source_name: String,
+ #[br(if(version == Version::Vb0), temp)]
+ _x: u32,
+}
+
+#[derive(Debug)]
+struct Data {
+ variables: Vec<Variable>,
+}
+
+#[binrw::parser(reader, endian)]
+fn parse_data(metadata: &[Metadata]) -> BinResult<Vec<Data>> {
+ let mut data = Vec::with_capacity(metadata.len());
+ for metadata in metadata {
+ reader.seek(SeekFrom::Start(metadata.data_offset as u64))?;
+ let mut variables = Vec::with_capacity(metadata.n_variables as usize);
+ for _ in 0..metadata.n_variables {
+ variables.push(Variable::read_options(
+ reader,
+ endian,
+ (metadata.n_values,),
+ )?);
+ }
+ data.push(Data { variables });
+ }
+ Ok(data)
+}
+
+impl BinRead for Data {
+ type Args<'a> = &'a [Metadata];
+
+ fn read_options<R: Read + Seek>(
+ reader: &mut R,
+ endian: binrw::Endian,
+ metadata: Self::Args<'_>,
+ ) -> binrw::BinResult<Self> {
+ let mut variables = Vec::with_capacity(metadata.len());
+ for metadata in metadata {
+ reader.seek(SeekFrom::Start(metadata.data_offset as u64))?;
+ variables.push(Variable::read_options(
+ reader,
+ endian,
+ (metadata.n_values,),
+ )?);
+ }
+ Ok(Self { variables })
+ }
+}
+
+#[binread]
+#[br(little, import(n_values: u32))]
+#[derive(Debug)]
+struct Variable {
+ #[br(parse_with(parse_fixed_utf8_string), args(288))]
+ variable_name: String,
+ #[br(count(n_values))]
+ values: Vec<f64>,
+}
+
+#[binrw::parser(reader, endian)]
+fn parse_strings() -> BinResult<Option<Strings>> {
+ let position = reader.stream_position()?;
+ let length = reader.seek(SeekFrom::End(0))?;
+ if position != length {
+ reader.seek(SeekFrom::Start(position))?;
+ Ok(Some(Strings::read_options(reader, endian, ())?))
+ } else {
+ Ok(None)
+ }
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct Strings {
+ #[br(parse_with(parse_vec))]
+ source_maps: Vec<SourceMap>,
+ #[br(parse_with(parse_vec))]
+ labels: Vec<Label>,
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct SourceMap {
+ #[br(parse_with(parse_utf8_string))]
+ source_name: String,
+ #[br(parse_with(parse_vec))]
+ variable_maps: Vec<VariableMap>,
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct VariableMap {
+ #[br(parse_with(parse_utf8_string))]
+ variable_name: String,
+ #[br(parse_with(parse_vec))]
+ datum_maps: Vec<DatumMap>,
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct DatumMap {
+ #[br(map(|x: u32| x as usize))]
+ value_idx: usize,
+ #[br(map(|x: u32| x as usize))]
+ label_idx: usize,
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct Label {
+ #[br(temp)]
+ _frequency: u32,
+ #[br(parse_with(parse_utf8_string))]
+ label: String,
+}
+
+/// Parses a UTF-8 string preceded by a 32-bit length.
+#[binrw::parser(reader, endian)]
+pub(super) fn parse_utf8_string() -> BinResult<String> {
+ Ok(U32String::read_options(reader, endian, ())?.decode(UTF_8))
+}
+
+/// Parses a UTF-8 string that is exactly `n` bytes long and whose contents end
+/// at the first null byte.
+#[binrw::parser(reader)]
+pub(super) fn parse_fixed_utf8_string(n: usize) -> BinResult<String> {
+ let mut buf = vec![0; n];
+ reader.read_exact(&mut buf)?;
+ let len = buf.iter().take_while(|b| **b != 0).count();
+ Ok(
+ std::str::from_utf8(&buf[..len]).unwrap().into(), // XXX unwrap
+ )
+}
--- /dev/null
+// 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/>.
+
+use std::{
+ cell::{Cell, RefCell},
+ collections::{BTreeMap, HashMap},
+ marker::PhantomData,
+ mem::take,
+ num::NonZeroUsize,
+ ops::Range,
+ sync::Arc,
+};
+
+use chrono::{NaiveDateTime, NaiveTime};
+use enum_map::{Enum, EnumMap};
+use hashbrown::HashSet;
+use ordered_float::OrderedFloat;
+use serde::Deserialize;
+
+use crate::{
+ calendar::{date_time_to_pspp, time_to_pspp},
+ data::Datum,
+ format::{self, Decimal::Dot, F8_0, F40_2, Type, UncheckedFormat},
+ output::pivot::{
+ self, Area, AreaStyle, Axis2, Axis3, Category, CategoryLocator, CellStyle, Color,
+ Dimension, Group, HeadingRegion, HorzAlign, Leaf, Length, Look, NumberValue, PivotTable,
+ RowParity, Value, ValueInner, VertAlign,
+ },
+ spv::read::legacy_bin::DataValue,
+};
+
+#[derive(Debug)]
+struct Ref<T> {
+ references: String,
+ _phantom: PhantomData<T>,
+}
+
+impl<T> Ref<T> {
+ fn get<'a>(&self, table: &HashMap<&str, &'a T>) -> Option<&'a T> {
+ table.get(self.references.as_str()).map(|v| &**v)
+ }
+}
+
+impl<'de, T> Deserialize<'de> for Ref<T> {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ Ok(Self {
+ references: String::deserialize(deserializer)?,
+ _phantom: PhantomData,
+ })
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+struct Map(HashMap<OrderedFloat<f64>, Datum<String>>);
+
+impl Map {
+ fn new() -> Self {
+ Self::default()
+ }
+
+ fn remap_formats(
+ &mut self,
+ format: &Option<Format>,
+ string_format: &Option<StringFormat>,
+ ) -> (crate::format::Format, Vec<Affix>) {
+ let (format, affixes, relabels, try_strings_as_numbers) = if let Some(format) = &format {
+ (
+ Some(format.decode()),
+ format.affixes.clone(),
+ format.relabels.as_slice(),
+ format.try_strings_as_numbers.unwrap_or_default(),
+ )
+ } else if let Some(string_format) = &string_format {
+ (
+ None,
+ string_format.affixes.clone(),
+ string_format.relabels.as_slice(),
+ false,
+ )
+ } else {
+ (None, Vec::new(), [].as_slice(), false)
+ };
+ for relabel in relabels {
+ let value = if try_strings_as_numbers && let Ok(to) = relabel.to.trim().parse::<f64>() {
+ Datum::Number(Some(to))
+ } else if let Some(format) = format
+ && let Ok(to) = relabel.to.trim().parse::<f64>()
+ {
+ Datum::String(
+ Datum::<String>::Number(Some(to))
+ .display(format)
+ .with_stretch()
+ .to_string(),
+ )
+ } else {
+ Datum::String(relabel.to.clone())
+ };
+ self.0.insert(OrderedFloat(relabel.from), value);
+ // XXX warn on duplicate
+ }
+ (format.unwrap_or(F8_0), affixes)
+ }
+
+ fn apply(&self, data: &mut Vec<DataValue>) {
+ for value in data {
+ let Datum::Number(Some(number)) = value.value else {
+ continue;
+ };
+ if let Some(to) = self.0.get(&OrderedFloat(number)) {
+ value.index = Some(number);
+ value.value = to.clone();
+ }
+ }
+ }
+
+ fn insert_labels(
+ &mut self,
+ data: &[DataValue],
+ label_series: &Series,
+ format: crate::format::Format,
+ ) {
+ for (value, label) in data.iter().zip(label_series.values.iter()) {
+ if let Some(Some(number)) = value.value.as_number() {
+ let dest = match &label.value {
+ Datum::Number(_) => label.value.display(format).with_stretch().to_string(),
+ Datum::String(s) => s.clone(),
+ };
+ self.0.insert(OrderedFloat(number), Datum::String(dest));
+ }
+ }
+ }
+
+ fn remap_vmes(&mut self, value_map: &[ValueMapEntry]) {
+ for vme in value_map {
+ for from in vme.from.split(';') {
+ let from = from.trim().parse::<f64>().unwrap(); // XXX
+ let to = if let Ok(to) = vme.to.trim().parse::<f64>() {
+ Datum::Number(Some(to))
+ } else {
+ Datum::String(vme.to.clone())
+ };
+ self.0.insert(OrderedFloat(from), to);
+ }
+ }
+ }
+
+ fn lookup<'a>(&'a self, dv: &'a DataValue) -> &'a Datum<String> {
+ if let Datum::Number(Some(number)) = &dv.value
+ && let Some(value) = self.0.get(&OrderedFloat(*number))
+ {
+ value
+ } else {
+ &dv.value
+ }
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct Visualization {
+ /// In format `YYYY-MM-DD`.
+ #[serde(rename = "@date")]
+ date: String,
+ // Locale used for output, e.g. `en-US`.
+ #[serde(rename = "@lang")]
+ lang: String,
+ /// Localized title of the pivot table.
+ #[serde(rename = "@name")]
+ name: String,
+ /// Base style for the pivot table.
+ #[serde(rename = "@style")]
+ style: Ref<Style>,
+
+ #[serde(rename = "$value")]
+ children: Vec<VisChild>,
+}
+
+impl Visualization {
+ pub fn decode(
+ &self,
+ data: HashMap<String, HashMap<String, Vec<DataValue>>>,
+ mut look: Look,
+ ) -> Result<PivotTable, super::Error> {
+ let mut extension = None;
+ let mut user_source = None;
+ let mut source_variables = Vec::new();
+ let mut derived_variables = Vec::new();
+ let mut graph = None;
+ let mut labels = EnumMap::from_fn(|_| Vec::new());
+ let mut styles = HashMap::new();
+ let mut _layer_controller = None;
+ for child in &self.children {
+ match child {
+ VisChild::Extension(e) => extension = Some(e),
+ VisChild::UserSource(us) => user_source = Some(us),
+ VisChild::SourceVariable(source_variable) => source_variables.push(source_variable),
+ VisChild::DerivedVariable(derived_variable) => {
+ derived_variables.push(derived_variable)
+ }
+ VisChild::CategoricalDomain(_) => (),
+ VisChild::Graph(g) => graph = Some(g),
+ VisChild::LabelFrame(label_frame) => {
+ if let Some(label) = &label_frame.label
+ && let Some(purpose) = label.purpose
+ {
+ labels[purpose].push(label);
+ }
+ }
+ VisChild::Container(c) => {
+ for label_frame in &c.label_frames {
+ if let Some(label) = &label_frame.label
+ && let Some(purpose) = label.purpose
+ {
+ labels[purpose].push(label);
+ }
+ }
+ }
+ VisChild::Style(style) => {
+ if let Some(id) = &style.id {
+ styles.insert(id.as_str(), style);
+ }
+ }
+ VisChild::LayerController(lc) => _layer_controller = Some(lc),
+ }
+ }
+ let Some(graph) = graph else { todo!() };
+ let Some(_user_source) = user_source else {
+ todo!()
+ };
+
+ let mut axes = HashMap::new();
+ let mut major_ticks = HashMap::new();
+ for child in &graph.facet_layout.children {
+ if let FacetLayoutChild::FacetLevel(facet_level) = child {
+ axes.insert(facet_level.level, &facet_level.axis);
+ major_ticks.insert(
+ facet_level.axis.major_ticks.id.as_str(),
+ &facet_level.axis.major_ticks,
+ );
+ }
+ }
+
+ // Footnotes.
+ //
+ // Any [Value] might refer to footnotes, so it's important to process
+ // the footnotes early to ensure that those references can be resolved.
+ // There is a possible problem that a footnote might itself reference an
+ // as-yet-unprocessed footnote, but that's OK because footnote
+ // references don't actually look at the footnote contents but only
+ // resolve a pointer to where the footnote will go later.
+ //
+ // Before we really start, create all the footnotes we'll fill in. This
+ // is because sometimes footnotes refer to themselves or to each other
+ // and we don't want to reject those references.
+ let mut footnote_builder = BTreeMap::<usize, Footnote>::new();
+ if let Some(f) = &graph.interval.footnotes {
+ f.decode(&mut footnote_builder);
+ }
+ for child in &graph.interval.labeling.children {
+ if let LabelingChild::Footnotes(f) = child {
+ f.decode(&mut footnote_builder);
+ }
+ }
+ for label in &labels[Purpose::Footnote] {
+ for (index, text) in label.text().iter().enumerate() {
+ if let Some(uses_reference) = text.uses_reference {
+ let entry = footnote_builder
+ .entry(uses_reference.get() - 1)
+ .or_default();
+ if index % 2 == 0 {
+ entry.content = text.text.strip_suffix('\n').unwrap_or(&text.text).into();
+ } else {
+ entry.marker =
+ Some(text.text.strip_suffix('.').unwrap_or(&text.text).into());
+ }
+ }
+ }
+ }
+ let mut footnotes = Vec::new();
+ for (index, footnote) in footnote_builder {
+ while footnotes.len() < index {
+ footnotes.push(pivot::Footnote::default());
+ }
+ footnotes.push(
+ pivot::Footnote::new(footnote.content)
+ .with_marker(footnote.marker.map(|s| Value::new_user_text(s))),
+ );
+ }
+ let footnotes = pivot::Footnotes::from_iter(footnotes);
+
+ for (purpose, area) in [
+ (Purpose::Title, Area::Title),
+ (Purpose::SubTitle, Area::Caption),
+ (Purpose::Layer, Area::Layers),
+ (Purpose::Footnote, Area::Footer),
+ ] {
+ for label in &labels[purpose] {
+ label.decode_style(&mut look.areas[area], &styles);
+ }
+ }
+ let title = LabelFrame::decode_label(&labels[Purpose::Title]);
+ let caption = LabelFrame::decode_label(&labels[Purpose::SubTitle]);
+ if let Some(style) = &graph.interval.labeling.style
+ && let Some(style) = styles.get(style.references.as_str())
+ {
+ Style::decode_area(
+ Some(*style),
+ graph.cell_style.get(&styles),
+ &mut look.areas[Area::Data(RowParity::Even)],
+ );
+ look.areas[Area::Data(RowParity::Odd)] =
+ look.areas[Area::Data(RowParity::Even)].clone();
+ }
+
+ let _show_grid_lines = extension
+ .as_ref()
+ .and_then(|extension| extension.show_gridline);
+ if let Some(style) = styles.get(graph.cell_style.references.as_str())
+ && let Some(width) = &style.width
+ {
+ let mut parts = width.split(';');
+ parts.next();
+ if let Some(min_width) = parts.next()
+ && let Some(max_width) = parts.next()
+ && let Ok(min_width) = min_width.parse::<Length>()
+ && let Ok(max_width) = max_width.parse::<Length>()
+ {
+ look.heading_widths[HeadingRegion::Columns] =
+ min_width.as_pt_f64() as isize..=max_width.as_pt_f64() as isize;
+ }
+ }
+
+ let mut series = HashMap::<&str, Series>::new();
+ while let n_source = source_variables.len()
+ && let n_derived = derived_variables.len()
+ && (n_source > 0 || n_derived > 0)
+ {
+ for sv in take(&mut source_variables) {
+ match sv.decode(&data, &series) {
+ Ok(s) => {
+ series.insert(&sv.id, s);
+ }
+ Err(()) => source_variables.push(sv),
+ }
+ }
+
+ for dv in take(&mut derived_variables) {
+ match dv.decode(&series) {
+ Ok(s) => {
+ series.insert(&dv.id, s);
+ }
+ Err(()) => derived_variables.push(dv),
+ }
+ }
+
+ if n_source == source_variables.len() && n_derived == derived_variables.len() {
+ unreachable!();
+ }
+ }
+
+ fn decode_dimension<'a>(
+ variables: &[(&'a Series, usize)],
+ axes: &HashMap<usize, &Axis>,
+ styles: &HashMap<&str, &Style>,
+ a: Axis3,
+ look: &mut Look,
+ rotate_inner_column_labels: &mut bool,
+ rotate_outer_row_labels: &mut bool,
+ footnotes: &pivot::Footnotes,
+ dims: &mut Vec<Dim<'a>>,
+ ) {
+ let base_level = variables[0].1;
+ let show_label = if let Ok(a) = Axis2::try_from(a)
+ && let Some(axis) = axes.get(&(base_level + variables.len()))
+ && let Some(label) = &axis.label
+ {
+ let out = &mut look.areas[Area::Labels(a)];
+ *out = AreaStyle::default_for_area(Area::Labels(a));
+ let style = label.style.get(&styles);
+ Style::decode_area(
+ style,
+ label.text_frame_style.as_ref().and_then(|r| r.get(styles)),
+ out,
+ );
+ style.is_some_and(|s| s.visible.unwrap_or_default())
+ } else {
+ false
+ };
+ if a == Axis3::Y
+ && let Some(axis) = axes.get(&(base_level + variables.len() - 1))
+ {
+ Style::decode_area(
+ axis.major_ticks.style.get(&styles),
+ axis.major_ticks.tick_frame_style.get(&styles),
+ &mut look.areas[Area::Labels(Axis2::Y)],
+ );
+ }
+
+ if let Some(axis) = axes.get(&base_level)
+ && axis.major_ticks.label_angle == -90.0
+ {
+ if a == Axis3::X {
+ *rotate_inner_column_labels = true;
+ } else {
+ *rotate_outer_row_labels = true;
+ }
+ }
+
+ let variables = variables
+ .into_iter()
+ .map(|(series, _level)| *series)
+ .collect::<Vec<_>>();
+
+ #[derive(Clone)]
+ struct CatBuilder {
+ /// The category we've built so far.
+ category: Category,
+
+ /// The range of leaf indexes covered by `category`.
+ ///
+ /// If `category` is a leaf, the range has a length of 1.
+ /// If `category` is a group, the length is at least 1.
+ leaves: Range<usize>,
+
+ /// How to find this category in its dimension.
+ location: CategoryLocator,
+ }
+
+ // Make leaf categories.
+ let mut coordinate_to_index = HashMap::new();
+ let mut cats = Vec::new();
+ for (index, value) in variables[0].values.iter().enumerate() {
+ let Some(row) = value.category() else {
+ continue;
+ };
+ coordinate_to_index.insert(row, CategoryLocator::new_leaf(index));
+ let name = variables[0].new_name(value, footnotes);
+ cats.push(CatBuilder {
+ category: Category::from(Leaf::new(name)),
+ leaves: cats.len()..cats.len() + 1,
+ location: CategoryLocator::new_leaf(cats.len()),
+ });
+ }
+ *variables[0].coordinate_to_index.borrow_mut() = coordinate_to_index;
+
+ // Now group them, in one pass per grouping variable, innermost first.
+ for j in 1..variables.len() {
+ let mut coordinate_to_index = HashMap::new();
+ let mut next_cats = Vec::with_capacity(cats.len());
+ let mut start = 0;
+ for end in 1..=cats.len() {
+ let dv1 = &variables[j].values[cats[start].leaves.start];
+ if end < cats.len()
+ && variables[j].values[cats[end].leaves.clone()]
+ .iter()
+ .all(|dv| &dv.value == &dv1.value)
+ {
+ } else {
+ let name = variables[j].map.lookup(dv1);
+ let next_cat = if end - start > 1 || name.is_number_or(|s| s.is_empty()) {
+ let name = variables[j].new_name(dv1, footnotes);
+ let mut group = Group::new(name);
+ for i in start..end {
+ group.push(cats[i].category.clone());
+ }
+ CatBuilder {
+ category: Category::from(group),
+ leaves: cats[start].leaves.start..cats[end - 1].leaves.end,
+ location: cats[start].location.parent(),
+ }
+ } else {
+ cats[start].clone()
+ };
+ coordinate_to_index
+ .insert(dv1.category().unwrap() /*XXX?*/, next_cat.location);
+ next_cats.push(next_cat);
+ start = end;
+ }
+ }
+ *variables[j].coordinate_to_index.borrow_mut() = coordinate_to_index;
+ cats = next_cats;
+ }
+
+ let dimension = Dimension::new(
+ Group::new(
+ variables[0]
+ .label
+ .as_ref()
+ .map_or_else(|| Value::empty(), |label| Value::new_user_text(label)),
+ )
+ .with_multiple(cats.into_iter().map(|cb| cb.category))
+ .with_show_label(show_label),
+ );
+
+ for variable in &variables {
+ variable.dimension_index.set(Some(dims.len()));
+ }
+ dims.push(Dim {
+ axis: a,
+ dimension,
+ coordinate: variables[0],
+ });
+ }
+
+ fn decode_dimensions<'a, 'b>(
+ variables: impl IntoIterator<Item = &'a str>,
+ series: &'b HashMap<&str, Series>,
+ axes: &HashMap<usize, &Axis>,
+ styles: &HashMap<&str, &Style>,
+ a: Axis3,
+ look: &mut Look,
+ rotate_inner_column_labels: &mut bool,
+ rotate_outer_row_labels: &mut bool,
+ footnotes: &pivot::Footnotes,
+ level_ofs: usize,
+ dims: &mut Vec<Dim<'b>>,
+ ) {
+ let variables = variables
+ .into_iter()
+ .zip(level_ofs..)
+ .map(|(variable_name, level)| {
+ series
+ .get(variable_name)
+ .filter(|s| !s.values.is_empty())
+ .map(|s| (s, level))
+ })
+ .collect::<Vec<_>>();
+ let mut dim_vars = Vec::new();
+ for var in variables {
+ if let Some((var, level)) = var {
+ dim_vars.push((var, level));
+ } else if !dim_vars.is_empty() {
+ decode_dimension(
+ &dim_vars,
+ axes,
+ styles,
+ a,
+ look,
+ rotate_inner_column_labels,
+ rotate_outer_row_labels,
+ footnotes,
+ dims,
+ );
+ dim_vars.clear();
+ }
+ }
+ if !dim_vars.is_empty() {
+ decode_dimension(
+ &dim_vars,
+ axes,
+ styles,
+ a,
+ look,
+ rotate_inner_column_labels,
+ rotate_outer_row_labels,
+ footnotes,
+ dims,
+ );
+ }
+ }
+
+ struct Dim<'a> {
+ axis: Axis3,
+ dimension: pivot::Dimension,
+ coordinate: &'a Series,
+ }
+
+ let mut rotate_inner_column_labels = false;
+ let mut rotate_outer_row_labels = false;
+ let cross = &graph.faceting.cross.children;
+ let columns = cross
+ .first()
+ .map(|child| child.variables())
+ .unwrap_or_default();
+ let mut dims = Vec::new();
+ decode_dimensions(
+ columns.into_iter().map(|vr| vr.reference.as_str()),
+ &series,
+ &axes,
+ &styles,
+ Axis3::X,
+ &mut look,
+ &mut rotate_inner_column_labels,
+ &mut rotate_outer_row_labels,
+ &footnotes,
+ 1,
+ &mut dims,
+ );
+ let rows = cross
+ .get(1)
+ .map(|child| child.variables())
+ .unwrap_or_default();
+ decode_dimensions(
+ rows.into_iter().map(|vr| vr.reference.as_str()),
+ &series,
+ &axes,
+ &styles,
+ Axis3::Y,
+ &mut look,
+ &mut rotate_inner_column_labels,
+ &mut rotate_outer_row_labels,
+ &footnotes,
+ 1 + columns.len(),
+ &mut dims,
+ );
+
+ let mut level_ofs = columns.len() + rows.len() + 1;
+ for layers in [&graph.faceting.layers1, &graph.faceting.layers2] {
+ decode_dimensions(
+ layers.iter().map(|layer| layer.variable.as_str()),
+ &series,
+ &axes,
+ &styles,
+ Axis3::Y,
+ &mut look,
+ &mut rotate_inner_column_labels,
+ &mut rotate_outer_row_labels,
+ &footnotes,
+ level_ofs,
+ &mut dims,
+ );
+ level_ofs += layers.len();
+ }
+
+ let cell = series.get("cell").unwrap()/*XXX*/;
+ let mut coords = Vec::with_capacity(dims.len());
+ let (cell_formats, format_map) = graph.interval.labeling.decode_format_map(&series);
+ let cell_footnotes =
+ graph
+ .interval
+ .labeling
+ .children
+ .iter()
+ .find_map(|child| match child {
+ LabelingChild::Footnotes(footnotes) => series.get(footnotes.variable.as_str()),
+ _ => None,
+ });
+ let mut data = HashMap::new();
+ for (i, cell) in cell.values.iter().enumerate() {
+ coords.clear();
+ for dim in &dims {
+ // XXX indexing of values, and unwrap
+ let coordinate = dim.coordinate.values[i].category().unwrap();
+ let Some(index) = dim
+ .coordinate
+ .coordinate_to_index
+ .borrow()
+ .get(&coordinate)
+ .and_then(CategoryLocator::as_leaf)
+ else {
+ panic!("can't find {coordinate}") // XXX
+ };
+ coords.push(index);
+ }
+
+ let format = if let Some(cell_formats) = &cell_formats {
+ // XXX indexing of values
+ cell_formats.values[i].as_format(&format_map)
+ } else {
+ F40_2
+ };
+ let mut value = cell.as_pivot_value(format);
+
+ if let Some(cell_footnotes) = &cell_footnotes {
+ // XXX indexing
+ let dv = &cell_footnotes.values[i];
+ if let Some(s) = dv.value.as_string() {
+ for part in s.split(',') {
+ if let Ok(index) = part.parse::<usize>()
+ && let Some(index) = index.checked_sub(1)
+ && let Some(footnote) = footnotes.get(index)
+ {
+ value = value.with_footnote(footnote);
+ }
+ }
+ }
+ }
+ if let Value {
+ inner: ValueInner::Number(NumberValue { value: None, .. }),
+ styling: None,
+ } = &value
+ {
+ // A system-missing value without a footnote represents an empty cell.
+ } else {
+ // XXX cell_index might be invalid?
+ data.insert(coords.clone(), value);
+ }
+ }
+
+ for child in &graph.facet_layout.children {
+ let FacetLayoutChild::SetCellProperties(scp) = child else {
+ continue;
+ };
+
+ #[derive(Copy, Clone, Debug, PartialEq)]
+ enum TargetType {
+ Graph,
+ Labeling,
+ Interval,
+ MajorTicks,
+ }
+
+ impl TargetType {
+ fn from_id(
+ target: &str,
+ graph: &Graph,
+ major_ticks: &HashMap<&str, &MajorTicks>,
+ ) -> Option<Self> {
+ if let Some(id) = &graph.id
+ && id == target
+ {
+ Some(Self::Graph)
+ } else if let Some(id) = &graph.interval.labeling.id
+ && id == target
+ {
+ Some(Self::Labeling)
+ } else if let Some(id) = &graph.interval.id
+ && id == target
+ {
+ Some(Self::Interval)
+ } else if major_ticks.contains_key(target) {
+ Some(Self::MajorTicks)
+ } else {
+ None
+ }
+ }
+ }
+
+ #[derive(Default)]
+ struct Target<'a> {
+ graph: Option<&'a Style>,
+ labeling: Option<&'a Style>,
+ interval: Option<&'a Style>,
+ major_ticks: Option<&'a Style>,
+ frame: Option<&'a Style>,
+ format: Option<(&'a SetFormat, Option<TargetType>)>,
+ }
+ impl<'a> Target<'a> {
+ fn decode(
+ &self,
+ intersect: &Intersect,
+ look: &mut Look,
+ series: &HashMap<&str, Series>,
+ dims: &mut [Dim],
+ data: &mut HashMap<Vec<usize>, Value>,
+ ) {
+ let mut wheres = Vec::new();
+ let mut alternating = false;
+ for child in &intersect.children {
+ match child {
+ IntersectChild::Where(w) => wheres.push(w),
+ IntersectChild::IntersectWhere(_) => {
+ // Presumably we should do something (but we don't).
+ }
+ IntersectChild::Alternating => alternating = true,
+ IntersectChild::Empty => (),
+ }
+ }
+
+ match self {
+ Self {
+ graph: Some(_),
+ labeling: Some(_),
+ interval: None,
+ major_ticks: None,
+ frame: None,
+ format: None,
+ } if alternating => {
+ let mut style = AreaStyle::default_for_area(Area::Data(RowParity::Odd));
+ Style::decode_area(self.labeling, self.graph, &mut style);
+ let font_style = &mut look.areas[Area::Data(RowParity::Odd)].font_style;
+ font_style.fg = style.font_style.fg;
+ font_style.bg = style.font_style.bg;
+ }
+ Self {
+ graph: Some(_),
+ labeling: None,
+ interval: None,
+ major_ticks: None,
+ frame: None,
+ format: None,
+ } => {
+ // `graph.width` likely just sets the width of the table as a whole.
+ }
+ Self {
+ graph: None,
+ labeling: None,
+ interval: None,
+ major_ticks: None,
+ frame: None,
+ format: None,
+ } => {
+ // No-op. (Presumably there's a setMetaData we don't care about.)
+ }
+ Self {
+ format: Some((_, Some(TargetType::MajorTicks))),
+ ..
+ }
+ | Self {
+ major_ticks: Some(_),
+ ..
+ }
+ | Self { frame: Some(_), .. }
+ if !wheres.is_empty() =>
+ {
+ // Formatting for individual row or column labels.
+ for w in &wheres {
+ let Some(s) = series.get(w.variable.as_str()) else {
+ continue;
+ };
+ let Some(dim_index) = s.dimension_index.get() else {
+ continue;
+ };
+ let dimension = &mut dims[dim_index].dimension;
+ let Ok(axis) = Axis2::try_from(dims[dim_index].axis) else {
+ continue;
+ };
+ for index in
+ w.include.split(';').filter_map(|s| s.parse::<usize>().ok())
+ {
+ if let Some(locator) =
+ s.coordinate_to_index.borrow().get(&index).copied()
+ && let Some(category) = dimension.root.category_mut(locator)
+ {
+ Style::apply_to_value(
+ category.name_mut(),
+ self.format.map(|(sf, _)| sf),
+ self.major_ticks,
+ self.frame,
+ &look.areas[Area::Labels(axis)],
+ );
+ }
+ }
+ }
+ }
+ Self {
+ format: Some((_, Some(TargetType::Labeling))),
+ ..
+ }
+ | Self {
+ labeling: Some(_), ..
+ }
+ | Self {
+ interval: Some(_), ..
+ } => {
+ // Formatting for individual cells or groups of them
+ // with some dimensions in common.
+ let mut include = vec![HashSet::new(); dims.len()];
+ for w in &wheres {
+ let Some(s) = series.get(w.variable.as_str()) else {
+ continue;
+ };
+ let Some(dim_index) = s.dimension_index.get() else {
+ // Group indexes may be included even though
+ // they are redundant. Ignore them.
+ continue;
+ };
+ for index in
+ w.include.split(';').filter_map(|s| s.parse::<usize>().ok())
+ {
+ if let Some(locator) =
+ s.coordinate_to_index.borrow().get(&index).copied()
+ && let Some(leaf_index) = locator.as_leaf()
+ {
+ include[dim_index].insert(leaf_index);
+ }
+ }
+ }
+
+ // XXX This is inefficient in the common case where
+ // all of the dimensions are matched. We should use
+ // a heuristic where if all of the dimensions are
+ // matched and the product of n[*] is less than the
+ // number of cells then iterate through all the
+ // possibilities rather than all the cells. Or even
+ // only do it if there is just one possibility.
+ for (indexes, value) in data {
+ let mut skip = false;
+ for (dimension, index) in indexes.iter().enumerate() {
+ if !include[dimension].is_empty()
+ && !include[dimension].contains(index)
+ {
+ skip = true;
+ break;
+ }
+ }
+ if !skip {
+ Style::apply_to_value(
+ value,
+ self.format.map(|(sf, _)| sf),
+ self.major_ticks,
+ self.frame,
+ &look.areas[Area::Data(RowParity::Even)],
+ );
+ }
+ }
+ }
+ _ => (),
+ }
+ }
+ }
+ let mut target = Target::default();
+ for set in &scp.sets {
+ match set {
+ Set::SetStyle(set_style) => {
+ if let Some(style) = set_style.style.get(&styles) {
+ match TargetType::from_id(&set_style.target, graph, &major_ticks) {
+ Some(TargetType::Graph) => target.graph = Some(style),
+ Some(TargetType::Interval) => target.interval = Some(style),
+ Some(TargetType::Labeling) => target.labeling = Some(style),
+ Some(TargetType::MajorTicks) => target.major_ticks = Some(style),
+ None => (),
+ }
+ }
+ }
+ Set::SetFrameStyle(set_frame_style) => {
+ target.frame = set_frame_style.style.get(&styles)
+ }
+ Set::SetFormat(sf) => {
+ let target_type = TargetType::from_id(&sf.target, graph, &major_ticks);
+ target.format = Some((sf, target_type))
+ }
+ Set::SetMetaData(_) => (),
+ }
+ }
+
+ match (
+ scp.union_.as_ref(),
+ scp.apply_to_converse.unwrap_or_default(),
+ ) {
+ (Some(union_), false) => {
+ for intersect in &union_.intersects {
+ target.decode(
+ intersect,
+ &mut look,
+ &series,
+ dims.as_mut_slice(),
+ &mut data,
+ );
+ }
+ }
+ (Some(_), true) => {
+ // Not implemented, not seen in the corpus.
+ }
+ (None, true) => {
+ if target
+ .format
+ .is_some_and(|(_sf, target_type)| target_type == Some(TargetType::Labeling))
+ || target.labeling.is_some()
+ || target.interval.is_some()
+ {
+ for value in data.values_mut() {
+ Style::apply_to_value(
+ value,
+ target.format.map(|(sf, _target_type)| sf),
+ None,
+ None,
+ &look.areas[Area::Data(RowParity::Even)],
+ );
+ }
+ }
+ }
+ (None, false) => {
+ // Appears to be used to set the font for something—but what?
+ }
+ }
+ }
+
+ let dimensions = dims
+ .into_iter()
+ .map(|dim| (dim.axis, dim.dimension))
+ .collect::<Vec<_>>();
+ let mut pivot_table = PivotTable::new(dimensions)
+ .with_look(Arc::new(look))
+ .with_data(data);
+ if let Some(title) = title {
+ pivot_table = pivot_table.with_title(title);
+ }
+ if let Some(caption) = caption {
+ pivot_table = pivot_table.with_caption(caption);
+ }
+ Ok(pivot_table)
+ }
+}
+
+struct Series {
+ name: String,
+ label: Option<String>,
+ format: crate::format::Format,
+ remapped: bool,
+ values: Vec<DataValue>,
+ map: Map,
+ affixes: Vec<Affix>,
+ coordinate_to_index: RefCell<HashMap<usize, CategoryLocator>>,
+ dimension_index: Cell<Option<usize>>,
+}
+
+impl Series {
+ fn new(name: String, values: Vec<DataValue>, map: Map) -> Self {
+ Self {
+ name,
+ label: None,
+ format: F8_0,
+ remapped: false,
+ values,
+ map,
+ affixes: Vec::new(),
+ coordinate_to_index: Default::default(),
+ dimension_index: Default::default(),
+ }
+ }
+ fn with_format(self, format: crate::format::Format) -> Self {
+ Self { format, ..self }
+ }
+ fn with_label(self, label: Option<String>) -> Self {
+ Self { label, ..self }
+ }
+ fn with_affixes(self, affixes: Vec<Affix>) -> Self {
+ Self { affixes, ..self }
+ }
+ fn add_affixes(&self, mut value: Value, footnotes: &pivot::Footnotes) -> Value {
+ for affix in &self.affixes {
+ if let Some(index) = affix.defines_reference.checked_sub(1)
+ && let Ok(index) = usize::try_from(index)
+ && let Some(footnote) = footnotes.get(index)
+ {
+ value = value.with_footnote(footnote);
+ }
+ }
+ value
+ }
+
+ fn new_name(&self, dv: &DataValue, footnotes: &pivot::Footnotes) -> Value {
+ let dv = self.map.lookup(dv);
+ let name = Value::new_datum(dv);
+ self.add_affixes(name, &footnotes)
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum VisChild {
+ Extension(VisualizationExtension),
+ UserSource(UserSource),
+ SourceVariable(SourceVariable),
+ DerivedVariable(DerivedVariable),
+ CategoricalDomain(CategoricalDomain),
+ Graph(Graph),
+ LabelFrame(LabelFrame),
+ Container(Container),
+ Style(Style),
+ LayerController(LayerController),
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename = "extension", rename_all = "camelCase")]
+struct VisualizationExtension {
+ #[serde(rename = "@showGridline")]
+ show_gridline: Option<bool>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct SourceVariable {
+ #[serde(rename = "@id")]
+ id: String,
+
+ /// The `source-name` in the `tableData.bin` member.
+ #[serde(rename = "@source")]
+ source: String,
+
+ /// The name of a variable within the source, corresponding to the
+ /// `variable-name` in the `tableData.bin` member.
+ #[serde(rename = "@sourceName")]
+ source_name: String,
+
+ /// Variable label, if any.
+ #[serde(rename = "@label")]
+ label: Option<String>,
+
+ /// A variable whose string values correspond one-to-one with the values of
+ /// this variable and are suitable as value labels.
+ #[serde(rename = "@labelVariable")]
+ label_variable: Option<Ref<SourceVariable>>,
+
+ #[serde(default, rename = "extension")]
+ extensions: Vec<VariableExtension>,
+ format: Option<Format>,
+ string_format: Option<StringFormat>,
+}
+
+impl SourceVariable {
+ fn decode(
+ &self,
+ data: &HashMap<String, HashMap<String, Vec<DataValue>>>,
+ series: &HashMap<&str, Series>,
+ ) -> Result<Series, ()> {
+ let label_series = if let Some(label_variable) = &self.label_variable {
+ let Some(label_series) = series.get(label_variable.references.as_str()) else {
+ return Err(());
+ };
+ Some(label_series)
+ } else {
+ None
+ };
+
+ let Some(data) = data
+ .get(&self.source)
+ .and_then(|source| source.get(&self.source_name))
+ else {
+ todo!()
+ };
+ let mut map = Map::new();
+ let (format, affixes) = map.remap_formats(&self.format, &self.string_format);
+ let mut data = data.clone();
+ if !map.0.is_empty() {
+ map.apply(&mut data);
+ } else if let Some(label_series) = label_series {
+ map.insert_labels(&data, label_series, format);
+ }
+ Ok(Series::new(self.id.clone(), data, map)
+ .with_format(format)
+ .with_affixes(affixes)
+ .with_label(self.label.clone()))
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct DerivedVariable {
+ #[serde(rename = "@id")]
+ id: String,
+
+ /// An expression that defines the variable's value.
+ #[serde(rename = "@value")]
+ value: String,
+ #[serde(default, rename = "extension")]
+ extensions: Vec<VariableExtension>,
+ format: Option<Format>,
+ string_format: Option<StringFormat>,
+ #[serde(default, rename = "valueMapEntry")]
+ value_map: Vec<ValueMapEntry>,
+}
+
+impl DerivedVariable {
+ fn decode(&self, series: &HashMap<&str, Series>) -> Result<Series, ()> {
+ let mut values = if self.value == "constant(0)" {
+ let n_values = if let Some(series) = series.values().next() {
+ series.values.len()
+ } else {
+ return Err(());
+ };
+ (0..n_values)
+ .map(|_| DataValue {
+ index: Some(0.0),
+ value: Datum::Number(Some(0.0)),
+ })
+ .collect()
+ } else if self.value.starts_with("constant") {
+ vec![]
+ } else if let Some(rest) = self.value.strip_prefix("map(")
+ && let Some(var_name) = rest.strip_suffix(")")
+ {
+ let Some(dependency) = series.get(var_name) else {
+ return Err(());
+ };
+ dependency.values.clone()
+ } else {
+ unreachable!()
+ };
+ let mut map = Map::new();
+ map.remap_vmes(&self.value_map);
+ map.apply(&mut values);
+ map.remap_formats(&self.format, &self.string_format);
+ if values
+ .iter()
+ .all(|value| value.value.is_string_and(|s| s.is_empty()))
+ {
+ values.clear();
+ }
+ Ok(Series::new(self.id.clone(), values, map))
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename = "extension", rename_all = "camelCase")]
+struct VariableExtension {
+ #[serde(rename = "@from")]
+ from: String,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct UserSource {
+ #[serde(rename = "@id")]
+ id: String,
+
+ #[serde(rename = "@missing")]
+ missing: Option<Missing>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct CategoricalDomain {
+ variable_reference: VariableReference,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct VariableReference {
+ #[serde(rename = "@ref")]
+ reference: String,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum Missing {
+ Listwise,
+ Pairwise,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct StringFormat {
+ #[serde(default, rename = "relabel")]
+ relabels: Vec<Relabel>,
+ #[serde(default, rename = "affix")]
+ affixes: Vec<Affix>,
+}
+
+#[derive(Deserialize, Debug, Default)]
+#[serde(rename_all = "camelCase")]
+struct Format {
+ #[serde(rename = "@baseFormat")]
+ base_format: Option<BaseFormat>,
+ #[serde(rename = "@errorCharacter")]
+ error_character: Option<char>,
+ #[serde(rename = "@separatorChars")]
+ separator_chars: Option<String>,
+ #[serde(rename = "@mdyOrder")]
+ mdy_order: Option<MdyOrder>,
+ #[serde(rename = "@showYear")]
+ show_year: Option<bool>,
+ #[serde(rename = "@showQuarter")]
+ show_quarter: Option<bool>,
+ #[serde(rename = "@quarterPrefix")]
+ quarter_prefix: Option<String>,
+ #[serde(rename = "@quarterSuffix")]
+ quarter_suffix: Option<String>,
+ #[serde(rename = "@yearAbbreviation")]
+ year_abbreviation: Option<bool>,
+ #[serde(rename = "@showMonth")]
+ show_month: Option<bool>,
+ #[serde(rename = "@monthFormat")]
+ month_format: Option<MonthFormat>,
+ #[serde(rename = "@dayPadding")]
+ day_padding: Option<bool>,
+ #[serde(rename = "@dayOfMonthPadding")]
+ day_of_month_padding: Option<bool>,
+ #[serde(rename = "@showWeek")]
+ show_week: Option<bool>,
+ #[serde(rename = "@weekPadding")]
+ week_padding: Option<bool>,
+ #[serde(rename = "@weekSuffix")]
+ week_suffix: Option<String>,
+ #[serde(rename = "@showDayOfWeek")]
+ show_day_of_week: Option<bool>,
+ #[serde(rename = "@dayOfWeekAbbreviation")]
+ day_of_week_abbreviation: Option<bool>,
+ #[serde(rename = "hourPadding")]
+ hour_padding: Option<bool>,
+ #[serde(rename = "minutePadding")]
+ minute_padding: Option<bool>,
+ #[serde(rename = "secondPadding")]
+ second_padding: Option<bool>,
+ #[serde(rename = "@showDay")]
+ show_day: Option<bool>,
+ #[serde(rename = "@showHour")]
+ show_hour: Option<bool>,
+ #[serde(rename = "@showMinute")]
+ show_minute: Option<bool>,
+ #[serde(rename = "@showSecond")]
+ show_second: Option<bool>,
+ #[serde(rename = "@showMillis")]
+ show_millis: Option<bool>,
+ #[serde(rename = "@dayType")]
+ day_type: Option<DayType>,
+ #[serde(rename = "@hourFormat")]
+ hour_format: Option<HourFormat>,
+ #[serde(rename = "@minimumIntegerDigits")]
+ minimum_integer_digits: Option<usize>,
+ #[serde(rename = "@maximumFractionDigits")]
+ maximum_fraction_digits: Option<i64>,
+ #[serde(rename = "@minimumFractionDigits")]
+ minimum_fraction_digits: Option<usize>,
+ #[serde(rename = "@useGrouping")]
+ use_grouping: Option<bool>,
+ #[serde(rename = "@scientific")]
+ scientific: Option<Scientific>,
+ #[serde(rename = "@small")]
+ small: Option<f64>,
+ #[serde(default, rename = "@prefix")]
+ prefix: String,
+ #[serde(default, rename = "@suffix")]
+ suffix: String,
+ #[serde(rename = "@tryStringsAsNumbers")]
+ try_strings_as_numbers: Option<bool>,
+ #[serde(rename = "@negativesOutside")]
+ negatives_outside: Option<bool>,
+ #[serde(default, rename = "relabel")]
+ relabels: Vec<Relabel>,
+ #[serde(default, rename = "affix")]
+ affixes: Vec<Affix>,
+}
+
+impl Format {
+ fn decode(&self) -> crate::format::Format {
+ if self.base_format.is_some() {
+ SignificantDateTimeFormat::from(self).decode()
+ } else {
+ SignificantNumberFormat::from(self).decode()
+ }
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct NumberFormat {
+ #[serde(rename = "@minimumIntegerDigits")]
+ minimum_integer_digits: Option<i64>,
+ #[serde(rename = "@maximumFractionDigits")]
+ maximum_fraction_digits: Option<i64>,
+ #[serde(rename = "@minimumFractionDigits")]
+ minimum_fraction_digits: Option<i64>,
+ #[serde(rename = "@useGrouping")]
+ use_grouping: Option<bool>,
+ #[serde(rename = "@scientific")]
+ scientific: Option<Scientific>,
+ #[serde(rename = "@small")]
+ small: Option<f64>,
+ #[serde(default, rename = "@prefix")]
+ prefix: String,
+ #[serde(default, rename = "@suffix")]
+ suffix: String,
+ #[serde(default, rename = "affix")]
+ affixes: Vec<Affix>,
+}
+
+struct SignificantNumberFormat<'a> {
+ scientific: Option<Scientific>,
+ prefix: &'a str,
+ suffix: &'a str,
+ use_grouping: Option<bool>,
+ maximum_fraction_digits: Option<i64>,
+}
+
+impl<'a> From<&'a NumberFormat> for SignificantNumberFormat<'a> {
+ fn from(value: &'a NumberFormat) -> Self {
+ Self {
+ scientific: value.scientific,
+ prefix: &value.prefix,
+ suffix: &value.suffix,
+ use_grouping: value.use_grouping,
+ maximum_fraction_digits: value.maximum_fraction_digits,
+ }
+ }
+}
+
+impl<'a> From<&'a Format> for SignificantNumberFormat<'a> {
+ fn from(value: &'a Format) -> Self {
+ Self {
+ scientific: value.scientific,
+ prefix: &value.prefix,
+ suffix: &value.suffix,
+ use_grouping: value.use_grouping,
+ maximum_fraction_digits: value.maximum_fraction_digits,
+ }
+ }
+}
+
+impl<'a> SignificantNumberFormat<'a> {
+ fn decode(&self) -> crate::format::Format {
+ let type_ = if self.scientific == Some(Scientific::True) {
+ Type::E
+ } else if self.prefix == "$" {
+ Type::Dollar
+ } else if self.suffix == "%" {
+ Type::Pct
+ } else if self.use_grouping == Some(true) {
+ Type::Comma
+ } else {
+ Type::F
+ };
+ let d = match self.maximum_fraction_digits {
+ Some(d) if (0..=15).contains(&d) => d,
+ _ => 2,
+ };
+ UncheckedFormat {
+ type_,
+ w: 40,
+ d: d as u8,
+ }
+ .fix()
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct DateTimeFormat {
+ #[serde(rename = "@baseFormat")]
+ base_format: BaseFormat,
+ #[serde(rename = "@separatorChars")]
+ separator_chars: Option<String>,
+ #[serde(rename = "@mdyOrder")]
+ mdy_order: Option<MdyOrder>,
+ #[serde(rename = "@showYear")]
+ show_year: Option<bool>,
+ #[serde(rename = "@showQuarter")]
+ show_quarter: Option<bool>,
+ #[serde(rename = "@quarterPrefix")]
+ quarter_prefix: Option<String>,
+ #[serde(rename = "@quarterSuffix")]
+ quarter_suffix: Option<String>,
+ #[serde(rename = "@yearAbbreviation")]
+ year_abbreviation: Option<bool>,
+ #[serde(rename = "@showMonth")]
+ show_month: Option<bool>,
+ #[serde(rename = "@monthFormat")]
+ month_format: Option<MonthFormat>,
+ #[serde(rename = "@dayPadding")]
+ day_padding: Option<bool>,
+ #[serde(rename = "@dayOfMonthPadding")]
+ day_of_month_padding: Option<bool>,
+ #[serde(rename = "@showWeek")]
+ show_week: Option<bool>,
+ #[serde(rename = "@weekPadding")]
+ week_padding: Option<bool>,
+ #[serde(rename = "@weekSuffix")]
+ week_suffix: Option<String>,
+ #[serde(rename = "@showDayOfWeek")]
+ show_day_of_week: Option<bool>,
+ #[serde(rename = "@dayOfWeekAbbreviation")]
+ day_of_week_abbreviation: Option<bool>,
+ #[serde(rename = "hourPadding")]
+ hour_padding: Option<bool>,
+ #[serde(rename = "minutePadding")]
+ minute_padding: Option<bool>,
+ #[serde(rename = "secondPadding")]
+ second_padding: Option<bool>,
+ #[serde(rename = "@showDay")]
+ show_day: Option<bool>,
+ #[serde(rename = "@showHour")]
+ show_hour: Option<bool>,
+ #[serde(rename = "@showMinute")]
+ show_minute: Option<bool>,
+ #[serde(rename = "@showSecond")]
+ show_second: Option<bool>,
+ #[serde(rename = "@showMillis")]
+ show_millis: Option<bool>,
+ #[serde(rename = "@dayType")]
+ day_type: Option<DayType>,
+ #[serde(rename = "@hourFormat")]
+ hour_format: Option<HourFormat>,
+ #[serde(default, rename = "affix")]
+ affixes: Vec<Affix>,
+}
+
+struct SignificantDateTimeFormat {
+ base_format: Option<BaseFormat>,
+ show_quarter: Option<bool>,
+ show_week: Option<bool>,
+ show_day: Option<bool>,
+ show_hour: Option<bool>,
+ show_second: Option<bool>,
+ show_millis: Option<bool>,
+ mdy_order: Option<MdyOrder>,
+ month_format: Option<MonthFormat>,
+ year_abbreviation: Option<bool>,
+}
+
+impl From<&Format> for SignificantDateTimeFormat {
+ fn from(value: &Format) -> Self {
+ Self {
+ base_format: value.base_format,
+ show_quarter: value.show_quarter,
+ show_week: value.show_week,
+ show_day: value.show_day,
+ show_hour: value.show_hour,
+ show_second: value.show_second,
+ show_millis: value.show_millis,
+ mdy_order: value.mdy_order,
+ month_format: value.month_format,
+ year_abbreviation: value.year_abbreviation,
+ }
+ }
+}
+impl From<&DateTimeFormat> for SignificantDateTimeFormat {
+ fn from(value: &DateTimeFormat) -> Self {
+ Self {
+ base_format: Some(value.base_format),
+ show_quarter: value.show_quarter,
+ show_week: value.show_week,
+ show_day: value.show_day,
+ show_hour: value.show_hour,
+ show_second: value.show_second,
+ show_millis: value.show_millis,
+ mdy_order: value.mdy_order,
+ month_format: value.month_format,
+ year_abbreviation: value.year_abbreviation,
+ }
+ }
+}
+impl SignificantDateTimeFormat {
+ fn decode(&self) -> crate::format::Format {
+ let type_ = match self.base_format {
+ Some(BaseFormat::Date) => {
+ let type_ = if self.show_quarter == Some(true) {
+ Type::QYr
+ } else if self.show_week == Some(true) {
+ Type::WkYr
+ } else {
+ match (self.mdy_order, self.month_format) {
+ (Some(MdyOrder::DayMonthYear), Some(MonthFormat::Number)) => Type::EDate,
+ (Some(MdyOrder::DayMonthYear), Some(MonthFormat::PaddedNumber)) => {
+ Type::EDate
+ }
+ (Some(MdyOrder::DayMonthYear), _) => Type::Date,
+ (Some(MdyOrder::YearMonthDay), _) => Type::SDate,
+ _ => Type::ADate,
+ }
+ };
+ let mut w = type_.min_width();
+ if self.year_abbreviation != Some(true) {
+ w += 2;
+ };
+ return UncheckedFormat { type_, w, d: 0 }.try_into().unwrap();
+ }
+ Some(BaseFormat::DateTime) => {
+ if self.mdy_order == Some(MdyOrder::YearMonthDay) {
+ Type::YmdHms
+ } else {
+ Type::DateTime
+ }
+ }
+ _ => {
+ if self.show_day == Some(true) {
+ Type::DTime
+ } else if self.show_hour == Some(true) {
+ Type::Time
+ } else {
+ Type::MTime
+ }
+ }
+ };
+ date_time_format(type_, self.show_second, self.show_millis)
+ }
+}
+
+impl DateTimeFormat {
+ fn decode(&self) -> crate::format::Format {
+ SignificantDateTimeFormat::from(self).decode()
+ }
+}
+
+fn date_time_format(
+ type_: Type,
+ show_second: Option<bool>,
+ show_millis: Option<bool>,
+) -> crate::format::Format {
+ let mut w = type_.min_width();
+ let mut d = 0;
+ if show_second == Some(true) {
+ w += 3;
+ if show_millis == Some(true) {
+ d = 3;
+ w += d as u16 + 1;
+ }
+ }
+ UncheckedFormat { type_, w, d }.try_into().unwrap()
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct ElapsedTimeFormat {
+ #[serde(rename = "@dayPadding")]
+ day_padding: Option<bool>,
+ #[serde(rename = "hourPadding")]
+ hour_padding: Option<bool>,
+ #[serde(rename = "minutePadding")]
+ minute_padding: Option<bool>,
+ #[serde(rename = "secondPadding")]
+ second_padding: Option<bool>,
+ #[serde(rename = "@showDay")]
+ show_day: Option<bool>,
+ #[serde(rename = "@showHour")]
+ show_hour: Option<bool>,
+ #[serde(rename = "@showMinute")]
+ show_minute: Option<bool>,
+ #[serde(rename = "@showSecond")]
+ show_second: Option<bool>,
+ #[serde(rename = "@showMillis")]
+ show_millis: Option<bool>,
+ #[serde(rename = "@showYear")]
+ show_year: Option<bool>,
+ #[serde(default, rename = "affix")]
+ affixes: Vec<Affix>,
+}
+
+impl ElapsedTimeFormat {
+ fn decode(&self) -> crate::format::Format {
+ let type_ = if self.show_day == Some(true) {
+ Type::DTime
+ } else if self.show_hour == Some(true) {
+ Type::Time
+ } else {
+ Type::MTime
+ };
+ date_time_format(type_, self.show_second, self.show_millis)
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum BaseFormat {
+ Date,
+ Time,
+ DateTime,
+ ElapsedTime,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum MdyOrder {
+ DayMonthYear,
+ MonthDayYear,
+ YearMonthDay,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum MonthFormat {
+ Long,
+ Short,
+ Number,
+ PaddedNumber,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum DayType {
+ Month,
+ Year,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum HourFormat {
+ #[serde(rename = "AMPM")]
+ AmPm,
+ #[serde(rename = "AS_24")]
+ As24,
+ #[serde(rename = "AS_12")]
+ As12,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum Scientific {
+ OnlyForSmall,
+ WhenNeeded,
+ True,
+ False,
+}
+
+#[derive(Clone, Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct Affix {
+ /// The footnote number as a natural number: 1 for the first footnote, 2 for
+ /// the second, and so on.
+ #[serde(rename = "@definesReference")]
+ defines_reference: u64,
+
+ /// Position for the footnote label.
+ #[serde(rename = "@position")]
+ position: Position,
+
+ /// Whether the affix is a suffix (true) or a prefix (false).
+ #[serde(rename = "@suffix")]
+ suffix: bool,
+
+ /// The text of the suffix or prefix. Typically a letter, e.g. `a` for
+ /// footnote 1, `b` for footnote 2, ...
+ #[serde(rename = "@value")]
+ value: String,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum Position {
+ Subscript,
+ Superscript,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Relabel {
+ #[serde(rename = "@from")]
+ from: f64,
+ #[serde(rename = "@to")]
+ to: String,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct ValueMapEntry {
+ #[serde(rename = "@from")]
+ from: String,
+ #[serde(rename = "@to")]
+ to: String,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Style {
+ #[serde(rename = "@id")]
+ id: Option<String>,
+
+ /// The text color or, in some cases, background color.
+ #[serde(rename = "@color")]
+ color: Option<Color>,
+
+ /// Not used.
+ #[serde(rename = "@color2")]
+ color2: Option<Color>,
+
+ /// Normally 0. The value -90 causes inner column or outer row labels to be
+ /// rotated vertically.
+ #[serde(rename = "@labelAngle")]
+ label_angle: Option<f64>,
+
+ #[serde(rename = "@border-bottom")]
+ border_bottom: Option<Border>,
+
+ #[serde(rename = "@border-top")]
+ border_top: Option<Border>,
+
+ #[serde(rename = "@border-left")]
+ border_left: Option<Border>,
+
+ #[serde(rename = "@border-right")]
+ border_right: Option<Border>,
+
+ #[serde(rename = "@border-bottom-color")]
+ border_bottom_color: Option<Color>,
+
+ #[serde(rename = "@border-top-color")]
+ border_top_color: Option<Color>,
+
+ #[serde(rename = "@border-left-color")]
+ border_left_color: Option<Color>,
+
+ #[serde(rename = "@border-right-color")]
+ border_right_color: Option<Color>,
+
+ #[serde(rename = "@font-family")]
+ font_family: Option<String>,
+
+ #[serde(rename = "@font-size")]
+ font_size: Option<String>,
+
+ #[serde(rename = "@font-weight")]
+ font_weight: Option<FontWeight>,
+
+ #[serde(rename = "@font-style")]
+ font_style: Option<FontStyle>,
+
+ #[serde(rename = "@font-underline")]
+ font_underline: Option<FontUnderline>,
+
+ #[serde(rename = "@margin-bottom")]
+ margin_bottom: Option<Length>,
+
+ #[serde(rename = "@margin-top")]
+ margin_top: Option<Length>,
+
+ #[serde(rename = "@margin-left")]
+ margin_left: Option<Length>,
+
+ #[serde(rename = "@margin-right")]
+ margin_right: Option<Length>,
+
+ #[serde(rename = "@textAlignment")]
+ text_alignment: Option<TextAlignment>,
+
+ #[serde(rename = "@labelLocationHorizontal")]
+ label_location_horizontal: Option<LabelLocation>,
+
+ #[serde(rename = "@labelLocationVertical")]
+ label_location_vertical: Option<LabelLocation>,
+
+ #[serde(rename = "@size")]
+ size: Option<String>,
+
+ #[serde(rename = "@width")]
+ width: Option<String>,
+
+ #[serde(rename = "@visible")]
+ visible: Option<bool>,
+
+ #[serde(rename = "@decimal-offset")]
+ decimal_offset: Option<Length>,
+}
+
+impl Style {
+ fn apply_to_value(
+ value: &mut Value,
+ sf: Option<&SetFormat>,
+ fg: Option<&Style>,
+ bg: Option<&Style>,
+ base_style: &AreaStyle,
+ ) {
+ if let Some(sf) = sf {
+ if sf.reset == Some(true) {
+ value.styling_mut().footnotes.clear();
+ }
+
+ let format = match &sf.child {
+ Some(SetFormatChild::Format(format)) => Some(format.decode()),
+ Some(SetFormatChild::NumberFormat(format)) => {
+ Some(SignificantNumberFormat::from(format).decode())
+ }
+ Some(SetFormatChild::StringFormat(_)) => None,
+ Some(SetFormatChild::DateTimeFormat(format)) => Some(format.decode()),
+ Some(SetFormatChild::ElapsedTimeFormat(format)) => Some(format.decode()),
+ None => None,
+ };
+ if let Some(format) = format {
+ match &mut value.inner {
+ ValueInner::Number(number) => {
+ number.format = format;
+ }
+ ValueInner::String(string) => {
+ if format.type_().category() == format::Category::Date
+ && let Ok(date_time) =
+ NaiveDateTime::parse_from_str(&string.s, "%Y-%m-%dT%H:%M:%S%.3f")
+ {
+ value.inner = ValueInner::Number(NumberValue {
+ show: None,
+ format,
+ honor_small: false,
+ value: Some(date_time_to_pspp(date_time)),
+ variable: None,
+ value_label: None,
+ })
+ } else if format.type_().category() == format::Category::Time
+ && let Ok(time) = NaiveTime::parse_from_str(&string.s, "%H:%M:%S%.3f")
+ {
+ value.inner = ValueInner::Number(NumberValue {
+ show: None,
+ format,
+ honor_small: false,
+ value: Some(time_to_pspp(time)),
+ variable: None,
+ value_label: None,
+ })
+ } else if let Ok(number) = string.s.parse::<f64>() {
+ value.inner = ValueInner::Number(NumberValue {
+ show: None,
+ format,
+ honor_small: false,
+ value: Some(number),
+ variable: None,
+ value_label: None,
+ })
+ }
+ }
+ _ => (),
+ }
+ }
+ }
+
+ if fg.is_some() || bg.is_some() {
+ let styling = value.styling_mut();
+ let font_style = styling
+ .font_style
+ .get_or_insert_with(|| base_style.font_style.clone());
+ let cell_style = styling
+ .cell_style
+ .get_or_insert_with(|| base_style.cell_style.clone());
+ Self::decode(fg, bg, cell_style, font_style);
+ }
+ }
+
+ fn decode(
+ fg: Option<&Style>,
+ bg: Option<&Style>,
+ cell_style: &mut CellStyle,
+ font_style: &mut pivot::FontStyle,
+ ) {
+ if let Some(fg) = fg {
+ if let Some(weight) = fg.font_weight {
+ font_style.bold = weight.is_bold();
+ }
+ if let Some(style) = fg.font_style {
+ font_style.italic = style.is_italic();
+ }
+ if let Some(underline) = fg.font_underline {
+ font_style.underline = underline.is_underline();
+ }
+ if let Some(color) = fg.color {
+ font_style.fg = color;
+ }
+ if let Some(font_size) = &fg.font_size {
+ if let Ok(size) = font_size
+ .trim_end_matches(|c: char| c.is_alphabetic())
+ .parse()
+ {
+ font_style.size = size;
+ } else {
+ // XXX warn?
+ }
+ }
+ if let Some(alignment) = fg.text_alignment {
+ cell_style.horz_align = alignment.as_horz_align(fg.decimal_offset);
+ }
+ if let Some(label_local_vertical) = fg.label_location_vertical {
+ cell_style.vert_align = label_local_vertical.into();
+ }
+ }
+ if let Some(bg) = bg {
+ if let Some(color) = bg.color {
+ font_style.bg = color;
+ }
+ }
+ }
+
+ fn decode_area(fg: Option<&Style>, bg: Option<&Style>, out: &mut AreaStyle) {
+ Self::decode(fg, bg, &mut out.cell_style, &mut out.font_style);
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum Border {
+ Solid,
+ Thick,
+ Thin,
+ Double,
+ None,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum FontWeight {
+ Regular,
+ Bold,
+}
+
+impl FontWeight {
+ fn is_bold(&self) -> bool {
+ *self == Self::Bold
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum FontStyle {
+ Regular,
+ Italic,
+}
+
+impl FontStyle {
+ fn is_italic(&self) -> bool {
+ *self == Self::Italic
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum FontUnderline {
+ None,
+ Underline,
+}
+
+impl FontUnderline {
+ fn is_underline(&self) -> bool {
+ *self == Self::Underline
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum TextAlignment {
+ Left,
+ Right,
+ Center,
+ Decimal,
+ Mixed,
+}
+
+impl TextAlignment {
+ fn as_horz_align(&self, decimal_offset: Option<Length>) -> Option<HorzAlign> {
+ match self {
+ TextAlignment::Left => Some(HorzAlign::Left),
+ TextAlignment::Right => Some(HorzAlign::Right),
+ TextAlignment::Center => Some(HorzAlign::Center),
+ TextAlignment::Decimal => Some(HorzAlign::Decimal {
+ offset: decimal_offset.unwrap_or_default().as_px_f64(),
+ decimal: Dot,
+ }),
+ TextAlignment::Mixed => None,
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum LabelLocation {
+ Positive,
+ Negative,
+ Center,
+}
+
+impl From<LabelLocation> for VertAlign {
+ fn from(value: LabelLocation) -> Self {
+ match value {
+ LabelLocation::Positive => VertAlign::Top,
+ LabelLocation::Negative => VertAlign::Bottom,
+ LabelLocation::Center => VertAlign::Middle,
+ }
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Graph {
+ #[serde(rename = "@id")]
+ id: Option<String>,
+
+ #[serde(rename = "@cellStyle")]
+ cell_style: Ref<Style>,
+
+ #[serde(rename = "@style")]
+ style: Ref<Style>,
+
+ #[serde(rename = "location")]
+ locations: Vec<Location>,
+ coordinates: Coordinates,
+ faceting: Faceting,
+ facet_layout: FacetLayout,
+ interval: Interval,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Coordinates;
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Location {
+ /// The part of the table being located.
+ #[serde(rename = "@part")]
+ part: Part,
+
+ /// How the location is determined.
+ #[serde(rename = "@method")]
+ method: Method,
+
+ /// Minimum size.
+ #[serde(rename = "@min")]
+ min: Option<Length>,
+
+ /// Maximum size.
+ #[serde(rename = "@max")]
+ max: Option<Length>,
+
+ /// An element to attach to. Required when method is attach or same, not
+ /// observed otherwise.
+ #[serde(rename = "@target")]
+ target: Option<String>,
+
+ #[serde(rename = "@value")]
+ value: Option<String>,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum Part {
+ Height,
+ Width,
+ Top,
+ Bottom,
+ Left,
+ Right,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum Method {
+ SizeToContent,
+ Attach,
+ Fixed,
+ Same,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Faceting {
+ #[serde(rename = "@id")]
+ id: Option<String>,
+
+ #[serde(default)]
+ layers1: Vec<Layer>,
+ cross: Cross,
+ #[serde(default)]
+ layers2: Vec<Layer>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Cross {
+ #[serde(rename = "$value")]
+ children: Vec<CrossChild>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum CrossChild {
+ /// No dimensions along this axis.
+ Unity,
+ /// Dimensions along this axis.
+ Nest(Nest),
+}
+
+impl CrossChild {
+ fn variables(&self) -> &[VariableReference] {
+ match self {
+ CrossChild::Unity => &[],
+ CrossChild::Nest(nest) => &nest.variable_references,
+ }
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Nest {
+ #[serde(rename = "variableReference")]
+ variable_references: Vec<VariableReference>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Layer {
+ #[serde(rename = "@variable")]
+ variable: String,
+
+ #[serde(rename = "@value")]
+ value: String,
+
+ #[serde(rename = "@visible")]
+ visible: Option<bool>,
+
+ #[serde(rename = "@titleVisible")]
+ title_visible: Option<bool>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct FacetLayout {
+ table_layout: TableLayout,
+ #[serde(rename = "$value")]
+ children: Vec<FacetLayoutChild>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum FacetLayoutChild {
+ SetCellProperties(SetCellProperties),
+ FacetLevel(FacetLevel),
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct TableLayout {
+ #[serde(rename = "@verticalTitlesInCorner")]
+ vertical_titles_in_corner: bool,
+
+ #[serde(rename = "@style")]
+ style: Option<Ref<Style>>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct SetCellProperties {
+ #[serde(rename = "@id")]
+ id: Option<String>,
+
+ #[serde(rename = "@applyToConverse")]
+ apply_to_converse: Option<bool>,
+
+ #[serde(rename = "$value")]
+ sets: Vec<Set>,
+
+ #[serde(rename = "union")]
+ union_: Option<Union>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Union {
+ #[serde(default, rename = "intersect")]
+ intersects: Vec<Intersect>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Intersect {
+ #[serde(default, rename = "$value")]
+ children: Vec<IntersectChild>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum IntersectChild {
+ Where(Where),
+ IntersectWhere(IntersectWhere),
+ Alternating,
+ #[serde(other)]
+ Empty,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Where {
+ #[serde(rename = "@variable")]
+ variable: String,
+ #[serde(rename = "@include")]
+ include: String,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct IntersectWhere {
+ #[serde(rename = "@variable")]
+ variable: String,
+
+ #[serde(rename = "@variable2")]
+ variable2: String,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum Set {
+ SetStyle(SetStyle),
+ SetFrameStyle(SetFrameStyle),
+ SetFormat(SetFormat),
+ SetMetaData(SetMetaData),
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct SetStyle {
+ #[serde(rename = "@target")]
+ target: String,
+
+ #[serde(rename = "@style")]
+ style: Ref<Style>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct SetMetaData {
+ #[serde(rename = "@target")]
+ target: Ref<Graph>,
+
+ #[serde(rename = "@key")]
+ key: String,
+
+ #[serde(rename = "@value")]
+ value: String,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct SetFormat {
+ #[serde(rename = "@target")]
+ target: String,
+
+ #[serde(rename = "@reset")]
+ reset: Option<bool>,
+
+ #[serde(rename = "$value")]
+ child: Option<SetFormatChild>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum SetFormatChild {
+ Format(Format),
+ NumberFormat(NumberFormat),
+ StringFormat(Vec<StringFormat>),
+ DateTimeFormat(DateTimeFormat),
+ ElapsedTimeFormat(ElapsedTimeFormat),
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct SetFrameStyle {
+ #[serde(rename = "@style")]
+ style: Ref<Style>,
+
+ #[serde(rename = "@target")]
+ target: Ref<MajorTicks>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Interval {
+ #[serde(rename = "@id")]
+ id: Option<String>,
+
+ #[serde(rename = "@style")]
+ style: Ref<Style>,
+
+ labeling: Labeling,
+ footnotes: Option<Footnotes>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Labeling {
+ #[serde(rename = "@id")]
+ id: Option<String>,
+
+ #[serde(rename = "@style")]
+ style: Option<Ref<Style>>,
+
+ #[serde(rename = "@variable")]
+ variable: String,
+
+ #[serde(default)]
+ children: Vec<LabelingChild>,
+}
+
+impl Labeling {
+ fn decode_format_map<'a>(
+ &self,
+ series: &'a HashMap<&str, Series>,
+ ) -> (Option<&'a Series>, HashMap<i64, crate::format::Format>) {
+ let mut map = HashMap::new();
+ let mut cell_format = None;
+ for child in &self.children {
+ if let LabelingChild::Formatting(formatting) = child {
+ cell_format = series.get(formatting.variable.as_str());
+ for mapping in &formatting.mappings {
+ if let Some(format) = &mapping.format {
+ map.insert(mapping.from, format.decode());
+ }
+ }
+ }
+ }
+ (cell_format, map)
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum LabelingChild {
+ Formatting(Formatting),
+ Format(Format),
+ Footnotes(Footnotes),
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Formatting {
+ #[serde(rename = "@variable")]
+ variable: String,
+
+ mappings: Vec<FormatMapping>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct FormatMapping {
+ #[serde(rename = "@from")]
+ from: i64,
+
+ format: Option<Format>,
+}
+
+#[derive(Clone, Debug, Default)]
+struct Footnote {
+ content: String,
+ marker: Option<String>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Footnotes {
+ #[serde(rename = "@superscript")]
+ superscript: Option<bool>,
+
+ #[serde(rename = "@variable")]
+ variable: String,
+
+ #[serde(default, rename = "footnoteMapping")]
+ mappings: Vec<FootnoteMapping>,
+}
+
+impl Footnotes {
+ fn decode(&self, dst: &mut BTreeMap<usize, Footnote>) {
+ for f in &self.mappings {
+ dst.entry(f.defines_reference.get() - 1)
+ .or_default()
+ .content = f.to.clone();
+ }
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct FootnoteMapping {
+ #[serde(rename = "@definesReference")]
+ defines_reference: NonZeroUsize,
+
+ #[serde(rename = "@from")]
+ from: i64,
+
+ #[serde(rename = "@to")]
+ to: String,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct FacetLevel {
+ #[serde(rename = "@id")]
+ id: Option<String>,
+
+ #[serde(rename = "@level")]
+ level: usize,
+
+ #[serde(rename = "@gap")]
+ gap: Option<Length>,
+ axis: Axis,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Axis {
+ #[serde(rename = "@style")]
+ style: Ref<Style>,
+
+ label: Option<Label>,
+ major_ticks: MajorTicks,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct MajorTicks {
+ #[serde(rename = "@id")]
+ id: String,
+
+ #[serde(rename = "@labelAngle")]
+ label_angle: f64,
+
+ #[serde(rename = "@length")]
+ length: Length,
+
+ #[serde(rename = "@style")]
+ style: Ref<Style>,
+
+ #[serde(rename = "@tickFrameStyle")]
+ tick_frame_style: Ref<Style>,
+
+ #[serde(rename = "@labelFrequency")]
+ label_frequency: Option<i64>,
+
+ #[serde(rename = "@stagger")]
+ stagger: Option<bool>,
+
+ gridline: Option<Gridline>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Gridline {
+ #[serde(rename = "@style")]
+ style: Ref<Style>,
+
+ #[serde(rename = "@zOrder")]
+ z_order: i64,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Label {
+ #[serde(rename = "@style")]
+ style: Ref<Style>,
+
+ #[serde(rename = "@textFrameStyle")]
+ text_frame_style: Option<Ref<Style>>,
+
+ #[serde(rename = "@purpose")]
+ purpose: Option<Purpose>,
+
+ #[serde(rename = "$value")]
+ child: LabelChild,
+}
+
+impl Label {
+ fn text(&self) -> &[Text] {
+ match &self.child {
+ LabelChild::Text(texts) => texts.as_slice(),
+ LabelChild::DescriptionGroup(_) => &[],
+ }
+ }
+
+ fn decode_style(&self, area_style: &mut AreaStyle, styles: &HashMap<&str, &Style>) {
+ let fg = self.style.get(styles);
+ let bg = self.text_frame_style.as_ref().and_then(|r| r.get(styles));
+ Style::decode_area(fg, bg, area_style);
+ }
+}
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Enum)]
+#[serde(rename_all = "camelCase")]
+enum Purpose {
+ Title,
+ SubTitle,
+ SubSubTitle,
+ Layer,
+ Footnote,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum LabelChild {
+ Text(Vec<Text>),
+ DescriptionGroup(DescriptionGroup),
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Text {
+ #[serde(rename = "@usesReference")]
+ uses_reference: Option<NonZeroUsize>,
+
+ #[serde(rename = "@definesReference")]
+ defines_reference: Option<NonZeroUsize>,
+
+ #[serde(rename = "@position")]
+ position: Option<Position>,
+
+ #[serde(rename = "@style")]
+ style: Option<Ref<Style>>,
+
+ #[serde(default, rename = "$text")]
+ text: String,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct DescriptionGroup {
+ #[serde(rename = "@target")]
+ target: Ref<Faceting>,
+
+ #[serde(rename = "@separator")]
+ separator: Option<String>,
+
+ #[serde(rename = "$value")]
+ children: Vec<DescriptionGroupChild>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum DescriptionGroupChild {
+ Description(Description),
+ Text(Text),
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Description {
+ #[serde(rename = "@name")]
+ name: Name,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum Name {
+ Variable,
+ Value,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct LabelFrame {
+ #[serde(rename = "@id")]
+ id: Option<String>,
+
+ #[serde(rename = "@style")]
+ style: Option<Ref<Style>>,
+
+ #[serde(rename = "location")]
+ locations: Vec<Location>,
+
+ label: Option<Label>,
+ paragraph: Option<Paragraph>,
+}
+
+impl LabelFrame {
+ fn decode_label(labels: &[&Label]) -> Option<Value> {
+ if !labels.is_empty() {
+ let mut s = String::new();
+ for t in labels {
+ if let LabelChild::Text(text) = &t.child {
+ for t in text {
+ if let Some(_defines_reference) = t.defines_reference {
+ // XXX footnote
+ }
+ s += &t.text;
+ }
+ }
+ }
+ Some(Value::new_user_text(s))
+ } else {
+ None
+ }
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Paragraph;
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Container {
+ #[serde(rename = "@style")]
+ style: Ref<Style>,
+
+ #[serde(default, rename = "extension")]
+ extensions: Option<ContainerExtension>,
+ #[serde(default)]
+ locations: Vec<Location>,
+ #[serde(default)]
+ label_frames: Vec<LabelFrame>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename = "extension", rename_all = "camelCase")]
+struct ContainerExtension {
+ #[serde(rename = "@combinedFootnotes")]
+ combined_footnotes: Option<bool>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct LayerController {
+ #[serde(rename = "@target")]
+ target: Option<Ref<Label>>,
+}
--- /dev/null
+use std::{
+ fmt::Debug,
+ io::{Read, Seek},
+ ops::Deref,
+ str::FromStr,
+ sync::Arc,
+};
+
+use binrw::{
+ BinRead, BinResult, Endian, Error as BinError, VecArgs, binread, error::ContextExt,
+ io::TakeSeekExt,
+};
+use chrono::DateTime;
+use displaydoc::Display;
+use encoding_rs::{Encoding, WINDOWS_1252};
+use enum_map::{EnumMap, enum_map};
+
+use crate::{
+ format::{
+ CC, Decimal, Decimals, Epoch, F40, Format, NumberStyle, Settings, Type, UncheckedFormat,
+ Width,
+ },
+ output::pivot::{
+ self, AreaStyle, Axis2, Axis3, BoxBorder, Color, FootnoteMarkerPosition,
+ FootnoteMarkerType, Footnotes, Group, HeadingRegion, HorzAlign, LabelPosition, Look,
+ PivotTable, PivotTableMetadata, PivotTableStyle, PrecomputedIndex, RowColBorder, RowParity,
+ StringValue, Stroke, TemplateValue, ValueStyle, VariableValue, VertAlign, parse_bool,
+ },
+ settings::Show,
+};
+
+#[derive(Debug, Display, thiserror::Error)]
+pub enum LightError {
+ /// Expected {expected} dimensions along axes, found {actual} dimensions ({n_layers} layers + {n_rows} rows + {n_columns} columns).
+ WrongAxisCount {
+ expected: usize,
+ actual: usize,
+ n_layers: usize,
+ n_rows: usize,
+ n_columns: usize,
+ },
+
+ /// Invalid dimension index {index} in table with {n} dimensions.
+ InvalidDimensionIndex { index: usize, n: usize },
+
+ /// Dimension with index {0} appears twice in table axes.
+ DuplicateDimensionIndex(usize),
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+pub struct LightTable {
+ header: Header,
+ #[br(args(header.version))]
+ titles: Titles,
+ #[br(parse_with(parse_vec), args(header.version))]
+ footnotes: Vec<Footnote>,
+ #[br(args(header.version))]
+ areas: Areas,
+ #[br(parse_with(parse_counted))]
+ borders: Borders,
+ #[br(parse_with(parse_counted))]
+ print_settings: PrintSettings,
+ #[br(if(header.version == Version::V3), parse_with(parse_counted))]
+ table_settings: TableSettings,
+ #[br(if(header.version == Version::V1), temp)]
+ _ts: Option<Counted<Sponge>>,
+ #[br(args(header.version))]
+ formats: Formats,
+ #[br(parse_with(parse_vec), args(header.version))]
+ dimensions: Vec<Dimension>,
+ axes: Axes,
+ #[br(parse_with(parse_vec), args(header.version))]
+ cells: Vec<Cell>,
+}
+
+impl LightTable {
+ fn decode_look(&self, encoding: &'static Encoding) -> Look {
+ Look {
+ name: self.table_settings.table_look.decode_optional(encoding),
+ hide_empty: self.table_settings.omit_empty,
+ row_label_position: if self.table_settings.show_row_labels_in_corner {
+ LabelPosition::Corner
+ } else {
+ LabelPosition::Nested
+ },
+ heading_widths: enum_map! {
+ HeadingRegion::Rows => self.header.min_row_heading_width as isize..=self.header.max_row_heading_width as isize,
+ HeadingRegion::Columns => self.header.min_column_heading_width as isize..=self.header.max_column_heading_width as isize,
+ },
+ footnote_marker_type: if self.table_settings.show_alphabetic_markers {
+ FootnoteMarkerType::Alphabetic
+ } else {
+ FootnoteMarkerType::Numeric
+ },
+ footnote_marker_position: if self.table_settings.footnote_marker_subscripts {
+ FootnoteMarkerPosition::Subscript
+ } else {
+ FootnoteMarkerPosition::Superscript
+ },
+ areas: self.areas.decode(encoding),
+ borders: self.borders.decode(),
+ print_all_layers: self.print_settings.alll_layers,
+ paginate_layers: self.print_settings.paginate_layers,
+ shrink_to_fit: enum_map! {
+ 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,
+ continuation: self
+ .print_settings
+ .continuation_string
+ .decode_optional(encoding),
+ n_orphan_lines: self.print_settings.n_orphan_lines,
+ }
+ }
+
+ pub fn decode(&self) -> Result<PivotTable, LightError> {
+ let encoding = self.formats.encoding();
+
+ let n1 = self.formats.n1();
+ let n2 = self.formats.n2();
+ let n3 = self.formats.n3();
+ let n3_inner = n3.and_then(|n3| n3.inner.as_ref());
+ let y1 = self.formats.y1();
+ let footnotes = self
+ .footnotes
+ .iter()
+ .map(|f| f.decode(encoding, &Footnotes::new()))
+ .collect();
+ let cells = self
+ .cells
+ .iter()
+ .map(|cell| {
+ (
+ PrecomputedIndex(cell.index as usize),
+ cell.value.decode(encoding, &footnotes),
+ )
+ })
+ .collect::<Vec<_>>();
+ let dimensions = self
+ .dimensions
+ .iter()
+ .map(|d| {
+ let mut root = Group::new(d.name.decode(encoding, &footnotes))
+ .with_show_label(!d.hide_dim_label);
+ for category in &d.categories {
+ category.decode(encoding, &footnotes, &mut root);
+ }
+ pivot::Dimension {
+ presentation_order: (0..root.len()).collect(), /*XXX*/
+ root,
+ hide_all_labels: d.hide_all_labels,
+ }
+ })
+ .collect::<Vec<_>>();
+ let pivot_table = PivotTable::new(self.axes.decode(dimensions)?)
+ .with_style(PivotTableStyle {
+ look: Arc::new(self.decode_look(encoding)),
+ rotate_inner_column_labels: self.header.rotate_inner_column_labels,
+ rotate_outer_row_labels: self.header.rotate_outer_row_labels,
+ show_grid_lines: self.borders.show_grid_lines,
+ show_title: n1.map_or(true, |x1| x1.show_title != 10),
+ show_caption: n1.map_or(true, |x1| x1.show_caption),
+ show_values: n1.map_or(None, |x1| x1.show_values),
+ show_variables: n1.map_or(None, |x1| x1.show_variables),
+ sizing: self.table_settings.sizing.decode(
+ &self.formats.column_widths,
+ n2.map_or(&[], |x2| &x2.row_heights),
+ ),
+ settings: Settings {
+ epoch: self.formats.y0.epoch(),
+ decimal: self.formats.y0.decimal(),
+ leading_zero: y1.map_or(false, |y1| y1.include_leading_zero),
+ ccs: self.formats.custom_currency.decode(encoding),
+ },
+ grouping: {
+ let grouping = self.formats.y0.grouping;
+ b",.' ".contains(&grouping).then_some(grouping as char)
+ },
+ small: n3.map_or(0.0, |n3| n3.small),
+ weight_format: F40,
+ })
+ .with_metadata(PivotTableMetadata {
+ command_local: y1.map(|y1| y1.command_local.decode(encoding)),
+ command_c: y1.map(|y1| y1.command.decode(encoding)),
+ language: y1.map(|y1| y1.language.decode(encoding)),
+ locale: y1.map(|y1| y1.locale.decode(encoding)),
+ dataset: n3_inner.and_then(|strings| strings.dataset.decode_optional(encoding)),
+ datafile: n3_inner.and_then(|strings| strings.datafile.decode_optional(encoding)),
+ date: n3_inner.and_then(|inner| {
+ if inner.date != 0 {
+ DateTime::from_timestamp(inner.date as i64, 0).map(|dt| dt.naive_utc())
+ } else {
+ None
+ }
+ }),
+ title: Some(Box::new(self.titles.title.decode(encoding, &footnotes))),
+ subtype: Some(Box::new(self.titles.subtype.decode(encoding, &footnotes))),
+ corner_text: self
+ .titles
+ .corner_text
+ .as_ref()
+ .map(|corner| Box::new(corner.decode(encoding, &footnotes))),
+ caption: self
+ .titles
+ .caption
+ .as_ref()
+ .map(|caption| Box::new(caption.decode(encoding, &footnotes))),
+ notes: self.table_settings.notes.decode_optional(encoding),
+ })
+ .with_footnotes(footnotes)
+ .with_data(cells);
+ Ok(pivot_table)
+ }
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct Header {
+ #[br(magic = b"\x01\0")]
+ version: Version,
+ #[br(parse_with(parse_bool), temp)]
+ _x0: bool,
+ #[br(parse_with(parse_bool), temp)]
+ _x1: bool,
+ #[br(parse_with(parse_bool))]
+ rotate_inner_column_labels: bool,
+ #[br(parse_with(parse_bool))]
+ rotate_outer_row_labels: bool,
+ #[br(parse_with(parse_bool), temp)]
+ _x2: bool,
+ #[br(temp)]
+ _x3: i32,
+ min_column_heading_width: u32,
+ max_column_heading_width: u32,
+ min_row_heading_width: u32,
+ max_row_heading_width: u32,
+ #[br(temp)]
+ _table_id: i64,
+}
+
+#[binread]
+#[br(little)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+enum Version {
+ #[br(magic = 1u32)]
+ V1,
+ #[br(magic = 3u32)]
+ V3,
+}
+
+#[binread]
+#[br(little, import(version: Version))]
+#[derive(Debug)]
+struct Titles {
+ #[br(args(version))]
+ title: Value,
+ #[br(temp)]
+ _1: Optional<One>,
+ #[br(args(version))]
+ subtype: Value,
+ #[br(temp)]
+ _2: Optional<One>,
+ #[br(magic = b'1')]
+ #[br(args(version))]
+ user_title: Value,
+ #[br(temp)]
+ _3: Optional<One>,
+ #[br(parse_with(parse_explicit_optional), args(version))]
+ corner_text: Option<Value>,
+ #[br(parse_with(parse_explicit_optional), args(version))]
+ caption: Option<Value>,
+}
+
+#[binread]
+#[br(little, magic = 1u8)]
+#[derive(Debug)]
+struct One;
+
+#[binread]
+#[br(little, magic = 0u8)]
+#[derive(Debug)]
+struct Zero;
+
+#[binrw::parser(reader, endian)]
+pub fn parse_explicit_optional<'a, T, A>(args: A, ...) -> BinResult<Option<T>>
+where
+ T: BinRead<Args<'a> = A>,
+{
+ let byte = <u8>::read_options(reader, endian, ())?;
+ match byte {
+ b'1' => Ok(Some(T::read_options(reader, endian, args)?)),
+ b'X' => Ok(None),
+ _ => Err(BinError::NoVariantMatch {
+ pos: reader.stream_position()? - 1,
+ }),
+ }
+}
+
+#[binrw::parser(reader, endian)]
+pub(super) fn parse_vec<T, A>(inner: A, ...) -> BinResult<Vec<T>>
+where
+ for<'a> T: BinRead<Args<'a> = A>,
+ A: Clone,
+ T: 'static,
+{
+ let count = u32::read_options(reader, endian, ())? as usize;
+ <Vec<T>>::read_options(reader, endian, VecArgs { count, inner })
+}
+
+#[binread]
+#[br(little, import(version: Version))]
+#[derive(Debug)]
+struct Footnote {
+ #[br(args(version))]
+ text: Value,
+ #[br(parse_with(parse_explicit_optional))]
+ #[br(args(version))]
+ marker: Option<Value>,
+ show: i32,
+}
+
+impl Footnote {
+ fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> pivot::Footnote {
+ pivot::Footnote::new(self.text.decode(encoding, footnotes))
+ .with_marker(self.marker.as_ref().map(|m| m.decode(encoding, footnotes)))
+ .with_show(self.show > 0)
+ }
+}
+
+#[binread]
+#[br(little, import(version: Version))]
+#[derive(Debug)]
+struct Areas {
+ #[br(temp)]
+ _1: Optional<Zero>,
+ #[br(args(version))]
+ areas: [Area; 8],
+}
+
+impl Areas {
+ fn decode(&self, encoding: &'static Encoding) -> EnumMap<pivot::Area, AreaStyle> {
+ EnumMap::from_fn(|area| {
+ let index = match area {
+ pivot::Area::Title => 0,
+ pivot::Area::Caption => 1,
+ pivot::Area::Footer => 2,
+ pivot::Area::Corner => 3,
+ pivot::Area::Labels(Axis2::X) => 4,
+ pivot::Area::Labels(Axis2::Y) => 5,
+ pivot::Area::Data(_) => 6,
+ pivot::Area::Layers => 7,
+ };
+ let data_row = match area {
+ pivot::Area::Data(row) => row,
+ _ => RowParity::default(),
+ };
+ self.areas[index].decode(encoding, data_row)
+ })
+ }
+}
+
+#[binrw::parser(reader, endian)]
+fn parse_color() -> BinResult<Color> {
+ let pos = reader.stream_position()?;
+ let string = U32String::read_options(reader, endian, ())?;
+ let string = string.decode(WINDOWS_1252);
+ if string.is_empty() {
+ Ok(Color::BLACK)
+ } else {
+ Color::from_str(&string).map_err(|error| binrw::Error::Custom {
+ pos,
+ err: Box::new(error),
+ })
+ }
+}
+
+#[binread]
+#[br(little, import(version: Version))]
+#[derive(Debug)]
+struct Area {
+ #[br(temp)]
+ _index: u8,
+ #[br(magic = b'1')]
+ typeface: U32String,
+ size: f32,
+ style: i32,
+ #[br(parse_with(parse_bool))]
+ underline: bool,
+ halign: i32,
+ valign: i32,
+ #[br(parse_with(parse_color))]
+ fg: Color,
+ #[br(parse_with(parse_color))]
+ bg: Color,
+ #[br(parse_with(parse_bool))]
+ alternate: bool,
+ #[br(parse_with(parse_color))]
+ alt_fg: Color,
+ #[br(parse_with(parse_color))]
+ alt_bg: Color,
+ #[br(if(version == Version::V3))]
+ margins: Margins,
+}
+
+impl Area {
+ fn decode(&self, encoding: &'static Encoding, data_row: RowParity) -> AreaStyle {
+ AreaStyle {
+ cell_style: pivot::CellStyle {
+ horz_align: match self.halign {
+ 0 => Some(HorzAlign::Center),
+ 2 => Some(HorzAlign::Left),
+ 4 => Some(HorzAlign::Right),
+ _ => None,
+ },
+ vert_align: match self.valign {
+ 0 => VertAlign::Middle,
+ 3 => VertAlign::Bottom,
+ _ => VertAlign::Top,
+ },
+ margins: enum_map! {
+ Axis2::X => [self.margins.left_margin, self.margins.right_margin],
+ Axis2::Y => [self.margins.top_margin, self.margins.bottom_margin]
+ },
+ },
+ font_style: pivot::FontStyle {
+ bold: (self.style & 1) != 0,
+ italic: (self.style & 2) != 0,
+ underline: self.underline,
+ font: self.typeface.decode(encoding),
+ fg: if data_row == RowParity::Odd && self.alternate {
+ self.alt_fg
+ } else {
+ self.fg
+ },
+ bg: if data_row == RowParity::Odd && self.alternate {
+ self.alt_bg
+ } else {
+ self.bg
+ },
+ size: (self.size / 1.33) as i32,
+ },
+ }
+ }
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug, Default)]
+struct Margins {
+ left_margin: i32,
+ right_margin: i32,
+ top_margin: i32,
+ bottom_margin: i32,
+}
+
+#[binread]
+#[br(big)]
+#[derive(Debug)]
+struct Borders {
+ #[br(magic(1u32), parse_with(parse_vec))]
+ borders: Vec<Border>,
+
+ #[br(parse_with(parse_bool))]
+ show_grid_lines: bool,
+
+ #[br(temp, magic(b"\0\0\0"))]
+ _1: (),
+}
+
+impl Borders {
+ fn decode(&self) -> EnumMap<pivot::Border, pivot::BorderStyle> {
+ let mut borders = pivot::Border::default_borders();
+ for border in &self.borders {
+ if let Some((border, style)) = border.decode() {
+ borders[border] = style;
+ } else {
+ // warning
+ }
+ }
+ borders
+ }
+}
+
+#[binread]
+#[br(big)]
+#[derive(Debug)]
+struct Border {
+ #[br(map(|index: u32| index as usize))]
+ index: usize,
+ stroke: i32,
+ color: u32,
+}
+
+impl Border {
+ fn decode(&self) -> Option<(pivot::Border, pivot::BorderStyle)> {
+ let border = match self.index {
+ 0 => pivot::Border::Title,
+ 1 => pivot::Border::OuterFrame(BoxBorder::Left),
+ 2 => pivot::Border::OuterFrame(BoxBorder::Top),
+ 3 => pivot::Border::OuterFrame(BoxBorder::Right),
+ 4 => pivot::Border::OuterFrame(BoxBorder::Bottom),
+ 5 => pivot::Border::InnerFrame(BoxBorder::Left),
+ 6 => pivot::Border::InnerFrame(BoxBorder::Top),
+ 7 => pivot::Border::InnerFrame(BoxBorder::Right),
+ 8 => pivot::Border::InnerFrame(BoxBorder::Bottom),
+ 9 => pivot::Border::DataLeft,
+ 10 => pivot::Border::DataLeft,
+ 11 => pivot::Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::X)),
+ 12 => pivot::Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::X)),
+ 13 => pivot::Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::X)),
+ 14 => pivot::Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::X)),
+ 15 => pivot::Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::X)),
+ 16 => pivot::Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::X)),
+ 17 => pivot::Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::X)),
+ 18 => pivot::Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::X)),
+ _ => return None,
+ };
+
+ let stroke = match self.stroke {
+ 0 => Stroke::None,
+ 2 => Stroke::Dashed,
+ 3 => Stroke::Thick,
+ 4 => Stroke::Thin,
+ 6 => Stroke::Double,
+ _ => Stroke::Solid,
+ };
+
+ let color = Color::new(
+ (self.color >> 16) as u8,
+ (self.color >> 8) as u8,
+ self.color as u8,
+ )
+ .with_alpha((self.color >> 24) as u8);
+
+ Some((border, pivot::BorderStyle { stroke, color }))
+ }
+}
+
+#[binread]
+#[br(big)]
+#[derive(Debug)]
+struct PrintSettings {
+ #[br(magic = b"\0\0\0\x01")]
+ #[br(parse_with(parse_bool))]
+ alll_layers: bool,
+ #[br(parse_with(parse_bool))]
+ paginate_layers: bool,
+ #[br(parse_with(parse_bool))]
+ fit_width: bool,
+ #[br(parse_with(parse_bool))]
+ fit_length: bool,
+ #[br(parse_with(parse_bool))]
+ top_continuation: bool,
+ #[br(parse_with(parse_bool))]
+ bottom_continuation: bool,
+ #[br(map(|n: u32| n as usize))]
+ n_orphan_lines: usize,
+ continuation_string: U32String,
+}
+
+#[binread]
+#[br(big)]
+#[derive(Debug, Default)]
+struct TableSettings {
+ #[br(temp, magic = 1u32)]
+ _x5: i32,
+ current_layer: i32,
+ #[br(parse_with(parse_bool))]
+ omit_empty: bool,
+ #[br(parse_with(parse_bool))]
+ show_row_labels_in_corner: bool,
+ #[br(parse_with(parse_bool))]
+ show_alphabetic_markers: bool,
+ #[br(parse_with(parse_bool))]
+ footnote_marker_subscripts: bool,
+ #[br(temp)]
+ _x6: u8,
+ #[br(big, parse_with(parse_counted))]
+ sizing: Sizing,
+ notes: U32String,
+ table_look: U32String,
+ #[br(temp)]
+ _sponge: Sponge,
+}
+
+#[binread]
+#[br(big)]
+#[derive(Debug, Default)]
+struct Sizing {
+ #[br(parse_with(parse_vec))]
+ row_breaks: Vec<u32>,
+ #[br(parse_with(parse_vec))]
+ column_breaks: Vec<u32>,
+ #[br(parse_with(parse_vec))]
+ row_keeps: Vec<(i32, i32)>,
+ #[br(parse_with(parse_vec))]
+ column_keeps: Vec<(i32, i32)>,
+ #[br(parse_with(parse_vec))]
+ row_point_keeps: Vec<[i32; 3]>,
+ #[br(parse_with(parse_vec))]
+ column_point_keeps: Vec<[i32; 3]>,
+}
+
+impl Sizing {
+ fn decode(
+ &self,
+ column_widths: &[i32],
+ row_heights: &[i32],
+ ) -> EnumMap<Axis2, Option<Box<pivot::Sizing>>> {
+ fn decode_axis(
+ widths: &[i32],
+ breaks: &[u32],
+ keeps: &[(i32, i32)],
+ ) -> Option<Box<pivot::Sizing>> {
+ if widths.is_empty() && breaks.is_empty() && keeps.is_empty() {
+ None
+ } else {
+ Some(Box::new(pivot::Sizing {
+ widths: widths.into(),
+ breaks: breaks.into_iter().map(|b| *b as usize).collect(),
+ keeps: keeps
+ .into_iter()
+ .map(|(low, high)| *low as usize..*high as usize)
+ .collect(),
+ }))
+ }
+ }
+
+ enum_map! {
+ Axis2::X => decode_axis(column_widths, &self.column_breaks, &self.column_keeps),
+ Axis2::Y => decode_axis(row_heights, &self.row_breaks, &self.row_keeps),
+ }
+ }
+}
+
+#[binread]
+#[derive(Default)]
+pub(super) struct U32String {
+ #[br(parse_with(parse_vec))]
+ string: Vec<u8>,
+}
+
+impl U32String {
+ pub(super) fn decode(&self, encoding: &'static Encoding) -> String {
+ if let Ok(string) = str::from_utf8(&self.string) {
+ string.into()
+ } else {
+ encoding
+ .decode_without_bom_handling(&self.string)
+ .0
+ .into_owned()
+ }
+ }
+ pub(super) fn decode_optional(&self, encoding: &'static Encoding) -> Option<String> {
+ let string = self.decode(encoding);
+ if !string.is_empty() {
+ Some(string)
+ } else {
+ None
+ }
+ }
+}
+
+impl Debug for U32String {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let s = self.string.iter().map(|c| *c as char).collect::<String>();
+ write!(f, "{s:?}")
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+struct Counted<T>(T);
+
+impl<T> Deref for Counted<T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl<T> BinRead for Counted<T>
+where
+ T: BinRead,
+{
+ type Args<'a> = T::Args<'a>;
+
+ fn read_options<R: Read + Seek>(
+ reader: &mut R,
+ endian: binrw::Endian,
+ args: Self::Args<'_>,
+ ) -> BinResult<Self> {
+ let count = u32::read_options(reader, endian, ())? as u64;
+ let start = reader.stream_position()?;
+ let end = start + count;
+ let mut inner = reader.take_seek(count);
+ let result = <T>::read_options(&mut inner, Endian::Little, args)?;
+ let pos = inner.stream_position()?;
+ if pos != end {
+ let consumed = pos - start;
+ return Err(binrw::Error::Custom {
+ pos,
+ err: Box::new(format!(
+ "counted data not exhausted (consumed {consumed} bytes out of {count})"
+ )),
+ });
+ }
+ Ok(Self(result))
+ }
+}
+
+#[binrw::parser(reader, endian)]
+fn parse_counted<T, A>(args: A, ...) -> BinResult<T>
+where
+ for<'a> T: BinRead<Args<'a> = A>,
+ A: Clone,
+ T: 'static,
+{
+ Ok(<Counted<T>>::read_options(reader, endian, args)?.0)
+}
+
+/// `BinRead` for `Option<T>` always requires the value to be there. This
+/// instead tries to read it and falls back to None if there's no match.
+#[derive(Clone, Debug)]
+struct Optional<T>(Option<T>);
+
+impl<T> Default for Optional<T> {
+ fn default() -> Self {
+ Self(None)
+ }
+}
+
+impl<T> Deref for Optional<T> {
+ type Target = Option<T>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl<T> BinRead for Optional<T>
+where
+ T: BinRead,
+{
+ type Args<'a> = T::Args<'a>;
+
+ fn read_options<R: Read + Seek>(
+ reader: &mut R,
+ endian: binrw::Endian,
+ args: Self::Args<'_>,
+ ) -> BinResult<Self> {
+ let start = reader.stream_position()?;
+ let result = <T>::read_options(reader, endian, args).ok();
+ if result.is_none() {
+ reader.seek(std::io::SeekFrom::Start(start))?;
+ }
+ Ok(Self(result))
+ }
+}
+
+#[binread]
+#[br(little)]
+#[br(import(version: Version))]
+#[derive(Debug)]
+struct Formats {
+ #[br(parse_with(parse_vec))]
+ column_widths: Vec<i32>,
+ locale: U32String,
+ current_layer: i32,
+ #[br(temp, parse_with(parse_bool))]
+ _x7: bool,
+ #[br(temp, parse_with(parse_bool))]
+ _x8: bool,
+ #[br(temp, parse_with(parse_bool))]
+ _x9: bool,
+ y0: Y0,
+ custom_currency: CustomCurrency,
+ #[br(if(version == Version::V1))]
+ v1: Counted<Optional<N0>>,
+ #[br(if(version == Version::V3))]
+ v3: Option<Counted<FormatsV3>>,
+}
+
+impl Formats {
+ fn y1(&self) -> Option<&Y1> {
+ self.v1
+ .as_ref()
+ .map(|n0| &n0.y1)
+ .or_else(|| self.v3.as_ref().map(|v3| &v3.n3.y1))
+ }
+
+ fn n1(&self) -> Option<&N1> {
+ self.v3.as_ref().map(|v3| &v3.n1_n2.x1)
+ }
+
+ fn n2(&self) -> Option<&N2> {
+ self.v3.as_ref().map(|v3| &v3.n1_n2.x2)
+ }
+
+ fn n3(&self) -> Option<&N3> {
+ self.v3.as_ref().map(|v3| &v3.n3)
+ }
+
+ fn charset(&self) -> Option<&U32String> {
+ self.y1().map(|y1| &y1.charset)
+ }
+
+ fn encoding(&self) -> &'static Encoding {
+ // XXX We should probably warn for unknown encodings below
+ if let Some(charset) = self.charset()
+ && let Some(encoding) = Encoding::for_label(&charset.string)
+ {
+ encoding
+ } else if let Ok(locale) = str::from_utf8(&self.locale.string)
+ && let Some(dot) = locale.find('.')
+ && let Some(encoding) = Encoding::for_label(locale[dot + 1..].as_bytes())
+ {
+ encoding
+ } else {
+ WINDOWS_1252
+ }
+ }
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct FormatsV3 {
+ #[br(parse_with(parse_counted))]
+ n1_n2: N1N2,
+ #[br(parse_with(parse_counted))]
+ n3: N3,
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct N1N2 {
+ x1: N1,
+ #[br(parse_with(parse_counted))]
+ x2: N2,
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct N0 {
+ #[br(temp)]
+ _bytes: [u8; 14],
+ y1: Y1,
+ _y2: Y2,
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct Y1 {
+ command: U32String,
+ command_local: U32String,
+ language: U32String,
+ charset: U32String,
+ locale: U32String,
+ #[br(temp, parse_with(parse_bool))]
+ _x10: bool,
+ #[br(parse_with(parse_bool))]
+ include_leading_zero: bool,
+ #[br(temp, parse_with(parse_bool))]
+ _x12: bool,
+ #[br(temp, parse_with(parse_bool))]
+ _x13: bool,
+ _y0: Y0,
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct Y2 {
+ custom_currency: CustomCurrency,
+ missing: u8,
+ #[br(temp, parse_with(parse_bool))]
+ _x17: bool,
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct N1 {
+ #[br(temp, parse_with(parse_bool))]
+ _x14: bool,
+ show_title: u8,
+ #[br(temp, parse_with(parse_bool))]
+ _x16: bool,
+ _lang: u8,
+ #[br(parse_with(parse_show))]
+ show_variables: Option<Show>,
+ #[br(parse_with(parse_show))]
+ show_values: Option<Show>,
+ #[br(temp)]
+ _x18: i32,
+ #[br(temp)]
+ _x19: i32,
+ #[br(temp)]
+ _zeros: [u8; 17],
+ #[br(temp, parse_with(parse_bool))]
+ _x20: bool,
+ #[br(parse_with(parse_bool))]
+ show_caption: bool,
+}
+
+#[binrw::parser(reader, endian)]
+fn parse_show() -> BinResult<Option<Show>> {
+ match <u8>::read_options(reader, endian, ())? {
+ 0 => Ok(None),
+ 1 => Ok(Some(Show::Value)),
+ 2 => Ok(Some(Show::Label)),
+ 3 => Ok(Some(Show::Both)),
+ _ => {
+ // XXX warn about invalid value
+ Ok(None)
+ }
+ }
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct N2 {
+ #[br(parse_with(parse_vec))]
+ row_heights: Vec<i32>,
+ #[br(parse_with(parse_vec))]
+ style_map: Vec<(i64, i16)>,
+ #[br(parse_with(parse_vec))]
+ styles: Vec<StylePair>,
+ #[br(parse_with(parse_counted))]
+ tail: Optional<[u8; 8]>,
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct N3 {
+ #[br(temp, magic = b"\x01\0")]
+ _x21: u8,
+ #[br(magic = b"\0\0\0")]
+ y1: Y1,
+ small: f64,
+ #[br(magic = 1u8, temp)]
+ _one: (),
+ inner: Optional<N3Inner>,
+ y2: Y2,
+ #[br(temp)]
+ _tail: Optional<N3Tail>,
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct N3Inner {
+ dataset: U32String,
+ datafile: U32String,
+ notes_unexpanded: U32String,
+ date: i32,
+ #[br(magic = 0u32, temp)]
+ _tail: (),
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct N3Tail {
+ #[br(temp)]
+ _x22: i32,
+ #[br(temp, assert(_zero == 0))]
+ _zero: i32,
+ #[br(temp, assert(_x25.is_none_or(|x25| x25 == 0 || x25 == 1)))]
+ _x25: Optional<u8>,
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct Y0 {
+ epoch: i32,
+ decimal: u8,
+ grouping: u8,
+}
+
+impl Y0 {
+ fn epoch(&self) -> Epoch {
+ if (1000..=9999).contains(&self.epoch) {
+ Epoch(self.epoch)
+ } else {
+ Epoch::default()
+ }
+ }
+
+ fn decimal(&self) -> Decimal {
+ // XXX warn about bad decimal point?
+ Decimal::try_from(self.decimal as char).unwrap_or_default()
+ }
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct CustomCurrency {
+ #[br(parse_with(parse_vec))]
+ ccs: Vec<U32String>,
+}
+
+impl CustomCurrency {
+ fn decode(&self, encoding: &'static Encoding) -> EnumMap<CC, Option<Box<NumberStyle>>> {
+ let mut ccs = EnumMap::default();
+ for (cc, string) in enum_iterator::all().zip(&self.ccs) {
+ if let Ok(style) = NumberStyle::from_str(&string.decode(encoding)) {
+ ccs[cc] = Some(Box::new(style));
+ } else {
+ // XXX warning
+ }
+ }
+ ccs
+ }
+}
+
+#[binread]
+#[br(little, import(version: Version))]
+#[derive(Debug)]
+struct ValueNumber {
+ #[br(parse_with(parse_explicit_optional), args(version))]
+ mods: Option<ValueMods>,
+ #[br(parse_with(parse_format))]
+ format: Format,
+ x: f64,
+}
+
+#[binread]
+#[br(little, import(version: Version))]
+#[derive(Debug)]
+struct ValueVarNumber {
+ #[br(parse_with(parse_explicit_optional), args(version))]
+ mods: Option<ValueMods>,
+ #[br(parse_with(parse_format))]
+ format: Format,
+ x: f64,
+ var_name: U32String,
+ value_label: U32String,
+ #[br(parse_with(parse_show))]
+ show: Option<Show>,
+}
+
+#[binread]
+#[br(little, import(version: Version))]
+#[derive(Debug)]
+struct ValueText {
+ local: U32String,
+ #[br(parse_with(parse_explicit_optional), args(version))]
+ mods: Option<ValueMods>,
+ id: U32String,
+ c: U32String,
+ #[br(parse_with(parse_bool))]
+ fixed: bool,
+}
+
+#[binread]
+#[br(little, import(version: Version))]
+#[derive(Debug)]
+struct ValueString {
+ #[br(parse_with(parse_explicit_optional), args(version))]
+ mods: Option<ValueMods>,
+ #[br(parse_with(parse_format))]
+ format: Format,
+ value_label: U32String,
+ var_name: U32String,
+ #[br(parse_with(parse_show))]
+ show: Option<Show>,
+ s: U32String,
+}
+
+#[binread]
+#[br(little, import(version: Version))]
+#[derive(Debug)]
+struct ValueVarName {
+ #[br(parse_with(parse_explicit_optional), args(version))]
+ mods: Option<ValueMods>,
+ var_name: U32String,
+ var_label: U32String,
+ #[br(parse_with(parse_show))]
+ show: Option<Show>,
+}
+
+#[binread]
+#[br(little, import(version: Version))]
+#[derive(Debug)]
+struct ValueFixedText {
+ local: U32String,
+ #[br(parse_with(parse_explicit_optional), args(version))]
+ mods: Option<ValueMods>,
+ id: U32String,
+ c: U32String,
+}
+
+#[binread]
+#[br(little, import(version: Version))]
+#[derive(Debug)]
+struct ValueTemplate {
+ #[br(parse_with(parse_explicit_optional), args(version))]
+ mods: Option<ValueMods>,
+ template: U32String,
+ #[br(parse_with(parse_vec), args(version))]
+ args: Vec<Argument>,
+}
+
+#[derive(Debug)]
+enum Value {
+ Number(ValueNumber),
+ VarNumber(ValueVarNumber),
+ Text(ValueText),
+ String(ValueString),
+ VarName(ValueVarName),
+ FixedText(ValueFixedText),
+ Template(ValueTemplate),
+}
+
+impl BinRead for Value {
+ type Args<'a> = (Version,);
+
+ fn read_options<R: Read + Seek>(
+ reader: &mut R,
+ endian: Endian,
+ args: Self::Args<'_>,
+ ) -> BinResult<Self> {
+ let start = reader.stream_position()?;
+ let kind = loop {
+ let x = <u8>::read_options(reader, endian, ())?;
+ if x != 0 {
+ break x;
+ }
+ };
+ match kind {
+ 1 => ValueNumber::read_options(reader, endian, args).map(Self::Number),
+ 2 => Ok(Self::VarNumber(ValueVarNumber::read_options(
+ reader, endian, args,
+ )?)),
+ 3 => Ok(Self::Text(ValueText::read_options(reader, endian, args)?)),
+ 4 => Ok(Self::String(ValueString::read_options(
+ reader, endian, args,
+ )?)),
+ 5 => Ok(Self::VarName(ValueVarName::read_options(
+ reader, endian, args,
+ )?)),
+ 6 => Ok(Self::FixedText(ValueFixedText::read_options(
+ reader, endian, args,
+ )?)),
+ b'1' | b'X' => {
+ reader.seek(std::io::SeekFrom::Current(-1))?;
+ Ok(Self::Template(ValueTemplate::read_options(
+ reader, endian, args,
+ )?))
+ }
+ _ => Err(BinError::NoVariantMatch { pos: start }),
+ }
+ .map_err(|e| e.with_message(format!("while parsing Value starting at offset {start:#x}")))
+ }
+}
+
+pub(super) fn decode_format(raw: u32) -> Format {
+ if raw == 0 || raw == 0x10000 || raw == 1 {
+ return Format::new(Type::F, 40, 2).unwrap();
+ }
+
+ let raw_type = (raw >> 16) as u16;
+ let type_ = if raw_type >= 40 {
+ Type::F
+ } else if let Ok(type_) = Type::try_from(raw_type) {
+ type_
+ } else {
+ // XXX warn
+ Type::F
+ };
+ let w = ((raw >> 8) & 0xff) as Width;
+ let d = raw as Decimals;
+
+ UncheckedFormat::new(type_, w, d).fix()
+}
+
+#[binrw::parser(reader, endian)]
+fn parse_format() -> BinResult<Format> {
+ Ok(decode_format(u32::read_options(reader, endian, ())?))
+}
+
+impl ValueNumber {
+ fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> pivot::Value {
+ pivot::Value::new_number_with_format((self.x != -f64::MAX).then_some(self.x), self.format)
+ .with_styling(ValueMods::decode_optional(&self.mods, encoding, footnotes))
+ }
+}
+
+impl ValueVarNumber {
+ fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> pivot::Value {
+ pivot::Value::new_number_with_format((self.x != -f64::MAX).then_some(self.x), self.format)
+ .with_styling(ValueMods::decode_optional(&self.mods, encoding, footnotes))
+ .with_value_label(self.value_label.decode_optional(encoding))
+ .with_variable_name(Some(self.var_name.decode(encoding)))
+ .with_show_value_label(self.show)
+ }
+}
+
+impl ValueText {
+ fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> pivot::Value {
+ pivot::Value::new_general_text(
+ self.local.decode(encoding),
+ self.c.decode(encoding),
+ self.id.decode(encoding),
+ !self.fixed,
+ )
+ .with_styling(ValueMods::decode_optional(&self.mods, encoding, footnotes))
+ }
+}
+
+impl ValueString {
+ fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> pivot::Value {
+ pivot::Value::new(pivot::ValueInner::String(StringValue {
+ s: self.s.decode(encoding),
+ hex: self.format.type_() == Type::AHex,
+ show: self.show,
+ var_name: self.var_name.decode_optional(encoding),
+ value_label: self.value_label.decode_optional(encoding),
+ }))
+ .with_styling(ValueMods::decode_optional(&self.mods, encoding, footnotes))
+ }
+}
+
+impl ValueVarName {
+ fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> pivot::Value {
+ pivot::Value::new(pivot::ValueInner::Variable(VariableValue {
+ show: self.show,
+ var_name: self.var_name.decode(encoding),
+ variable_label: self.var_label.decode_optional(encoding),
+ }))
+ .with_styling(ValueMods::decode_optional(&self.mods, encoding, footnotes))
+ }
+}
+impl ValueFixedText {
+ fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> pivot::Value {
+ pivot::Value::new_general_text(
+ self.local.decode(encoding),
+ self.c.decode(encoding),
+ self.id.decode(encoding),
+ false,
+ )
+ .with_styling(ValueMods::decode_optional(&self.mods, encoding, footnotes))
+ }
+}
+
+impl ValueTemplate {
+ fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> pivot::Value {
+ pivot::Value::new(pivot::ValueInner::Template(TemplateValue {
+ args: self
+ .args
+ .iter()
+ .map(|argument| argument.decode(encoding, footnotes))
+ .collect(),
+ localized: self.template.decode(encoding),
+ id: self
+ .mods
+ .as_ref()
+ .and_then(|mods| mods.template_id(encoding)),
+ }))
+ .with_styling(ValueMods::decode_optional(&self.mods, encoding, footnotes))
+ }
+}
+
+impl Value {
+ fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> pivot::Value {
+ match self {
+ Value::Number(number) => number.decode(encoding, footnotes),
+ Value::VarNumber(var_number) => var_number.decode(encoding, footnotes),
+ Value::Text(text) => text.decode(encoding, footnotes),
+ Value::String(string) => string.decode(encoding, footnotes),
+ Value::VarName(var_name) => var_name.decode(encoding, footnotes),
+ Value::FixedText(fixed_text) => fixed_text.decode(encoding, footnotes),
+ Value::Template(template) => template.decode(encoding, footnotes),
+ }
+ }
+}
+
+#[derive(Debug)]
+struct Argument(Vec<Value>);
+
+impl BinRead for Argument {
+ type Args<'a> = (Version,);
+
+ fn read_options<R: Read + Seek>(
+ reader: &mut R,
+ endian: Endian,
+ (version,): (Version,),
+ ) -> BinResult<Self> {
+ let count = u32::read_options(reader, endian, ())? as usize;
+ if count == 0 {
+ Ok(Self(vec![Value::read_options(reader, endian, (version,))?]))
+ } else {
+ let zero = u32::read_options(reader, endian, ())?;
+ assert_eq!(zero, 0);
+ let values = <Vec<_>>::read_options(
+ reader,
+ endian,
+ VecArgs {
+ count,
+ inner: (version,),
+ },
+ )?;
+ Ok(Self(values))
+ }
+ }
+}
+
+impl Argument {
+ fn decode(
+ &self,
+ encoding: &'static Encoding,
+ footnotes: &pivot::Footnotes,
+ ) -> Vec<pivot::Value> {
+ self.0
+ .iter()
+ .map(|value| value.decode(encoding, footnotes))
+ .collect()
+ }
+}
+
+#[binread]
+#[br(little, import(version: Version))]
+#[derive(Debug)]
+struct ValueMods {
+ #[br(parse_with(parse_vec))]
+ refs: Vec<i16>,
+ #[br(parse_with(parse_vec))]
+ subscripts: Vec<U32String>,
+ #[br(if(version == Version::V1))]
+ v1: Option<ValueModsV1>,
+ #[br(if(version == Version::V3), parse_with(parse_counted))]
+ v3: ValueModsV3,
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug, Default)]
+struct ValueModsV1 {
+ #[br(temp, magic(0u8), assert(_1 == 1 || _1 == 2))]
+ _1: i32,
+ #[br(temp)]
+ _0: Optional<Zero>,
+ #[br(temp)]
+ _1: Optional<Zero>,
+ #[br(temp)]
+ _2: i32,
+ #[br(temp)]
+ _3: Optional<Zero>,
+ #[br(temp)]
+ _4: Optional<Zero>,
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug, Default)]
+struct ValueModsV3 {
+ #[br(parse_with(parse_counted))]
+ template_string: Optional<TemplateString>,
+ style_pair: StylePair,
+}
+
+impl ValueMods {
+ fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> ValueStyle {
+ let font_style =
+ self.v3
+ .style_pair
+ .font_style
+ .as_ref()
+ .map(|font_style| pivot::FontStyle {
+ bold: font_style.bold,
+ italic: font_style.italic,
+ underline: font_style.underline,
+ font: font_style.typeface.decode(encoding),
+ fg: font_style.fg,
+ bg: font_style.bg,
+ size: (font_style.size as i32) * 4 / 3,
+ });
+ let cell_style = self.v3.style_pair.cell_style.as_ref().map(|cell_style| {
+ pivot::CellStyle {
+ horz_align: match cell_style.halign {
+ 0 => Some(HorzAlign::Center),
+ 2 => Some(HorzAlign::Left),
+ 4 => Some(HorzAlign::Right),
+ 6 => Some(HorzAlign::Decimal {
+ offset: cell_style.decimal_offset,
+ decimal: Decimal::Dot, /*XXX*/
+ }),
+ _ => None,
+ },
+ vert_align: match cell_style.valign {
+ 0 => VertAlign::Middle,
+ 3 => VertAlign::Bottom,
+ _ => VertAlign::Top,
+ },
+ margins: enum_map! {
+ Axis2::X => [cell_style.left_margin as i32, cell_style.right_margin as i32],
+ Axis2::Y => [cell_style.top_margin as i32, cell_style.bottom_margin as i32],
+ },
+ }
+ });
+ ValueStyle {
+ cell_style,
+ font_style,
+ subscripts: self.subscripts.iter().map(|s| s.decode(encoding)).collect(),
+ footnotes: self
+ .refs
+ .iter()
+ .flat_map(|index| footnotes.get(*index as usize))
+ .cloned()
+ .collect(),
+ }
+ }
+ fn decode_optional(
+ mods: &Option<Self>,
+ encoding: &'static Encoding,
+ footnotes: &pivot::Footnotes,
+ ) -> Option<Box<pivot::ValueStyle>> {
+ mods.as_ref()
+ .map(|mods| Box::new(mods.decode(encoding, footnotes)))
+ }
+ fn template_id(&self, encoding: &'static Encoding) -> Option<String> {
+ self.v3
+ .template_string
+ .as_ref()
+ .and_then(|template_string| template_string.id.as_ref())
+ .map(|s| s.decode(encoding))
+ }
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct TemplateString {
+ #[br(parse_with(parse_counted), temp)]
+ _sponge: Sponge,
+ #[br(parse_with(parse_explicit_optional))]
+ id: Option<U32String>,
+}
+
+#[derive(Debug, Default)]
+struct Sponge;
+
+impl BinRead for Sponge {
+ type Args<'a> = ();
+
+ fn read_options<R: Read + Seek>(reader: &mut R, _endian: Endian, _args: ()) -> BinResult<Self> {
+ let mut buf = [0; 32];
+ while reader.read(&mut buf)? > 0 {}
+ Ok(Self)
+ }
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug, Default)]
+struct StylePair {
+ #[br(parse_with(parse_explicit_optional))]
+ font_style: Option<FontStyle>,
+ #[br(parse_with(parse_explicit_optional))]
+ cell_style: Option<CellStyle>,
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct FontStyle {
+ #[br(parse_with(parse_bool))]
+ bold: bool,
+ #[br(parse_with(parse_bool))]
+ italic: bool,
+ #[br(parse_with(parse_bool))]
+ underline: bool,
+ #[br(parse_with(parse_bool))]
+ show: bool,
+ #[br(parse_with(parse_color))]
+ fg: Color,
+ #[br(parse_with(parse_color))]
+ bg: Color,
+ typeface: U32String,
+ size: u8,
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct CellStyle {
+ halign: i32,
+ valign: i32,
+ decimal_offset: f64,
+ left_margin: i16,
+ right_margin: i16,
+ top_margin: i16,
+ bottom_margin: i16,
+}
+
+#[binread]
+#[br(little)]
+#[br(import(version: Version))]
+#[derive(Debug)]
+struct Dimension {
+ #[br(args(version))]
+ name: Value,
+ #[br(temp)]
+ _x1: u8,
+ #[br(temp)]
+ _x2: u8,
+ #[br(temp)]
+ _x3: u32,
+ #[br(parse_with(parse_bool))]
+ hide_dim_label: bool,
+ #[br(parse_with(parse_bool))]
+ hide_all_labels: bool,
+ #[br(magic(1u8), temp)]
+ _dim_index: i32,
+ #[br(parse_with(parse_vec), args(version))]
+ categories: Vec<Category>,
+}
+
+#[binread]
+#[br(little, import(version: Version))]
+#[derive(Debug)]
+struct Category {
+ #[br(args(version))]
+ name: Value,
+ #[br(args(version))]
+ child: Child,
+}
+
+impl Category {
+ fn decode(&self, encoding: &'static Encoding, footnotes: &Footnotes, group: &mut pivot::Group) {
+ let name = self.name.decode(encoding, footnotes);
+ match &self.child {
+ Child::Leaf { leaf_index: _ } => {
+ group.push(pivot::Leaf::new(name));
+ }
+ Child::Group {
+ merge: true,
+ subcategories,
+ } => {
+ for subcategory in subcategories {
+ subcategory.decode(encoding, footnotes, group);
+ }
+ }
+ Child::Group {
+ merge: false,
+ subcategories,
+ } => {
+ let mut subgroup = Group::new(name).with_label_shown();
+ for subcategory in subcategories {
+ subcategory.decode(encoding, footnotes, &mut subgroup);
+ }
+ group.push(subgroup);
+ }
+ }
+ }
+}
+
+#[binread]
+#[br(little, import(version: Version))]
+#[derive(Debug)]
+enum Child {
+ Leaf {
+ #[br(magic(0u16), parse_with(parse_bool), temp)]
+ _x24: bool,
+ #[br(magic(b"\x02\0\0\0"))]
+ leaf_index: u32,
+ #[br(magic(0u32), temp)]
+ _tail: (),
+ },
+ Group {
+ #[br(parse_with(parse_bool))]
+ merge: bool,
+ #[br(temp, magic(b"\0\x01"))]
+ _x23: i32,
+ #[br(magic(-1i32), parse_with(parse_vec), args(version))]
+ subcategories: Vec<Box<Category>>,
+ },
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug)]
+struct Axes {
+ #[br(temp)]
+ n_layers: u32,
+ #[br(temp)]
+ n_rows: u32,
+ #[br(temp)]
+ n_columns: u32,
+ #[br(count(n_layers))]
+ layers: Vec<u32>,
+ #[br(count(n_rows))]
+ rows: Vec<u32>,
+ #[br(count(n_columns))]
+ columns: Vec<u32>,
+}
+
+impl Axes {
+ fn decode(
+ &self,
+ dimensions: Vec<pivot::Dimension>,
+ ) -> Result<Vec<(Axis3, pivot::Dimension)>, LightError> {
+ let n = self.layers.len() + self.rows.len() + self.columns.len();
+ if n != dimensions.len() {
+ /* XXX warn
+ return Err(LightError::WrongAxisCount {
+ expected: dimensions.len(),
+ actual: n,
+ n_layers: self.layers.len(),
+ n_rows: self.rows.len(),
+ n_columns: self.columns.len(),
+ });*/
+ return Ok(dimensions
+ .into_iter()
+ .map(|dimension| (Axis3::Y, dimension))
+ .collect());
+ }
+
+ fn axis_dims(axis: Axis3, dimensions: &[u32]) -> impl Iterator<Item = (Axis3, usize)> {
+ dimensions.iter().map(move |d| (axis, *d as usize))
+ }
+
+ let mut axes = vec![None; n];
+ for (axis, index) in axis_dims(Axis3::Z, &self.layers)
+ .chain(axis_dims(Axis3::Y, &self.rows))
+ .chain(axis_dims(Axis3::X, &self.columns))
+ {
+ if index >= n {
+ return Err(LightError::InvalidDimensionIndex { index, n });
+ } else if axes[index].is_some() {
+ return Err(LightError::DuplicateDimensionIndex(index));
+ }
+ axes[index] = Some(axis);
+ }
+ Ok(axes
+ .into_iter()
+ .map(|axis| axis.unwrap())
+ .zip(dimensions)
+ .collect())
+ }
+}
+
+#[binread]
+#[br(little, import(version: Version))]
+#[derive(Debug)]
+struct Cell {
+ index: u64,
+ #[br(if(version == Version::V1), temp)]
+ _zero: Optional<Zero>,
+ #[br(args(version))]
+ value: Value,
+}
Footnotes, Group, HeadingRegion, HorzAlign, LabelPosition, Leaf, PivotTable,
RowColBorder, RowParity, Stroke, Value, ValueInner, ValueStyle, VertAlign,
},
- spv::html::Document,
},
settings::Show,
+ spv::read::html::Document,
util::ToSmallString,
};
+/// SPSS viewer (SPV) file writer.
pub struct Writer<W>
where
W: Write + Seek,
where
W: Write + Seek,
{
+ /// Creates a new `Writer` to write an SPV file to underlying stream
+ /// `writer`.
pub fn for_writer(writer: W) -> Self {
let mut writer = ZipWriter::new(writer);
writer
}
}
+ /// Returns this `Writer` with `page_setup` set up to be written with the
+ /// next call to [write](Writer::write).
+ ///
+ /// Page setup is only significant if it is written before the first call to
+ /// [write](Writer::writer).
pub fn with_page_setup(mut self, page_setup: PageSetup) -> Self {
self.set_page_setup(page_setup);
self
}
+ /// Sets `page_setup` to be written with the next call to
+ /// [write](Writer::write).
+ ///
+ /// Page setup is only significant if it is written before the first call to
+ /// [write](Writer::writer).
+ pub fn set_page_setup(&mut self, page_setup: PageSetup) {
+ self.page_setup = Some(page_setup);
+ }
+
+ /// Closes the underlying file and returns the inner writer and the final
+ /// I/O result.
pub fn close(mut self) -> ZipResult<W> {
self.writer
.start_file("META-INF/MANIFEST.MF", SimpleFileOptions::default())?;
self.writer.finish()
}
- pub fn set_page_setup(&mut self, page_setup: PageSetup) {
- self.page_setup = Some(page_setup);
- }
-
fn page_break_before(&mut self) -> bool {
let page_break_before = self.needs_page_break;
self.needs_page_break = false;
impl<W> Writer<W>
where
- W: Write + Seek + 'static,
+ W: Write + Seek,
{
pub fn write(&mut self, item: &Item) {
if item.details.is_page_break() {