diff --git a/crates/engine/src/session/view.rs b/crates/engine/src/session/view.rs index d581fc81bd..afb38f3d65 100644 --- a/crates/engine/src/session/view.rs +++ b/crates/engine/src/session/view.rs @@ -74,7 +74,25 @@ impl Session { /// stamps' "By" line has them. Cut to [`MAX_STAMP_LINE`](crate::MAX_STAMP_LINE) characters. pub fn stamp_line(&self, template: &str, author: &str) -> String { let (date, time) = self.stamp_date_time(); - let filled = template.trim().replace("{name}", author.trim()).replace("{date}", &date).replace("{time}", &time); + // One pass over the template, so a placeholder the author's name happens to contain + // (`Ada {date}`) stays as written rather than being filled in too. + let mut filled = String::new(); + let mut rest = template.trim(); + while let Some(c) = rest.chars().next() { + if let Some(after) = rest.strip_prefix("{name}") { + filled.push_str(author.trim()); + rest = after; + } else if let Some(after) = rest.strip_prefix("{date}") { + filled.push_str(&date); + rest = after; + } else if let Some(after) = rest.strip_prefix("{time}") { + filled.push_str(&time); + rest = after; + } else { + filled.push(c); + rest = rest.get(c.len_utf8()..).unwrap_or_default(); + } + } filled.chars().take(crate::MAX_STAMP_LINE).collect() } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 8891fe18c3..fc22d78d0a 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -9420,6 +9420,9 @@ fn repeated_backdrop_artwork_survives_background_undo_redo_and_both_save_modes() fn stamp_lines_fill_in_the_name_date_and_time() { let s = Session::new().with_clock(|| 1_700_000_000); assert_eq!(s.stamp_line("Received {date} {time} by {name}", " Ada "), "Received Nov 14, 2023 10:13 pm by Ada"); + // Review on #2345 (gabglus): placeholders inside the author's name are kept as written. + assert_eq!(s.stamp_line("By {name} — {date} {time}", "Ada {date} {time}"), "By Ada {date} {time} — Nov 14, 2023 10:13 pm"); + assert_eq!(s.stamp_line("{name}{name}", "{name}"), "{name}{name}"); assert_eq!(s.stamp_by_line("Ada"), "By Ada at 10:13 pm, Nov 14, 2023"); assert_eq!(s.stamp_by_line(""), "at 10:13 pm, Nov 14, 2023"); assert_eq!(s.stamp_line(&"{date}".repeat(100), "Ada").chars().count(), MAX_STAMP_LINE); diff --git a/crates/ui-egui/tests/it/stamps.rs b/crates/ui-egui/tests/it/stamps.rs index c161488778..1cea1cf638 100644 --- a/crates/ui-egui/tests/it/stamps.rs +++ b/crates/ui-egui/tests/it/stamps.rs @@ -177,6 +177,21 @@ fn a_dynamic_custom_stamp_keeps_its_line_and_fills_it_in_when_placed() { h.drop_at(p); h.run_steps(4); let s = h.state(); + let saved = s.session.save_bytes(s.views[0].id).unwrap(); + let pdf = pdfcraft_cos::Document::open(saved).unwrap(); + let line = pdf + .scan_objects() + .find_map(|(_, object)| match object.as_ref() { + pdfcraft_cos::Object::Dict(dict) => dict.get(b"PCByLine").and_then(pdfcraft_cos::Object::as_string).map(pdfcraft_cos::PdfString::to_text), + _ => None, + }) + .expect("placed custom stamp stores PCByLine in the PDF"); + assert!(line.starts_with("By Ada at "), "expanded stamp line: {line:?}"); + assert!( + !line.contains("{name}") && !line.contains("{date}") && !line.contains("{time}"), + "the saved stamp line must not contain unexpanded placeholders: {line:?}" + ); + let doc = s.session.get(s.views[0].id).unwrap(); let r = doc.info.annotations.iter().find(|a| a.subtype == "Stamp").expect("placed").rect; let aspect = (r[2] - r[0]) / (r[3] - r[1]);