Text: carriage returns in typed or created text start paragraphs

frame.create's text and text.insert kept CR and CR LF as characters, so text
written with InDesign's, classic Mac or Windows line ends ("One\rTwo") became
a single paragraph, and paragraph formatting (spaceAfter) never applied
between the parts. They now become \n, the paragraph separator, as edit.paste
and text import already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
japbcoelho 2026-10-05 22:44:18 -03:00 committed by Brandon Thomas
parent 316e1ffe62
commit b54fa8d9bb
3 changed files with 29 additions and 2 deletions

View File

@ -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<ItemId> {
p.get(key).and_then(Value::as_u64).map(ItemId)
}

View File

@ -795,7 +795,7 @@ fn frame_create(s: &mut Session, p: &Value) -> Result<Value> {
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);
}
}

View File

@ -327,7 +327,7 @@ fn release(s: &mut Session, p: &Value) -> Result<Value> {
}
fn insert(s: &mut Session, p: &Value) -> Result<Value> {
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);