From 21acf88a80fd90216c1034e621fc221305847dfd Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Mon, 26 Oct 2026 05:55:05 +0000 Subject: [PATCH] Escape font family and typeface values in Tagged Text export Found while testing crates/textimport/src/tagged.rs:~93-100 (tagged_text, the `` and `` writes) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/textimport/src/tagged.rs | 48 ++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/crates/textimport/src/tagged.rs b/crates/textimport/src/tagged.rs index 74806ae4c..12f8919fc 100644 --- a/crates/textimport/src/tagged.rs +++ b/crates/textimport/src/tagged.rs @@ -56,9 +56,18 @@ fn unescape_value(v: &str) -> String { out } +/// The first format version whose `cFont` and `cTypeface` values are escaped like style names. +/// Older files hold them raw, so a backslash in them is literal. +const ESCAPED_FONTS_VERSION: u32 = 8; + +/// A `cFont` or `cTypeface` value: decoded in files that escape it, literal in older ones. +fn font_value(v: &str, escaped: bool) -> String { + if escaped { unescape_value(v) } else { v.to_string() } +} + /// `story` as Tagged Text. pub fn tagged_text(doc: &Document, story: &Story) -> String { - let mut out = String::from("\r\n\r\n"); + let mut out = String::from("\r\n\r\n"); let ranges = story.para_ranges(); for (i, r) in ranges.iter().enumerate() { if i > 0 { @@ -80,11 +89,11 @@ pub fn tagged_text(doc: &Document, story: &Story) -> String { open.push("CharStyle"); } if let Some(v) = &f.over.font_family { - let _ = write!(out, ""); + let _ = write!(out, "", escape_value(v)); open.push("cFont"); } if let Some(v) = &f.over.font_style { - let _ = write!(out, ""); + let _ = write!(out, "", escape_value(v)); open.push("cTypeface"); } if let Some(v) = f.over.size { @@ -142,6 +151,7 @@ pub fn import(bytes: &[u8]) -> Result { _ => runs.push((c.to_string(), fmt.clone())), } }; + let mut escaped_fonts = false; let mut units = crate::Utf16Units::default(); let mut i = 0; let mut skipped = 0; @@ -199,8 +209,8 @@ pub fn import(bytes: &[u8]) -> Result { char_styles.push(v); } } - "cFont" => fmt.over.font_family = (!value.is_empty()).then(|| value.to_string()), - "cTypeface" => fmt.over.font_style = (!value.is_empty()).then(|| value.to_string()), + "cFont" => fmt.over.font_family = (!value.is_empty()).then(|| font_value(value, escaped_fonts)), + "cTypeface" => fmt.over.font_style = (!value.is_empty()).then(|| font_value(value, escaped_fonts)), // A size the engine cannot mean (0, negative, 1e300) is dropped, so the text // keeps its style's size (#1593). "cSize" => fmt.over.size = value.trim().parse().ok().and_then(designcraft_doc::attrs::imported_type_size), @@ -211,7 +221,8 @@ pub fn import(bytes: &[u8]) -> Result { list.push(nm); } } - n if n.starts_with("ASCII-") || n.starts_with("UNICODE-") || n.starts_with("ANSI-") || n == "Version" => {} + "Version" => escaped_fonts = value.trim().parse::().is_ok_and(|v| v >= ESCAPED_FONTS_VERSION), + n if n.starts_with("ASCII-") || n.starts_with("UNICODE-") || n.starts_with("ANSI-") => {} _ => skipped += 1, } } @@ -338,6 +349,31 @@ mod tests { } } + #[test] + fn font_values_with_tag_characters_round_trip() { + let doc = Document::new(&designcraft_doc::build::NewDocument::default()); + for name in ["Weird>Font", "Ac", "x\\y", "end\\"] { + let mut story = Story::new(StoryId(0)); + let mut f = CharFormat::default(); + f.over.font_family = Some(name.into()); + f.over.font_style = Some(name.into()); + story.insert_with(0, "hello", f); + let out = tagged_text(&doc, &story); + let back = import(out.as_bytes()).unwrap(); + assert_eq!(back.story.text, "hello", "{out}"); + assert_eq!(back.story.char_format_at(1).over.font_family.as_deref(), Some(name), "{out}"); + assert_eq!(back.story.char_format_at(1).over.font_style.as_deref(), Some(name), "{out}"); + } + } + + /// Version 7 files hold font values raw, so their backslashes stay literal. + #[test] + fn old_format_font_values_stay_literal() { + let i = import("\r\n\r\nxy".as_bytes()).unwrap(); + assert_eq!(i.story.char_format_at(1).over.font_family.as_deref(), Some("a\\b")); + assert_eq!(i.story.char_format_at(2).over.font_style.as_deref(), Some("c\\d")); + } + /// A backslash that isn't one of our escapes (older files, InDesign's `\:`) stays literal. #[test] fn unknown_backslashes_in_style_names_stay_literal() {