work on legacy spvs
authorBen Pfaff <blp@cs.stanford.edu>
Fri, 17 Oct 2025 20:44:33 +0000 (13:44 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Fri, 17 Oct 2025 20:44:33 +0000 (13:44 -0700)
rust/doc/src/spv/legacy-detail-xml.md
rust/pspp/src/output/spv.rs
rust/pspp/src/output/spv/legacy.rs [new file with mode: 0644]

index 5f0fe3399af3f0ee49490a8a60c62344406f633a..f7e77f0c322e9b5ad07b216cb28d2dae9d9052fa 100644 (file)
@@ -67,6 +67,8 @@ categoricalDomain => variableReference simpleSort
 
 simpleSort :method[sort_method]=(custom) => categoryOrder
 
+categoryOrder => TEXT
+
 container :style=ref style => container_extension? location+ labelFrame*
 
 extension[container_extension] :combinedFootnotes=(true) => EMPTY
index 39afb651b3dd717944c0f074743774dfd073f3fe..292d91926e845298dc90e38ecf4f7e480901b85b 100644 (file)
@@ -31,11 +31,15 @@ use crate::output::{
     Details, Item, SpvInfo, SpvMembers, Text,
     page::PageSetup,
     pivot::{PivotTable, TableProperties, Value},
-    spv::light::{LightError, LightTable},
+    spv::{
+        legacy::Visualization,
+        light::{LightError, LightTable},
+    },
 };
 
 mod css;
 pub mod html;
+mod legacy;
 mod light;
 
 #[derive(Debug, Display, thiserror::Error)]
@@ -396,24 +400,36 @@ impl Table {
     where
         R: Read + Seek,
     {
-        if self.table_structure.path.is_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"
+        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())),
                 ))
-            })?;
-            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())),
-            ))
-        } else {
-            Ok(PivotTable::new([]).into_item())
+            }
+            Some(xml_member_name) => {
+                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:?}"),
+                };
+                Ok(PivotTable::new([]).into_item())
+            }
         }
     }
 }
@@ -455,8 +471,11 @@ enum TextType {
 #[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>,
 }
 
diff --git a/rust/pspp/src/output/spv/legacy.rs b/rust/pspp/src/output/spv/legacy.rs
new file mode 100644 (file)
index 0000000..209f553
--- /dev/null
@@ -0,0 +1,1104 @@
+// 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::marker::PhantomData;
+
+use serde::Deserialize;
+
+use crate::output::pivot::Color;
+
+#[derive(Debug)]
+struct Ref<T> {
+    references: String,
+    _phantom: PhantomData<T>,
+}
+
+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(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>,
+
+    extension: Option<VisualizationExtension>,
+    user_source: UserSource,
+    variables: Vec<Variable>,
+    categorical_domain: Option<CategoricalDomain>,
+    graph: Graph,
+    lf1: LabelFrame,
+    container: Option<Container>,
+    lf2: LabelFrame,
+    styles: Vec<Style>,
+    layer_controller: Option<LayerController>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename = "extension", rename_all = "camelCase")]
+struct VisualizationExtension;
+
+#[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 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>>,
+
+    extensions: Vec<VariableExtension>,
+    format: Option<Format>,
+    string_format: Option<StringFormat>,
+}
+
+#[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,
+    format: Option<Format>,
+    string_format: Option<StringFormat>,
+    value_map: Vec<ValueMapEntry>,
+}
+
+#[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 = "@missing")]
+    missing: Option<Missing>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct CategoricalDomain {
+    variable_reference: VariableReference,
+    simple_sort: SimpleSort,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct VariableReference {
+    #[serde(rename = "@ref")]
+    reference: Option<String>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct SimpleSort {
+    #[serde(rename = "@method")]
+    category_order: CategoryOrder,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct CategoryOrder {}
+
+#[derive(Deserialize, Debug)]
+#[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)]
+#[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<usize>,
+    #[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>,
+    relabel: Vec<Relabel>,
+    #[serde(default, rename = "affix")]
+    affixes: Vec<Affix>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct NumberFormat {
+    #[serde(rename = "@minimumIntegerDigits")]
+    minimum_integer_digits: Option<usize>,
+    #[serde(rename = "@maximumFractionDigits")]
+    maximum_fraction_digits: Option<usize>,
+    #[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(default, rename = "affix")]
+    affixes: Vec<Affix>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct DateTimeFormat {
+    #[serde(rename = "@baseFormat")]
+    base_format: Option<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>,
+}
+
+#[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")]
+    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>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum BaseFormat {
+    Date,
+    Time,
+    DateTime,
+    ElapsedTime,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum MdyOrder {
+    DayMonthYear,
+    MonthDayYear,
+    YearMonthDay,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum MonthFormat {
+    Long,
+    Short,
+    Number,
+    PaddedNumber,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum DayType {
+    Month,
+    Year,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum HourFormat {
+    #[serde(rename = "AMPM")]
+    AmPm,
+    #[serde(rename = "AS_24")]
+    As24,
+    #[serde(rename = "AS_12")]
+    As12,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum Scientific {
+    OnlyForSmall,
+    WhenNeeded,
+    True,
+    False,
+}
+
+#[derive(Deserialize, Debug)]
+#[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(Deserialize, Debug)]
+#[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: f64,
+}
+
+#[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 {
+    /// 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<String>,
+
+    #[serde(rename = "@margin-top")]
+    margin_top: Option<String>,
+
+    #[serde(rename = "@margin-left")]
+    margin_left: Option<String>,
+
+    #[serde(rename = "@margin-right")]
+    margin_right: Option<String>,
+
+    #[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>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum Border {
+    Solid,
+    Thick,
+    Thin,
+    Double,
+    None,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum FontWeight {
+    Regular,
+    Bold,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum FontStyle {
+    Regular,
+    Italic,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum FontUnderline {
+    None,
+    Underline,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum TextAlignment {
+    Left,
+    Right,
+    Center,
+    Decimal,
+    Mixed,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum LabelLocation {
+    Positive,
+    Negative,
+    Center,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Graph {
+    #[serde(rename = "@cellStyle")]
+    cell_style: Ref<Style>,
+
+    #[serde(rename = "@style")]
+    style: Ref<Style>,
+
+    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<String>,
+
+    /// Maximum size.
+    #[serde(rename = "@max")]
+    max: Option<String>,
+
+    /// 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(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum Part {
+    Height,
+    Wdith,
+    Top,
+    Bottom,
+    Left,
+    Right,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum Method {
+    SizeToContent,
+    Attach,
+    Fixed,
+    Same,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Faceting {
+    #[serde(default)]
+    layers1: Vec<Layer>,
+    cross: Cross,
+    #[serde(default)]
+    layers2: Vec<Layer>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Cross {
+    #[serde(rename = "$value")]
+    child: CrossChild,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum CrossChild {
+    /// No dimensions along this axis.
+    Unity,
+    /// Dimensions along this axis.
+    Nest(
+        /// From innermost to outermost.
+        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(default)]
+    scp1: Vec<SetCellProperties>,
+    #[serde(rename = "facetLevel")]
+    facet_levels: Vec<FacetLevel>,
+    #[serde(default)]
+    #[serde(rename = "setCellProperties")]
+    scp2: Vec<SetCellProperties>,
+}
+
+#[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 = "@applyToConverse")]
+    apply_to_converse: Option<bool>,
+
+    #[serde(rename = "$value")]
+    sets: Vec<Set>,
+
+    #[serde(rename = "union")]
+    unions: Option<Union>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Union {
+    #[serde(rename = "intersect")]
+    intersects: Vec<Intersect>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum Intersect {
+    Where(Where),
+    IntersectWhere(IntersectWhere),
+    Alternating,
+    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: 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 = "@style")]
+    style: Ref<Style>,
+
+    labeling: Labeling,
+    footnotes: Option<Footnotes>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Labeling {
+    #[serde(rename = "@style")]
+    style: Ref<Style>,
+
+    #[serde(rename = "@variable")]
+    variable: String,
+
+    children: Vec<LabelingChild>,
+}
+
+#[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(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Footnotes {
+    #[serde(rename = "@superscript")]
+    superscript: Option<bool>,
+
+    #[serde(rename = "@variable")]
+    variable: String,
+
+    mappings: Vec<FootnoteMapping>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct FootnoteMapping {
+    #[serde(rename = "@definesReference")]
+    defines_reference: i64,
+
+    #[serde(rename = "@from")]
+    from: i64,
+
+    #[serde(rename = "@to")]
+    to: String,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct FacetLevel {
+    #[serde(rename = "@level")]
+    level: usize,
+
+    #[serde(rename = "@gap")]
+    gap: Option<String>,
+    //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 = "@labelAngle")]
+    label_angle: f64,
+
+    #[serde(rename = "@length")]
+    length: String,
+
+    #[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: 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: Ref<Style>,
+
+    #[serde(rename = "@purpose")]
+    purpose: Option<Purpose>,
+
+    child: LabelChild,
+}
+
+#[derive(Deserialize, Debug)]
+#[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<i64>,
+
+    #[serde(rename = "@definesReference")]
+    defines_reference: Option<i64>,
+
+    #[serde(rename = "@position")]
+    position: Option<Position>,
+
+    #[serde(rename = "@style")]
+    style: 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>,
+
+    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(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum Name {
+    Variable,
+    Value,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct LabelFrame {
+    #[serde(rename = "@style")]
+    style: Ref<Style>,
+    locations: Vec<Location>,
+    label: Option<Label>,
+    paragraph: Option<Paragraph>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Paragraph;
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Container {
+    #[serde(rename = "@style")]
+    style: Ref<Style>,
+
+    extensions: Option<ContainerExtension>,
+    locations: Vec<Location>,
+    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>>,
+}