mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-26 19:26:46 -06:00
Style delete: redirect every reference, validate name and replacement
delete_para and delete_char previously reassigned only the story text runs/paragraphs using the deleted style, leaving other styles' Based On, paragraph styles' nested/nested-line/GREP style refs, the drop cap style, and the footnote reference style pointed at a name that no longer exists. Extract redirect_style_refs() from rename_style() (everything it does except updating the registry entry's own name) and reuse it from both delete commands, so deleting a style now redirects every reference type to the replacement style before removing the registry entry. Also teach rename_char_refs() about drop_cap_style, which it missed even for renames. Add validation: name must be non-empty and refer to an existing style, and replaceWith (if given) must refer to an existing style and can't be the style being deleted. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot App
parent
7c5b248dfc
commit
bfcd6ef71f
@@ -511,12 +511,29 @@ fn edit_para(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
/// Rename a paragraph or character style everywhere it's used.
|
||||
fn rename_style(d: &mut designcraft_doc::Document, para: bool, from: &str, to: &str) {
|
||||
let st = d.styles_mut();
|
||||
if para {
|
||||
for s in &mut st.paragraph {
|
||||
for s in &mut d.styles_mut().paragraph {
|
||||
if s.name == from {
|
||||
s.name = to.to_string();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for s in &mut d.styles_mut().character {
|
||||
if s.name == from {
|
||||
s.name = to.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
redirect_style_refs(d, para, from, to);
|
||||
}
|
||||
|
||||
/// Point every reference to style `from` at `to`, except the style's own registry entry (its
|
||||
/// `name` field) — shared by renaming (which also updates the entry's name) and deletion (which
|
||||
/// 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();
|
||||
if para {
|
||||
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());
|
||||
@@ -530,9 +547,6 @@ fn rename_style(d: &mut designcraft_doc::Document, para: bool, from: &str, to: &
|
||||
}
|
||||
} else {
|
||||
for s in &mut st.character {
|
||||
if s.name == from {
|
||||
s.name = to.to_string();
|
||||
}
|
||||
if s.based_on.as_deref() == Some(from) {
|
||||
s.based_on = Some(to.to_string());
|
||||
}
|
||||
@@ -564,7 +578,8 @@ fn rename_style(d: &mut designcraft_doc::Document, para: bool, from: &str, to: &
|
||||
}
|
||||
}
|
||||
|
||||
/// Point the nested, nested line and GREP styles of paragraph attributes at a renamed character style.
|
||||
/// Point the nested, nested line and GREP styles, and the drop cap style, of paragraph
|
||||
/// attributes at a renamed (or deleted, pointed at its replacement) character style.
|
||||
fn rename_char_refs(p: &mut ParaAttrs, from: &str, to: &str) {
|
||||
let names = p.nested_styles.iter_mut().flatten().map(|n| &mut n.style);
|
||||
let names = names.chain(p.nested_line_styles.iter_mut().flatten().map(|n| &mut n.style));
|
||||
@@ -573,6 +588,9 @@ fn rename_char_refs(p: &mut ParaAttrs, from: &str, to: &str) {
|
||||
*n = to.to_string();
|
||||
}
|
||||
}
|
||||
if p.drop_cap_style.as_deref() == Some(from) {
|
||||
p.drop_cap_style = Some(to.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
fn group_styles(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -610,22 +628,23 @@ fn group_styles(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn delete_para(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = str_param(p, "name").unwrap_or("").to_string();
|
||||
let name = str_param(p, "name").filter(|n| !n.is_empty()).ok_or_else(|| bad("style.paragraph.delete", "missing name"))?.to_string();
|
||||
let repl = str_param(p, "replaceWith").unwrap_or(designcraft_doc::BASIC_PARAGRAPH).to_string();
|
||||
if name.starts_with('[') {
|
||||
return Err(bad("style.paragraph.delete", "built-in styles can't be deleted"));
|
||||
}
|
||||
if repl == name {
|
||||
return Err(bad("style.paragraph.delete", "`replaceWith` can't be the deleted style"));
|
||||
}
|
||||
s.edit(|d, _| {
|
||||
d.styles_mut().paragraph.retain(|x| x.name != name);
|
||||
for sid in d.stories.keys().copied().collect::<Vec<_>>() {
|
||||
if let Some(story) = d.story_mut(sid) {
|
||||
for f in &mut story.paras {
|
||||
if f.style == name {
|
||||
f.style = repl.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
if d.styles.para(&name).is_none() {
|
||||
return Err(bad("style.paragraph.delete", format!("no paragraph style `{name}`")));
|
||||
}
|
||||
if d.styles.para(&repl).is_none() {
|
||||
return Err(bad("style.paragraph.delete", format!("no paragraph style `{repl}`")));
|
||||
}
|
||||
redirect_style_refs(d, true, &name, &repl);
|
||||
d.styles_mut().paragraph.retain(|x| x.name != name);
|
||||
ok()
|
||||
})
|
||||
}
|
||||
@@ -718,22 +737,23 @@ fn edit_char(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn delete_char(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = str_param(p, "name").unwrap_or("").to_string();
|
||||
let name = str_param(p, "name").filter(|n| !n.is_empty()).ok_or_else(|| bad("style.character.delete", "missing name"))?.to_string();
|
||||
let repl = str_param(p, "replaceWith").unwrap_or(designcraft_doc::NO_CHAR_STYLE).to_string();
|
||||
if name.starts_with('[') {
|
||||
return Err(bad("style.character.delete", "built-in styles can't be deleted"));
|
||||
}
|
||||
if repl == name {
|
||||
return Err(bad("style.character.delete", "`replaceWith` can't be the deleted style"));
|
||||
}
|
||||
s.edit(|d, _| {
|
||||
d.styles_mut().character.retain(|x| x.name != name);
|
||||
for sid in d.stories.keys().copied().collect::<Vec<_>>() {
|
||||
if let Some(story) = d.story_mut(sid) {
|
||||
story.for_each_text_mut(&mut |st| {
|
||||
for r in st.chars.iter_mut().filter(|r| r.format.style == name) {
|
||||
r.format.style = repl.clone();
|
||||
}
|
||||
});
|
||||
}
|
||||
if d.styles.char_style(&name).is_none() {
|
||||
return Err(bad("style.character.delete", format!("no character style `{name}`")));
|
||||
}
|
||||
if d.styles.char_style(&repl).is_none() {
|
||||
return Err(bad("style.character.delete", format!("no character style `{repl}`")));
|
||||
}
|
||||
redirect_style_refs(d, false, &name, &repl);
|
||||
d.styles_mut().character.retain(|x| x.name != name);
|
||||
ok()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -681,6 +681,56 @@ fn character_style_delete_rejects_builtin_styles() {
|
||||
assert!(r.is_err(), "built-in styles can't be deleted");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_style_delete_rejects_missing_unknown_and_self_replacement() {
|
||||
let mut s = session();
|
||||
let name = s.execute("style.character.create", &json!({"name": "Key Term"})).unwrap()["name"].as_str().unwrap().to_string();
|
||||
assert!(s.execute("style.character.delete", &json!({"name": ""})).is_err(), "empty name is rejected");
|
||||
assert!(s.execute("style.character.delete", &json!({"name": "No Such Style"})).is_err(), "unknown name is rejected");
|
||||
assert!(s.execute("style.character.delete", &json!({"name": name, "replaceWith": "No Such Style"})).is_err(), "unknown replaceWith is rejected");
|
||||
assert!(s.execute("style.character.delete", &json!({"name": name, "replaceWith": name})).is_err(), "replaceWith can't be the deleted style");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paragraph_style_delete_rejects_missing_unknown_and_self_replacement() {
|
||||
let mut s = session();
|
||||
let name = s.execute("style.paragraph.create", &json!({"name": "Caption"})).unwrap()["name"].as_str().unwrap().to_string();
|
||||
assert!(s.execute("style.paragraph.delete", &json!({"name": ""})).is_err(), "empty name is rejected");
|
||||
assert!(s.execute("style.paragraph.delete", &json!({"name": "No Such Style"})).is_err(), "unknown name is rejected");
|
||||
assert!(s.execute("style.paragraph.delete", &json!({"name": name, "replaceWith": "No Such Style"})).is_err(), "unknown replaceWith is rejected");
|
||||
assert!(s.execute("style.paragraph.delete", &json!({"name": name, "replaceWith": name})).is_err(), "replaceWith can't be the deleted style");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_style_delete_reassigns_every_kind_of_reference() {
|
||||
let mut s = session();
|
||||
let name = s.execute("style.character.create", &json!({"name": "Key Term"})).unwrap()["name"].as_str().unwrap().to_string();
|
||||
let repl = s.execute("style.character.create", &json!({"name": "Emphasis"})).unwrap()["name"].as_str().unwrap().to_string();
|
||||
// Another character style's basedOn.
|
||||
let based = s.execute("style.character.create", &json!({"name": "Strong", "basedOn": name})).unwrap()["name"].as_str().unwrap().to_string();
|
||||
// A paragraph style's nested, nested line, GREP and drop cap styles, and the footnote reference style.
|
||||
s.execute(
|
||||
"style.paragraph.create",
|
||||
&json!({"name": "Body", "para": {
|
||||
"nestedStyles": [{"style": name, "through": true, "count": 1, "until": {"kind": "words"}}],
|
||||
"grepStyles": [{"style": name, "pattern": "x"}],
|
||||
"nestedLineStyles": [{"style": name, "lines": 1}],
|
||||
"dropCapStyle": name}}),
|
||||
)
|
||||
.unwrap();
|
||||
s.execute("footnote.options", &json!({"refCharStyle": name})).unwrap();
|
||||
s.execute("style.character.delete", &json!({"name": name, "replaceWith": repl})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
assert!(st.doc.styles.char_style(&name).is_none(), "deleted style is gone");
|
||||
assert_eq!(st.doc.styles.char_style(&based).unwrap().based_on.as_deref(), Some(repl.as_str()), "basedOn redirected");
|
||||
let body = st.doc.styles.para("Body").unwrap();
|
||||
assert_eq!(body.para.nested_styles.as_ref().unwrap()[0].style, repl, "nested style redirected");
|
||||
assert_eq!(body.para.nested_line_styles.as_ref().unwrap()[0].style, repl, "nested line style redirected");
|
||||
assert_eq!(body.para.grep_styles.as_ref().unwrap()[0].style, repl, "grep style redirected");
|
||||
assert_eq!(body.para.drop_cap_style.as_deref(), Some(repl.as_str()), "drop cap style redirected");
|
||||
assert_eq!(st.doc.footnote_options.ref_char_style, repl, "footnote reference style redirected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_tool_selects_a_page_and_its_handles_preview_or_resize_it() {
|
||||
use designcraft_tools::{CanvasLayout, Mods, PointerEvent, PointerKind};
|
||||
|
||||
Reference in New Issue
Block a user