work
authorBen Pfaff <blp@cs.stanford.edu>
Sun, 30 Nov 2025 19:08:32 +0000 (11:08 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Sun, 30 Nov 2025 19:08:32 +0000 (11:08 -0800)
rust/pspp/src/output.rs
rust/pspp/src/output/drivers/text.rs

index a794de3d37f82b6df9f82e907c0b82c9b2fdec3a..a90ef6e198c76b67c5a75331b025bac8b150de49 100644 (file)
@@ -132,6 +132,14 @@ impl Item {
             ..self
         }
     }
+
+    /// 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.kind() == ItemKind::Heading || self.show
+    }
 }
 
 impl<T> From<T> for Item
@@ -161,6 +169,12 @@ impl PivotTable {
     }
 }
 
+/// A group of output items.
+///
+/// The name "heading" is used in SPV files.  There is only a visible
+/// heading if the output items inside the heading include a [Text] item.
+/// The grouping itself is only visible in the outline pane in a viewer
+/// window.
 #[derive(Clone, Debug, Default, Serialize)]
 pub struct Heading(pub Vec<Arc<Item>>);
 
@@ -474,16 +488,65 @@ impl TextType {
     }
 }
 
+pub struct ItemRefIterator<'a> {
+    next: Option<&'a Item>,
+    stack: Vec<(&'a Item, usize)>,
+}
+
+impl<'a> ItemRefIterator<'a> {
+    pub fn without_hidden(start: &'a Item) -> impl Iterator<Item = &'a Item> {
+        Self::with_hidden(start).filter(|item| item.is_shown())
+    }
+
+    pub fn with_hidden(start: &'a Item) -> Self {
+        Self {
+            next: Some(start),
+            stack: Vec::new(),
+        }
+    }
+}
+
+impl<'a> Iterator for ItemRefIterator<'a> {
+    type Item = &'a Item;
+
+    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);
+            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);
+                    self.stack.push((item, index + 1));
+                    return Some(cur);
+                }
+            }
+        }
+        Some(cur)
+    }
+}
+
 pub struct ItemCursor {
     cur: Option<Arc<Item>>,
     stack: Vec<(Arc<Item>, usize)>,
+    include_hidden: bool,
 }
 
 impl ItemCursor {
     pub fn new(start: Arc<Item>) -> Self {
+        Self {
+            cur: start.is_shown().then_some(start),
+            stack: Vec::new(),
+            include_hidden: false,
+        }
+    }
+
+    pub fn with_hidden(start: Arc<Item>) -> Self {
         Self {
             cur: Some(start),
             stack: Vec::new(),
+            include_hidden: true,
         }
     }
 
index 42b0eacdeb68596718509e7fd9f14753ba193cdb..181a6c2ddbbf15ee47c399ea5ab0ca8d5e36b089 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::{ItemRefIterator, render::Extreme, table::DrawCell};
 
 use crate::output::{
     Details, Item,
@@ -381,22 +381,25 @@ impl TextRenderer {
     where
         W: FmtWrite,
     {
-        match &item.details {
-            Details::Chart | Details::Image(_) => todo!(),
-            Details::Heading(children) => {
-                for (index, child) in children.0.iter().enumerate() {
-                    if index > 0 {
-                        writeln!(writer)?;
-                    }
-                    self.render(child, writer)?;
+        for (index, item) in ItemRefIterator::without_hidden(item)
+            .filter(|item| !item.details.is_heading())
+            .enumerate()
+        {
+            if index > 0 {
+                writeln!(writer)?;
+            }
+            match &item.details {
+                Details::Chart | Details::Image(_) => todo!(),
+                Details::Heading(_) => unreachable!(),
+                Details::Message(_diagnostic) => todo!(),
+                Details::PageBreak => (),
+                Details::Table(pivot_table) => self.render_table(pivot_table, writer)?,
+                Details::Text(text) => {
+                    self.render_table(&PivotTable::from((**text).clone()), writer)?
                 }
-                Ok(())
             }
-            Details::Message(_diagnostic) => todo!(),
-            Details::PageBreak => Ok(()),
-            Details::Table(pivot_table) => self.render_table(pivot_table, writer),
-            Details::Text(text) => self.render_table(&PivotTable::from((**text).clone()), writer),
         }
+        Ok(())
     }
 
     fn render_table<W>(&mut self, table: &PivotTable, writer: &mut W) -> FmtResult