mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-20 18:28:50 +00:00
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:
1 parent
4b85aa4e6a
commit
a930276fd7
7 files changed
+37
-13
No files matched your search
Generated
+2
-2
@@ -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",
|
||||
|
||||
@@ -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})),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1830,4 +1830,5 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Convert to Flex Layout", "მოქნილ განლაგებად გარდაქმნა"),
|
||||
("Remove Flex Layout", "მოქნილი განლაგების წაშლა"),
|
||||
("Show Flex Controls", "მოქნილი მართვის ელემენტების ჩვენება"),
|
||||
("Export PowerPoint…", "PowerPoint-ად ექსპორტი…"),
|
||||
];
|
||||
Reference in new issue
Block a user