mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 02:06:54 -06:00
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:
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user