#![cfg_attr(not(test), warn(missing_docs))]
#![warn(dead_code)]
use std::{
fmt::{Debug, Display},
io::Read,
ops::{Range, RangeInclusive},
str::{FromStr, Utf8Error},
sync::{Arc, OnceLock},
};
use color::{AlphaColor, Rgba8, Srgb, palette::css::TRANSPARENT};
use enum_iterator::Sequence;
use enum_map::{Enum, EnumMap, enum_map};
use quick_xml::{DeError, de::from_str};
use serde::{Deserialize, Serialize, de::Visitor, ser::SerializeStruct};
use crate::{
output::pivot::{
Axis2, FootnoteMarkerPosition, FootnoteMarkerType, TableProperties, tlo::parse_tlo,
},
util::ToSmallString,
variable::VarType,
};
#[derive(Copy, Clone, Debug, Enum, PartialEq, Eq)]
pub enum Area {
Title,
Caption,
Footer,
Corner,
Labels(
Axis2,
),
Data(
RowParity,
),
Layers,
}
impl Area {
pub fn row_parity(self) -> Option<RowParity> {
match self {
Area::Data(row_parity) => Some(row_parity),
_ => None,
}
}
}
impl Default for Area {
fn default() -> Self {
Self::Data(RowParity::default())
}
}
impl Display for Area {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Area::Title => write!(f, "title"),
Area::Caption => write!(f, "caption"),
Area::Footer => write!(f, "footer"),
Area::Corner => write!(f, "corner"),
Area::Labels(axis2) => write!(f, "labels({axis2})"),
Area::Data(row) => write!(f, "data({row})"),
Area::Layers => write!(f, "layers"),
}
}
}
impl Serialize for Area {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_small_string::<16>())
}
}
#[derive(Copy, Clone, Debug, Default, Enum, PartialEq, Eq)]
pub enum RowParity {
#[default]
Even,
Odd,
}
impl From<usize> for RowParity {
fn from(value: usize) -> Self {
if value % 2 == 1 {
Self::Odd
} else {
Self::Even
}
}
}
impl Display for RowParity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RowParity::Even => write!(f, "even"),
RowParity::Odd => write!(f, "odd"),
}
}
}
#[derive(Copy, Clone, Debug, Enum, PartialEq, Eq)]
pub enum Border {
Title,
OuterFrame(
BoxBorder,
),
InnerFrame(
BoxBorder,
),
Dimension(
RowColBorder,
),
Category(
RowColBorder,
),
DataLeft,
DataTop,
}
impl Border {
pub fn default_stroke(self) -> Stroke {
match self {
Self::InnerFrame(_) | Self::DataLeft | Self::DataTop => Stroke::Thick,
Self::Dimension(
RowColBorder(HeadingRegion::Columns, _) | RowColBorder(_, Axis2::X),
)
| Self::Category(RowColBorder(HeadingRegion::Columns, _)) => Stroke::Solid,
_ => Stroke::None,
}
}
pub fn default_border_style(self) -> BorderStyle {
BorderStyle {
stroke: self.default_stroke(),
color: Color::BLACK,
}
}
pub fn fallback(self) -> Option<Self> {
if let Self::Dimension(row_col_border) = self {
Some(Self::Category(row_col_border))
} else {
None
}
}
pub fn default_borders() -> EnumMap<Border, BorderStyle> {
EnumMap::from_fn(Border::default_border_style)
}
}
impl Display for Border {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Border::Title => write!(f, "title"),
Border::OuterFrame(box_border) => write!(f, "outer_frame({box_border})"),
Border::InnerFrame(box_border) => write!(f, "inner_frame({box_border})"),
Border::Dimension(row_col_border) => write!(f, "dimension({row_col_border})"),
Border::Category(row_col_border) => write!(f, "category({row_col_border})"),
Border::DataLeft => write!(f, "data(left)"),
Border::DataTop => write!(f, "data(top)"),
}
}
}
impl Serialize for Border {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_small_string::<32>())
}
}
#[derive(Copy, Clone, Debug, Enum, PartialEq, Eq, Serialize, Sequence)]
#[serde(rename_all = "snake_case")]
pub enum BoxBorder {
Left,
Top,
Right,
Bottom,
}
impl BoxBorder {
fn as_str(&self) -> &'static str {
match self {
BoxBorder::Left => "left",
BoxBorder::Top => "top",
BoxBorder::Right => "right",
BoxBorder::Bottom => "bottom",
}
}
}
impl Display for BoxBorder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Copy, Clone, Debug, Enum, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct RowColBorder(
pub HeadingRegion,
) or vertical ([Axis2::Y]) borders.
pub Axis2,
);
impl Display for RowColBorder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.0, self.1)
}
}
#[derive(Default, Clone, Debug, Serialize)]
pub struct Sizing {
pub widths: Vec<i32>,
pub breaks: Vec<usize>,
pub keeps: Vec<Range<usize>>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Look {
pub name: Option<String>,
pub hide_empty: bool,
pub row_label_position: LabelPosition,
pub heading_widths: EnumMap<HeadingRegion, RangeInclusive<isize>>,
pub footnote_marker_type: FootnoteMarkerType,
pub footnote_marker_position: FootnoteMarkerPosition,
pub areas: EnumMap<Area, AreaStyle>,
pub borders: EnumMap<Border, BorderStyle>,
pub print_all_layers: bool,
pub paginate_layers: bool,
pub shrink_to_fit: EnumMap<Axis2, bool>,
pub show_continuations: [bool; 2],
pub continuation: Option<String>,
pub n_orphan_lines: usize,
}
impl Look {
pub fn with_omit_empty(mut self, omit_empty: bool) -> Self {
self.hide_empty = omit_empty;
self
}
pub fn with_row_label_position(mut self, row_label_position: LabelPosition) -> Self {
self.row_label_position = row_label_position;
self
}
pub fn with_borders(mut self, borders: EnumMap<Border, BorderStyle>) -> Self {
self.borders = borders;
self
}
}
impl Default for Look {
fn default() -> Self {
Self {
name: None,
hide_empty: true,
row_label_position: LabelPosition::default(),
heading_widths: EnumMap::from_fn(|region| match region {
HeadingRegion::Rows => 48..=96,
HeadingRegion::Columns => 48..=160,
}),
footnote_marker_type: FootnoteMarkerType::default(),
footnote_marker_position: FootnoteMarkerPosition::default(),
areas: EnumMap::from_fn(AreaStyle::default_for_area),
borders: Border::default_borders(),
print_all_layers: false,
paginate_layers: false,
shrink_to_fit: EnumMap::from_fn(|_| false),
show_continuations: [false, false],
continuation: None,
n_orphan_lines: 0,
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum ParseLookError {
#[error(transparent)]
XmlError(
#[from]
DeError,
),
#[error(transparent)]
Utf8Error(
#[from]
Utf8Error,
),
#[error(transparent)]
BinError(
#[from]
binrw::Error,
),
#[error(transparent)]
IoError(
#[from]
std::io::Error,
),
}
impl Look {
pub fn shared_default() -> Arc<Look> {
static LOOK: OnceLock<Arc<Look>> = OnceLock::new();
LOOK.get_or_init(|| Arc::new(Look::default())).clone()
}
pub fn from_xml(xml: &str) -> Result<Self, ParseLookError> {
Ok(from_str::<TableProperties>(xml)
.map_err(ParseLookError::from)?
.into())
}
pub fn from_binary(tlo: &[u8]) -> Result<Self, ParseLookError> {
parse_tlo(tlo).map_err(ParseLookError::from)
}
pub fn from_data(data: &[u8]) -> Result<Self, ParseLookError> {
if data.starts_with(b"\xff\xff\0\0") {
Self::from_binary(data)
} else {
Self::from_xml(str::from_utf8(data).map_err(ParseLookError::from)?)
}
}
pub fn from_reader<R>(mut reader: R) -> Result<Self, ParseLookError>
where
R: Read,
{
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer)?;
Self::from_data(&buffer)
}
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
pub enum LabelPosition {
#[serde(rename = "nested")]
Nested,
#[default]
#[serde(rename = "inCorner")]
Corner,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Enum, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum HeadingRegion {
Rows,
Columns,
}
impl HeadingRegion {
pub fn as_str(&self) -> &'static str {
match self {
HeadingRegion::Rows => "rows",
HeadingRegion::Columns => "columns",
}
}
}
impl Display for HeadingRegion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl From<Axis2> for HeadingRegion {
fn from(axis: Axis2) -> Self {
match axis {
Axis2::X => HeadingRegion::Columns,
Axis2::Y => HeadingRegion::Rows,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct AreaStyle {
pub cell_style: CellStyle,
pub font_style: FontStyle,
}
impl AreaStyle {
pub fn default_for_area(area: Area) -> Self {
Self {
cell_style: CellStyle::default_for_area(area),
font_style: FontStyle::default_for_area(area),
}
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
pub struct CellStyle {
pub horz_align: Option<HorzAlign>,
pub vert_align: VertAlign,
pub margins: EnumMap<Axis2, [i32; 2]>,
}
impl Default for CellStyle {
fn default() -> Self {
Self::default_for_area(Area::default())
}
}
impl CellStyle {
pub fn default_for_area(area: Area) -> Self {
use HorzAlign::*;
use VertAlign::*;
let (horz_align, vert_align, hmargins, vmargins) = match area {
Area::Title => (Some(Center), Middle, [8, 11], [1, 8]),
Area::Caption => (Some(Left), Top, [8, 11], [1, 1]),
Area::Footer => (Some(Left), Top, [11, 8], [2, 3]),
Area::Corner => (Some(Left), Bottom, [8, 11], [1, 1]),
Area::Labels(Axis2::X) => (Some(Center), Bottom, [8, 11], [1, 3]),
Area::Labels(Axis2::Y) => (Some(Left), Top, [8, 11], [1, 3]),
Area::Data(_) => (None, Top, [8, 11], [1, 1]),
Area::Layers => (Some(Left), Bottom, [8, 11], [1, 3]),
};
Self {
horz_align,
vert_align,
margins: enum_map! { Axis2::X => hmargins, Axis2::Y => vmargins },
}
}
pub fn with_horz_align(self, horz_align: Option<HorzAlign>) -> Self {
Self { horz_align, ..self }
}
pub fn with_vert_align(self, vert_align: VertAlign) -> Self {
Self { vert_align, ..self }
}
pub fn with_margins(self, margins: EnumMap<Axis2, [i32; 2]>) -> Self {
Self { margins, ..self }
}
}
#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum HorzAlign {
Right,
Left,
Center,
Decimal {
offset: f64,
},
}
impl HorzAlign {
pub fn for_mixed(var_type: VarType) -> Self {
match var_type {
VarType::Numeric => Self::Right,
VarType::String => Self::Left,
}
}
pub fn as_str(&self) -> Option<&'static str> {
match self {
HorzAlign::Right => Some("right"),
HorzAlign::Left => Some("left"),
HorzAlign::Center => Some("center"),
HorzAlign::Decimal { .. } => None,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct UnknownHorzAlign;
impl FromStr for HorzAlign {
type Err = UnknownHorzAlign;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.eq_ignore_ascii_case("left") {
Ok(Self::Left)
} else if s.eq_ignore_ascii_case("center") {
Ok(Self::Center)
} else if s.eq_ignore_ascii_case("right") {
Ok(Self::Right)
} else {
Err(UnknownHorzAlign)
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum VertAlign {
Top,
Middle,
Bottom,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct FontStyle {
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub font: String,
pub fg: Color,
pub bg: Color,
pub size: i32,
}
impl Default for FontStyle {
fn default() -> Self {
FontStyle {
bold: false,
italic: false,
underline: false,
font: String::from("Sans Serif"),
fg: Color::BLACK,
bg: Color::WHITE,
size: 9,
}
}
}
impl FontStyle {
pub fn default_for_area(area: Area) -> Self {
Self::default().with_bold(area == Area::Title)
}
pub fn with_size(self, size: i32) -> Self {
Self { size, ..self }
}
pub fn with_bold(self, bold: bool) -> Self {
Self { bold, ..self }
}
pub fn with_italic(self, italic: bool) -> Self {
Self { italic, ..self }
}
pub fn with_underline(self, underline: bool) -> Self {
Self { underline, ..self }
}
pub fn with_font(self, font: impl Into<String>) -> Self {
Self {
font: font.into(),
..self
}
}
pub fn with_fg(self, fg: Color) -> Self {
Self { fg, ..self }
}
pub fn with_bg(self, fg: Color) -> Self {
Self { fg, ..self }
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct Color {
pub alpha: u8,
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Color {
pub const BLACK: Color = Color::new(0, 0, 0);
pub const WHITE: Color = Color::new(255, 255, 255);
pub const RED: Color = Color::new(255, 0, 0);
pub const GREEN: Color = Color::new(0, 255, 0);
pub const BLUE: Color = Color::new(0, 0, 255);
pub const TRANSPARENT: Color = Color::new(0, 0, 0).with_alpha(0);
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self {
alpha: 255,
r,
g,
b,
}
}
pub const fn new_u32(rgb: u32) -> Self {
Self::new((rgb >> 16) as u8, (rgb >> 8) as u8, rgb as u8)
}
pub const fn with_alpha(self, alpha: u8) -> Self {
Self { alpha, ..self }
}
pub const fn without_alpha(self) -> Self {
self.with_alpha(255)
}
pub fn display_css(&self) -> impl Display {
ColorDisplayCss(*self)
}
pub fn into_rgb(&self) -> (u8, u8, u8) {
(self.r, self.g, self.b)
}
pub fn into_rgb16(&self) -> (u16, u16, u16) {
(
self.r as u16 * 257,
self.g as u16 * 257,
self.b as u16 * 257,
)
}
}
impl Debug for Color {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.display_css())
}
}
impl From<Rgba8> for Color {
fn from(Rgba8 { r, g, b, a }: Rgba8) -> Self {
Self::new(r, g, b).with_alpha(a)
}
}
impl FromStr for Color {
type Err = color::ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
fn is_bare_hex(s: &str) -> bool {
let s = s.trim();
s.chars().count() == 6 && s.chars().all(|c| c.is_ascii_hexdigit())
}
let color: AlphaColor<Srgb> = match s.parse() {
Err(_) if is_bare_hex(s) => ("#".to_owned() + s).parse(),
Err(_) if s.trim().eq_ignore_ascii_case("transparent") => Ok(TRANSPARENT),
other => other,
}?;
Ok(color.to_rgba8().into())
}
}
impl Serialize for Color {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.display_css().to_small_string::<32>())
}
}
impl<'de> Deserialize<'de> for Color {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct ColorVisitor;
impl<'de> Visitor<'de> for ColorVisitor {
type Value = Color;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("\"#rrggbb\" or \"rrggbb\" or web color name")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
v.parse().map_err(E::custom)
}
}
deserializer.deserialize_str(ColorVisitor)
}
}
struct ColorDisplayCss(Color);
impl Display for ColorDisplayCss {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Color { alpha, r, g, b } = self.0;
match alpha {
255 => write!(f, "#{r:02x}{g:02x}{b:02x}"),
_ => write!(f, "rgb({r}, {g}, {b}, {:.2})", alpha as f64 / 255.0),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Deserialize)]
pub struct BorderStyle {
#[serde(rename = "@borderStyleType")]
pub stroke: Stroke,
#[serde(rename = "@color")]
pub color: Color,
}
impl Serialize for BorderStyle {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut s = serializer.serialize_struct("BorderStyle", 2)?;
s.serialize_field("stroke", &self.stroke)?;
s.serialize_field("color", &self.color)?;
s.end()
}
}
impl From<Stroke> for BorderStyle {
fn from(value: Stroke) -> Self {
Self::new(value)
}
}
impl BorderStyle {
pub const fn new(stroke: Stroke) -> Self {
Self {
stroke,
color: Color::BLACK,
}
}
pub const fn none() -> Self {
Self::new(Stroke::None)
}
pub const fn solid() -> Self {
Self::new(Stroke::Solid)
}
pub fn is_none(&self) -> bool {
self.stroke.is_none()
}
pub fn with_stroke(self, stroke: Stroke) -> Self {
Self { stroke, ..self }
}
pub fn with_color(self, color: Color) -> Self {
Self { color, ..self }
}
pub fn combine(self, other: BorderStyle) -> Self {
Self {
stroke: self.stroke.combine(other.stroke),
color: self.color,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Enum, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum Stroke {
None,
Solid,
Dashed,
Thick,
Thin,
Double,
}
impl Stroke {
pub fn is_none(&self) -> bool {
self == &Self::None
}
pub fn combine(self, other: Stroke) -> Self {
self.max(other)
}
}