From 5de0a0b806246fe5b783d4e5ec8ac1dfc60a08b6 Mon Sep 17 00:00:00 2001 From: Trevor Brown Date: Mon, 2 Nov 2026 04:13:40 -0500 Subject: [PATCH] M3: variables.define refuses a text that is neither a string, a number nor a boolean (#2093) Co-Authored-By: Claude Fable 5.1 --- crates/engine/src/cmd/variables.rs | 11 +++++++++-- crates/engine/src/cmd/variables/tests.rs | 6 ++++++ docs/ui-parity.md | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/cmd/variables.rs b/crates/engine/src/cmd/variables.rs index 3b85c18df..33c3c8821 100644 --- a/crates/engine/src/cmd/variables.rs +++ b/crates/engine/src/cmd/variables.rs @@ -28,7 +28,7 @@ pub fn specs() -> Vec { "Define Text Variable…", ["Type", "Text Variables"], None, - "{name, type: custom|lastPageNumber|chapterNumber|fileName|creationDate|modificationDate|outputDate|runningHeader, text? (a string; a number is shown as its digits), format?, style?, use?: firstOnPage|lastOnPage, character?, section?, extension?, before?, after?} — creates or replaces by name", + "{name, type: custom|lastPageNumber|chapterNumber|fileName|creationDate|modificationDate|outputDate|runningHeader, text? (a string; a number or a boolean is shown as typed, anything else is refused), format?, style?, use?: firstOnPage|lastOnPage, character?, section?, extension?, before?, after?} — creates or replaces by name", has_doc, define ), @@ -407,7 +407,14 @@ fn define(s: &mut Session, p: &Value) -> Result { let name = str_param(p, "name").ok_or_else(|| bad("variables.define", "missing name"))?.to_string(); let fmt = || str_param(p, "format").unwrap_or("MM/dd/yy").to_string(); let kind = match str_param(p, "type").unwrap_or("custom") { - "custom" => VarKind::Custom { text: text_param(p, "text").unwrap_or_default() }, + "custom" => VarKind::Custom { + // No `text` is empty text; an object or an array is refused rather than stored as + // empty text in place of what a variable of that name holds. + text: match p.get("text") { + None | Some(Value::Null) => String::new(), + Some(_) => text_param(p, "text").ok_or_else(|| bad("variables.define", "`text` must be a string, a number or a boolean"))?, + }, + }, "lastPageNumber" => VarKind::LastPageNumber { section: p.get("section").and_then(Value::as_bool).unwrap_or(false) }, "chapterNumber" => VarKind::ChapterNumber, "fileName" => VarKind::FileName { extension: p.get("extension").and_then(Value::as_bool).unwrap_or(false) }, diff --git a/crates/engine/src/cmd/variables/tests.rs b/crates/engine/src/cmd/variables/tests.rs index 6345a11a4..6a2291ccb 100644 --- a/crates/engine/src/cmd/variables/tests.rs +++ b/crates/engine/src/cmd/variables/tests.rs @@ -444,9 +444,15 @@ fn a_custom_variable_defined_with_a_number_shows_its_digits() { s.execute("variables.define", &json!({"name": "Year", "type": "custom", "text": 2026})).unwrap(); s.execute("variables.define", &json!({"name": "Half", "type": "custom", "text": 0.5})).unwrap(); s.execute("variables.define", &json!({"name": "Word", "type": "custom", "text": "year"})).unwrap(); + s.execute("variables.define", &json!({"name": "Flag", "type": "custom", "text": true})).unwrap(); + // Anything else is refused, and a variable of that name keeps its text; no text is empty text. + assert!(s.execute("variables.define", &json!({"name": "Year", "type": "custom", "text": {"y": 2026}})).is_err()); + assert!(s.execute("variables.define", &json!({"name": "Year", "type": "custom", "text": [2026]})).is_err()); + s.execute("variables.define", &json!({"name": "Blank", "type": "custom"})).unwrap(); let list = s.execute("variables.list", &json!({})).unwrap(); let value = |name: &str| list.as_array().unwrap().iter().find(|v| v["name"] == name).unwrap()["value"].as_str().unwrap().to_string(); assert_eq!((value("Year"), value("Half"), value("Word")), ("2026".to_string(), "0.5".to_string(), "year".to_string())); + assert_eq!((value("Flag"), value("Blank")), ("true".to_string(), String::new())); // Inserted into a story, the instance composes as four digits. let r = s.execute("frame.create", &json!({"rect": [72, 72, 400, 300], "content": "text", "text": ""})).unwrap(); let sid = s.doc().unwrap().doc.item(designcraft_doc::ItemId(r["id"].as_u64().unwrap())).unwrap().text_frame().unwrap().story; diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 14d11a9c4..608df8059 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -48,7 +48,7 @@ basics of editing fail for too many users on Windows and Linux. | Numeric fields: units, arithmetic, scrubbing | ✓ | 21 pt spinner fields with arithmetic | ✓ | | | Colour fields: hex, picker from any swatch field | ✓ | ✓ | ~ | hex entry and picker issues (#323, #190) | | Panels: Pages, Layers, Swatches, Color, Stroke, Character, Paragraph, Styles, Links, Effects, Text Wrap, Align, Pathfinder, Table, Index, TOC, Hyperlinks, Bookmarks, Articles, Tags, Notes, Track Changes, Object States, Buttons and Forms, Preflight, Data Merge, Library, Scripts, Glyphs, Info, Tabs, Story | ✓ | all present | ~ | Navigator, Trap Presets, Animation, Timing missing; pull-down panels can't scroll on Linux (#267) | -| Dialogs: New Document, Document Setup, Preferences (all pages), Paragraph/Character/Object/Table/Cell Style Options, Export PDF, Print, Package, Find/Change | ✓ | present | ~ | a Custom text variable whose text is typed as a number shows its digits (`variables.define` takes a string or a number for `text`; #2093); generated command dialogs fill the long tail; depth varies | +| Dialogs: New Document, Document Setup, Preferences (all pages), Paragraph/Character/Object/Table/Cell Style Options, Export PDF, Print, Package, Find/Change | ✓ | present | ~ | a Custom text variable whose text is typed as a number shows its digits (`variables.define` takes a string, a number or a boolean for `text` and refuses anything else; #2093); generated command dialogs fill the long tail; depth varies | | Workspaces | Essentials, Essentials Classic, Advanced, Book, Digital Publishing, Interactive for PDF, Printing and Proofing, Review, Typography, Touch | ✓ named workspaces (Essentials Classic: Control panel and iconic panel groups) + custom; Window › Workspace; no Review or Touch | ~ | tab bar overlaps menus in Book/Advanced (#182) | | Story Editor | ✓ | ✓ | ~ | depth ruler and View › Story Editor › Show/Hide Depth Ruler (`story_editor.rs` `depth_ruler_shows_and_hides_from_the_view_menu`); hiding or resizing the style-name column missing | | Undo granularity (a drag = one step) | ✓ | ✓ (Begin/Preview/Commit) | ✓ | |