fix: integrate batched community PRs (#170's PPTX exporter on the page-indexed compose cache and crate::io, with a Georgian menu label; #171: a deleted style's replacement is never based on itself)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-15 00:35:27 -04:00
1 parent 4b85aa4e6a
commit a930276fd7
7 files changed
+37 -13

No files matched your search

Generated
+2 -2
View File
@@ -1112,7 +1112,7 @@ dependencies = [
[[package]]
name = "designcraft-pptx"
version = "0.5.0"
version = "0.8.0"
dependencies = [
"designcraft-color",
"designcraft-compose",
@@ -1121,7 +1121,7 @@ dependencies = [
"designcraft-geom",
"designcraft-images",
"image",
"quick-xml 0.38.4",
"quick-xml",
"skrifa 0.47.0",
"thiserror 2.0.21",
"zip",
+1 -1
View File
@@ -102,7 +102,7 @@ fn export_pptx(s: &mut Session, p: &Value) -> Result<Value> {
match str_param(p, "path") {
Some(path) => {
#[cfg(not(target_arch = "wasm32"))]
std::fs::write(path, &r.bytes).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
crate::io::write_file(path, &r.bytes).map_err(EngineError::Other)?;
Ok(json!({"path": path, "bytes": r.bytes.len(), "slides": r.slides, "warnings": r.warnings}))
}
None => Ok(json!({"base64": super::file::base64_encode(&r.bytes), "bytes": r.bytes.len(), "slides": r.slides, "warnings": r.warnings})),
+10 -5
View File
@@ -539,12 +539,16 @@ fn rename_style(d: &mut designcraft_doc::Document, para: bool, from: &str, to: &
/// instead removes the entry, after its former uses point at the replacement).
fn redirect_style_refs(d: &mut designcraft_doc::Document, para: bool, from: &str, to: &str) {
let st = d.styles_mut();
// A style is never based on itself: when `to` was based on `from`, it takes `from`'s parent.
let parent_of = |based_on: Option<&String>| based_on.filter(|b| b.as_str() != to).cloned();
if para {
let from_parent = parent_of(st.paragraph.iter().find(|x| x.name == from).and_then(|x| x.based_on.as_ref()));
for s in &mut st.paragraph {
for r in [&mut s.based_on, &mut s.next_style] {
if r.as_deref() == Some(from) {
*r = Some(to.to_string());
}
if s.based_on.as_deref() == Some(from) {
s.based_on = if s.name == to { from_parent.clone() } else { Some(to.to_string()) };
}
if s.next_style.as_deref() == Some(from) {
s.next_style = Some(to.to_string());
}
}
for o in &mut st.object {
@@ -553,9 +557,10 @@ fn redirect_style_refs(d: &mut designcraft_doc::Document, para: bool, from: &str
}
}
} else {
let from_parent = parent_of(st.character.iter().find(|x| x.name == from).and_then(|x| x.based_on.as_ref()));
for s in &mut st.character {
if s.based_on.as_deref() == Some(from) {
s.based_on = Some(to.to_string());
s.based_on = if s.name == to { from_parent.clone() } else { Some(to.to_string()) };
}
}
for s in &mut st.paragraph {
+17
View File
@@ -701,6 +701,23 @@ fn paragraph_style_delete_rejects_missing_unknown_and_self_replacement() {
assert!(s.execute("style.paragraph.delete", &json!({"name": name, "replaceWith": name})).is_err(), "replaceWith can't be the deleted style");
}
#[test]
fn deleting_a_style_never_leaves_its_replacement_based_on_itself() {
let mut s = session();
let para = |s: &Session, n: &str| s.doc().unwrap().doc.styles.para(n).unwrap().based_on.clone();
s.execute("style.paragraph.create", &json!({"name": "Head"})).unwrap();
s.execute("style.paragraph.create", &json!({"name": "Sub", "basedOn": "Head"})).unwrap();
s.execute("style.paragraph.create", &json!({"name": "Minor", "basedOn": "Sub"})).unwrap();
// Minor is based on Sub; deleting Sub in favour of Minor gives Minor Sub's parent.
s.execute("style.paragraph.delete", &json!({"name": "Sub", "replaceWith": "Minor"})).unwrap();
assert_eq!(para(&s, "Minor").as_deref(), Some("Head"));
s.execute("style.character.create", &json!({"name": "Strong"})).unwrap();
s.execute("style.character.create", &json!({"name": "Stronger", "basedOn": "Strong"})).unwrap();
s.execute("style.character.delete", &json!({"name": "Strong", "replaceWith": "Stronger"})).unwrap();
let based = s.doc().unwrap().doc.styles.character.iter().find(|c| c.name == "Stronger").unwrap().based_on.clone();
assert_ne!(based.as_deref(), Some("Stronger"));
}
#[test]
fn character_style_delete_reassigns_every_kind_of_reference() {
let mut s = session();
+5 -4
View File
@@ -208,7 +208,8 @@ pub struct Exporter<'a> {
pub to_slide: Affine,
/// The page's bleed area in spread space (items outside it are left out).
clip: Rect,
pub page_name: Option<String>,
/// The page parent items are composed for (page numbers, running heads).
pub page: Option<usize>,
}
impl<'a> Exporter<'a> {
@@ -227,7 +228,7 @@ impl<'a> Exporter<'a> {
rel_of: HashMap::new(),
to_slide: Affine::IDENTITY,
clip: Rect::ZERO,
page_name: None,
page: None,
}
}
@@ -276,9 +277,9 @@ impl<'a> Exporter<'a> {
continue;
}
// Parent items compose per page (page numbers, running heads).
self.page_name = Some(page_name.clone());
self.page = Some(abs);
self.item(it, Affine::translate((dx, 0.0)), 1.0);
self.page_name = None;
self.page = None;
}
}
for it in &sp.items {
+1 -1
View File
@@ -92,7 +92,7 @@ pub(crate) struct Segment {
pub fn frame(ex: &mut Exporter, it: &Item, tfr: &TextFrame, xf: Affine, opacity: f32) {
let doc = ex.doc;
let cs = ex.cache.get(doc, tfr.story, ex.page_name.as_deref());
let cs = ex.cache.get(doc, tfr.story, ex.page);
let Some(ft) = cs.frame(it.id) else {
ex.shape(it, xf, opacity, None);
return;
+1
View File
@@ -1830,4 +1830,5 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Convert to Flex Layout", "მოქნილ განლაგებად გარდაქმნა"),
("Remove Flex Layout", "მოქნილი განლაგების წაშლა"),
("Show Flex Controls", "მოქნილი მართვის ელემენტების ჩვენება"),
("Export PowerPoint…", "PowerPoint-ად ექსპორტი…"),
];