// 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::marker::PhantomData;
-
-use serde::Deserialize;
-
-use crate::output::pivot::Color;
+use std::{
+ collections::{BTreeMap, HashMap},
+ marker::PhantomData,
+ mem::take,
+ num::{NonZeroUsize, ParseFloatError},
+ str::FromStr,
+};
+
+use enum_map::{Enum, EnumMap};
+use ordered_float::OrderedFloat;
+use serde::{Deserialize, de::Error as _};
+
+use crate::{
+ data::Datum,
+ format::{Decimal::Dot, Type, UncheckedFormat},
+ output::{
+ pivot::{
+ Area, AreaStyle, Color, HeadingRegion, HorzAlign, Look, PivotTable, RowParity, Value,
+ VertAlign,
+ },
+ spv::legacy_bin::DataValue,
+ },
+};
#[derive(Debug)]
struct Ref<T> {
}
}
+struct Map(HashMap<OrderedFloat<f64>, Datum<String>>);
+
+impl Map {
+ 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(crate::format::Format::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();
+ }
+ }
+ }
+}
+
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Visualization {
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!()
+ };
+
+ // Footnotes.
+ //
+ // Any pivot_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 footnotes = BTreeMap::<usize, (String, String)>::new();
+ if let Some(f) = &graph.interval.footnotes {
+ f.decode(&mut footnotes);
+ }
+ for child in &graph.interval.labeling.children {
+ if let LabelingChild::Footnotes(f) = child {
+ f.decode(&mut footnotes);
+ }
+ }
+ for label in &labels[Purpose::Footnote] {
+ for (index, text) in label.text().iter().enumerate() {
+ if let Some(uses_reference) = text.uses_reference {
+ let entry = footnotes.entry(uses_reference.get() - 1).or_default();
+ if index % 2 == 0 {
+ entry.0 = text.text.strip_suffix('\n').unwrap_or(&text.text).into();
+ } else {
+ entry.1 = text.text.strip_suffix('.').unwrap_or(&text.text).into();
+ }
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+ if let Some(style) = &graph.interval.labeling.style
+ && let Some(style) = styles.get(style.references.as_str())
+ {
+ Style::decode(
+ Some(*style),
+ styles
+ .get(graph.cell_style.references.as_str())
+ .map(|v| &**v),
+ &mut look.areas[Area::Data(RowParity::Even)],
+ );
+ look.areas[Area::Data(RowParity::Odd)] =
+ look.areas[Area::Data(RowParity::Even)].clone();
+ }
+
+ let mut title = Value::empty();
+ let mut caption = Value::empty();
+ //Label::decode_
+
+ 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::<Dimension>()
+ && let Ok(max_width) = max_width.parse::<Dimension>()
+ {
+ look.heading_widths[HeadingRegion::Columns] =
+ min_width.as_pt() as usize..=max_width.as_pt() as usize;
+ }
+ }
+
+ 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) {
+ let label_series = if let Some(label_variable) = &sv.label_variable {
+ let Some(label_series) = series.get(label_variable.references.as_str()) else {
+ source_variables.push(sv);
+ continue;
+ };
+ Some(label_series)
+ } else {
+ None
+ };
+
+ let Some(data) = data
+ .get(&sv.source)
+ .and_then(|source| source.get(&sv.source_name))
+ else {
+ todo!()
+ };
+ fn remap_formats(
+ map: &mut HashMap<OrderedFloat<f64>, Datum<String>>,
+ 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())
+ };
+ map.insert(OrderedFloat(relabel.from), value);
+ // XXX warn on duplicate
+ }
+ (format.unwrap_or(crate::format::Format::F8_0), affixes)
+ }
+ let mut mapping = HashMap::new();
+ let (format, affixes) = remap_formats(&mut mapping, &sv.format, &sv.string_format);
+ fn execute_mapping(
+ mapping: &HashMap<OrderedFloat<f64>, Datum<String>>,
+ data: &mut Vec<DataValue>,
+ ) {
+ for value in data {
+ let Datum::Number(Some(number)) = value.value else {
+ continue;
+ };
+ if let Some(to) = mapping.get(&OrderedFloat(number)) {
+ value.index = Some(number);
+ value.value = to.clone();
+ }
+ }
+ }
+ let mut data = data.clone();
+ if !mapping.is_empty() {
+ execute_mapping(&mapping, &mut data);
+ } else if let Some(label_series) = label_series {
+ 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(),
+ };
+ mapping.insert(OrderedFloat(number), Datum::String(dest));
+ }
+ }
+ }
+ series.insert(
+ &sv.id,
+ Series {
+ label: sv.label.clone(),
+ format,
+ remapped: false,
+ values: data,
+ mapping,
+ affixes,
+ },
+ );
+ }
+
+ for dv in take(&mut derived_variables) {
+ let mut data = if dv.value == "constant(0)" {
+ let n_values = if let Some(series) = series.values().next() {
+ series.values.len()
+ } else {
+ derived_variables.push(dv);
+ continue;
+ };
+ (0..n_values)
+ .map(|_| DataValue {
+ index: Some(0.0),
+ value: Datum::Number(Some(0.0)),
+ })
+ .collect()
+ } else if dv.value.starts_with("constant") {
+ vec![]
+ } else if let Some(rest) = dv.value.strip_prefix("map(")
+ && let Some(var_name) = rest.strip_suffix(")")
+ {
+ let Some(dependency) = series.get(var_name) else {
+ derived_variables.push(dv);
+ continue;
+ };
+ dependency.values.clone()
+ } else {
+ unreachable!()
+ };
+ let mut mapping = HashMap::new();
+ for vme in &dv.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())
+ };
+ mapping.insert(OrderedFloat(from), to);
+ }
+ }
+ if !mapping.is_empty() {
+ for value in &mut data {
+ let Datum::Number(Some(number)) = value.value else {
+ continue;
+ };
+ if let Some(to) = mapping.get(&OrderedFloat(number)) {
+ value.index = Some(number);
+ value.value = to.clone();
+ }
+ }
+ }
+ }
+ }
+
+ todo!()
+ }
+}
+
+struct Series {
+ label: Option<String>,
+ format: crate::format::Format,
+ remapped: bool,
+ values: Vec<DataValue>,
+ mapping: HashMap<OrderedFloat<f64>, Datum<String>>,
+ affixes: Vec<Affix>,
+}
+
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
enum VisChild {
#[derive(Deserialize, Debug)]
#[serde(rename = "extension", rename_all = "camelCase")]
-struct VisualizationExtension;
+struct VisualizationExtension {
+ #[serde(rename = "@showGridline")]
+ show_gridline: Option<bool>,
+}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[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")]
reference: Option<String>,
}
-#[derive(Deserialize, Debug)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
enum Missing {
Listwise,
affixes: Vec<Affix>,
}
-#[derive(Deserialize, Debug)]
+#[derive(Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
struct Format {
#[serde(rename = "@baseFormat")]
#[serde(rename = "@minimumIntegerDigits")]
minimum_integer_digits: Option<usize>,
#[serde(rename = "@maximumFractionDigits")]
- maximum_fraction_digits: Option<usize>,
+ maximum_fraction_digits: Option<i64>,
#[serde(rename = "@minimumFractionDigits")]
minimum_fraction_digits: Option<usize>,
#[serde(rename = "@useGrouping")]
try_strings_as_numbers: Option<bool>,
#[serde(rename = "@negativesOutside")]
negatives_outside: Option<bool>,
- #[serde(default)]
- relabel: Vec<Relabel>,
+ #[serde(default, rename = "relabel")]
+ relabels: Vec<Relabel>,
#[serde(default, rename = "affix")]
affixes: Vec<Affix>,
}
+impl Format {
+ fn decode(&self) -> crate::format::Format {
+ if let Some(base_format) = self.base_format {
+ 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<usize>,
+ minimum_integer_digits: Option<i64>,
#[serde(rename = "@maximumFractionDigits")]
- maximum_fraction_digits: Option<usize>,
+ maximum_fraction_digits: Option<i64>,
#[serde(rename = "@minimumFractionDigits")]
- minimum_fraction_digits: Option<usize>,
+ minimum_fraction_digits: Option<i64>,
#[serde(rename = "@useGrouping")]
use_grouping: Option<bool>,
#[serde(rename = "@scientific")]
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: Option<BaseFormat>,
+ base_format: BaseFormat,
#[serde(rename = "@separatorChars")]
separator_chars: Option<String>,
#[serde(rename = "@mdyOrder")]
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 = "@baseFormat")]
- base_format: Option<BaseFormat>,
#[serde(rename = "@dayPadding")]
day_padding: Option<bool>,
#[serde(rename = "hourPadding")]
affixes: Vec<Affix>,
}
-#[derive(Deserialize, Debug)]
+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,
ElapsedTime,
}
-#[derive(Deserialize, Debug)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
enum MdyOrder {
DayMonthYear,
YearMonthDay,
}
-#[derive(Deserialize, Debug)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
enum MonthFormat {
Long,
PaddedNumber,
}
-#[derive(Deserialize, Debug)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
enum DayType {
Month,
Year,
}
-#[derive(Deserialize, Debug)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
enum HourFormat {
#[serde(rename = "AMPM")]
As12,
}
-#[derive(Deserialize, Debug)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
enum Scientific {
OnlyForSmall,
False,
}
-#[derive(Deserialize, Debug)]
+#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Affix {
/// The footnote number as a natural number: 1 for the first footnote, 2 for
value: String,
}
-#[derive(Deserialize, Debug)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
enum Position {
Subscript,
to: String,
}
+#[derive(Copy, Clone, Default, Debug, PartialEq, PartialOrd)]
+struct Dimension(f64);
+
+impl Dimension {
+ fn as_px(&self) -> f64 {
+ self.0 * 96.0
+ }
+ fn as_pt(&self) -> f64 {
+ self.0 * 72.0
+ }
+}
+
+impl<'de> Deserialize<'de> for Dimension {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let string = String::deserialize(deserializer)?;
+ Dimension::from_str(&string).map_err(D::Error::custom)
+ }
+}
+
+impl FromStr for Dimension {
+ type Err = ParseFloatError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ fn parse_unit(s: &str) -> (f64, &str) {
+ for (unit, per_inch) in &[
+ ("in", 1.0),
+ ("인치", 1.0),
+ ("pol.", 1.0),
+ ("cala", 1.0),
+ ("cali", 1.0),
+ ("cm", 2.54),
+ ("см", 2.54),
+ ("pt", 72.0),
+ ("пт", 72.0),
+ ("px", 96.0),
+ ] {
+ if let Some(rest) = s.strip_suffix(unit) {
+ return (*per_inch, rest);
+ }
+ }
+ (72.0, s)
+ }
+
+ let (per_inch, s) = parse_unit(s);
+ Ok(Self(s.trim().parse::<f64>()? / per_inch))
+ }
+}
+
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct Style {
font_underline: Option<FontUnderline>,
#[serde(rename = "@margin-bottom")]
- margin_bottom: Option<String>,
+ margin_bottom: Option<Dimension>,
#[serde(rename = "@margin-top")]
- margin_top: Option<String>,
+ margin_top: Option<Dimension>,
#[serde(rename = "@margin-left")]
- margin_left: Option<String>,
+ margin_left: Option<Dimension>,
#[serde(rename = "@margin-right")]
- margin_right: Option<String>,
+ margin_right: Option<Dimension>,
#[serde(rename = "@textAlignment")]
text_alignment: Option<TextAlignment>,
#[serde(rename = "@visible")]
visible: Option<bool>,
+
+ #[serde(rename = "@decimal-offset")]
+ decimal_offset: Option<Dimension>,
+}
+
+impl Style {
+ fn decode(fg: Option<&Style>, bg: Option<&Style>, out: &mut AreaStyle) {
+ if let Some(fg) = fg {
+ if let Some(weight) = fg.font_weight {
+ out.font_style.bold = weight.is_bold();
+ }
+ if let Some(style) = fg.font_style {
+ out.font_style.italic = style.is_italic();
+ }
+ if let Some(underline) = fg.font_underline {
+ out.font_style.underline = underline.is_underline();
+ }
+ if let Some(color) = fg.color {
+ out.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()
+ {
+ out.font_style.size = size;
+ } else {
+ // XXX warn?
+ }
+ }
+ if let Some(alignment) = fg.text_alignment {
+ out.cell_style.horz_align = alignment.as_horz_align(fg.decimal_offset);
+ }
+ if let Some(label_local_vertical) = fg.label_location_vertical {
+ out.cell_style.vert_align = label_local_vertical.into();
+ }
+ }
+ if let Some(bg) = bg {
+ if let Some(color) = bg.color {
+ out.font_style.bg = color;
+ }
+ }
+ }
}
-#[derive(Deserialize, Debug)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
enum Border {
Solid,
None,
}
-#[derive(Deserialize, Debug)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
enum FontWeight {
Regular,
Bold,
}
-#[derive(Deserialize, Debug)]
+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,
}
-#[derive(Deserialize, Debug)]
+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,
}
-#[derive(Deserialize, Debug)]
+impl FontUnderline {
+ fn is_underline(&self) -> bool {
+ *self == Self::Underline
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
enum TextAlignment {
Left,
Mixed,
}
-#[derive(Deserialize, Debug)]
+impl TextAlignment {
+ fn as_horz_align(&self, decimal_offset: Option<Dimension>) -> 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(),
+ decimal: Dot,
+ }),
+ TextAlignment::Mixed => None,
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
enum LabelLocation {
Positive,
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 {
/// Minimum size.
#[serde(rename = "@min")]
- min: Option<String>,
+ min: Option<Dimension>,
/// Maximum size.
#[serde(rename = "@max")]
- max: Option<String>,
+ max: Option<Dimension>,
/// An element to attach to. Required when method is attach or same, not
/// observed otherwise.
value: Option<String>,
}
-#[derive(Deserialize, Debug)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
enum Part {
Height,
Right,
}
-#[derive(Deserialize, Debug)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
enum Method {
SizeToContent,
mappings: Vec<FootnoteMapping>,
}
+impl Footnotes {
+ fn decode(&self, dst: &mut BTreeMap<usize, (String, String)>) {
+ for f in &self.mappings {
+ dst.entry(f.defines_reference.get() - 1).or_default().0 = f.to.clone();
+ }
+ }
+}
+
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct FootnoteMapping {
#[serde(rename = "@definesReference")]
- defines_reference: i64,
+ defines_reference: NonZeroUsize,
#[serde(rename = "@from")]
from: i64,
level: usize,
#[serde(rename = "@gap")]
- gap: Option<String>,
- //axis: Axis,
+ gap: Option<Dimension>,
+ axis: Axis,
}
#[derive(Deserialize, Debug)]
label_angle: f64,
#[serde(rename = "@length")]
- length: String,
+ length: Dimension,
#[serde(rename = "@style")]
style: Ref<Style>,
child: LabelChild,
}
-#[derive(Deserialize, Debug)]
+impl Label {
+ fn text(&self) -> &[Text] {
+ match &self.child {
+ LabelChild::Text(texts) => texts.as_slice(),
+ LabelChild::DescriptionGroup(description_group) => &[],
+ }
+ }
+
+ fn decode_style(&self, area_style: &mut AreaStyle, styles: &HashMap<&str, &Style>) {
+ let fg = styles.get(self.style.references.as_str()).map(|v| &**v);
+ let bg = if let Some(text_frame_style) = &self.text_frame_style {
+ styles
+ .get(text_frame_style.references.as_str())
+ .map(|v| &**v)
+ } else {
+ None
+ };
+ Style::decode(fg, bg, area_style);
+ }
+}
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Enum)]
#[serde(rename_all = "camelCase")]
enum Purpose {
Title,
#[serde(rename_all = "camelCase")]
struct Text {
#[serde(rename = "@usesReference")]
- uses_reference: Option<i64>,
+ uses_reference: Option<NonZeroUsize>,
#[serde(rename = "@definesReference")]
- defines_reference: Option<i64>,
+ defines_reference: Option<NonZeroUsize>,
#[serde(rename = "@position")]
position: Option<Position>,
name: Name,
}
-#[derive(Deserialize, Debug)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
enum Name {
Variable,
paragraph: Option<Paragraph>,
}
+impl LabelFrame {
+ fn decode(&self, look: &mut Look, styles: &HashMap<&String, &Style>) {
+ let Some(label) = &self.label else { return };
+ let Some(purpose) = label.purpose else { return };
+ let area = match purpose {
+ Purpose::Title => Area::Title,
+ Purpose::SubTitle => Area::Caption,
+ Purpose::SubSubTitle => return,
+ Purpose::Layer => Area::Layers,
+ Purpose::Footnote => Area::Footer,
+ };
+ let fg = styles.get(&label.style.references).map(|v| &**v);
+ let bg = if let Some(text_frame_style) = &label.text_frame_style {
+ styles.get(&&text_frame_style.references).map(|v| &**v)
+ } else {
+ None
+ };
+ Style::decode(fg, bg, &mut look.areas[area]);
+ todo!()
+ }
+}
+
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct Paragraph;
#[serde(rename = "@target")]
target: Option<Ref<Label>>,
}
+
+#[cfg(test)]
+mod tests {
+ use std::str::FromStr;
+
+ use crate::output::spv::legacy_xml::Dimension;
+
+ #[test]
+ fn dimension() {
+ for s in [
+ "1in",
+ "1.0인치",
+ "1.0e0 cali",
+ "96px",
+ "72 ",
+ "72 пт",
+ " 2.54см",
+ ] {
+ assert_eq!(Dimension(1.0), Dimension::from_str(s).unwrap());
+ }
+ }
+}