diff --git a/crates/engine/src/cmd/pathops.rs b/crates/engine/src/cmd/pathops.rs index a8306d25c..82de6fa93 100644 --- a/crates/engine/src/cmd/pathops.rs +++ b/crates/engine/src/cmd/pathops.rs @@ -221,25 +221,41 @@ pub(crate) fn len_param(p: &Value, key: &str) -> Option { } } -/// The number `p` gives `cmd` for `key`, as [`num_param`] reads it, or `None` when `p` leaves it out -/// or gives null. Any other value, such as `true` or `"wide"`, is an error: "`{key}` must be a -/// number, not {v}". +/// The number `p` gives `cmd` for `key`, or `None` when `p` leaves it out or gives null. A string +/// counts only when all of it is a number (`"20"`). Any other value, such as `true`, `"wide"` or +/// `"20abc"`, is an error: "`{key}` must be a number, not {v}". pub(crate) fn number_given(p: &Value, key: &str, cmd: &str) -> Result> { match p.get(key) { None | Some(Value::Null) => Ok(None), - Some(v) => num_param(p, key).map(Some).ok_or_else(|| bad(cmd, format!("`{key}` must be a number, not {v}"))), + Some(v) => match v { + Value::String(s) => whole_number(s), + _ => num_param(p, key), + } + .map(Some) + .ok_or_else(|| bad(cmd, format!("`{key}` must be a number, not {v}"))), } } -/// The distance `p` gives `cmd` for `key`, as [`len_param`] reads it (`"5 mm"` included), or `None` -/// when `p` leaves it out or gives null. Any other value is an error, as [`number_given`] gives it. +/// The distance `p` gives `cmd` for `key`, or `None` when `p` leaves it out or gives null. A string +/// counts only when all of it is a length that [`vectorcraft_doc::Unit::parse`] reads (`"5 mm"`) or a +/// number. Any other value, such as `"20abc"`, is an error, as [`number_given`] gives it. pub(crate) fn length_given(p: &Value, key: &str, cmd: &str) -> Result> { match p.get(key) { None | Some(Value::Null) => Ok(None), - Some(v) => len_param(p, key).map(Some).ok_or_else(|| bad(cmd, format!("`{key}` must be a number, not {v}"))), + Some(v) => match v { + Value::String(s) => vectorcraft_doc::Unit::Points.parse(s).filter(|v| v.is_finite()).or_else(|| whole_number(s)), + _ => num_param(p, key), + } + .map(Some) + .ok_or_else(|| bad(cmd, format!("`{key}` must be a number, not {v}"))), } } +/// `s` as a finite number when all of it, spaces around it aside, is one. +fn whole_number(s: &str) -> Option { + s.trim().parse::().ok().filter(|v| v.is_finite()) +} + fn ids_json(ids: &[NodeId]) -> Value { json!({ "ids": ids.iter().map(|i| i.0).collect::>() }) } diff --git a/crates/engine/src/cmd/typecmd.rs b/crates/engine/src/cmd/typecmd.rs index 42b35d33d..0ddd14594 100644 --- a/crates/engine/src/cmd/typecmd.rs +++ b/crates/engine/src/cmd/typecmd.rs @@ -8,7 +8,7 @@ use vectorcraft_doc::{AreaFit, Document, Justify, Node, NodeId, NodeKind, RubyAl use vectorcraft_geom::{Affine, PathData, Vec2}; use super::edit::selected_roots; -use super::pathops::{len_param, num_param, shape_node}; +use super::pathops::{len_param, length_given, number_given, shape_node}; use super::{ CommandSpec, EngineError, Result, Session, bad, check_numbers, cmd, color_value, f64_req, has_doc, has_selection, id_param, index, number_or, ok, str_param, switch_or, targets, text_param, @@ -810,25 +810,6 @@ pub(crate) fn justify_param(v: &str) -> Option { }) } -/// The number `p` gives `cmd` for `key`, as [`num_param`] reads it (`"20"` included), or `None` when -/// `p` leaves it out or gives null. Any other value, such as `true` or `"wide"`, is an error: -/// "`{key}` must be a number, not {v}". -fn number_given(p: &Value, key: &str, cmd: &str) -> Result> { - match p.get(key) { - None | Some(Value::Null) => Ok(None), - Some(v) => num_param(p, key).map(Some).ok_or_else(|| bad(cmd, format!("`{key}` must be a number, not {v}"))), - } -} - -/// The distance `p` gives `cmd` for `key`, as [`len_param`] reads it (`"5 mm"` included), or `None` -/// when `p` leaves it out or gives null. Any other value is an error, as [`number_given`] gives it. -fn length_given(p: &Value, key: &str, cmd: &str) -> Result> { - match p.get(key) { - None | Some(Value::Null) => Ok(None), - Some(v) => len_param(p, key).map(Some).ok_or_else(|| bad(cmd, format!("`{key}` must be a number, not {v}"))), - } -} - fn set_style(s: &mut Session, p: &Value) -> Result { const C: &str = "text.setStyle"; let font = text_param(p, "font", C)?.map(str::to_string); diff --git a/crates/engine/src/tests/pathops.rs b/crates/engine/src/tests/pathops.rs index 57094355c..72aa559ae 100644 --- a/crates/engine/src/tests/pathops.rs +++ b/crates/engine/src/tests/pathops.rs @@ -863,6 +863,8 @@ fn path_commands_refuse_a_param_of_the_wrong_type() { for (cmd, p, want) in [ ("object.path.offsetPath", json!({"offset": true}), number("offset", "true")), ("object.path.offsetPath", json!({"offset": "wide"}), number("offset", "\"wide\"")), + ("object.path.offsetPath", json!({"offset": "10abc"}), number("offset", "\"10abc\"")), + ("object.path.offsetPath", json!({"offset": 5, "miterLimit": "4x"}), number("miterLimit", "\"4x\"")), ("object.path.offsetPath", json!({"offset": 5, "joins": 3}), "`joins` must be a string".to_string()), ("object.path.offsetPath", json!({"offset": 5, "joins": "pointy"}), "`joins` must be one of miter, round, bevel".to_string()), ("object.path.offsetPath", json!({"offset": 5, "miterLimit": [4]}), number("miterLimit", "[4]")), diff --git a/crates/engine/src/tests/textedit.rs b/crates/engine/src/tests/textedit.rs index 425d545df..e584e3e32 100644 --- a/crates/engine/src/tests/textedit.rs +++ b/crates/engine/src/tests/textedit.rs @@ -505,6 +505,8 @@ fn character_ruby_and_path_handle_refuse_a_param_of_the_wrong_type() { [ ("text.setStyle", json!({"size": true}), "size"), ("text.setStyle", json!({"size": "big"}), "size"), + ("text.setStyle", json!({"size": "20abc"}), "size"), + ("text.setStyle", json!({"tracking": "20abc"}), "tracking"), ("text.setStyle", json!({"font": "Inter", "size": [12]}), "size"), ("text.setStyle", json!({"tracking": "wide"}), "tracking"), ("text.setStyle", json!({"size": 30, "tracking": false}), "tracking"),