From ed218b9817e79cfafa3277a123de5ef03859f8e9 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Thu, 15 Oct 2026 21:45:11 +0200 Subject: [PATCH] Text parameters: a vertical tab is a forced line break frame.create, story.setText, text.insert and the other commands that take text kept U+000B as a character, drawn as a missing-glyph box. It is the forced line break of Word and PowerPoint text (Chr(11), Shift+Enter), and what people porting scripts try first, so text_param now reads it as U+2028, the forced line break, next to the CR handling it already has. script --help and docs/agents.md say so. --- apps/designcraft-cli/src/main.rs | 1 + crates/engine/src/cmd/mod.rs | 9 +++++++-- crates/engine/src/cmd/object.rs | 16 ++++++++++++++++ docs/agents.md | 2 +- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/apps/designcraft-cli/src/main.rs b/apps/designcraft-cli/src/main.rs index c4373ffb..d768bc34 100644 --- a/apps/designcraft-cli/src/main.rs +++ b/apps/designcraft-cli/src/main.rs @@ -62,6 +62,7 @@ References to earlier results (in string parameters): "$$" a literal `$` anywhere: "$$1" passes `$1` (a GREP group), "$${0.id}" passes `${0.id}`. Text parameters: "\n" starts a new paragraph, "\u2028" is a forced line break, "\t" a tab. +"\u000b" (Word's line break, Chr(11)) is read as a forced line break too. Most commands ignore parameters they do not know instead of rejecting them: check names with `designcraft-cli describe COMMAND`. diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs index 0bfb9727..9b0acfe5 100644 --- a/crates/engine/src/cmd/mod.rs +++ b/crates/engine/src/cmd/mod.rs @@ -240,9 +240,14 @@ pub(crate) fn str_param<'a>(p: &'a Value, key: &str) -> Option<&'a str> { } /// Plain text from a parameter, with CR LF and lone CR (classic Mac and InDesign line ends) as /// `\n`, the paragraph separator, as edit.paste and text import already do. Kept as they were, -/// the CRs joined the paragraphs into one. +/// the CRs joined the paragraphs into one. A vertical tab (Word's and PowerPoint's Shift+Enter, +/// Chr(11)) is a forced line break: kept, it was drawn as a missing-glyph box. pub(crate) fn text_param(p: &Value, key: &str) -> String { - str_param(p, key).unwrap_or("").replace("\r\n", "\n").replace('\r', "\n") + str_param(p, key) + .unwrap_or("") + .replace("\r\n", "\n") + .replace('\r', "\n") + .replace('\u{B}', designcraft_doc::story::FORCED_LINE_BREAK.encode_utf8(&mut [0; 4])) } pub(crate) fn id_param(p: &Value, key: &str) -> Option { p.get(key).and_then(Value::as_u64).map(ItemId) diff --git a/crates/engine/src/cmd/object.rs b/crates/engine/src/cmd/object.rs index bbc0a36a..fb014353 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -3124,6 +3124,22 @@ mod line_end_tests { s.execute("note.edit", &json!({"story": n["story"], "id": n["id"], "text": "Three\r\nFour"})).unwrap(); assert_eq!(note(&mut s), "Three\nFour"); } + + /// "One\u{B}Two" kept the vertical tab, drawn as a missing-glyph box, where Word and + /// PowerPoint text (Chr(11)) means a forced line break. + #[test] + fn vertical_tabs_are_forced_line_breaks() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let r = s.execute("frame.create", &json!({"rect": [36, 36, 300, 300], "content": "text", "text": "One\u{B}Two"})).unwrap(); + let sid = designcraft_doc::StoryId(r["story"].as_u64().unwrap()); + let story = |s: &Session| s.doc().unwrap().doc.story(sid).unwrap().clone(); + assert_eq!((story(&s).text.as_str(), story(&s).paras.len()), ("One\u{2028}Two", 1)); + s.execute("text.insert", &json!({"text": "\u{B}Three", "raw": true})).unwrap(); + assert_eq!((story(&s).text.as_str(), story(&s).paras.len()), ("One\u{2028}Two\u{2028}Three", 1)); + s.execute("story.setText", &json!({"story": sid.0, "text": "A\u{B}B\rC"})).unwrap(); + assert_eq!((story(&s).text.as_str(), story(&s).paras.len()), ("A\u{2028}B\nC", 2)); + } } #[cfg(test)] diff --git a/docs/agents.md b/docs/agents.md index b737ebb8..49ff7d5b 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -26,7 +26,7 @@ of step N's result: N is the 0-based step index (comments and blank lines are no step) and the path goes through result fields and array indices (`"$1.story"`, `"$3.ids.0"`); `"${N.path}"` interpolates inside longer strings. Any other string starting with `$` stays as written (`"$1-$2"`, `"$5 off"`), and `$$` writes a literal `$` anywhere (`"$$1"` passes `$1`, a GREP group). In text parameters `\n` starts a new paragraph and `\u2028` is a forced line -break. Most commands ignore parameters they don't know instead of rejecting them, so check names with `describe`. +break (`\u000b`, Word's line break, is read as one too). Most commands ignore parameters they don't know instead of rejecting them, so check names with `describe`. `designcraft-cli script --help` prints this reference. ```sh