diff --git a/crates/idml/src/names.rs b/crates/idml/src/names.rs index 569244470..b9102c9ae 100644 --- a/crates/idml/src/names.rs +++ b/crates/idml/src/names.rs @@ -144,16 +144,37 @@ pub const TABLE_BUILTINS: &[(&str, &str)] = &[("[Basic Table]", "$ID/[Basic Tabl pub const OBJECT_BUILTINS: &[(&str, &str)] = &[("[None]", "$ID/[None]"), ("[Basic Graphics Frame]", "$ID/[Normal Graphics Frame]"), ("[Basic Text Frame]", "$ID/[Normal Text Frame]")]; -/// Our style name → IDML `Name` (groups: `A/B` → `A:B`). +/// Our style name → IDML `Name` (groups: `A/B` → `A:B`; a literal `:` or `\` is escaped as `\:` +/// or `\\`, so it is not read back as a group separator). pub fn style_name_out(builtins: &[(&str, &str)], name: &str) -> String { - builtins.iter().find(|(o, _)| *o == name).map(|(_, i)| i.to_string()).unwrap_or_else(|| name.replace('/', ":")) + builtins + .iter() + .find(|(o, _)| *o == name) + .map(|(_, i)| i.to_string()) + .unwrap_or_else(|| name.replace('\\', "\\\\").replace(':', "\\:").replace('/', ":")) } /// IDML `Name` → our style name. pub fn style_name_in(builtins: &[(&str, &str)], name: &str) -> String { if let Some((o, _)) = builtins.iter().find(|(_, i)| *i == name) { return o.to_string(); } - name.strip_prefix("$ID/").unwrap_or(name).replace(':', "/") + let name = name.strip_prefix("$ID/").unwrap_or(name); + let mut out = String::with_capacity(name.len()); + let mut chars = name.chars(); + while let Some(c) = chars.next() { + match c { + '\\' => match chars.clone().next() { + Some(next @ ('\\' | ':')) => { + out.push(next); + chars.next(); + } + _ => out.push('\\'), + }, + ':' => out.push('/'), + _ => out.push(c), + } + } + out } /// `ParagraphStyle/…` Self id for one of our names. pub fn style_self(kind: &str, builtins: &[(&str, &str)], name: &str) -> String { diff --git a/crates/idml/src/tests.rs b/crates/idml/src/tests.rs index 37fb2af50..d7106ad48 100644 --- a/crates/idml/src/tests.rs +++ b/crates/idml/src/tests.rs @@ -4368,3 +4368,42 @@ fn cell_first_baseline_comes_from_cell_styles_table_regions_and_cells_and_round_ let s2 = back.styles.cell.iter().find(|c| c.name == "Cap").unwrap(); assert_eq!(s2.first_baseline, Some(FirstBaseline::CapHeight)); } + +#[test] +fn style_names_with_a_literal_colon_survive_a_round_trip() { + use designcraft_doc::styles::{CellStyle, CharacterStyle, ObjectStyle, ParagraphStyle, TableStyle}; + let names = ["Title: Big", "Group/Child: X", "Back\\slash: \\:"]; + let mut d = small_doc(); + for n in names { + let st = d.styles_mut(); + st.paragraph.push(ParagraphStyle { + name: n.into(), + based_on: None, + next_style: None, + para: Default::default(), + chars: Default::default(), + shortcut: String::new(), + }); + st.character.push(CharacterStyle { name: n.into(), based_on: None, chars: Default::default(), shortcut: String::new() }); + st.cell.push(CellStyle { name: n.into(), ..Default::default() }); + st.table.push(TableStyle { name: n.into(), ..Default::default() }); + st.object.push(ObjectStyle { name: n.into(), ..Default::default() }); + } + let back = import_idml(&export_idml(&d)).unwrap(); + for n in names { + assert!( + back.styles.paragraph.iter().any(|s| s.name == n), + "paragraph {n:?}: {:?}", + back.styles.paragraph.iter().map(|s| &s.name).collect::>() + ); + assert!(back.styles.character.iter().any(|s| s.name == n), "character {n:?}"); + assert!(back.styles.cell.iter().any(|s| s.name == n), "cell {n:?}"); + assert!(back.styles.table.iter().any(|s| s.name == n), "table {n:?}"); + assert!(back.styles.object.iter().any(|s| s.name == n), "object {n:?}"); + } + // A group is still written as an IDML `:` group path. + assert_eq!(names::style_name_out(names::PARA_BUILTINS, "Group/Child"), "Group:Child"); + for n in names { + assert_eq!(names::style_name_in(names::PARA_BUILTINS, &names::style_name_out(names::PARA_BUILTINS, n)), n); + } +}