From 79b871b94fe03052a5f00eb10085ee2f31523a70 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:18:35 +0000 Subject: [PATCH] Strip story marker characters from placed plain text files Found while testing crates/textimport/src/lib.rs:140 (plain) and crates/engine/src/cmd/place_text.rs:372 (place_text) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/links.rs | 10 ++++++++++ crates/textimport/src/lib.rs | 11 ++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/links.rs b/crates/engine/src/cmd/links.rs index 7a6d3a001..9c19a9a10 100644 --- a/crates/engine/src/cmd/links.rs +++ b/crates/engine/src/cmd/links.rs @@ -1092,6 +1092,16 @@ mod text_link_tests { let _ = std::fs::remove_dir_all(&dir); } + #[test] + fn placed_plain_text_drops_story_markers() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let b64 = crate::cmd::base64_encode("a\u{E009}b\u{E000}c\u{E002}d".as_bytes()); + let r = s.execute("file.place", &json!({"base64": b64, "name": "n.txt", "rect": [72, 72, 300, 200]})).unwrap(); + let st = s.doc().unwrap().doc.story(StoryId(r["story"].as_u64().unwrap())).unwrap(); + assert_eq!(st.text, "abcd"); + } + #[test] fn create_links_when_placing_text_files_preference() { let dir = std::env::temp_dir().join(format!("dc-text-link-pref-{}", std::process::id())); diff --git a/crates/textimport/src/lib.rs b/crates/textimport/src/lib.rs index 56eac1a00..eb4189156 100644 --- a/crates/textimport/src/lib.rs +++ b/crates/textimport/src/lib.rs @@ -117,9 +117,12 @@ pub fn import(name: &str, bytes: &[u8]) -> Result { Ok(plain(&tagged::decode(bytes))) } -/// Plain text: line breaks become paragraphs (CRLF / CR normalised). +/// Plain text: line breaks become paragraphs (CRLF / CR normalised). The private-use characters +/// (U+E000–U+E1FF) that stories use as markers are dropped, since a stray one would be read as a +/// marker without its payload. pub fn plain(text: &str) -> Imported { let t = text.trim_start_matches('\u{FEFF}').replace("\r\n", "\n").replace('\r', "\n"); + let t: String = t.chars().filter(|c| !('\u{E000}'..='\u{E1FF}').contains(c)).collect(); let story = Story::with_text(designcraft_doc::StoryId(0), &t, Default::default()); Imported { story, para_styles: vec![], char_styles: vec![], warnings: vec![] } } @@ -137,6 +140,12 @@ mod tests { assert!(matches!(import("x.doc", b"\xD0\xCF"), Err(ImportError::Unsupported(_)))); } + #[test] + fn plain_text_drops_story_markers() { + let i = import("a.txt", "a\u{E009}b\u{E000}c\u{E002}d".as_bytes()).unwrap(); + assert_eq!(i.story.text, "abcd"); + } + #[test] fn plain_text_utf16() { let mut le = vec![0xFF, 0xFE];