Keep ':', '<', '>' and '\' in Tagged Text style names

Found while testing crates/textimport/src/tagged.rs:35 (also :52)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
NetterSilver
2026-10-20 18:02:07 +00:00
co-authored by Claude Haiku 5.5
parent 8dff7ecdb0
commit e1e9923471
+51 -7
View File
@@ -25,6 +25,31 @@ fn escape(out: &mut String, c: char) {
}
}
/// A tag value with '<', '>' and '\\' escaped by a backslash, so it keeps the tag intact.
fn escape_value(v: &str) -> String {
let mut out = String::with_capacity(v.len());
for c in v.chars() {
if matches!(c, '<' | '>' | '\\') {
out.push('\\');
}
out.push(c);
}
out
}
/// A tag value with backslash escapes removed.
fn unescape_value(v: &str) -> String {
let mut out = String::with_capacity(v.len());
let mut it = v.chars();
while let Some(c) = it.next() {
match c {
'\\' => out.extend(it.next()),
c => out.push(c),
}
}
out
}
/// `story` as Tagged Text.
pub fn tagged_text(doc: &Document, story: &Story) -> String {
let mut out = String::from("<ASCII-WIN>\r\n<Version:7>\r\n");
@@ -34,7 +59,7 @@ pub fn tagged_text(doc: &Document, story: &Story) -> String {
out.push_str("\r\n");
}
let pf = story.paras.get(i).cloned().unwrap_or_default();
let _ = write!(out, "<ParaStyle:{}>", pf.style.replace(['<', '>', ':'], ""));
let _ = write!(out, "<ParaStyle:{}>", escape_value(&pf.style));
for (rr, f) in story.runs() {
let (a, b) = (rr.start.max(r.start), rr.end.min(r.end));
if a >= b {
@@ -45,7 +70,7 @@ pub fn tagged_text(doc: &Document, story: &Story) -> String {
}
let mut open = Vec::new();
if f.style != NO_CHAR_STYLE {
let _ = write!(out, "<CharStyle:{}>", f.style.replace(['<', '>', ':'], ""));
let _ = write!(out, "<CharStyle:{}>", escape_value(&f.style));
open.push("CharStyle");
}
if let Some(v) = &f.over.font_family {
@@ -153,7 +178,7 @@ pub fn import(bytes: &[u8]) -> Result<Imported, ImportError> {
}
"ParaStyle" => {
started = true;
let v = value.trim().to_string();
let v = unescape_value(value.trim());
if !v.is_empty() && !para_styles.contains(&v) {
para_styles.push(v.clone());
}
@@ -162,10 +187,10 @@ pub fn import(bytes: &[u8]) -> Result<Imported, ImportError> {
}
}
"CharStyle" => {
let v = value.trim();
fmt.style = if v.is_empty() { NO_CHAR_STYLE.into() } else { v.to_string() };
if !v.is_empty() && !char_styles.contains(&v.to_string()) {
char_styles.push(v.to_string());
let v = unescape_value(value.trim());
fmt.style = if v.is_empty() { NO_CHAR_STYLE.into() } else { v.clone() };
if !v.is_empty() && !char_styles.contains(&v) {
char_styles.push(v);
}
}
"cFont" => fmt.over.font_family = (!value.is_empty()).then(|| value.to_string()),
@@ -268,6 +293,25 @@ mod tests {
assert_eq!(back.story.paras, story.paras);
}
/// Style names with ':', '<', '>' or '\' were written with those characters dropped, so
/// re-import gave the paragraphs a different style.
#[test]
fn style_names_with_tag_characters_round_trip() {
let doc = Document::new(&designcraft_doc::build::NewDocument::default());
for name in ["Body: Indent", "A<b>c", "x\\y", "end\\"] {
let mut story = Story::new(StoryId(0));
story.insert_with(0, "Text", CharFormat { style: name.into(), ..Default::default() });
story.paras[0].style = name.into();
let out = tagged_text(&doc, &story);
let back = import(out.as_bytes()).unwrap();
assert_eq!(back.story.text, "Text", "{out}");
assert_eq!(back.story.paras[0].style, name, "{out}");
assert_eq!(back.story.char_format_at(1).style, name, "{out}");
assert_eq!(back.para_styles.iter().map(|s| s.name.as_str()).collect::<Vec<_>>(), [name]);
assert_eq!(back.char_styles.iter().map(|s| s.name.as_str()).collect::<Vec<_>>(), [name]);
}
}
#[test]
fn tagged_text_round_trips() {
let src = "<ASCII-WIN>\r\n<Version:7>\r\n<ParaStyle:Head>Caf<0x00E9> \\<1\\>\r\n<ParaStyle:Body>A <CharStyle:Em>word<CharStyle:> and <cSize:18.000000>big<cSize:> text";