work on outlines
authorBen Pfaff <blp@cs.stanford.edu>
Wed, 31 Dec 2025 16:44:50 +0000 (08:44 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Wed, 31 Dec 2025 16:44:50 +0000 (08:44 -0800)
rust/pspp/src/cli/show_spv.rs
rust/pspp/src/output.rs
rust/pspp/src/output/drivers/cairo/driver.rs
rust/pspp/src/output/drivers/text.rs
rust/pspp/src/spv/read.rs
rust/pspp/src/spv/read/structure.rs
rust/pspp/src/spv/write.rs

index 1a28c46f129064181833314241514fb834b842cc..4bf125bf85a53bade02d78ef6ac679409ddedfc8 100644 (file)
@@ -17,7 +17,7 @@
 use anyhow::Result;
 use clap::{Args, ValueEnum};
 use pspp::{
-    output::{Criteria, Item},
+    output::{Criteria, Item, ItemInfo},
     spv::SpvArchive,
 };
 use std::{fmt::Display, path::PathBuf, sync::Arc};
index 7ef4d325ad80657a8eecc1e2170a3503582034e4..a915914a754b16248475046bd9eed440ebad2cf3 100644 (file)
@@ -80,6 +80,88 @@ pub struct Item {
     pub spv_info: Option<Box<SpvInfo>>,
 }
 
+pub trait ItemInfo {
+    fn label(&self) -> Cow<'static, str>;
+    fn command_name(&self) -> Option<&str>;
+    fn subtype(&self) -> Option<String>;
+    fn is_shown(&self) -> bool;
+    fn spv_info(&self) -> Option<&SpvInfo>;
+    fn iter_in_order(&self) -> ItemRefIterator<'_, Self>
+    where
+        Self: Sized;
+    fn kind(&self) -> ItemKind;
+    fn class(&self) -> Class {
+        match self.kind() {
+            ItemKind::Graph => Class::Graphs,
+            ItemKind::Image => Class::Other,
+            ItemKind::Heading => Class::OutlineHeaders,
+            ItemKind::Message(severity) => match severity {
+                Severity::Note => Class::Notes,
+                Severity::Error | Severity::Warning => Class::Warnings,
+            },
+            ItemKind::PageBreak => Class::Other,
+            ItemKind::Table => match self.label().as_ref() {
+                "Warnings" => Class::Warnings,
+                "Notes" => Class::Notes,
+                _ => Class::Tables,
+            },
+            ItemKind::Text => match self.label().as_ref() {
+                "Title" => Class::Headings,
+                "Log" => Class::Logs,
+                "Page Title" => Class::PageTitle,
+                _ => Class::Texts,
+            },
+        }
+    }
+
+    type Child: AsRef<Self>;
+    fn children(&self) -> &[Self::Child];
+}
+
+impl ItemInfo for Item {
+    fn label(&self) -> Cow<'static, str> {
+        match &self.label {
+            Some(label) => Cow::from(label.clone()),
+            None => self.details.label(),
+        }
+    }
+
+    fn command_name(&self) -> Option<&str> {
+        self.command_name.as_deref()
+    }
+
+    fn subtype(&self) -> Option<String> {
+        self.details
+            .as_table()
+            .map(|table| table.subtype().display(table).to_string())
+    }
+
+    /// Should the item be shown?
+    ///
+    /// This always returns true for headings because their contents are always
+    /// shown (although headings can be collapsed in an outline view).
+    fn is_shown(&self) -> bool {
+        self.details.is_heading() || self.show
+    }
+
+    fn spv_info(&self) -> Option<&SpvInfo> {
+        self.spv_info.as_deref()
+    }
+
+    fn iter_in_order(&self) -> ItemRefIterator<'_, Item> {
+        ItemRefIterator::new(self)
+    }
+
+    type Child = Arc<Item>;
+    fn children(&self) -> &[Self::Child] {
+        self.details.children()
+    }
+
+    fn kind(&self) -> ItemKind {
+        self.details.kind()
+    }
+}
+
 impl Item {
     pub fn new(details: impl Into<Details>) -> Self {
         let details = details.into();
@@ -92,13 +174,6 @@ impl Item {
         }
     }
 
-    pub fn label(&self) -> Cow<'static, str> {
-        match &self.label {
-            Some(label) => Cow::from(label.clone()),
-            None => self.details.label(),
-        }
-    }
-
     pub fn subtype(&self) -> Option<String> {
         self.details
             .as_table()
@@ -134,16 +209,8 @@ impl Item {
         }
     }
 
-    /// Should the item be shown?
-    ///
-    /// This always returns true for headings because their contents are always
-    /// shown (although headings can be collapsed in an outline view).
-    pub fn is_shown(&self) -> bool {
-        self.details.is_heading() || self.show
-    }
-
-    pub fn iter_in_order(&self) -> ItemRefIterator<'_> {
-        ItemRefIterator::new(self)
+    pub fn cursor(self: Arc<Self>) -> ItemCursor<Self> {
+        ItemCursor::new(self)
     }
 }
 
@@ -203,7 +270,7 @@ pub enum ItemKind {
     Graph,
     Image,
     Heading,
-    Message,
+    Message(Severity),
     PageBreak,
     Table,
     Text,
@@ -215,7 +282,7 @@ impl ItemKind {
             ItemKind::Graph => "graph",
             ItemKind::Image => "image",
             ItemKind::Heading => "heading",
-            ItemKind::Message => "message",
+            ItemKind::Message(_) => "message",
             ItemKind::PageBreak => "page break",
             ItemKind::Table => "table",
             ItemKind::Text => "text",
@@ -336,7 +403,7 @@ impl Details {
             Details::Graph => ItemKind::Graph,
             Details::Image(_) => ItemKind::Image,
             Details::Heading(_) => ItemKind::Heading,
-            Details::Message(_) => ItemKind::Message,
+            Details::Message(diagnostic) => ItemKind::Message(diagnostic.severity),
             Details::PageBreak => ItemKind::PageBreak,
             Details::Table(_) => ItemKind::Table,
             Details::Text(_) => ItemKind::Text,
@@ -493,17 +560,20 @@ impl TextType {
     }
 }
 
-pub struct ItemRefIterator<'a> {
-    next: Option<&'a Item>,
-    stack: Vec<(&'a Item, usize)>,
+pub struct ItemRefIterator<'a, T> {
+    next: Option<&'a T>,
+    stack: Vec<(&'a T, usize)>,
 }
 
-impl<'a> ItemRefIterator<'a> {
-    pub fn without_hidden(self) -> impl Iterator<Item = &'a Item> {
+impl<'a, T> ItemRefIterator<'a, T> {
+    pub fn without_hidden(self) -> impl Iterator<Item = &'a T>
+    where
+        T: ItemInfo,
+    {
         self.filter(|item| item.is_shown())
     }
 
-    pub fn new(start: &'a Item) -> Self {
+    pub fn new(start: &'a T) -> Self {
         Self {
             next: Some(start),
             stack: Vec::new(),
@@ -511,18 +581,21 @@ impl<'a> ItemRefIterator<'a> {
     }
 }
 
-impl<'a> Iterator for ItemRefIterator<'a> {
-    type Item = &'a Item;
+impl<'a, T> Iterator for ItemRefIterator<'a, T>
+where
+    T: ItemInfo,
+{
+    type Item = &'a T;
 
     fn next(&mut self) -> Option<Self::Item> {
         let cur = self.next.take()?;
-        if let Some(first_child) = cur.details.children().first() {
-            self.next = Some(&*first_child);
+        if let Some(first_child) = cur.children().first() {
+            self.next = Some(first_child.as_ref());
             self.stack.push((cur, 1));
         } else {
             while let Some((item, index)) = self.stack.pop() {
-                if let Some(child) = item.details.children().get(index) {
-                    self.next = Some(&*child);
+                if let Some(child) = item.children().get(index) {
+                    self.next = Some(child.as_ref());
                     self.stack.push((item, index + 1));
                     return Some(cur);
                 }
@@ -532,22 +605,30 @@ impl<'a> Iterator for ItemRefIterator<'a> {
     }
 }
 
-pub struct ItemCursor {
-    cur: Option<Arc<Item>>,
-    stack: Vec<(Arc<Item>, usize)>,
+pub struct ItemCursor<T>
+where
+    T: ItemInfo,
+    T::Child: Clone,
+{
+    cur: Option<T::Child>,
+    stack: Vec<(T::Child, usize)>,
     include_hidden: bool,
 }
 
-impl ItemCursor {
-    pub fn new(start: Arc<Item>) -> Self {
+impl<T> ItemCursor<T>
+where
+    T: ItemInfo,
+    T::Child: Clone,
+{
+    pub fn new(start: T::Child) -> Self {
         Self {
-            cur: start.is_shown().then_some(start),
+            cur: start.as_ref().is_shown().then_some(start),
             stack: Vec::new(),
             include_hidden: false,
         }
     }
 
-    pub fn with_hidden(start: Arc<Item>) -> Self {
+    pub fn with_hidden(start: T::Child) -> Self {
         Self {
             cur: Some(start),
             stack: Vec::new(),
@@ -555,21 +636,25 @@ impl ItemCursor {
         }
     }
 
-    pub fn cur(&self) -> Option<&Arc<Item>> {
+    pub fn cur(&self) -> Option<&T::Child> {
         self.cur.as_ref()
     }
 
     pub fn next(&mut self) {
-        fn inner(this: &mut ItemCursor) {
+        fn inner<T>(this: &mut ItemCursor<T>)
+        where
+            T: ItemInfo,
+            T::Child: Clone,
+        {
             let Some(cur) = this.cur.take() else {
                 return;
             };
-            if let Some(first_child) = cur.details.children().first() {
+            if let Some(first_child) = cur.as_ref().children().first() {
                 this.cur = Some(first_child.clone());
                 this.stack.push((cur, 1));
             } else {
                 while let Some((item, index)) = this.stack.pop() {
-                    if let Some(child) = item.details.children().get(index) {
+                    if let Some(child) = item.as_ref().children().get(index) {
                         this.cur = Some(child.clone());
                         this.stack.push((item, index + 1));
                         return;
@@ -580,7 +665,7 @@ impl ItemCursor {
 
         inner(self);
         while let Some(cur) = &self.cur
-            && !cur.is_shown()
+            && !cur.as_ref().is_shown()
         {
             inner(self);
         }
@@ -589,12 +674,12 @@ impl ItemCursor {
     // Returns the label for the heading with the given `level` in the stack
     // above the current item.  Level 0 is the top level.  Levels without a
     // label are skipped.
-    pub fn heading(&self, level: usize) -> Option<&str> {
+    pub fn heading(&self, level: usize) -> Option<Cow<'static, str>> {
         self.stack
             .iter()
-            .filter_map(|(item, _index)| item.label.as_ref())
+            .map(|(item, _index)| item.as_ref().label())
+            .filter(|label| !label.is_empty())
             .nth(level)
-            .map(|s| s.as_str())
     }
 }
 
@@ -729,33 +814,6 @@ impl FromStr for Class {
     }
 }
 
-impl Item {
-    fn class(&self) -> Class {
-        let label = self.label.as_ref().map(|s| s.as_str());
-        match &self.details {
-            Details::Graph => Class::Graphs,
-            Details::Image(_) => Class::Other,
-            Details::Heading(_) => Class::OutlineHeaders,
-            Details::Message(diagnostic) => match diagnostic.severity {
-                Severity::Note => Class::Notes,
-                Severity::Error | Severity::Warning => Class::Warnings,
-            },
-            Details::PageBreak => Class::Other,
-            Details::Table(_) => match label {
-                Some("Warnings") => Class::Warnings,
-                Some("Notes") => Class::Notes,
-                _ => Class::Tables,
-            },
-            Details::Text(_) => match label {
-                Some("Title") => Class::Headings,
-                Some("Log") => Class::Logs,
-                Some("Page Title") => Class::PageTitle,
-                _ => Class::Texts,
-            },
-        }
-    }
-}
-
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct Selection {
     /// - `None`: Include all objects.
index fe783a0b04e3c33c17eedde95e6a84f9829c7aea..623b4631f9a0621c121c4fb4e8f4bd19c277c472 100644 (file)
@@ -29,7 +29,7 @@ use serde::{Deserialize, Serialize};
 
 use crate::{
     output::{
-        Item, ItemCursor, TextType,
+        Item, TextType,
         drivers::{
             Driver,
             cairo::{
@@ -138,7 +138,7 @@ impl Driver for CairoDriver {
         let pager = self.pager.get_or_insert_with(|| {
             CairoPager::new(self.page_style.clone(), self.fsm_style.clone())
         });
-        let mut cursor = ItemCursor::new(item.clone());
+        let mut cursor = item.clone().cursor();
         while let Some(item) = cursor.cur() {
             if let Some(text) = item.details.as_text()
                 && text.type_ == TextType::PageTitle
index dadcec87bd23a1446eecc3be93be26733b922671..f43796b0585d1ba495a6ad176071cb4e7ee0f52e 100644 (file)
@@ -29,7 +29,7 @@ use serde::{Deserialize, Serialize};
 use unicode_linebreak::{BreakOpportunity, linebreaks};
 use unicode_width::UnicodeWidthStr;
 
-use crate::output::{render::Extreme, table::DrawCell};
+use crate::output::{ItemInfo, render::Extreme, table::DrawCell};
 
 use crate::output::{
     Details, Item,
index 2f1fe041a8cf2ce6cc3566ea6766e6c62c18b856..804a286e20fd297b5e7041ecd402c225ffee2b4a 100644 (file)
@@ -128,16 +128,17 @@ where
     where
         F: FnMut(Warning),
     {
+        todo!() /*
         // Read all the items.
         let mut members = Vec::new();
         for i in 0..self.0.len() {
-            let name = String::from(self.0.name_for_index(i).unwrap());
-            if name.starts_with("outputViewer") && name.ends_with(".xml") {
-                let member = BufReader::new(self.0.by_index(i)?);
-                members.push(StructureMember::read(member, &name, &mut warn)?);
-            }
+        let name = String::from(self.0.name_for_index(i).unwrap());
+        if name.starts_with("outputViewer") && name.ends_with(".xml") {
+        let member = BufReader::new(self.0.by_index(i)?);
+        members.push(StructureMember::read(member, &name, &mut warn)?);
         }
-        Ok(SpvOutline { members })
+        }
+        Ok(SpvOutline { members })*/
     }
 
     /// Reads and returns the whole SPV file contents.
index ba10e98fbf3fc78d12938cb78ae89b2710aebd23..4f5952493e7015e3b4179ec4a61c49111ae94784 100644 (file)
@@ -50,29 +50,23 @@ impl StructureMember {
         reader: R,
         member_name: &str,
         warn: &mut dyn FnMut(Warning),
-    ) -> Result<Self, Error>
+    ) -> Result<Heading, Error>
     where
         R: BufRead,
     {
-        let mut heading: raw::Heading =
+        let 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 {
-            member_name: member_name.into(),
-            page_setup: heading
-                .page_setup
-                .take()
-                .map(|ps| ps.decode(warn, member_name)),
-            root: heading.decode(member_name, warn),
-        })
+        Ok(heading.decode(member_name, warn))
     }
 }
 
 #[derive(Clone, Debug)]
 pub struct Heading {
+    structure_member: String,
     page_setup: Option<PageSetup>,
     expand: bool,
     label: String,
@@ -416,6 +410,7 @@ mod raw {
                 }
             }
             super::Heading {
+                structure_member: structure_member.into(),
                 page_setup: self
                     .page_setup
                     .map(|page_setup| page_setup.decode(warn, structure_member)),
index c0cd91050f77e780c99e81c830f9bd5a78936d57..1776bd96d9197685a692b3daa0f1e1de658981cc 100644 (file)
@@ -33,7 +33,7 @@ use crate::{
     data::{Datum, EncodedString},
     format::{Format, Type},
     output::{
-        Details, Item, Text,
+        Details, Item, ItemInfo, Text,
         page::{ChartSize, PageSetup},
         pivot::{
             Axis2, Axis3, Category, Dimension, Footnote, FootnoteMarkerPosition,