diff --git a/ROADMAP.md b/ROADMAP.md index 8ea976e6..7dfa96f8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -131,6 +131,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | What landed | |---|---| +| 2026-10-15 | CLI: .txt and .md export includes table cell text, which it used to skip silently (#814) | | 2026-10-15 | PDF export and rendering: `anchorCtr` centres the drawn text block — right-aligned text no longer ran past the box edge (#736) | | 2026-10-14 | connected MCP rejects unrelated, malformed, incomplete and oversized replies without replay; buffered unsolicited bytes force a fresh connection before sending (#718) | | 2026-10-14 | Empty bulleted and numbered paragraphs show gray markers while editing, including on first click and after Enter. Markers use the list style and stay out of slide shows and exports. | diff --git a/crates/format/src/lib.rs b/crates/format/src/lib.rs index 524b7e70..a9db66e2 100644 --- a/crates/format/src/lib.rs +++ b/crates/format/src/lib.rs @@ -10,7 +10,7 @@ use std::io::{Cursor, Read, Write}; use std::sync::Arc; use deckcraft_model::text::{Paragraph, TextBody}; -use deckcraft_model::{LayoutType, Presentation, defaults}; +use deckcraft_model::{LayoutType, Presentation, ShapeKind, defaults}; pub const MIMETYPE: &str = "application/vnd.storyteller.deckcraft+zip"; /// Files written before the SlideCraft → DeckCraft rename (`.slidecraft`) are still read. @@ -510,6 +510,13 @@ pub fn slides_to_outline(p: &Presentation) -> String { out.push('\n'); } } + if let ShapeKind::Table(t) = &sh.kind { + for row in &t.rows { + let cells: Vec = row.cells.iter().map(|c| c.text.text().replace(['\u{b}', '\n'], " ")).collect(); + out.push_str(&cells.join("\t")); + out.push('\n'); + } + } } } out @@ -844,3 +851,32 @@ mod tests { } } } + +/// #814: slides_to_outline includes table cell text, which it used to skip. +#[test] +fn outline_includes_table_text() { + use deckcraft_model::table::{Cell, Row, Table}; + use deckcraft_model::{Shape, ShapeKind, Xfrm}; + let mut p = defaults::new_presentation(None); + let layout = defaults::layout_of_kind(&p, LayoutType::TitleAndContent).unwrap(); + let mut s = defaults::new_slide(&mut p, layout).expect("slide"); + s.shapes.retain(|sh| !sh.ph_type().is_some_and(|k| k.is_title())); + let mut body = Shape { xfrm: Some(Xfrm::new(0.0, 0.0, 600.0, 100.0)), ..Default::default() }; + body.text = Some(TextBody::from_text("Text box body")); + s.shapes.push(body); + + let mut table = Table { cols: vec![200.0, 200.0], ..Default::default() }; + for texts in [["Cell A1", "Cell B1"], ["Cell A2", "Cell B2"]] { + let cells: Vec = texts.iter().map(|t| Cell { text: TextBody::from_text(t), ..Default::default() }).collect(); + table.rows.push(Row { height: 30.0, cells }); + } + let mut tbl = Shape { xfrm: Some(Xfrm::new(0.0, 100.0, 400.0, 60.0)), ..Default::default() }; + tbl.kind = ShapeKind::Table(table); + s.shapes.push(tbl); + p.slides = vec![std::sync::Arc::new(s)]; + + let out = slides_to_outline(&p); + assert!(out.contains("Text box body"), "{out}"); + assert!(out.contains("Cell A1\tCell B1"), "{out}"); + assert!(out.contains("Cell A2\tCell B2"), "{out}"); +}