#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ItemKind {
- Chart,
+ Graph,
Image,
Heading,
Message,
impl ItemKind {
pub fn as_str(&self) -> &'static str {
match self {
- ItemKind::Chart => "chart",
+ ItemKind::Graph => "graph",
ItemKind::Image => "image",
ItemKind::Heading => "heading",
ItemKind::Message => "message",
#[derive(Clone, Debug, Serialize)]
pub enum Details {
- Chart,
+ Graph,
Image(#[serde(skip_serializing)] ImageSurface),
Heading(Heading),
Message(Box<Diagnostic>),
pub fn command_name(&self) -> Option<&String> {
match self {
- Details::Chart
+ Details::Graph
| Details::Image(_)
| Details::Heading(_)
| Details::Message(_)
pub fn label(&self) -> Cow<'static, str> {
match self {
- Details::Chart => Cow::from("chart"),
+ Details::Graph => Cow::from("Graph"),
Details::Image(_) => Cow::from("Image"),
Details::Heading(_) => Cow::from("Group"),
Details::Message(diagnostic) => Cow::from(diagnostic.severity.as_title_str()),
pub fn kind(&self) -> ItemKind {
match self {
- Details::Chart => ItemKind::Chart,
+ Details::Graph => ItemKind::Graph,
Details::Image(_) => ItemKind::Image,
Details::Heading(_) => ItemKind::Heading,
Details::Message(_) => ItemKind::Message,
}
#[derive(Clone, Debug, Serialize)]
-pub struct Chart;
+pub struct Graph;
-impl Chart {
+impl Graph {
pub fn into_item(self) -> Item {
- Details::Chart.into_item()
+ Details::Graph.into_item()
}
}
}
}
- pub fn with_members(self, members: SpvMembers) -> Self {
- Self {
- members: Some(members),
- ..self
- }
+ pub fn with_members(self, members: Option<SpvMembers>) -> Self {
+ Self { members, ..self }
}
- pub fn with_error(self) -> Self {
- Self {
- error: true,
- ..self
- }
+ pub fn with_error(self, error: bool) -> Self {
+ Self { error, ..self }
}
pub fn member_names(&self) -> Vec<&str> {
/// `.png` file.
String,
),
- /// Chart members.
+ /// Graph members.
Graph {
/// Data member name.
data: Option<String>,
/// output item types; for example, "warnings" are a subset of text items.
#[derive(Debug, EnumSetType)]
pub enum Class {
- Charts,
+ Graphs,
Headings,
Logs,
Models,
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
- "charts" => Ok(Self::Charts),
+ "graphs" | "charts" => Ok(Self::Graphs),
"headings" => Ok(Self::Headings),
"logs" => Ok(Self::Logs),
"models" => Ok(Self::Models),
fn class(&self) -> Class {
let label = self.label.as_ref().map(|s| s.as_str());
match &self.details {
- Details::Chart => Class::Charts,
+ Details::Graph => Class::Graphs,
Details::Image(_) => Class::Other,
Details::Heading(_) => Class::OutlineHeaders,
Details::Message(diagnostic) => match diagnostic.severity {
fn write(&mut self, item: &Arc<Item>) {
// todo: error handling (should not unwrap)
match &item.details {
- Details::Chart | Details::Image(_) | Details::Heading(_) => (),
+ Details::Graph | Details::Image(_) | Details::Heading(_) => (),
Details::Message(diagnostic) => {
self.start_item();
let text = diagnostic.to_string();
fn write(&mut self, item: &Arc<Item>) {
match &item.details {
- Details::Chart | Details::Image(_) | Details::Heading(_) => todo!(),
+ Details::Graph | Details::Image(_) | Details::Heading(_) => todo!(),
Details::Message(_diagnostic) => todo!(),
Details::PageBreak => (),
Details::Table(pivot_table) => {
for item in ItemRefIterator::without_hidden(item).filter(|item| !item.details.is_heading())
{
match &item.details {
- Details::Chart => {
+ Details::Graph => {
self.start_object(writer)?;
writeln!(writer, "Omitting chart from text output")?
}
cell::RefCell,
fmt::Display,
fs::File,
- io::{BufReader, Cursor, Read, Seek},
+ io::{BufReader, Read, Seek},
path::Path,
rc::Rc,
};
-use anyhow::Context;
-use binrw::{BinRead, error::ContextExt};
-use cairo::ImageSurface;
use displaydoc::Display;
-use paper_sizes::PaperSize;
use serde::Deserialize;
use zip::{ZipArchive, result::ZipError};
use crate::{
crypto::EncryptedReader,
- output::{
- Details, Item, SpvInfo, SpvMembers, Text,
- page::{self, Orientation},
- pivot::{Axis2, Length, TableProperties, look::Look, value::Value},
- },
- spv::read::{
- html::Document,
- legacy_bin::LegacyBin,
- legacy_xml::Visualization,
- light::{LightTable, LightWarning},
- },
+ output::{Item, page::PageSetup},
+ spv::read::light::LightWarning,
};
mod css;
pub mod legacy_bin;
mod legacy_xml;
mod light;
+mod structure;
#[cfg(test)]
mod tests;
ZipError::InvalidArchive(_) => Error::NotSpv,
other => other.into(),
})?;
- Ok(self.open_zip_archive(archive)?)
+ Ok(self.open_archive(archive)?)
}
/// Opens the provided Zip `archive`.
///
/// Any password provided for reading the file is unused, because if one was
/// needed then it must have already been used to open the archive.
- pub fn open_zip_archive(
+ pub fn open_archive(
self,
mut archive: ZipArchive<Box<dyn ReadSeek>>,
) -> Result<SpvFile, Error> {
for i in 0..archive.len() {
let name = String::from(archive.name_for_index(i).unwrap());
if name.starts_with("outputViewer") && name.ends_with(".xml") {
- let (mut new_items, ps) = read_heading(&mut archive, i, &name, &warn)?;
- items.append(&mut new_items);
- page_setup = page_setup.or(ps);
+ let member = BufReader::new(archive.by_index(i)?);
+ let member = structure::StructureMember::read(member, &name, &warn)?;
+ items.extend(member.root.read_items(&mut archive, &name, &warn));
+ page_setup = page_setup.or(member.page_setup);
}
}
pub items: Vec<Item>,
/// The page setup in the SPV file, if any.
- pub page_setup: Option<page::PageSetup>,
+ pub page_setup: Option<PageSetup>,
/// The Zip archive that the file was read from.
pub archive: ZipArchive<Box<dyn ReadSeek>>,
impl SpvFile {
/// Returns the contents of the `SpvFile`.
- pub fn into_contents(self) -> (Vec<Item>, Option<page::PageSetup>) {
+ pub fn into_contents(self) -> (Vec<Item>, Option<PageSetup>) {
(self.items, self.page_setup)
}
/// {0}
CairoError(#[from] cairo::IoError),
-}
-
-fn new_error_item(message: impl Into<Value>) -> Item {
- Text::new_log(message).into_item().with_label("Error")
-}
-
-fn read_heading<R>(
- archive: &mut ZipArchive<R>,
- file_number: usize,
- structure_member: &str,
- warn: &Rc<RefCell<Box<dyn FnMut(Warning)>>>,
-) -> Result<(Vec<Item>, Option<page::PageSetup>), Error>
-where
- R: Read + Seek,
-{
- 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),
- )
- .with_context(|| format!("Failed to parse {structure_member}"))
- {
- Ok(result) => result,
- Err(error) => panic!("{error:?}"),
- };
- let page_setup = heading
- .page_setup
- .take()
- .map(|ps| ps.decode(warn, structure_member));
- Ok((heading.decode(archive, structure_member, warn)?, page_setup))
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Heading {
- #[serde(rename = "@visibility")]
- visibility: Option<String>,
- #[serde(rename = "@commandName")]
- command_name: Option<String>,
- label: Label,
- page_setup: Option<PageSetup>,
-
- #[serde(rename = "$value")]
- #[serde(default)]
- children: Vec<HeadingContent>,
-}
-
-impl Heading {
- fn decode<R>(
- self,
- archive: &mut ZipArchive<R>,
- structure_member: &str,
- warn: &Rc<RefCell<Box<dyn FnMut(Warning)>>>,
- ) -> Result<Vec<Item>, Error>
- where
- R: Read + Seek,
- {
- let mut items = Vec::new();
- for child in self.children {
- match child {
- HeadingContent::Container(container) => {
- if container.page_break_before == PageBreakBefore::Always {
- items.push(
- Details::PageBreak
- .into_item()
- .with_spv_info(SpvInfo::new(structure_member)),
- );
- }
- let item = match container.content {
- ContainerContent::Table(table) => table
- .decode(archive, structure_member, warn)
- .unwrap_or_else(|error| {
- new_error_item(format!("Error reading table: {error}"))
- .with_spv_info(SpvInfo::new(structure_member).with_error())
- }),
- ContainerContent::Graph(graph) => graph.decode(structure_member),
- ContainerContent::Text(container_text) => 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)),
- ContainerContent::Image(image) => image
- .decode(archive, structure_member)
- .unwrap_or_else(|error| {
- new_error_item(format!("Error reading image: {error}"))
- .with_spv_info(SpvInfo::new(structure_member).with_error())
- }),
- ContainerContent::Object(object) => object
- .decode(archive, structure_member)
- .unwrap_or_else(|error| {
- new_error_item(format!("Error reading object: {error}"))
- .with_spv_info(SpvInfo::new(structure_member).with_error())
- }),
- ContainerContent::Model => new_error_item("models not yet implemented")
- .with_spv_info(SpvInfo::new(structure_member).with_error()),
- ContainerContent::Tree => new_error_item("trees not yet implemented")
- .with_spv_info(SpvInfo::new(structure_member).with_error()),
- };
- items.push(item.with_show(container.visibility == Visibility::Visible));
- }
- 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, warn)?
- .into_iter()
- .collect::<Item>()
- .with_show(show)
- .with_label(label)
- .with_command_name(command_name)
- .with_spv_info(SpvInfo::new(structure_member)),
- );
- }
- }
- }
- Ok(items)
- }
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct PageSetup {
- #[serde(rename = "@initial-page-number")]
- pub initial_page_number: Option<i32>,
- #[serde(rename = "@chart-size")]
- pub chart_size: Option<ChartSize>,
- #[serde(rename = "@margin-left")]
- pub margin_left: Option<Length>,
- #[serde(rename = "@margin-right")]
- pub margin_right: Option<Length>,
- #[serde(rename = "@margin-top")]
- pub margin_top: Option<Length>,
- #[serde(rename = "@margin-bottom")]
- pub margin_bottom: Option<Length>,
- #[serde(rename = "@paper-height")]
- pub paper_height: Option<Length>,
- #[serde(rename = "@paper-width")]
- pub paper_width: Option<Length>,
- #[serde(rename = "@reference-orientation")]
- pub reference_orientation: Option<String>,
- #[serde(rename = "@space-after")]
- pub space_after: Option<Length>,
- pub page_header: PageHeader,
- pub page_footer: PageFooter,
-}
-
-impl PageSetup {
- fn decode(
- &self,
- warn: &Rc<RefCell<Box<dyn FnMut(Warning)>>>,
- structure_member: &str,
- ) -> page::PageSetup {
- let mut setup = page::PageSetup::default();
- if let Some(initial_page_number) = self.initial_page_number {
- setup.initial_page_number = initial_page_number;
- }
- if let Some(chart_size) = self.chart_size {
- setup.chart_size = chart_size.into();
- }
- if let Some(margin_left) = self.margin_left {
- setup.margins.0[Axis2::X][0] = margin_left.into();
- }
- if let Some(margin_right) = self.margin_right {
- setup.margins.0[Axis2::X][1] = margin_right.into();
- }
- if let Some(margin_top) = self.margin_top {
- setup.margins.0[Axis2::Y][0] = margin_top.into();
- }
- if let Some(margin_bottom) = self.margin_bottom {
- setup.margins.0[Axis2::Y][1] = margin_bottom.into();
- }
- match (self.paper_width, self.paper_height) {
- (Some(width), Some(height)) => {
- setup.paper = PaperSize::new(width.0, height.0, paper_sizes::Unit::Inch)
- }
- (Some(length), None) | (None, Some(length)) => {
- setup.paper = PaperSize::new(length.0, length.0, paper_sizes::Unit::Inch)
- }
- (None, None) => (),
- }
- if let Some(reference_orientation) = &self.reference_orientation {
- if reference_orientation.starts_with("0") {
- setup.orientation = Orientation::Portrait;
- } else if reference_orientation.starts_with("90") {
- setup.orientation = Orientation::Landscape;
- } else {
- (warn.borrow_mut())(Warning {
- member: structure_member.into(),
- details: WarningDetails::UnknownOrientation(reference_orientation.clone()),
- });
- }
- }
- if let Some(space_after) = self.space_after {
- setup.object_spacing = space_after.into();
- }
- if let Some(PageParagraph { text }) = &self.page_header.page_paragraph {
- setup.header = text.decode();
- }
- if let Some(PageParagraph { text }) = &self.page_footer.page_paragraph {
- setup.footer = text.decode();
- }
- setup
- }
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct PageHeader {
- page_paragraph: Option<PageParagraph>,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct PageFooter {
- page_paragraph: Option<PageParagraph>,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct PageParagraph {
- text: PageParagraphText,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct PageParagraphText {
- #[serde(default, rename = "$text")]
- text: String,
-}
-
-impl PageParagraphText {
- fn decode(&self) -> Document {
- Document::from_html(&self.text)
- }
-}
-
-#[derive(Copy, Clone, Debug, Default, Deserialize)]
-#[serde(rename = "snake_case")]
-enum ReferenceOrientation {
- #[serde(alias = "0")]
- #[serde(alias = "0deg")]
- #[serde(alias = "inherit")]
- #[default]
- Portrait,
-
- #[serde(alias = "90")]
- #[serde(alias = "90deg")]
- #[serde(alias = "-270")]
- #[serde(alias = "-270deg")]
- Landscape,
-
- #[serde(alias = "180")]
- #[serde(alias = "180deg")]
- #[serde(alias = "-1280")]
- #[serde(alias = "-180deg")]
- ReversePortrait,
-
- #[serde(alias = "270")]
- #[serde(alias = "270deg")]
- #[serde(alias = "-90")]
- #[serde(alias = "-90deg")]
- Seascape,
-}
-
-impl From<ReferenceOrientation> for page::Orientation {
- fn from(value: ReferenceOrientation) -> Self {
- match value {
- ReferenceOrientation::Portrait | ReferenceOrientation::ReversePortrait => {
- page::Orientation::Portrait
- }
- ReferenceOrientation::Landscape | ReferenceOrientation::Seascape => {
- page::Orientation::Landscape
- }
- }
- }
-}
-
-/// Chart size.
-#[derive(Copy, Clone, Debug, Default, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-enum ChartSize {
- FullHeight,
- HalfHeight,
- QuarterHeight,
- #[default]
- #[serde(other)]
- AsIs,
-}
-
-impl From<ChartSize> for page::ChartSize {
- fn from(value: ChartSize) -> Self {
- match value {
- ChartSize::AsIs => page::ChartSize::AsIs,
- ChartSize::FullHeight => page::ChartSize::FullHeight,
- ChartSize::HalfHeight => page::ChartSize::HalfHeight,
- ChartSize::QuarterHeight => page::ChartSize::QuarterHeight,
- }
- }
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-enum HeadingContent {
- Container(Container),
- Heading(Box<Heading>),
-}
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Label {
- #[serde(default, rename = "$text")]
- text: String,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Container {
- #[serde(default, rename = "@visibility")]
- visibility: Visibility,
- #[serde(rename = "@page-break-before")]
- #[serde(default)]
- page_break_before: PageBreakBefore,
- #[serde(rename = "@text-align")]
- text_align: Option<TextAlign>,
- #[serde(rename = "@width")]
- width: Option<String>,
- label: Label,
-
- #[serde(rename = "$value")]
- content: ContainerContent,
-}
-
-#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum PageBreakBefore {
- #[default]
- Auto,
- Always,
- Avoid,
- Left,
- Right,
- Inherit,
-}
-
-#[derive(Deserialize, Debug, Default, PartialEq, Eq)]
-#[serde(rename_all = "camelCase")]
-enum Visibility {
- #[default]
- Visible,
- Hidden,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-enum TextAlign {
- Left,
- Center,
- Right,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-enum ContainerContent {
- Table(Table),
- Text(ContainerText),
- Graph(Graph),
- Model,
- Object(Object),
- Image(Image),
- Tree,
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Graph {
- #[serde(rename = "@commandName")]
- command_name: String,
- data_path: Option<String>,
- path: String,
- csv_path: Option<String>,
-}
-
-impl Graph {
- fn decode(&self, structure_member: &str) -> Item {
- crate::output::Chart
- .into_item()
- .with_spv_info(
- SpvInfo::new(structure_member).with_members(SpvMembers::Graph {
- data: self.data_path.clone(),
- xml: self.path.clone(),
- csv: self.csv_path.clone(),
- }),
- )
- }
-}
-
-fn decode_image<R>(
- archive: &mut ZipArchive<R>,
- structure_member: &str,
- command_name: &Option<String>,
- image_name: &str,
-) -> Result<Item, Error>
-where
- R: Read + Seek,
-{
- let mut png = archive.by_name(image_name)?;
- let image = ImageSurface::create_from_png(&mut png)?;
- Ok(Details::Image(image)
- .into_item()
- .with_command_name(command_name.clone())
- .with_spv_info(
- SpvInfo::new(structure_member).with_members(SpvMembers::Image(image_name.into())),
- ))
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Image {
- #[serde(rename = "@commandName")]
- command_name: Option<String>,
- data_path: String,
-}
-
-impl Image {
- fn decode<R>(&self, archive: &mut ZipArchive<R>, structure_member: &str) -> Result<Item, Error>
- where
- R: Read + Seek,
- {
- decode_image(
- archive,
- structure_member,
- &self.command_name,
- &self.data_path,
- )
- }
-}
+ /// Error parsing {member_name}: {error}
+ DeserializeError {
+ /// The name of the file inside the ZIP file that caused the error.
+ member_name: String,
+ /// Underlying error.
+ error: serde_path_to_error::Error<quick_xml::DeError>,
+ },
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Object {
- #[serde(rename = "@commandName")]
- command_name: Option<String>,
- #[serde(rename = "@uri")]
- uri: String,
-}
+ /// Graphs not yet implemented.
+ GraphTodo,
-impl Object {
- fn decode<R>(&self, archive: &mut ZipArchive<R>, structure_member: &str) -> Result<Item, Error>
- where
- R: Read + Seek,
- {
- decode_image(archive, structure_member, &self.command_name, &self.uri)
- }
-}
+ /// Models not yet implemented.
+ ModelTodo,
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Table {
- #[serde(rename = "@commandName")]
- command_name: String,
- #[serde(rename = "@subType")]
- sub_type: String,
- #[serde(rename = "@tableId")]
- table_id: Option<i64>,
- #[serde(rename = "@type")]
- table_type: TableType,
- table_properties: Option<TableProperties>,
- table_structure: TableStructure,
-}
-
-impl Table {
- fn decode<R>(
- &self,
- archive: &mut ZipArchive<R>,
- structure_member: &str,
- warn: &Rc<RefCell<Box<dyn FnMut(Warning)>>>,
- ) -> Result<Item, Error>
- where
- R: Read + Seek,
- {
- match &self.table_structure.path {
- None => {
- let member_name = self.table_structure.data_path.clone();
- 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 warn = warn.clone();
- let warning = Rc::new(RefCell::new(Box::new({
- let warn = warn.clone();
- let member_name = member_name.clone();
- move |w| {
- (warn.borrow_mut())(Warning {
- member: member_name.clone(),
- details: WarningDetails::LightWarning(w),
- })
- }
- })
- as Box<dyn FnMut(LightWarning)>));
- let table =
- LightTable::read_args(&mut cursor, (warning.clone(),)).map_err(|e| {
- e.with_message(format!(
- "While parsing {member_name:?} as light binary SPV member"
- ))
- })?;
- let pivot_table = table.decode(&mut *warning.borrow_mut());
- Ok(pivot_table.into_item().with_spv_info(
- SpvInfo::new(structure_member).with_members(SpvMembers::LightTable(
- self.table_structure.data_path.clone(),
- )),
- ))
- }
- Some(xml_member_name) => {
- let bin_member_name = &self.table_structure.data_path;
- let mut bin_member = archive.by_name(bin_member_name)?;
- let mut bin_data = Vec::with_capacity(bin_member.size() as usize);
- bin_member.read_to_end(&mut bin_data)?;
- let mut cursor = Cursor::new(bin_data);
- let legacy_bin = LegacyBin::read(&mut cursor).map_err(|e| {
- e.with_message(format!(
- "While parsing {bin_member_name:?} as legacy binary SPV member"
- ))
- })?;
- let data = legacy_bin.decode();
- drop(bin_member);
-
- 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:?}"),
- };
- let pivot_table = visualization.decode(
- data,
- self.table_properties
- .as_ref()
- .map_or_else(Look::default, |properties| properties.clone().into()),
- )?;
-
- Ok(pivot_table.into_item().with_spv_info(
- SpvInfo::new(structure_member).with_members(SpvMembers::LegacyTable {
- xml: xml_member_name.clone(),
- binary: bin_member_name.clone(),
- }),
- ))
- }
- }
- }
+ /// Trees not yet implemented.
+ TreeTodo,
}
#[derive(Deserialize, Debug)]
Note,
Warning,
}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct ContainerText {
- #[serde(rename = "@type")]
- text_type: TextType,
- #[serde(rename = "@commandName")]
- command_name: Option<String>,
- html: String,
-}
-
-impl ContainerText {
- fn decode(&self) -> Value {
- html::Document::from_html(&self.html).into_value()
- }
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-enum TextType {
- Title,
- Log,
- Text,
- #[serde(rename = "page-title")]
- PageTitle,
-}
-
-#[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.
- #[serde(rename = "csvPath")]
- _csv_path: Option<String>,
-}
--- /dev/null
+use std::{
+ cell::RefCell,
+ io::{BufRead, BufReader, Cursor, Read, Seek},
+ mem::take,
+ rc::Rc,
+};
+
+use anyhow::Context;
+use binrw::{BinRead, error::ContextExt};
+use cairo::ImageSurface;
+use zip::ZipArchive;
+
+use crate::{
+ output::{
+ Details, Item, SpvInfo, SpvMembers, Text,
+ page::PageSetup,
+ pivot::{
+ Length,
+ look::{HorzAlign, Look},
+ },
+ },
+ spv::{
+ Error,
+ legacy_bin::LegacyBin,
+ read::{
+ TableType, Warning, WarningDetails,
+ legacy_xml::Visualization,
+ light::{LightTable, LightWarning},
+ },
+ },
+};
+
+pub struct StructureMember {
+ pub page_setup: Option<PageSetup>,
+ pub root: Heading,
+}
+
+impl StructureMember {
+ pub fn read<R>(
+ reader: R,
+ member_name: &str,
+ warn: &Rc<RefCell<Box<dyn FnMut(Warning)>>>,
+ ) -> Result<Self, Error>
+ where
+ R: BufRead,
+ {
+ let mut heading: raw::Heading =
+ serde_path_to_error::deserialize(&mut quick_xml::de::Deserializer::from_reader(reader))
+ .map_err(|error| Error::DeserializeError {
+ member_name: member_name.into(),
+ error,
+ })?;
+ Ok(Self {
+ page_setup: heading
+ .page_setup
+ .take()
+ .map(|ps| ps.decode(warn, member_name)),
+ root: heading.decode(member_name, warn),
+ })
+ }
+}
+
+pub struct Heading {
+ page_setup: Option<PageSetup>,
+ expand: bool,
+ label: String,
+ children: Vec<HeadingChild>,
+ command_name: Option<String>,
+}
+
+impl Heading {
+ pub fn read_items<R>(
+ self,
+ archive: &mut ZipArchive<R>,
+ structure_member: &str,
+ warn: &Rc<RefCell<Box<dyn FnMut(Warning)>>>,
+ ) -> Vec<Item>
+ where
+ R: Read + Seek,
+ {
+ let mut items = Vec::new();
+ for child in self.children {
+ let mut spv_info = SpvInfo::new(structure_member).with_members(child.members());
+ let item = match child {
+ HeadingChild::Container(container) => {
+ if container.page_break_before {
+ items.push(
+ Details::PageBreak
+ .into_item()
+ .with_spv_info(SpvInfo::new(structure_member)),
+ );
+ }
+ let result = match container.content {
+ Content::Table(table) => table.decode(archive, warn),
+ Content::Graph(_) => Err(Error::GraphTodo),
+ Content::Text(container_text) => Ok(container_text.into_item()),
+ Content::Image(image) => image.decode(archive),
+ Content::Model => Err(Error::ModelTodo),
+ Content::Tree => Err(Error::TreeTodo),
+ };
+ spv_info.error = result.is_err();
+ result
+ .unwrap_or_else(|error| {
+ Text::new_log(error.to_string())
+ .into_item()
+ .with_label("Error")
+ })
+ .with_show(container.show)
+ .with_command_name(Some(container.command_name))
+ .with_label(container.label)
+ }
+ HeadingChild::Heading(mut heading) => {
+ let expand = heading.expand;
+ let label = take(&mut heading.label);
+ let command_name = take(&mut heading.command_name);
+ heading
+ .read_items(archive, structure_member, warn)
+ .into_iter()
+ .collect::<Item>()
+ .with_show(expand)
+ .with_label(label)
+ .with_command_name(command_name)
+ .with_spv_info(SpvInfo::new(structure_member))
+ }
+ };
+ items.push(item.with_spv_info(spv_info));
+ }
+ items
+ }
+}
+
+pub enum HeadingChild {
+ Heading(Heading),
+ Container(Container),
+}
+
+impl HeadingChild {
+ fn members(&self) -> Option<SpvMembers> {
+ match self {
+ HeadingChild::Heading(_) => None,
+ HeadingChild::Container(container) => container.content.members(),
+ }
+ }
+}
+
+pub struct Container {
+ show: bool,
+ page_break_before: bool,
+ text_align: Option<HorzAlign>,
+ command_name: String,
+ width: Option<Length>,
+ label: String,
+ content: Content,
+}
+
+pub enum Content {
+ Text(Text),
+ Table(Table),
+ Image(Image),
+ Graph(Graph),
+ Tree,
+ Model,
+}
+
+impl Content {
+ fn members(&self) -> Option<SpvMembers> {
+ match self {
+ Content::Text(_text) => None,
+ Content::Table(table) => Some(table.members()),
+ Content::Image(image) => Some(image.members()),
+ Content::Graph(graph) => Some(graph.members()),
+ Content::Tree => None,
+ Content::Model => None,
+ }
+ }
+}
+
+pub struct Table {
+ subtype: String,
+ table_type: TableType,
+ look: Option<Box<Look>>,
+ bin_member: String,
+ xml_member: Option<String>,
+}
+
+impl Table {
+ fn members(&self) -> SpvMembers {
+ if let Some(xml_member) = &self.xml_member {
+ SpvMembers::LegacyTable {
+ xml: xml_member.clone(),
+ binary: self.bin_member.clone(),
+ }
+ } else {
+ SpvMembers::LightTable(self.bin_member.clone())
+ }
+ }
+
+ fn decode<R>(
+ self,
+ archive: &mut ZipArchive<R>,
+ warn: &Rc<RefCell<Box<dyn FnMut(Warning)>>>,
+ ) -> Result<Item, Error>
+ where
+ R: Read + Seek,
+ {
+ if let Some(xml_member_name) = &self.xml_member {
+ let bin_member_name = &self.bin_member;
+ let mut bin_member = archive.by_name(bin_member_name)?;
+ let mut bin_data = Vec::with_capacity(bin_member.size() as usize);
+ bin_member.read_to_end(&mut bin_data)?;
+ let mut cursor = Cursor::new(bin_data);
+ let legacy_bin = LegacyBin::read(&mut cursor).map_err(|e| {
+ e.with_message(format!(
+ "While parsing {bin_member_name:?} as legacy binary SPV member"
+ ))
+ })?;
+ let data = legacy_bin.decode();
+ drop(bin_member);
+
+ 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:?}"),
+ };
+ let pivot_table = visualization.decode(data, *self.look.unwrap_or_default())?;
+
+ Ok(pivot_table.into_item())
+ } else {
+ let member_name = self.bin_member.clone();
+ 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 warn = warn.clone();
+ let warning = Rc::new(RefCell::new(Box::new({
+ let warn = warn.clone();
+ let member_name = member_name.clone();
+ move |w| {
+ (warn.borrow_mut())(Warning {
+ member: member_name.clone(),
+ details: WarningDetails::LightWarning(w),
+ })
+ }
+ }) as Box<dyn FnMut(LightWarning)>));
+ let table = LightTable::read_args(&mut cursor, (warning.clone(),)).map_err(|e| {
+ e.with_message(format!(
+ "While parsing {member_name:?} as light binary SPV member"
+ ))
+ })?;
+ let pivot_table = table.decode(&mut *warning.borrow_mut());
+ Ok(pivot_table.into_item())
+ }
+ }
+}
+
+pub struct Image {
+ member: String,
+}
+
+impl Image {
+ fn members(&self) -> SpvMembers {
+ SpvMembers::Image(self.member.clone())
+ }
+ fn decode<R>(self, archive: &mut ZipArchive<R>) -> Result<Item, Error>
+ where
+ R: Read + Seek,
+ {
+ let mut png = archive.by_name(&self.member)?;
+ let image = ImageSurface::create_from_png(&mut png)?;
+ Ok(Details::Image(image).into_item())
+ }
+}
+
+pub struct Graph {
+ xml_member: String,
+ data_member: Option<String>,
+ csv_member: Option<String>,
+}
+
+impl Graph {
+ fn members(&self) -> SpvMembers {
+ SpvMembers::Graph {
+ data: self.data_member.clone(),
+ xml: self.xml_member.clone(),
+ csv: self.csv_member.clone(),
+ }
+ }
+}
+
+mod raw {
+ use std::{cell::RefCell, rc::Rc};
+
+ use paper_sizes::PaperSize;
+ use serde::Deserialize;
+
+ use crate::{
+ output::{
+ Text,
+ page::{self, Orientation},
+ pivot::{
+ Axis2, Length, TableProperties,
+ look::{HorzAlign, Look},
+ value::Value,
+ },
+ },
+ spv::{
+ html::{self, Document},
+ read::{
+ TableType, Warning, WarningDetails,
+ structure::{Content, HeadingChild},
+ },
+ },
+ };
+
+ #[derive(Deserialize, Debug)]
+ #[serde(rename_all = "camelCase")]
+ pub struct Heading {
+ #[serde(rename = "@visibility")]
+ visibility: Option<String>,
+ #[serde(rename = "@commandName")]
+ command_name: Option<String>,
+ label: Label,
+ pub page_setup: Option<PageSetup>,
+
+ #[serde(rename = "$value")]
+ #[serde(default)]
+ children: Vec<HeadingContent>,
+ }
+
+ impl Heading {
+ pub fn decode(
+ self,
+ structure_member: &str,
+ warn: &Rc<RefCell<Box<dyn FnMut(Warning)>>>,
+ ) -> super::Heading {
+ let mut children = Vec::new();
+ for child in self.children {
+ match child {
+ HeadingContent::Container(container) => {
+ let (content, command_name) = match container.content {
+ ContainerContent::Table(table) => (
+ Content::Table(super::Table {
+ subtype: table.subtype,
+ table_type: table.table_type,
+ look: table.table_properties.map(|table_properties| {
+ Box::new(Look::from(*table_properties))
+ }),
+ bin_member: table.table_structure.bin_member,
+ xml_member: table.table_structure.xml_member,
+ }),
+ table.command_name,
+ ),
+ ContainerContent::Graph(graph) => (
+ Content::Graph(super::Graph {
+ xml_member: graph.xml_member,
+ data_member: graph.data_member,
+ csv_member: graph.csv_member,
+ }),
+ graph.command_name,
+ ),
+ ContainerContent::Text(text) => (
+ Content::Text(Text::new(
+ match text.text_type {
+ TextType::Title => crate::output::TextType::Title,
+ TextType::Log | TextType::Text => {
+ crate::output::TextType::Log
+ }
+ TextType::PageTitle => crate::output::TextType::PageTitle,
+ },
+ text.decode(),
+ )),
+ text.command_name.unwrap_or_default(),
+ ),
+ ContainerContent::Image(image) => (
+ Content::Image(super::Image {
+ member: image.data_path,
+ }),
+ image.command_name.unwrap_or_default(),
+ ),
+ ContainerContent::Object(object) => (
+ Content::Image(super::Image { member: object.uri }),
+ object.command_name.unwrap_or_default(),
+ ),
+ ContainerContent::Model(model) => (Content::Model, model.command_name),
+ ContainerContent::Tree(tree) => (Content::Tree, tree.command_name),
+ };
+ children.push(HeadingChild::Container(super::Container {
+ show: container.visibility != Visibility::Hidden,
+ page_break_before: container.page_break_before
+ == PageBreakBefore::Always,
+ text_align: container.text_align.map(HorzAlign::from),
+ width: container.width,
+ label: container.label.text,
+ command_name,
+ content,
+ }));
+ }
+ HeadingContent::Heading(heading) => {
+ children.push(HeadingChild::Heading(
+ heading.decode(structure_member, warn),
+ ));
+ }
+ }
+ }
+ super::Heading {
+ page_setup: self
+ .page_setup
+ .map(|page_setup| page_setup.decode(warn, structure_member)),
+ expand: !self.visibility.is_some(),
+ label: self.label.text,
+ command_name: self.command_name,
+ children,
+ }
+ }
+ }
+
+ #[derive(Debug, Deserialize)]
+ #[serde(rename_all = "camelCase")]
+ pub struct PageSetup {
+ #[serde(rename = "@initial-page-number")]
+ pub initial_page_number: Option<i32>,
+ #[serde(rename = "@chart-size")]
+ pub chart_size: Option<ChartSize>,
+ #[serde(rename = "@margin-left")]
+ pub margin_left: Option<Length>,
+ #[serde(rename = "@margin-right")]
+ pub margin_right: Option<Length>,
+ #[serde(rename = "@margin-top")]
+ pub margin_top: Option<Length>,
+ #[serde(rename = "@margin-bottom")]
+ pub margin_bottom: Option<Length>,
+ #[serde(rename = "@paper-height")]
+ pub paper_height: Option<Length>,
+ #[serde(rename = "@paper-width")]
+ pub paper_width: Option<Length>,
+ #[serde(rename = "@reference-orientation")]
+ pub reference_orientation: Option<String>,
+ #[serde(rename = "@space-after")]
+ pub space_after: Option<Length>,
+ pub page_header: PageHeader,
+ pub page_footer: PageFooter,
+ }
+
+ impl PageSetup {
+ pub fn decode(
+ self,
+ warn: &Rc<RefCell<Box<dyn FnMut(Warning)>>>,
+ structure_member: &str,
+ ) -> page::PageSetup {
+ let mut setup = page::PageSetup::default();
+ if let Some(initial_page_number) = self.initial_page_number {
+ setup.initial_page_number = initial_page_number;
+ }
+ if let Some(chart_size) = self.chart_size {
+ setup.chart_size = chart_size.into();
+ }
+ if let Some(margin_left) = self.margin_left {
+ setup.margins.0[Axis2::X][0] = margin_left.into();
+ }
+ if let Some(margin_right) = self.margin_right {
+ setup.margins.0[Axis2::X][1] = margin_right.into();
+ }
+ if let Some(margin_top) = self.margin_top {
+ setup.margins.0[Axis2::Y][0] = margin_top.into();
+ }
+ if let Some(margin_bottom) = self.margin_bottom {
+ setup.margins.0[Axis2::Y][1] = margin_bottom.into();
+ }
+ match (self.paper_width, self.paper_height) {
+ (Some(width), Some(height)) => {
+ setup.paper = PaperSize::new(width.0, height.0, paper_sizes::Unit::Inch)
+ }
+ (Some(length), None) | (None, Some(length)) => {
+ setup.paper = PaperSize::new(length.0, length.0, paper_sizes::Unit::Inch)
+ }
+ (None, None) => (),
+ }
+ if let Some(reference_orientation) = &self.reference_orientation {
+ if reference_orientation.starts_with("0") {
+ setup.orientation = Orientation::Portrait;
+ } else if reference_orientation.starts_with("90") {
+ setup.orientation = Orientation::Landscape;
+ } else {
+ (warn.borrow_mut())(Warning {
+ member: structure_member.into(),
+ details: WarningDetails::UnknownOrientation(reference_orientation.clone()),
+ });
+ }
+ }
+ if let Some(space_after) = self.space_after {
+ setup.object_spacing = space_after.into();
+ }
+ if let Some(PageParagraph { text }) = &self.page_header.page_paragraph {
+ setup.header = text.decode();
+ }
+ if let Some(PageParagraph { text }) = &self.page_footer.page_paragraph {
+ setup.footer = text.decode();
+ }
+ setup
+ }
+ }
+
+ #[derive(Debug, Deserialize)]
+ #[serde(rename_all = "camelCase")]
+ pub struct PageHeader {
+ page_paragraph: Option<PageParagraph>,
+ }
+
+ #[derive(Debug, Deserialize)]
+ #[serde(rename_all = "camelCase")]
+ pub struct PageFooter {
+ page_paragraph: Option<PageParagraph>,
+ }
+
+ #[derive(Debug, Deserialize)]
+ #[serde(rename_all = "camelCase")]
+ struct PageParagraph {
+ text: PageParagraphText,
+ }
+
+ #[derive(Debug, Deserialize)]
+ #[serde(rename_all = "camelCase")]
+ struct PageParagraphText {
+ #[serde(default, rename = "$text")]
+ text: String,
+ }
+
+ impl PageParagraphText {
+ fn decode(&self) -> Document {
+ Document::from_html(&self.text)
+ }
+ }
+
+ #[derive(Copy, Clone, Debug, Default, Deserialize)]
+ #[serde(rename = "snake_case")]
+ enum ReferenceOrientation {
+ #[serde(alias = "0")]
+ #[serde(alias = "0deg")]
+ #[serde(alias = "inherit")]
+ #[default]
+ Portrait,
+
+ #[serde(alias = "90")]
+ #[serde(alias = "90deg")]
+ #[serde(alias = "-270")]
+ #[serde(alias = "-270deg")]
+ Landscape,
+
+ #[serde(alias = "180")]
+ #[serde(alias = "180deg")]
+ #[serde(alias = "-1280")]
+ #[serde(alias = "-180deg")]
+ ReversePortrait,
+
+ #[serde(alias = "270")]
+ #[serde(alias = "270deg")]
+ #[serde(alias = "-90")]
+ #[serde(alias = "-90deg")]
+ Seascape,
+ }
+
+ impl From<ReferenceOrientation> for page::Orientation {
+ fn from(value: ReferenceOrientation) -> Self {
+ match value {
+ ReferenceOrientation::Portrait | ReferenceOrientation::ReversePortrait => {
+ page::Orientation::Portrait
+ }
+ ReferenceOrientation::Landscape | ReferenceOrientation::Seascape => {
+ page::Orientation::Landscape
+ }
+ }
+ }
+ }
+
+ /// Chart size.
+ #[derive(Copy, Clone, Debug, Default, Deserialize)]
+ #[serde(rename_all = "kebab-case")]
+ pub enum ChartSize {
+ FullHeight,
+ HalfHeight,
+ QuarterHeight,
+ #[default]
+ #[serde(other)]
+ AsIs,
+ }
+
+ impl From<ChartSize> for page::ChartSize {
+ fn from(value: ChartSize) -> Self {
+ match value {
+ ChartSize::AsIs => page::ChartSize::AsIs,
+ ChartSize::FullHeight => page::ChartSize::FullHeight,
+ ChartSize::HalfHeight => page::ChartSize::HalfHeight,
+ ChartSize::QuarterHeight => page::ChartSize::QuarterHeight,
+ }
+ }
+ }
+
+ #[derive(Deserialize, Debug)]
+ #[serde(rename_all = "camelCase")]
+ enum HeadingContent {
+ Container(Container),
+ Heading(Box<Heading>),
+ }
+
+ #[derive(Deserialize, Debug)]
+ #[serde(rename_all = "camelCase")]
+ struct Label {
+ #[serde(default, rename = "$text")]
+ text: String,
+ }
+
+ #[derive(Deserialize, Debug)]
+ #[serde(rename_all = "camelCase")]
+ struct Container {
+ #[serde(default, rename = "@visibility")]
+ visibility: Visibility,
+ #[serde(rename = "@page-break-before")]
+ #[serde(default)]
+ page_break_before: PageBreakBefore,
+ #[serde(rename = "@text-align")]
+ text_align: Option<TextAlign>,
+ #[serde(rename = "@width")]
+ width: Option<Length>,
+ label: Label,
+
+ #[serde(rename = "$value")]
+ content: ContainerContent,
+ }
+
+ #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Deserialize)]
+ #[serde(rename_all = "camelCase")]
+ enum PageBreakBefore {
+ #[default]
+ Auto,
+ Always,
+ Avoid,
+ Left,
+ Right,
+ Inherit,
+ }
+
+ #[derive(Deserialize, Debug, Default, PartialEq, Eq)]
+ #[serde(rename_all = "camelCase")]
+ enum Visibility {
+ #[default]
+ Visible,
+ Hidden,
+ }
+
+ #[derive(Copy, Clone, Deserialize, Debug)]
+ #[serde(rename_all = "camelCase")]
+ enum TextAlign {
+ Left,
+ Center,
+ Right,
+ }
+
+ impl From<TextAlign> for HorzAlign {
+ fn from(value: TextAlign) -> Self {
+ match value {
+ TextAlign::Left => HorzAlign::Left,
+ TextAlign::Center => HorzAlign::Center,
+ TextAlign::Right => HorzAlign::Right,
+ }
+ }
+ }
+
+ #[derive(Deserialize, Debug)]
+ #[serde(rename_all = "camelCase")]
+ enum ContainerContent {
+ Table(Table),
+ Text(ContainerText),
+ Graph(Graph),
+ Model(Model),
+ Object(Object),
+ Image(Image),
+ Tree(Tree),
+ }
+
+ #[derive(Deserialize, Debug)]
+ #[serde(rename_all = "camelCase")]
+ struct Graph {
+ #[serde(rename = "@commandName")]
+ command_name: String,
+
+ /// Data.
+ #[serde(rename = "dataPath")]
+ data_member: Option<String>,
+
+ /// XML in VizML format.
+ #[serde(rename = "path")]
+ xml_member: String,
+
+ /// Rarely used, not understood.
+ #[serde(rename = "csvPath")]
+ csv_member: Option<String>,
+ }
+
+ #[derive(Deserialize, Debug)]
+ #[serde(rename_all = "camelCase")]
+ struct Model {
+ #[serde(rename = "@commandName")]
+ command_name: String,
+ }
+
+ #[derive(Deserialize, Debug)]
+ #[serde(rename_all = "camelCase")]
+ struct Tree {
+ #[serde(rename = "@commandName")]
+ command_name: String,
+ }
+
+ #[derive(Deserialize, Debug)]
+ #[serde(rename_all = "camelCase")]
+ struct Image {
+ #[serde(rename = "@commandName")]
+ command_name: Option<String>,
+ data_path: String,
+ }
+
+ #[derive(Deserialize, Debug)]
+ #[serde(rename_all = "camelCase")]
+ struct Object {
+ #[serde(rename = "@commandName")]
+ command_name: Option<String>,
+ #[serde(rename = "@uri")]
+ uri: String,
+ }
+
+ #[derive(Deserialize, Debug)]
+ #[serde(rename_all = "camelCase")]
+ struct Table {
+ #[serde(rename = "@commandName")]
+ command_name: String,
+ #[serde(rename = "@subType")]
+ subtype: String,
+ #[serde(rename = "@tableId")]
+ table_id: Option<i64>,
+ #[serde(rename = "@type")]
+ table_type: TableType,
+ table_properties: Option<Box<TableProperties>>,
+ table_structure: TableStructure,
+ }
+
+ #[derive(Deserialize, Debug)]
+ #[serde(rename_all = "camelCase")]
+ struct ContainerText {
+ #[serde(rename = "@type")]
+ text_type: TextType,
+ #[serde(rename = "@commandName")]
+ command_name: Option<String>,
+ html: String,
+ }
+
+ impl ContainerText {
+ fn decode(&self) -> Value {
+ html::Document::from_html(&self.html).into_value()
+ }
+ }
+ #[derive(Deserialize, Debug)]
+ #[serde(rename_all = "camelCase")]
+ enum TextType {
+ Title,
+ Log,
+ Text,
+ #[serde(rename = "page-title")]
+ PageTitle,
+ }
+
+ #[derive(Deserialize, Debug)]
+ #[serde(rename_all = "camelCase")]
+ struct TableStructure {
+ /// The `.xml` member name, for legacy members only.
+ #[serde(rename = "path")]
+ xml_member: Option<String>,
+ /// The `.bin` member name.
+ #[serde(rename = "dataPath")]
+ bin_member: String,
+ /// Rarely used, not understood.
+ #[serde(rename = "csvPath")]
+ _csv_member: Option<String>,
+ }
+}
X: Write,
{
match &item.details {
- Details::Chart | Details::Image(_) => todo!(),
+ Details::Graph | Details::Image(_) => todo!(),
Details::Heading(children) => {
let mut attributes = Vec::<Attribute>::new();
if let Some(command_name) = &item.command_name {