use std::{
borrow::Cow,
collections::BTreeMap,
+ fmt::Display,
iter::once,
mem::take,
str::FromStr,
/// The localized label for the item that appears in the outline pane in the
/// output viewer and in PDF outlines. This is `None` if no label has been
/// explicitly set.
- label: Option<String>,
+ pub label: Option<String>,
/// A locale-invariant identifier for the command that produced the output,
/// which may be `None` if unknown or if a command did not produce this
/// output.
- command_name: Option<String>,
+ pub command_name: Option<String>,
/// For a heading item, this is true if the heading's subtree should
/// be expanded in an outline view, false otherwise.
/// For other kinds of output items, this is true to show the item's
/// content, false to hide it. The item's label is always shown in an
/// outline view.
- show: bool,
+ pub show: bool,
/// Item details.
- details: Details,
+ pub details: Details,
/// If the item was read from an SPV file, this is additional information
/// related to that file.
#[serde(skip_serializing)]
- spv_info: Option<Box<SpvInfo>>,
+ pub spv_info: Option<Box<SpvInfo>>,
}
impl Item {
}
}
- /// Returns a new heading item suitable as the root node of an output document.
- ///
- /// A root node is a heading whose own properties are mostly disregarded.
- /// Instead of having root nodes, it would make just as much sense to just
- /// keep around arrays of nodes that would serve as the top level of an
- /// output document, but we'd need more special cases instead of just using
- /// the existing support for heading items.
- pub fn new_root() -> Self {
- Self::new(Details::Heading(Heading(Vec::new()))).with_label(Some(String::from("Output")))
- }
-
pub fn label(&self) -> Cow<'static, str> {
match &self.label {
Some(label) => Cow::from(label.clone()),
}
}
+ pub fn subtype(&self) -> Option<String> {
+ self.details
+ .as_table()
+ .map(|table| table.subtype().display(table).to_string())
+ }
+
pub fn with_show(self, show: bool) -> Self {
Self { show, ..self }
}
- pub fn with_label(self, label: Option<String>) -> Self {
- Self { label, ..self }
+ pub fn with_label(self, label: impl Into<String>) -> Self {
+ Self {
+ label: Some(label.into()),
+ ..self
+ }
}
pub fn with_command_name(self, command_name: Option<String>) -> Self {
}
}
+ pub fn with_some_command_name(self, command_name: impl Into<String>) -> Self {
+ self.with_command_name(Some(command_name.into()))
+ }
+
pub fn with_spv_info(self, spv_info: SpvInfo) -> Self {
Self {
spv_info: Some(Box::new(spv_info)),
}
}
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum ItemKind {
+ Chart,
+ Image,
+ Heading,
+ Message,
+ PageBreak,
+ Table,
+ Text,
+}
+
+impl ItemKind {
+ pub fn as_str(&self) -> &'static str {
+ match self {
+ ItemKind::Chart => "chart",
+ ItemKind::Image => "image",
+ ItemKind::Heading => "heading",
+ ItemKind::Message => "message",
+ ItemKind::PageBreak => "page break",
+ ItemKind::Table => "table",
+ ItemKind::Text => "text",
+ }
+ }
+}
+
+impl Display for ItemKind {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.as_str())
+ }
+}
+
#[derive(Clone, Debug, Serialize)]
pub enum Details {
Chart,
pub fn is_text(&self) -> bool {
matches!(self, Self::Text(_))
}
+
+ pub fn kind(&self) -> ItemKind {
+ match self {
+ Details::Chart => ItemKind::Chart,
+ Details::Image => ItemKind::Image,
+ Details::Heading(_) => ItemKind::Heading,
+ Details::Message(_) => ItemKind::Message,
+ Details::PageBreak => ItemKind::PageBreak,
+ Details::Table(_) => ItemKind::Table,
+ Details::Text(_) => ItemKind::Text,
+ }
+ }
}
impl<A> FromIterator<A> for Details
}
impl Text {
- pub fn new_log(value: impl Into<Value>) -> Self {
+ pub fn new(type_: TextType, content: impl Into<Value>) -> Self {
Self {
- type_: TextType::Log,
- content: value.into(),
+ type_,
+ content: content.into(),
}
}
+ pub fn new_log(content: impl Into<Value>) -> Self {
+ Self::new(TextType::Log, content)
+ }
pub fn into_item(self) -> Item {
Details::Text(Box::new(self)).into_item()
impl Criteria {
/// Returns a new output item whose children are all the (direct and
/// indirect) children of `item` that meet the criteria.
- fn apply(&self, item: Item) -> Item {
+ pub fn apply(&self, item: Item) -> Item {
fn take_children(item: &Item) -> Vec<&Item> {
item.details.children().iter().map(|item| &**item).collect()
}
}
}
fn unflatten_item(mut item: Item, include: &mut bit_vec::Iter, out: &mut Vec<Arc<Item>>) {
- let include_item = include.next().unwrap();
+ let include_item = include.next().unwrap_or_default(); //XXX should just be unwrap
if let Some(children) = item.details.mut_children() {
if !include_item {
unflatten_items(take(children), include, out);
select_matches(&items, selection, &mut include);
}
- let mut output = Item::new_root();
- unflatten_item(
- item,
- &mut include.iter(),
- output.details.mut_children().unwrap(),
- );
- output
+ let mut output = Vec::new();
+ unflatten_item(item, &mut include.iter(), &mut output);
+ Heading(output).into_item().with_label("Output")
}
}
fn extract<F, T: Clone + Send + Sync + 'static>(
matches: &ArgMatches,
- id: &clap::Id,
+ id: &str,
output: &mut BTreeMap<usize, Value>,
f: F,
) where
F: Fn(T) -> Value,
{
+ if !matches.contains_id(id) || matches.try_get_many::<clap::Id>(id).is_ok() {
+ // ignore groups
+ return;
+ }
+ let value_source = matches.value_source(id).expect("id came from matches");
+ if value_source != clap::parser::ValueSource::CommandLine {
+ // Any other source just gets tacked on at the end (like default values)
+ return;
+ }
for (value, index) in matches
- .try_get_many::<T>(id.as_str())
+ .try_get_many::<T>(id)
.unwrap()
.unwrap()
- .zip(matches.indices_of(id.as_str()).unwrap())
+ .zip(matches.indices_of(id).unwrap())
{
output.insert(index, f(value.clone()));
}
}
let mut values = BTreeMap::new();
- for id in matches.ids() {
- if matches.try_get_many::<clap::Id>(id.as_str()).is_ok() {
- // ignore groups
- continue;
- }
- let value_source = matches
- .value_source(id.as_str())
- .expect("id came from matches");
- if value_source != clap::parser::ValueSource::CommandLine {
- // Any other source just gets tacked on at the end (like default values)
- continue;
- }
- match id.as_str() {
- "_or" => extract(matches, id, &mut values, |_: bool| Value::Or),
- "select" => extract(matches, id, &mut values, Value::Classes),
- "commands" => extract(matches, id, &mut values, Value::Commands),
- "subtypes" => extract(matches, id, &mut values, Value::Subtypes),
- "labels" => extract(matches, id, &mut values, Value::Labels),
- "nth-commands" => extract(matches, id, &mut values, Value::NthCommands),
- "instances" => extract(matches, id, &mut values, Value::Instances),
- "show-hidden" => extract(matches, id, &mut values, Value::ShowHidden),
- "errors" => extract(matches, id, &mut values, Value::Errors),
- _ => unreachable!("{id}"),
- }
- }
-
- if !self.0.is_empty() {
+ extract(matches, "_or", &mut values, |_: bool| Value::Or);
+ extract(matches, "select", &mut values, Value::Classes);
+ extract(matches, "commands", &mut values, Value::Commands);
+ extract(matches, "subtypes", &mut values, Value::Subtypes);
+ extract(matches, "labels", &mut values, Value::Labels);
+ extract(matches, "nth_commands", &mut values, Value::NthCommands);
+ extract(matches, "instances", &mut values, Value::Instances);
+ extract(matches, "show_hidden", &mut values, Value::ShowHidden);
+ extract(matches, "errors", &mut values, Value::Errors);
+
+ if !values.is_empty() {
let mut selection = Selection::default();
for value in values.into_values() {
match value {
use clap::Parser;
use enumset::EnumSet;
- use crate::output::{Class, Criteria, Selection, StringMatch};
+ use crate::output::{
+ Class, Criteria, Heading, Item, Selection, StringMatch, pivot::PivotTable,
+ };
#[test]
fn parse_classes() {
);
}
- #[test]
- fn apply_criteria() {
- //let item = Details::Group();
- todo!()
+ fn regress_item() -> Item {
+ [
+ Heading::new()
+ .into_item()
+ .with_label("Set")
+ .with_some_command_name("Set"),
+ [Heading::new()
+ .into_item()
+ .with_label("Page Title")
+ .with_some_command_name("Title")]
+ .into_iter()
+ .collect::<Item>()
+ .with_label("Title")
+ .with_some_command_name("Title"),
+ [PivotTable::new([])
+ .with_title("Reading 1 record from INLINE.")
+ .with_subtype("Fixed Data Records")
+ .into_item()
+ .with_some_command_name("Data List")]
+ .into_iter()
+ .collect::<Item>()
+ .with_label("Data List")
+ .with_some_command_name("Data List"),
+ Heading::new()
+ .into_item()
+ .with_label("Begin Data")
+ .with_some_command_name("Begin Data"),
+ [PivotTable::new([])
+ .with_title("Data List")
+ .into_item()
+ .with_some_command_name("List")]
+ .into_iter()
+ .collect::<Item>()
+ .with_label("List")
+ .with_some_command_name("List"),
+ ]
+ .into_iter()
+ .collect::<Item>()
+ .with_label("Output")
}
}
}
impl Item {
- fn from_spv_file(path: impl AsRef<Path>) -> Result<(Self, Option<PageSetup>), Error> {
+ pub fn from_spv_file(path: impl AsRef<Path>) -> Result<(Self, Option<PageSetup>), Error> {
Self::from_spv_reader(File::open(path.as_ref())?)
}
- fn from_spv_zip_archive<R>(
+ pub fn from_spv_zip_archive<R>(
archive: &mut ZipArchive<R>,
) -> Result<(Self, Option<PageSetup>), Error>
where
Ok((items.into_iter().collect(), page_setup))
}
- fn from_spv_reader<R>(reader: R) -> Result<(Self, Option<PageSetup>), Error>
+ pub fn from_spv_reader<R>(reader: R) -> Result<(Self, Option<PageSetup>), Error>
where
R: Read + Seek,
{
where
R: Read + Seek,
{
- println!("{structure_member}");
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),
Ok(result) => result,
Err(error) => panic!("{error}"),
};
- dbg!(&heading);
let page_setup = heading.page_setup.take();
Ok((heading.decode(archive, structure_member)?, page_setup))
}
struct Heading {
#[serde(rename = "@visibility")]
visibility: Option<String>,
+ #[serde(rename = "@commandName")]
+ command_name: Option<String>,
label: Label,
page_setup: Option<PageSetup>,
}
ContainerContent::Text(container_text) => {
items.push(
- Text::new_log(container_text.decode())
- .into_item()
- .with_command_name(container_text.command_name)
- .with_spv_info(SpvInfo::new(structure_member)),
+ 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)),
);
}
}
}
- HeadingContent::Heading(heading) => {
+ 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)),
);
}
light.read_to_end(&mut data)?;
let table = LightTable::read(&mut Cursor::new(data))?;
let pivot_table = table.decode()?;
- println!("{}", &pivot_table);
Ok(pivot_table.into_item().with_spv_info(
SpvInfo::new(structure_member)
.with_members(SpvMembers::Light(self.table_structure.data_path.clone())),
.unwrap()
.0;
println!("{item}");
+ todo!()
}
use anyhow::Result;
use clap::{Args, ValueEnum};
-use pspp::output::Criteria;
+use pspp::output::{Criteria, Item};
use std::{fmt::Display, path::PathBuf};
/// Show information about SPSS viewer files (SPV files).
/// Input selection options.
#[command(flatten)]
- selection: Criteria,
+ criteria: Criteria,
+
+ /// Include ZIP member names in `dir` output.
+ #[arg(long = "member-names")]
+ show_member_names: bool,
}
/// What to show in a system file.
impl ShowSpv {
pub fn run(self) -> Result<()> {
- println!("{:#?}", &self);
- todo!()
+ match self.mode {
+ Mode::Directory => {
+ let item = Item::from_spv_file(&self.input)?.0;
+ //let item = self.criteria.apply(item);
+ for child in item.details.children() {
+ print_item_directory(&child, 0, self.show_member_names);
+ }
+ Ok(())
+ }
+ Mode::GetTableLook => todo!(),
+ Mode::ConvertTableLook => todo!(),
+ }
+ }
+}
+
+fn print_item_directory(item: &Item, level: usize, show_member_names: bool) {
+ for _ in 0..level {
+ print!(" ");
+ }
+ print!("- {} {:?}", item.details.kind(), item.label());
+ if let Some(table) = item.details.as_table() {
+ let title = table.title().display(table).to_string();
+ if item.label.as_ref().is_none_or(|label| label != &title) {
+ print!(" title {title:?}");
+ }
+ }
+ if let Some(command_name) = &item.command_name {
+ print!(" command {command_name:?}");
+ }
+ if let Some(subtype) = item.subtype()
+ && item.label.as_ref().is_none_or(|label| label != &subtype)
+ {
+ print!(" subtype {subtype:?}");
+ }
+ if !item.show {
+ if item.details.is_heading() {
+ print!(" (collapsed)");
+ } else {
+ print!(" (hidden)");
+ }
+ }
+ if show_member_names && let Some(spv_info) = &item.spv_info {
+ for (index, name) in spv_info.member_names().into_iter().enumerate() {
+ print!(" {} {name:?}", if index == 0 { "in" } else { "and" });
+ }
+ }
+ println!();
+ for child in item.details.children() {
+ print_item_directory(&child, level + 1, show_member_names);
}
}