Merge pull request #1397 from Silvertree2010/fix/h951-tagged-text-export-strips-from-s

Keep ':', '<', '>' and '\' in Tagged Text style names
This commit is contained in:
Brandon Thomas
2026-10-23 17:28:47 -04:00
committed by GitHub
+66 -7
View File
@@ -25,6 +25,37 @@ 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 the escapes `escape_value` writes (`\\`, `\<`, `\>`) decoded. Any other
/// backslash, including a trailing one, stays literal, as older files and InDesign's `\:` expect.
fn unescape_value(v: &str) -> String {
let mut out = String::with_capacity(v.len());
let mut it = v.chars().peekable();
while let Some(c) = it.next() {
if c == '\\'
&& let Some(&n) = it.peek()
&& matches!(n, '\\' | '<' | '>')
{
out.push(n);
it.next();
} else {
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 +65,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 +76,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 +184,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 +193,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 +299,34 @@ 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]);
}
}
/// A backslash that isn't one of our escapes (older files, InDesign's `\:`) stays literal.
#[test]
fn unknown_backslashes_in_style_names_stay_literal() {
assert_eq!(unescape_value(r"x\y"), r"x\y");
assert_eq!(unescape_value(r"a\:b"), r"a\:b");
assert_eq!(unescape_value("end\\"), "end\\");
assert_eq!(unescape_value(r"\<a\>\\"), r"<a>\");
}
#[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";