diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs index 85e4755..d1bb82d 100644 --- a/crates/engine/src/cmd/mod.rs +++ b/crates/engine/src/cmd/mod.rs @@ -230,6 +230,12 @@ pub(crate) fn bool_or(p: &Value, key: &str, default: bool) -> bool { pub(crate) fn str_param<'a>(p: &'a Value, key: &str) -> Option<&'a str> { p.get(key).and_then(Value::as_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. +pub(crate) fn text_param(p: &Value, key: &str) -> String { + str_param(p, key).unwrap_or("").replace("\r\n", "\n").replace('\r', "\n") +} 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 53915d9..489438a 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -795,7 +795,7 @@ fn frame_create(s: &mut Session, p: &Value) -> Result { let content = str_param(p, "content").unwrap_or("graphic"); let sr = spread_param(p, "spread"); let lid = s.doc()?.active_layer; - let text = str_param(p, "text").unwrap_or("").to_string(); + let text = super::text_param(p, "text"); let caret = bool_or(p, "caret", content == "text"); let vertical = bool_or(p, "vertical", false); let sides = p.get("sides").and_then(Value::as_u64).map_or(s.prefs.polygon_sides, |v| v as u32).clamp(3, 100); @@ -2369,3 +2369,24 @@ mod gap_tests { assert!(s.execute("gap.move", &json!({"at": [150, 200], "delta": 5})).is_err(), "inside an object"); } } + +#[cfg(test)] +mod line_end_tests { + use super::*; + + /// "One\rTwo" made one paragraph (the CR stayed in the text) instead of two. + #[test] + fn carriage_returns_separate_paragraphs() { + 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\rTwo\r\nThree"})).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, "One\nTwo\nThree"); + assert_eq!(story(&s).paras.len(), 3); + // The caret is at the end: typed text gets the same treatment. + s.execute("text.insert", &json!({"text": "\rFour", "raw": true})).unwrap(); + assert_eq!(story(&s).text, "One\nTwo\nThree\nFour"); + assert_eq!(story(&s).paras.len(), 4); + } +} diff --git a/crates/engine/src/cmd/text.rs b/crates/engine/src/cmd/text.rs index 4ac0348..a3cb09c 100644 --- a/crates/engine/src/cmd/text.rs +++ b/crates/engine/src/cmd/text.rs @@ -327,7 +327,7 @@ fn release(s: &mut Session, p: &Value) -> Result { } fn insert(s: &mut Session, p: &Value) -> Result { - let text = str_param(p, "text").unwrap_or("").to_string(); + let text = super::text_param(p, "text"); // Tab in a table cell moves to the next cell (Shift-Tab: `table.prevCell`). if text == "\t" && s.doc()?.selection.text.is_some_and(|t| t.cell.is_some()) { return super::table::step_cell(s, true);