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:
dotCooCoo
2026-10-18 01:32:01 +07:00
committed by bflatastic
parent 7da58e35cc
commit 598f1de3cc
7 changed files with 79 additions and 11 deletions
+3 -2
View File
@@ -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);
+1 -1
View File
@@ -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.
+33 -5
View File
@@ -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.
+17
View File
@@ -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();
+19
View File
@@ -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() {
+5 -3
View File
@@ -1120,7 +1120,8 @@ already walks Image Trace. Short agent entry points:
9-slice guides on or off; `nineSliceGuides` is `[left, top, right, bottom]`, fractions 0..1 of the symbol size; rows
of `symbol.list` carry `nineSlice`: the guides or `null`),
`symbol.place {name?, x?, y?}`, `symbol.breakLink {ids?}`, `symbol.replace`, `symbol.edit`, `symbol.duplicate`,
`symbol.selectInstances {name}`.
`symbol.selectInstances {name}`. `symbol.new` and `symbol.duplicate {name, newName?}` trim the white space around the
name they are given, as `symbol.options` trims `newName`; a blank one gives "New Symbol" or "<name> copy".
- Brushes: `brush.list`, `brush.get {name}`, `brush.apply {name, ids?}`, `brush.new {type, name?, params?, ids?}`,
`brush.setCurrent {name}`, `brush.options {name, params?}`, `brush.delete {name} | {names}`,
`brush.duplicate {name, newName?} | {names}`, `brush.freehand`, `object.expandBrush {ids?}`. `brush.new`,
@@ -1464,8 +1465,9 @@ first selected object's solid colour, else the default one, or `color`), `charSt
text's attributes). An item the library already has is reported as `existing`. `library.use {library?, kind, item,
center?, to?}` uses one: a graphic is placed centred on `center` (pasted with its resources; a swatch name the document
gives another colour merges into the document's), a colour paints the selection's fill or stroke, and a style is added
to the document (numbered when its name is taken by other attributes) and applied to the selected text, each as one
undo step. `library.removeItem {library?, kind, item}` removes one.
to the document under its name without the white space around it (numbered when that name is taken by other
attributes) and applied to the selected text, each as one undo step. `library.removeItem {library?, kind, item}` removes
one.
Items can be put in user-named groups, which the panel shows as collapsible sections after the ungrouped items (its
search field filters every item by name; `library.get` lists them all to filter). `library.createGroup {library?,
+1
View File
@@ -404,6 +404,7 @@ since 2026-10-01.
- **Artboard clean active-tab close (2026-10-15):** with the Artboard tool active, closing a clean active tab restores the surviving document's displayed board before Properties Delete. Nine targeted checks passed: three existing tab regressions, one clean close/Delete regression and five existing unsaved-document controls. The close regression checks the correct surviving board, preserved artwork/symbols and object selection, affected guides, one Delete edit, saved state and Undo/Redo. Three reviewed original full-app headless captures show the active tab before closing, the surviving document's shown board after closing, and the correct survivor after Properties Delete, using the actual tab × and Delete controls.
- **Character-style Auto leading:** capture and explicit style attributes retain Auto as a reset, distinct from omitted leading. Redefine and later numeric updates preserve local leading overrides; the Character Styles marker recognizes Auto correctly. Regression assertions cover partial definitions, Undo/Redo and native save/load serialization. Public references: [Character and paragraph styles](https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html) and [Leading](https://helpx.adobe.com/illustrator/using/line-character-spacing.html).
- **Text styles redefined to the defaults:** New and Redefine Character Style and Paragraph Style capture every attribute of the selected text, including those at their defaults (Auto kerning, Roman baseline alignment, the default Hyphenation options, the Every-line Composer, no tab stops), and `charStyle.setAttrs` and `paraStyle.setAttrs` keep an attribute set to its default value. After a redefinition, text whose value still equals the old definition's takes the new value, a default included; local overrides and attributes the new definition leaves out stay. The style panels show "+" only where the text differs from its style, including type with Auto alignment. Public reference: [Character and paragraph styles](https://web.archive.org/web/20260825113735/https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html).
- **Names of new styles and symbols:** `charStyle.new` and `paraStyle.new` trim the white space around the name they are given, as `charStyle.rename` and `paraStyle.rename` do, and refuse a name that is then empty; Duplicate names its copy from the trimmed name. `symbol.new` and `symbol.duplicate` trim the name as Symbol Options does, and `library.use` adds a library's text style under its trimmed name. Public reference: [Character and paragraph styles](https://web.archive.org/web/20260825113735/https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html).
- **Width Point Edit:** Shift-double-click enables Adjust Adjoining Width Points; plain double-click and direct opening retain the default. Tool dispatch and headless dialog tests cover neighboring widths, Undo/Redo and Cancel. Public reference: [Variable-width strokes](https://helpx.adobe.com/illustrator/using/stroke-object.html).
Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a feature lands.