Object styles: unknown swatches and settings are refused

- style.object.create and edit refuse a fill, stroke or gap swatch the
  document lacks, and a fill or stroke key that is not a setting of it
  (a misspelled "wieght" no longer succeeds without a change).
- fromSelection must be true or false.
This commit is contained in:
dotCooCoo committed 2026-11-11 21:15:03 -08:00
1 parent 16d60dc03a
commit 4973d11569
3 files changed
+55 -10

No files matched your search

+2 -2
View File
@@ -182,7 +182,7 @@ pub fn specs() -> Vec<CommandSpec> {
"New Object Style…",
[],
None,
"{name, fromSelection?: true (fill, stroke, corners, effects and text frame options of the first selected item), fill?: swatch | {swatch?, tint?}, stroke?: {swatch?, weight?, tint?, type?, …}, corners?: {shape?, size?} | {corners: [{shape, size}] ×4}, paragraphStyle?: a paragraph style (null: none; the text of a frame the style is applied to takes it), applyNextStyle?: bool (each paragraph after the first takes the Next Style of the one before), textFrame?: {columns? (1–40), columnsKind?, gutter?, columnWidth?, balanceColumns?, inset?: [top, left, bottom, right] | one number for all four, verticalJustification?: top|center|bottom|justify, vjParagraphSpacingLimit?, ignoreWrap?} | null (Text Frame General Options, named as `document.inspect` names text frame options; lengths 0–15552 pt; other keys are refused; a frame keeps its baseline, auto-size and column rule settings; null: the style sets none), effects?: {dropShadow?, feather?, innerShadow?, outerGlow?, innerGlow?, bevel?, satin?, gradientFeather?, directionalFeather?}, dropShadow?: {on?, color?, opacity? (0–1), angle?, globalLight?, distance?, size?, spread?}, feather?: width, innerShadow?, outerGlow?, innerGlow?, bevel?, satin?, gradientFeather?, directionalFeather?} → {name} (made unique)",
"{name, fromSelection?: true (fill, stroke, corners, effects and text frame options of the first selected item), fill?: swatch | {swatch?, tint?}, stroke?: {swatch?, weight?, tint?, type?, …} (swatches must exist; unknown fill and stroke keys are refused), corners?: {shape?, size?} | {corners: [{shape, size}] ×4}, paragraphStyle?: a paragraph style (null: none; the text of a frame the style is applied to takes it), applyNextStyle?: bool (each paragraph after the first takes the Next Style of the one before), textFrame?: {columns? (1–40), columnsKind?, gutter?, columnWidth?, balanceColumns?, inset?: [top, left, bottom, right] | one number for all four, verticalJustification?: top|center|bottom|justify, vjParagraphSpacingLimit?, ignoreWrap?} | null (Text Frame General Options, named as `document.inspect` names text frame options; lengths 0–15552 pt; other keys are refused; a frame keeps its baseline, auto-size and column rule settings; null: the style sets none), effects?: {dropShadow?, feather?, innerShadow?, outerGlow?, innerGlow?, bevel?, satin?, gradientFeather?, directionalFeather?}, dropShadow?: {on?, color?, opacity? (0–1), angle?, globalLight?, distance?, size?, spread?}, feather?: width, innerShadow?, outerGlow?, innerGlow?, bevel?, satin?, gradientFeather?, directionalFeather?} → {name} (made unique)",
has_doc,
create_object
),
@@ -191,7 +191,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Object Style Options…",
[],
None,
"{name, rename?: new name (every use follows), fill?: swatch | {swatch?, tint?} | null, stroke?: {swatch?, weight?, tint?, type?, …} | null, corners?: {shape?: none|rounded|inverseRounded|inset|bevel|fancy, size?} | {corners: [{shape, size}] ×4} | null (null: the style no longer sets it; items keep theirs), paragraphStyle?: a paragraph style (null: none; the text of a frame the style is applied to takes it), applyNextStyle?: bool (each paragraph after the first takes the Next Style of the one before), textFrame?: {columns? (1–40), columnsKind?, gutter?, columnWidth?, balanceColumns?, inset?: [top, left, bottom, right] | one number for all four, verticalJustification?: top|center|bottom|justify, vjParagraphSpacingLimit?, ignoreWrap?} | null (Text Frame General Options, named as `document.inspect` names text frame options; lengths 0–15552 pt; other keys are refused; a frame keeps its baseline, auto-size and column rule settings; null: the style sets none), effects?: {…} | null (the style no longer sets effects), dropShadow?, feather?, innerShadow?, outerGlow?, innerGlow?, bevel?, satin?, gradientFeather?, directionalFeather?} — effect objects merge field by field into the style's effects (fields as in `document.inspect` item effects); values that aren't finite numbers are refused, the others clamped (opacities 0–1, sizes and distances ≥ 0); items using the style whose effects (fill, stroke, corners) match the style's (an item's defaults, when the style set none) take the new ones, items with their own keep them; the text of frames still in the old paragraph styles takes the new ones → {name, updated, restyled}",
"{name, rename?: new name (every use follows), fill?: swatch | {swatch?, tint?} | null, stroke?: {swatch?, weight?, tint?, type?, …} | null (swatches must exist; unknown fill and stroke keys are refused), corners?: {shape?: none|rounded|inverseRounded|inset|bevel|fancy, size?} | {corners: [{shape, size}] ×4} | null (null: the style no longer sets it; items keep theirs), paragraphStyle?: a paragraph style (null: none; the text of a frame the style is applied to takes it), applyNextStyle?: bool (each paragraph after the first takes the Next Style of the one before), textFrame?: {columns? (1–40), columnsKind?, gutter?, columnWidth?, balanceColumns?, inset?: [top, left, bottom, right] | one number for all four, verticalJustification?: top|center|bottom|justify, vjParagraphSpacingLimit?, ignoreWrap?} | null (Text Frame General Options, named as `document.inspect` names text frame options; lengths 0–15552 pt; other keys are refused; a frame keeps its baseline, auto-size and column rule settings; null: the style sets none), effects?: {…} | null (the style no longer sets effects), dropShadow?, feather?, innerShadow?, outerGlow?, innerGlow?, bevel?, satin?, gradientFeather?, directionalFeather?} — effect objects merge field by field into the style's effects (fields as in `document.inspect` item effects); values that aren't finite numbers are refused, the others clamped (opacities 0–1, sizes and distances ≥ 0); items using the style whose effects (fill, stroke, corners) match the style's (an item's defaults, when the style set none) take the new ones, items with their own keep them; the text of frames still in the old paragraph styles takes the new ones → {name, updated, restyled}",
has_doc,
edit_object
),
+38 -8
View File
@@ -131,7 +131,9 @@ fn merge_json(base: &mut Value, top: Value, depth: usize) {
pub(super) fn create_object(s: &mut Session, p: &Value) -> Result<Value> {
let base = str_param(p, "name").unwrap_or("Object Style 1").to_string();
let from_sel = p.get("fromSelection").and_then(Value::as_bool).unwrap_or(true);
const CREATE: &str = "style.object.create";
check_swatches(CREATE, s, p)?;
let from_sel = bool_param(CREATE, p, "fromSelection")?.unwrap_or(true);
let src = if from_sel { s.doc()?.selection.items.first().and_then(|i| s.doc().ok()?.doc.item(*i).cloned()) } else { None };
let mut os = ObjectStyle::default();
if let Some(it) = &src {
@@ -141,7 +143,6 @@ pub(super) fn create_object(s: &mut Session, p: &Value) -> Result<Value> {
os.text_frame = it.text_frame().map(|t| t.options.clone());
os.effects = Some(it.effects.clone());
}
const CREATE: &str = "style.object.create";
if let Some(f) = fill_param(CREATE, p, os.fill.as_ref())? {
os.fill = f;
}
@@ -171,6 +172,23 @@ pub(super) fn create_object(s: &mut Session, p: &Value) -> Result<Value> {
})
}
/// The swatches this call gives (`fill` as a name or `fill.swatch`, `stroke.swatch` and
/// `stroke.gapSwatch`) must be swatches of the document.
fn check_swatches(cmd: &str, s: &Session, p: &Value) -> Result<()> {
let doc = &s.doc()?.doc;
let given = [
("fill", p.get("fill").and_then(|f| f.as_str().or_else(|| f.get("swatch").and_then(Value::as_str)))),
("stroke", p.get("stroke").and_then(|f| f.get("swatch")).and_then(Value::as_str)),
("stroke gap", p.get("stroke").and_then(|f| f.get("gapSwatch")).and_then(Value::as_str)),
];
for (key, name) in given {
if let Some(name) = name {
crate::cmd::object::known_swatch(doc, name, cmd, key)?;
}
}
Ok(())
}
/// `paragraphStyle`: a paragraph style's name, or null (the object style sets none).
fn paragraph_style_param(cmd: &str, s: &Session, p: &Value) -> Result<Option<Option<String>>> {
match p.get("paragraphStyle") {
@@ -200,7 +218,7 @@ fn text_frame_param(
o.insert("inset".into(), json!([n, n, n, n]));
}
}
let tf = group_param(cmd, &p, "textFrame", base.unwrap_or(&designcraft_doc::TextFrameOptions::default()))?;
let tf = group_param(cmd, &p, "textFrame", base.unwrap_or(&designcraft_doc::TextFrameOptions::default()), &[])?;
if let Some(Some(o)) = &tf {
// Only the values given in this call are checked.
let given = |k: &str| p.get("textFrame").is_some_and(|t| t.get(k).is_some());
@@ -228,7 +246,8 @@ fn bool_param(cmd: &str, p: &Value, key: &str) -> Result<Option<bool>> {
/// `p.stroke` merged over `base` (as [`group_param`]), refused when the weight isn't a number ≥ 0
/// or the tint isn't 0–1.
fn stroke_param(cmd: &str, p: &Value, base: Option<&designcraft_doc::Stroke>) -> Result<Option<Option<designcraft_doc::Stroke>>> {
let st = group_param(cmd, p, "stroke", base.unwrap_or(&designcraft_doc::Stroke::default()))?;
let optional = ["gapOverprint", "gradientAngle", "gradientVector"];
let st = group_param(cmd, p, "stroke", base.unwrap_or(&designcraft_doc::Stroke::default()), &optional)?;
if let Some(Some(st)) = &st {
if !(st.weight.is_finite() && st.weight >= 0.0) {
return Err(bad(cmd, "stroke weight must be a number ≥ 0"));
@@ -241,13 +260,23 @@ fn stroke_param(cmd: &str, p: &Value, base: Option<&designcraft_doc::Stroke>) ->
}
/// `p[key]`, an attribute group of a style, merged field by field over `base`: `None` when not
/// given, `Some(None)` for null (the style no longer sets it).
fn group_param<T: serde::Serialize + serde::de::DeserializeOwned>(cmd: &str, p: &Value, key: &str, base: &T) -> Result<Option<Option<T>>> {
/// given, `Some(None)` for null (the style no longer sets it). A field `base` doesn't write and
/// `optional` doesn't name is refused.
fn group_param<T: serde::Serialize + serde::de::DeserializeOwned>(
cmd: &str,
p: &Value,
key: &str,
base: &T,
optional: &[&str],
) -> Result<Option<Option<T>>> {
match p.get(key) {
None => Ok(None),
Some(Value::Null) => Ok(Some(None)),
Some(v @ Value::Object(_)) => {
Some(v @ Value::Object(given)) => {
let mut cur = serde_json::to_value(base).map_err(|e| bad(cmd, e.to_string()))?;
if let Some(k) = given.keys().find(|k| cur.get(k.as_str()).is_none() && !optional.contains(&k.as_str())) {
return Err(bad(cmd, format!("{key}.{k} is not a {key} setting")));
}
merge_json(&mut cur, v.clone(), 0);
serde_json::from_value(cur).map(|t| Some(Some(t))).map_err(|e| bad(cmd, format!("{key}: {e}")))
}
@@ -260,7 +289,7 @@ fn fill_param(cmd: &str, p: &Value, base: Option<&designcraft_doc::Fill>) -> Res
if let Some(sw) = p.get("fill").and_then(Value::as_str) {
return Ok(Some(Some(designcraft_doc::Fill::swatch(sw))));
}
let fill = group_param(cmd, p, "fill", base.unwrap_or(&designcraft_doc::Fill::default()))?;
let fill = group_param(cmd, p, "fill", base.unwrap_or(&designcraft_doc::Fill::default()), &["gradientAngle", "gradientVector"])?;
if let Some(Some(f)) = &fill
&& !(f.tint.is_finite() && (0.0..=1.0).contains(&f.tint))
{
@@ -307,6 +336,7 @@ pub(super) fn edit_object(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad(CMD, format!("`{name}` can't be edited")));
}
let old = s.doc()?.doc.styles.object_style(&name).cloned().ok_or_else(|| bad(CMD, format!("no object style `{name}`")))?;
check_swatches(CMD, s, p)?;
let mut new = old.clone();
if p.get("effects").is_some_and(Value::is_null) {
if EFFECT_KEYS.iter().any(|k| p.get(k).is_some()) {
+15
View File
@@ -792,6 +792,21 @@ fn object_styles_carry_corners_and_their_stroke_and_corners_can_be_edited() {
assert!(s.execute("style.object.edit", &json!({"name": "Framed", "rename": "[Basic Text Frame]"})).is_err());
assert!(s.execute("style.object.edit", &json!({"name": "[Basic Graphics Frame]", "rename": "Mine"})).is_err());
assert!(s.execute("style.object.edit", &json!({"name": "Framed", "corners": {"size": "big"}})).is_err());
// An unknown swatch, a misspelled setting and a word for a switch are refused.
for bad in [
json!({"fill": "Nope"}),
json!({"fill": {"swatch": "Nope"}}),
json!({"stroke": {"swatch": "Nope"}}),
json!({"stroke": {"wieght": 5}}),
json!({"fill": {"tnt": 0.5}}),
] {
let mut p = bad.clone();
p["name"] = json!("Framed");
assert!(s.execute("style.object.edit", &p).is_err(), "{bad}");
p["name"] = json!("Other");
assert!(s.execute("style.object.create", &p).is_err(), "{bad}");
}
assert!(s.execute("style.object.create", &json!({"name": "Other", "fromSelection": "yes"})).is_err());
// A size outside 0–10000 pt is refused, not changed.
for size in [-1.0, 10_001.0] {
assert!(s.execute("style.object.edit", &json!({"name": "Framed", "corners": {"size": size}})).is_err(), "{size}");