};
use binrw::{BinWrite, Endian};
+use chrono::Utc;
use enum_map::EnumMap;
use quick_xml::{
events::{attributes::Attribute, BytesText},
};
fn light_table_name(table_id: u64) -> String {
- format!("{:010}_lightTableData.bin", table_id)
+ format!("{table_id:011}_lightTableData.bin")
}
-pub struct SpvWriter<W>
+fn output_viewer_name(heading_id: u64, is_heading: bool) -> String {
+ format!(
+ "outputViewer{heading_id:010}{}.xml",
+ if is_heading { "_heading" } else { "" }
+ )
+}
+
+pub struct SpvDriver<W>
where
W: Write + Seek,
{
writer: ZipWriter<W>,
needs_page_break: bool,
next_table_id: u64,
+ next_heading_id: u64,
}
-impl<W> SpvWriter<W>
+impl<W> SpvDriver<W>
where
W: Write + Seek,
{
pub fn new(writer: W) -> Self {
+ let mut writer = ZipWriter::new(writer);
+ writer
+ .start_file("META-INF/MANIFEST.MF", SimpleFileOptions::default())
+ .unwrap();
+ writer.write_all("allowPivoting=true".as_bytes()).unwrap();
Self {
- writer: ZipWriter::new(writer),
+ writer,
needs_page_break: false,
next_table_id: 1,
+ next_heading_id: 1,
}
}
page_break_before
}
- fn write_table(&mut self, item: &Item, pivot_table: &PivotTable) -> Container {
+ fn write_table<X>(
+ &mut self,
+ item: &Item,
+ pivot_table: &PivotTable,
+ structure: &mut XmlWriter<X>,
+ ) where
+ X: Write,
+ {
let table_id = self.next_table_id;
self.next_table_id += 1;
let mut content = Vec::new();
let mut cursor = Cursor::new(&mut content);
- pivot_table.write_be(&mut cursor).unwrap();
+ pivot_table.write_le(&mut cursor).unwrap();
+ let table_name = light_table_name(table_id);
self.writer
- .start_file(light_table_name(table_id), SimpleFileOptions::default())
+ .start_file(&table_name, SimpleFileOptions::default())
.unwrap(); // XXX
self.writer.write_all(&content).unwrap(); // XXX
- Container {
- page_break_before: self.page_break_before(),
- label: Label(item.label().into_owned()),
- show: item.show,
- command_name: item.command_name.clone(),
- content: Content::Table(Table {
- table_properties: None,
- table_structure: TableStructure,
- table_id,
- subtype: match &pivot_table.subtype {
- Some(subtype) => subtype.display(pivot_table).to_string(),
- None => String::from("unknown"),
- },
- }),
- }
+ self.container(structure, item, "vtb:table", |element| {
+ element
+ .with_attribute(("tableId", Cow::from(table_id.to_string())))
+ .with_attribute((
+ "subType",
+ Cow::from(pivot_table.subtype().display(pivot_table).to_string()),
+ ))
+ .write_inner_content(|w| {
+ w.create_element("vtb:tableStructure")
+ .write_inner_content(|w| {
+ w.create_element("vtb:dataPath")
+ .write_text_content(BytesText::new(&table_name))?;
+ Ok(())
+ })?;
+ Ok(())
+ })
+ .unwrap();
+ });
}
- fn write_item(&mut self, item: &Item) -> Option<Container> {
+ fn write_item<X>(&mut self, item: &Item, structure: &mut XmlWriter<X>)
+ where
+ X: Write,
+ {
match &item.details {
super::Details::Chart => todo!(),
super::Details::Image => todo!(),
super::Details::Group(children) => {
- let containers = children
- .iter()
- .map(|child| self.write_item(child))
- .flatten()
- .collect::<Vec<_>>();
+ let mut attributes = Vec::<Attribute>::new();
+ if let Some(command_name) = &item.command_name {
+ attributes.push((("commandName", command_name.as_str())).into());
+ }
+ if !item.show {
+ attributes.push(("visibility", "collapsed").into());
+ }
+ structure
+ .create_element("heading")
+ .with_attributes(attributes)
+ .write_inner_content(|w| {
+ w.create_element("label")
+ .write_text_content(BytesText::new(&item.label()))?;
+ for child in children {
+ self.write_item(child, w);
+ }
+ Ok(())
+ })
+ .unwrap();
}
super::Details::Message(_diagnostic) => todo!(),
super::Details::PageBreak => {
self.needs_page_break = true;
- None
}
- super::Details::Table(pivot_table) => Some(self.write_table(&*item, pivot_table)),
+ super::Details::Table(pivot_table) => self.write_table(&*item, pivot_table, structure),
super::Details::Text(_text) => todo!(),
}
}
+
+ fn container<'a, X, F>(
+ &mut self,
+ writer: &'a mut XmlWriter<X>,
+ item: &Item,
+ inner_elem: &str,
+ closure: F,
+ ) where
+ X: Write,
+ F: FnOnce(ElementWriter<X>),
+ {
+ writer
+ .create_element("container")
+ .with_attributes(
+ self.page_break_before()
+ .then_some(("page-break-before", "always")),
+ )
+ .with_attribute(("visibility", if item.show { "visible" } else { "hidden" }))
+ .write_inner_content(|w| {
+ let mut element = w
+ .create_element("label")
+ .write_text_content(BytesText::new(&item.label()))
+ .unwrap()
+ .create_element(inner_elem);
+ if let Some(command_name) = &item.command_name {
+ element = element.with_attribute(("commandName", command_name.as_str()));
+ };
+ closure(element);
+ Ok(())
+ })
+ .unwrap();
+ }
}
impl BinWrite for PivotTable {
) -> binrw::BinResult<()> {
// Header.
(
- 1u16,
+ 1u8,
+ 0u8,
+ 3u32, // version
SpvBool(true), // x0
SpvBool(false), // x1
SpvBool(self.rotate_inner_column_labels),
SpvBool(self.rotate_outer_row_labels),
- SpvBool(true),
- 0x15u32,
+ SpvBool(true), // x2
+ 0x15u32, // x3
*self.look.heading_widths[HeadingRegion::Columns].start() as i32,
*self.look.heading_widths[HeadingRegion::Columns].end() as i32,
*self.look.heading_widths[HeadingRegion::Rows].start() as i32,
*self.look.heading_widths[HeadingRegion::Rows].end() as i32,
+ 0u64,
)
.write_le(writer)?;
(
0u32,
SpvString("en_US.ISO_8859-1:1987"),
- 0u32,
- SpvBool(false),
- SpvBool(false),
- SpvBool(true),
+ 0u32, // XXX current_layer
+ SpvBool(false), // x7
+ SpvBool(false), // x8
+ SpvBool(false), // x9
y0(self),
custom_currency(self),
- Counted::new(Counted::new(((x1(self), x2()), x3(self)))),
+ Counted::new((Counted::new((x1(self), x2())), x3(self))),
)
.write_le(writer)?;
}
}
-impl<W> Driver for SpvWriter<W>
+impl<W> Driver for SpvDriver<W>
where
W: Write + Seek,
{
}
fn write(&mut self, item: &Arc<Item>) {
- if let Some(container) = self.write_item(item) {}
+ if item.details.is_page_break() {
+ self.needs_page_break = true;
+ return;
+ }
+
+ let mut headings = XmlWriter::new(Cursor::new(Vec::new()));
+ let element = headings
+ .create_element("heading")
+ .with_attribute((
+ "creation-date-time",
+ Cow::from(Utc::now().format("%x %x").to_string()),
+ ))
+ .with_attribute((
+ "creator",
+ Cow::from(format!(
+ "{} {}",
+ env!("CARGO_PKG_NAME"),
+ env!("CARGO_PKG_VERSION")
+ )),
+ ))
+ .with_attribute(("creator-version", "21"))
+ .with_attribute(("xmlns", "http://xml.spss.com/spss/viewer/viewer-tree"))
+ .with_attribute((
+ "xmlns:vps",
+ "http://xml.spss.com/spss/viewer/viewer-pagesetup",
+ ))
+ .with_attribute(("xmlns:vtx", "http://xml.spss.com/spss/viewer/viewer-text"))
+ .with_attribute(("xmlns:vtb", "http://xml.spss.com/spss/viewer/viewer-table"));
+ element
+ .write_inner_content(|w| {
+ w.create_element("label")
+ .write_text_content(BytesText::new("Output"))?;
+ // XXX page setup
+ self.write_item(item, w);
+ Ok(())
+ })
+ .unwrap();
+
+ let headings = headings.into_inner().into_inner();
+ let heading_id = self.next_heading_id;
+ self.next_heading_id += 1;
+ self.writer
+ .start_file(
+ output_viewer_name(heading_id, item.details.as_group().is_some()),
+ SimpleFileOptions::default(),
+ )
+ .unwrap(); // XXX
+ self.writer.write_all(&headings).unwrap(); // XXX
}
}
}
}
-struct OptionalStyle<'a> {
+struct ValueMod<'a> {
style: &'a Option<Box<ValueStyle>>,
template: Option<&'a str>,
}
-impl<'a> OptionalStyle<'a> {
+impl<'a> ValueMod<'a> {
fn new(value: &'a Value) -> Self {
Self {
style: &value.styling,
}
}
-impl<'a> Default for OptionalStyle<'a> {
+impl<'a> Default for ValueMod<'a> {
fn default() -> Self {
Self {
style: &None,
}
}
-impl<'a> BinWrite for OptionalStyle<'a> {
+impl<'a> BinWrite for ValueMod<'a> {
type Args<'b> = ();
fn write_options<W: Write + Seek>(
if number.var_name.is_some() || number.value_label.is_some() {
(
2u8,
- OptionalStyle::new(self),
+ ValueMod::new(self),
SpvFormat {
format: number.format,
honor_small: number.honor_small,
} else {
(
1u8,
- OptionalStyle::new(self),
+ ValueMod::new(self),
number.value.unwrap_or(-f64::MAX),
Show::as_spv(&number.show),
)
ValueInner::String(string) => {
(
4u8,
- OptionalStyle::new(self),
+ ValueMod::new(self),
SpvFormat {
format: if string.hex {
Format::new(Type::AHex, (string.s.len() * 2) as u16, 0).unwrap()
ValueInner::Variable(variable) => {
(
5u8,
- OptionalStyle::new(self),
+ ValueMod::new(self),
SpvString(&variable.var_name),
SpvString::optional(&variable.variable_label),
Show::as_spv(&variable.show),
(
3u8,
SpvString(&text.local),
- OptionalStyle::new(self),
+ ValueMod::new(self),
SpvString(&text.id),
SpvString(&text.c),
SpvBool(true),
ValueInner::Template(template) => {
(
0u8,
- OptionalStyle::new(self),
+ ValueMod::new(self),
SpvString(&template.local),
template.args.len() as u32,
)
(
3u8,
SpvString(""),
- OptionalStyle::default(),
+ ValueMod::default(),
SpvString(""),
SpvString(""),
SpvBool(true),