pptx: preserve SmartArt node text when dgm:cxnLst has no parent-child connections (#1944)

* pptx: preserve SmartArt node text when dgm:cxnLst has no parent-child connections

* fix(pptx): apply unconnected-point SmartArt outline fallback when type="doc" point is absent

* fix(pptx): restrict SmartArt pt_order fallback to empty parOf graphs and preserve cached Picture/Connector children

* fix(pptx): convert default rect geometry to line for connectors in diagram drawings

* docs(pptx): note flat ptLst order fallback on dgm::outline
This commit is contained in:
neilcawse authored and GitHub committed 2026-11-11 18:48:47 +00:00
1 parent 203fa79211
commit 690171665a
2 files changed
+46 -6

No files matched your search

+31 -3
View File
@@ -403,7 +403,7 @@ pub fn data_xml(d: &Diagram, seed: u32, drawing_rel: &str, pics: &[(usize, Strin
w.finish()
}
/// The outline of a data part: the `parOf` tree under the document point, depth first.
/// The outline of a data part: the `parOf` tree under the document point, depth first, or the data points in `ptLst` order when no `parOf` connections exist.
pub fn outline(data: &El) -> Vec<DiagramNode> {
outline_ids(data).into_iter().map(|(_, n)| n).collect()
}
@@ -429,11 +429,13 @@ pub fn node_pictures(data: &El) -> Vec<(usize, String)> {
out
}
/// Extracts diagram outline `(modelId, DiagramNode)` pairs in tree order from a `dgm:dataModel` element.
fn outline_ids(data: &El) -> Vec<(String, DiagramNode)> {
let Some(pts) = data.child("ptLst") else { return vec![] };
let mut texts: HashMap<&str, String> = HashMap::new();
let mut assistants: std::collections::HashSet<&str> = std::collections::HashSet::new();
let mut titles: HashMap<&str, String> = HashMap::new();
let mut pt_order: Vec<&str> = Vec::new();
let mut doc: Option<&str> = None;
for pt in pts.children_named("pt").take(MAX_NODES * 4) {
let Some(id) = pt.attr("modelId") else { continue };
@@ -442,6 +444,7 @@ fn outline_ids(data: &El) -> Vec<(String, DiagramNode)> {
ty @ ("node" | "asst") => {
let t = pt.child("t").map(paragraphs).unwrap_or_default();
texts.insert(id, t);
pt_order.push(id);
if ty == "asst" {
assistants.insert(id);
}
@@ -459,7 +462,6 @@ fn outline_ids(data: &El) -> Vec<(String, DiagramNode)> {
_ => {}
}
}
let Some(doc) = doc else { return vec![] };
let mut kids: HashMap<&str, Vec<(i64, &str)>> = HashMap::new();
if let Some(cx) = data.child("cxnLst") {
for c in cx.children_named("cxn").take(MAX_NODES * 4) {
@@ -478,7 +480,7 @@ fn outline_ids(data: &El) -> Vec<(String, DiagramNode)> {
let mut out = vec![];
let mut seen = std::collections::HashSet::new();
// Depth first, iteratively; a node reached twice (a cycle in a hostile file) is skipped.
let mut stack: Vec<(&str, u8)> = kids.get(doc).map(|v| v.iter().rev().map(|(_, id)| (*id, 0u8)).collect()).unwrap_or_default();
let mut stack: Vec<(&str, u8)> = doc.and_then(|d| kids.get(d)).map(|v| v.iter().rev().map(|(_, id)| (*id, 0u8)).collect()).unwrap_or_default();
while let Some((id, level)) = stack.pop() {
if out.len() >= MAX_NODES || !seen.insert(id) {
continue;
@@ -497,6 +499,23 @@ fn outline_ids(data: &El) -> Vec<(String, DiagramNode)> {
stack.extend(v.iter().rev().map(|(_, c)| (*c, level.saturating_add(1))));
}
}
if out.is_empty() && kids.is_empty() {
for id in pt_order {
if out.len() >= MAX_NODES || !seen.insert(id) {
continue;
}
out.push((
id.to_string(),
DiagramNode {
text: texts.get(id).cloned().unwrap_or_default(),
level: 0,
picture: None,
assistant: assistants.contains(id),
title: titles.get(id).cloned().unwrap_or_default(),
},
));
}
}
out
}
@@ -878,5 +897,14 @@ mod tests {
assert_eq!(colors_for(&cs_id("accent4")), "accent4");
assert_eq!(colors_for(&cs_id("")), "");
assert_eq!(style_for(&qs_id("outline")), "outline");
let unconnected = br#"<dgm:dataModel xmlns:dgm="d"><dgm:ptLst><dgm:pt modelId="0" type="doc"/><dgm:pt modelId="1" type="node"><dgm:t><a:p xmlns:a="a"><a:r><a:t>Alpha</a:t></a:r></a:p></dgm:t></dgm:pt><dgm:pt modelId="2" type="node"><dgm:t><a:p xmlns:a="a"><a:r><a:t>Beta</a:t></a:r></a:p></dgm:t></dgm:pt></dgm:ptLst><dgm:cxnLst/></dgm:dataModel>"#;
let unconnected_doc = crate::xml::parse(unconnected).unwrap();
assert_eq!(outline(&unconnected_doc.root), nodes(&[("Alpha", 0), ("Beta", 0)]));
// When parOf connections exist but no root (type="doc") reaches them, do not flatten the hierarchy to level 0.
let unrooted_hierarchy = br#"<dgm:dataModel xmlns:dgm="d"><dgm:ptLst><dgm:pt modelId="1" type="node"><dgm:t><a:p xmlns:a="a"><a:r><a:t>Parent</a:t></a:r></a:p></dgm:t></dgm:pt><dgm:pt modelId="2" type="node"><dgm:t><a:p xmlns:a="a"><a:r><a:t>Child</a:t></a:r></a:p></dgm:t></dgm:pt></dgm:ptLst><dgm:cxnLst><dgm:cxn srcId="1" destId="2"/></dgm:cxnLst></dgm:dataModel>"#;
let unrooted_doc = crate::xml::parse(unrooted_hierarchy).unwrap();
assert!(outline(&unrooted_doc.root).is_empty());
}
}
+15 -3
View File
@@ -542,10 +542,22 @@ fn diagram(w: &mut W, x: &mut Exp, o: &mut Out, cx: &mut Ctx, s: &Shape, d: &dec
let (sx, sy) = (frame.w / child.w.max(1e-6), frame.h / child.h.max(1e-6));
let mut k = 0;
let mut flat = vec![];
walk(children, &mut |c, _| {
if matches!(c.kind, ShapeKind::Shape) {
flat.push(c.clone());
walk(children, &mut |c, _| match &c.kind {
ShapeKind::Shape => flat.push(c.clone()),
ShapeKind::Connector { .. } => {
let mut sc = c.clone();
if sc.geom.preset_name() == Some("rect") {
sc.geom = Geom::preset("line");
}
flat.push(sc);
}
ShapeKind::Picture { fill } => {
let mut sc = c.clone();
sc.kind = ShapeKind::Shape;
sc.fill = Some(deckcraft_model::style::Fill::Picture(fill.clone()));
flat.push(sc);
}
_ => {}
});
for mut c in flat.into_iter().take(10_000) {
if let Some(xf) = &mut c.xfrm {