mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
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:
1 parent
16d60dc03a
commit
4973d11569
3 files changed
+55
-10
No files matched your search
@@ -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
|
||||
),
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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}");
|
||||
|
||||
Reference in new issue
Block a user