mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
Character and Paragraph Styles: a new style's name is trimmed as a rename trims it
charStyle.new and paraStyle.new kept the name they were given as it was, while charStyle.rename and paraStyle.rename trim the white space around the new name. So a script could make a style " Body" beside "Body", and a blank name was refused with the message for a name that is taken. New Character Style and New Paragraph Style now trim the name as rename does, compare the trimmed name with the styles there are, and refuse a name that is then empty with rename's message. Duplicate names the copy from the trimmed name. symbol.new and symbol.duplicate trim the name as symbol.options trims newName, and a blank newName gives "<name> copy" instead of a symbol named with the blank. library.use adds a library's text style under its trimmed name, so a library file whose style is named with spaces around it still applies. Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -679,7 +679,7 @@ fn symbol_new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ids = roots(s, p)?;
|
||||
let art = selection_art(s, p)?.ok_or_else(|| EngineError::Other("nothing selected".into()))?;
|
||||
let (art, size, c) = normalize(art)?;
|
||||
let name = s.doc()?.doc.unique_symbol_name(str_param(p, "name").filter(|n| !n.trim().is_empty()).unwrap_or("New Symbol"));
|
||||
let name = s.doc()?.doc.unique_symbol_name(str_param(p, "name").map(str::trim).filter(|n| !n.is_empty()).unwrap_or("New Symbol"));
|
||||
let reg = registration_param(p, C)?.unwrap_or(SymbolRegistration::CENTER);
|
||||
let export = export_type_param(p, C)?.unwrap_or_default();
|
||||
let nine = nine_slice_param(p, C, None)?.flatten();
|
||||
@@ -928,7 +928,8 @@ fn symbol_duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = name_param(p, C)?.to_string();
|
||||
let d = &s.doc()?.doc;
|
||||
let sym = d.symbols.iter().find(|x| x.name == name).cloned().ok_or_else(|| bad(C, format!("no symbol named `{name}`")))?;
|
||||
let new = d.unique_symbol_name(str_param(p, "newName").unwrap_or(&format!("{name} copy")));
|
||||
let given = str_param(p, "newName").map(str::trim).filter(|n| !n.is_empty());
|
||||
let new = d.unique_symbol_name(&given.map_or_else(|| format!("{} copy", name.trim()), str::to_string));
|
||||
let size = natural_size(d, &name);
|
||||
let reg = d.symbol_registration(&name);
|
||||
let export = d.symbol_export_type(&name);
|
||||
|
||||
@@ -885,7 +885,7 @@ fn use_item(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
Kind::CharStyle | Kind::ParaStyle => {
|
||||
let para = kind == Kind::ParaStyle;
|
||||
let name = data.get("name").and_then(Value::as_str).unwrap_or("Style").to_string();
|
||||
let name = data.get("name").and_then(Value::as_str).map(str::trim).filter(|n| !n.is_empty()).unwrap_or("Style").to_string();
|
||||
let attrs = data.get("attrs").and_then(Value::as_object).cloned().unwrap_or_default();
|
||||
let doc = &s.doc()?.doc;
|
||||
// The document's style of that name when it is the same; else a new one.
|
||||
|
||||
@@ -58,7 +58,7 @@ macro_rules! style_cmds {
|
||||
concat!($p, ".new"),
|
||||
concat!("New ", $what, " Style"),
|
||||
&["Window", "Type", $menu],
|
||||
"{name?, attrs?: {…} (default: selected text), id?, start?: byte, end?: byte} → {name}; a nonempty character range samples its first selected character; start alone keeps insertion affinity; paragraph styles sample start",
|
||||
"{name? (trimmed, as rename trims `to`), attrs?: {…} (default: selected text), id?, start?: byte, end?: byte} → {name}; a nonempty character range samples its first selected character; start alone keeps insertion affinity; paragraph styles sample start",
|
||||
|s, p| new(s, p, $kind),
|
||||
true,
|
||||
),
|
||||
@@ -432,10 +432,13 @@ fn new(s: &mut Session, p: &Value, kind: Kind) -> Result<Value> {
|
||||
let d = &s.doc()?.doc;
|
||||
let base = if kind == Kind::Char { "Character Style" } else { "Paragraph Style" };
|
||||
let name = match str_param(p, "name") {
|
||||
Some(n) => n.to_string(),
|
||||
Some(n) => n.trim().to_string(),
|
||||
None => (1..).map(|i| format!("{base} {i}")).find(|n| kind.attrs(d, n).is_none()).unwrap_or_else(|| base.to_string()),
|
||||
};
|
||||
if name.trim().is_empty() || kind.attrs(d, &name).is_some() {
|
||||
if name.is_empty() {
|
||||
return Err(bad(&c, format!("`{name}` is empty or already used")));
|
||||
}
|
||||
if kind.attrs(d, &name).is_some() {
|
||||
return Err(bad(&c, format!("a style named `{name}` already exists")));
|
||||
}
|
||||
let n2 = name.clone();
|
||||
@@ -552,10 +555,12 @@ fn duplicate(s: &mut Session, p: &Value, kind: Kind) -> Result<Value> {
|
||||
let name = name_param(p, kind, "duplicate")?.to_string();
|
||||
let d = &s.doc()?.doc;
|
||||
let attrs = kind.attrs(d, &name).ok_or_else(|| bad(&kind.cmd("duplicate"), format!("no style `{name}`")))?;
|
||||
// `new` trims the name, so the copy is named from the trimmed one.
|
||||
let stem = name.trim();
|
||||
let copy = (1..)
|
||||
.map(|i| if i == 1 { format!("{name} copy") } else { format!("{name} copy {i}") })
|
||||
.map(|i| if i == 1 { format!("{stem} copy") } else { format!("{stem} copy {i}") })
|
||||
.find(|n| kind.attrs(d, n).is_none())
|
||||
.unwrap_or_else(|| format!("{name} copy"));
|
||||
.unwrap_or_else(|| format!("{stem} copy"));
|
||||
new(s, &json!({ "name": copy, "attrs": attrs }), kind)
|
||||
}
|
||||
|
||||
@@ -688,6 +693,29 @@ mod tests {
|
||||
assert_eq!(s.doc().unwrap().doc.char_styles.iter().find(|d| d.name == "Pink").unwrap().attrs["fill"], fill);
|
||||
}
|
||||
|
||||
/// New Character Style and New Paragraph Style trim the name they are given, as a rename
|
||||
/// trims `to`; the trimmed name is the one compared with the styles there are.
|
||||
#[test]
|
||||
fn new_styles_trim_their_names_as_a_rename_does() {
|
||||
let (mut s, _) = session_with_text("Body text");
|
||||
for p in ["charStyle", "paraStyle"] {
|
||||
assert_eq!(s.execute(&format!("{p}.new"), &json!({"name": " Body \t"})).unwrap()["name"], "Body", "{p}");
|
||||
let list = s.execute(&format!("{p}.list"), &json!({})).unwrap();
|
||||
let names: Vec<&str> = list["styles"].as_array().unwrap().iter().filter_map(|x| x["name"].as_str()).collect();
|
||||
assert_eq!(names[1..], ["Body"], "{p}");
|
||||
let e = s.execute(&format!("{p}.new"), &json!({"name": " Body"})).unwrap_err().to_string();
|
||||
assert!(e.contains("a style named `Body` already exists"), "{p}: {e}");
|
||||
let e = s.execute(&format!("{p}.new"), &json!({"name": " \t "})).unwrap_err().to_string();
|
||||
assert!(e.contains("`` is empty or already used"), "{p}: {e}");
|
||||
assert_eq!(s.execute(&format!("{p}.duplicate"), &json!({"name": "Body"})).unwrap()["name"], "Body copy", "{p}");
|
||||
}
|
||||
// A style whose name has spaces around it, as a saved file can hold, is copied under the
|
||||
// trimmed name, numbered past the copies there are.
|
||||
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).char_styles.push(TextStyleDef { name: " Old ".into(), attrs: Map::new() });
|
||||
s.execute("charStyle.new", &json!({"name": "Old copy"})).unwrap();
|
||||
assert_eq!(s.execute("charStyle.duplicate", &json!({"name": " Old "})).unwrap()["name"], "Old copy 2");
|
||||
}
|
||||
|
||||
/// #1001: attributes their JSON leaves out at the defaults (Auto leading and kerning, no
|
||||
/// mojikumi) are still attributes a style can set, and a style captured with one applies to
|
||||
/// text at the default, is duplicated and comes back out of a library.
|
||||
|
||||
@@ -567,6 +567,23 @@ fn delete_duplicate_and_replace_symbols() {
|
||||
assert!(s.execute("symbol.delete", &json!({"name": "Small"})).is_err());
|
||||
}
|
||||
|
||||
/// New Symbol and Duplicate Symbol trim the name they are given, as Symbol Options trims
|
||||
/// `newName`; the trimmed name is the one compared with the symbols there are.
|
||||
#[test]
|
||||
fn new_and_duplicate_symbols_trim_their_names_as_a_rename_does() {
|
||||
let mut s = session();
|
||||
rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
assert_eq!(s.execute("symbol.new", &json!({"name": " Box \t"})).unwrap()["name"], "Box");
|
||||
rect(&mut s, 20.0, 0.0, 10.0, 10.0);
|
||||
assert_eq!(s.execute("symbol.new", &json!({"name": " Box"})).unwrap()["name"], "Box 2");
|
||||
rect(&mut s, 40.0, 0.0, 10.0, 10.0);
|
||||
assert_eq!(s.execute("symbol.new", &json!({"name": " "})).unwrap()["name"], "New Symbol");
|
||||
assert_eq!(s.execute("symbol.duplicate", &json!({"name": "Box", "newName": " Logo\t"})).unwrap()["name"], "Logo");
|
||||
assert_eq!(s.execute("symbol.duplicate", &json!({"name": "Box", "newName": " "})).unwrap()["name"], "Box copy");
|
||||
let names: Vec<String> = s.doc().unwrap().doc.symbols.iter().map(|x| x.name.clone()).collect();
|
||||
assert_eq!(names, ["Box", "Box 2", "New Symbol", "Logo", "Box copy"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbol_export_type_defaults_to_movie_clip_and_round_trips() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -143,6 +143,25 @@ fn text_styles_come_from_the_selected_type_and_apply_in_one_step() {
|
||||
assert_eq!(r["style"], format!("{name} 2"));
|
||||
}
|
||||
|
||||
/// A text style that a library file names with spaces around it comes into the document with the
|
||||
/// name trimmed, as New Character Style and New Paragraph Style trim a name, and applies.
|
||||
#[test]
|
||||
fn a_library_style_named_with_spaces_comes_in_trimmed() {
|
||||
let mut s = session();
|
||||
let style = |attrs: Value| json!([{"name": " Body ", "attrs": attrs}]);
|
||||
let data = json!({"name": "Spaced", "charStyles": style(json!({"size": 31.0})), "paraStyles": style(json!({}))});
|
||||
let lib = run(&mut s, "library.import", json!({"data": data.to_string()}))["id"].as_str().unwrap().to_string();
|
||||
let t = run(&mut s, "text.create", json!({"x": 10, "y": 50, "text": "Body", "size": 9}))["id"].clone();
|
||||
run(&mut s, "select.set", json!({"ids": [t]}));
|
||||
for kind in ["charStyle", "paraStyle"] {
|
||||
assert_eq!(run(&mut s, "library.use", json!({"library": lib, "kind": kind, "item": " Body "}))["style"], "Body", "{kind}");
|
||||
}
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert_eq!((d.char_styles[0].name.as_str(), d.para_styles[0].name.as_str()), ("Body", "Body"));
|
||||
let vectorcraft_doc::NodeKind::Text(text) = &d.node(NodeId(t.as_u64().unwrap())).unwrap().kind else { panic!("not text") };
|
||||
assert_eq!((text.runs[0].style.size, text.runs[0].style.style_name.as_deref()), (31.0, Some("Body")));
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn libraries_are_kept_in_files_across_sessions() {
|
||||
|
||||
Reference in New Issue
Block a user