mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 16:30:41 +00:00
Merge pull request #5537 from dotCooCoo/fix/textedit-reject-bad-offsets
Type: text commands refuse negative or fractional byte offsets
This commit is contained in:
@@ -89,12 +89,6 @@ fn text_tweaker(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut rng = Rng::new(whole_or(p, "seed", 0, C)?);
|
||||
let skip = switch_or(p, "skipSpaces", true, C)?;
|
||||
let setup = s.doc()?.doc.setup.clone();
|
||||
// A range given as anything but whole bytes is refused, not read as all of the text.
|
||||
for k in ["start", "end"] {
|
||||
if p.get(k).is_some_and(|v| !v.is_null() && v.as_u64().is_none()) {
|
||||
return Err(bad(C, format!("`{k}` must be a whole number of bytes")));
|
||||
}
|
||||
}
|
||||
// At a caret, all of the object's text.
|
||||
let ranges = type_ranges(s, p, C, |text, _| Some((0, text.len())))?;
|
||||
// Each object's changed characters: (byte, its change), worked out before the edit.
|
||||
|
||||
@@ -196,7 +196,7 @@ fn bad_params_are_refused_without_a_change() {
|
||||
json!({"ids": [999_999]}),
|
||||
json!({"id": id.0, "start": -1}),
|
||||
json!({"id": id.0, "start": 1, "end": 1.5}),
|
||||
json!({"end": "2"}),
|
||||
json!({"id": id.0, "end": "2"}),
|
||||
] {
|
||||
assert!(s.execute(C, &p).is_err(), "{p}");
|
||||
assert_eq!(s.doc().unwrap().doc, before, "{p}");
|
||||
|
||||
@@ -7,6 +7,7 @@ use vectorcraft_doc::{Appearance, CharStyle, Node, NodeId, NodeKind, TextKind, T
|
||||
use vectorcraft_geom::{Affine, Point, Vec2};
|
||||
use vectorcraft_text::edit;
|
||||
|
||||
use super::menucmds::whole_or;
|
||||
use super::stroke::StrokeChange;
|
||||
use super::typecmd::refresh_bounds;
|
||||
use super::{
|
||||
@@ -233,12 +234,21 @@ fn byte_param(p: &Value, k: &str) -> Option<usize> {
|
||||
p.get(k).and_then(Value::as_u64).map(index)
|
||||
}
|
||||
|
||||
/// The byte offset that `p` gives for `k`, or `default` when `p` leaves it out or gives null. Any
|
||||
/// other value that is not a whole number of at least 0, such as `"2"`, `-1` or `1.5`, is an error.
|
||||
fn offset_or(p: &Value, k: &str, default: usize, cmd: &str) -> Result<usize> {
|
||||
match p.get(k) {
|
||||
None | Some(Value::Null) => Ok(default),
|
||||
Some(_) => Ok(index(whole_or(p, k, 0, cmd)?)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The bytes `start..end` of `p` in text of `len` bytes, clamped and ordered (without them, all of
|
||||
/// it). A `start` or `end` that is not a number or null, such as `"2"`, is an error.
|
||||
/// it). A `start` or `end` that is not a whole number of at least 0 or null, such as `"2"`, `-1`
|
||||
/// or `1.5`, is an error.
|
||||
fn range_of(p: &Value, len: usize, cmd: &str) -> Result<(usize, usize)> {
|
||||
check_numbers(p, &["start", "end"], cmd)?;
|
||||
let a = byte_param(p, "start").unwrap_or(0).min(len);
|
||||
let b = byte_param(p, "end").unwrap_or(len).min(len);
|
||||
let a = offset_or(p, "start", 0, cmd)?.min(len);
|
||||
let b = offset_or(p, "end", len, cmd)?.min(len);
|
||||
Ok((a.min(b), a.max(b)))
|
||||
}
|
||||
|
||||
@@ -310,10 +320,9 @@ fn clear_param(s: &Session, p: &Value, id: NodeId, c: &str) -> Result<Vec<NodeId
|
||||
fn start_list(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "text.startList";
|
||||
let id = id_param(p, "id").ok_or_else(|| bad(C, "missing `id`"))?;
|
||||
check_numbers(p, &["at"], C)?;
|
||||
let t = text_ref(s, id)?;
|
||||
let len = edit::runs_len(&t.runs);
|
||||
let at = byte_param(p, "at").unwrap_or(len).min(len);
|
||||
let at = offset_or(p, "at", len, C)?.min(len);
|
||||
let Some((start, list)) = t.typed_list_at(at) else {
|
||||
return Ok(json!({"id": id.0, "started": false, "caret": at}));
|
||||
};
|
||||
|
||||
@@ -1200,6 +1200,33 @@ fn start_list_command_removes_the_typed_characters_and_starts_the_list() {
|
||||
assert!(s.execute("text.startList", &json!({"at": 3})).is_err(), "id required");
|
||||
}
|
||||
|
||||
/// A byte offset given as a negative or fractional number is an error, not the end of the text
|
||||
/// (`at`) or its start or end (`start`, `end`), and the text is left as it was. Null keeps the default.
|
||||
#[test]
|
||||
fn byte_offsets_that_are_not_whole_numbers_are_refused() {
|
||||
let mut s = session();
|
||||
let id = text(&mut s, "x\n# ");
|
||||
for at in [json!(-1), json!(1.5)] {
|
||||
let e = s.execute("text.startList", &json!({"id": id.0, "at": at})).unwrap_err().to_string();
|
||||
assert!(e.contains("`at`"), "{e}");
|
||||
}
|
||||
assert_eq!(obj(&s, id).plain_text(), "x\n# ", "no list was started");
|
||||
assert_eq!(s.execute("text.startList", &json!({"id": id.0, "at": null})).unwrap()["started"], json!(true));
|
||||
let id = text(&mut s, "Hello");
|
||||
for (cmd, p, key) in [
|
||||
("text.getRange", json!({"id": id.0, "start": -1, "end": 2}), "start"),
|
||||
("text.getRange", json!({"id": id.0, "start": 1, "end": 2.5}), "end"),
|
||||
("text.editRange", json!({"id": id.0, "start": 1.5, "end": 2, "insert": "!"}), "start"),
|
||||
("text.editRange", json!({"id": id.0, "start": 1, "end": -2, "insert": "!"}), "end"),
|
||||
("text.setRangeStyle", json!({"id": id.0, "start": 0.5, "size": 30}), "start"),
|
||||
] {
|
||||
let e = s.execute(cmd, &p).unwrap_err().to_string();
|
||||
assert!(e.contains(&format!("`{key}`")), "{cmd} {p}: {e}");
|
||||
}
|
||||
assert_eq!(obj(&s, id).plain_text(), "Hello");
|
||||
assert_eq!(s.execute("text.getRange", &json!({"id": id.0, "start": 1.0, "end": null})).unwrap()["text"], json!("ello"));
|
||||
}
|
||||
|
||||
/// #1881: a Cmd/Ctrl drag while typing (the Selection tool lent to the Type tool) moves the type
|
||||
/// and keeps what was typed, and typing goes on after it.
|
||||
#[test]
|
||||
|
||||
@@ -1207,6 +1207,11 @@ and `amount` in `color.harmony`; `angle` in `paint.editGradient` and in the `gra
|
||||
`width` and `tolerance` in `livePaint.strokeEdge`. A null is read as a key left out. A gradient stop that names a
|
||||
swatch refuses a `tint` that is not a number with `` stop 0: `tint` must be a number ``.
|
||||
|
||||
A byte offset must be a whole number of at least 0. `at` in `text.startList`, and `start` and `end` in
|
||||
`text.editRange`, `text.getRange`, `text.setRangeStyle` and the `type.step`, `type.size.*`, `type.resetTracking`,
|
||||
`type.resetHorizontalScale` and `addon.type.textTweaker` commands given an `id`, refuse a negative or fractional number with
|
||||
`` `at` must be a whole number ``, naming the key, and change nothing. `2.0` is read as `2`, and a null as the key left out.
|
||||
|
||||
A step can use an earlier step's result, in `run` as in the `command_batch` tool. A parameter string that is exactly
|
||||
`$N` or `$N.path` takes that value from the result of step `N` (counting from 0, in the order they ran; `run --in`'s
|
||||
open isn't a step), and `$last…` from the step before. A path goes into objects by key and arrays by index:
|
||||
|
||||
Reference in New Issue
Block a user