M6: a custom stamp's line fills in the template's placeholders only (#1741)

Review on #2345 (gabglus): the line was filled by chained replaces, so a
placeholder inside the author's name ("Ada {date}") was filled in too.
stamp_line now walks the template once and expands only the {name},
{date} and {time} it finds there; the author's text is kept as written.
Trimming and the MAX_STAMP_LINE cut are unchanged. The loop advances by
whole characters (strip_prefix, or get() past the next char), so it
cannot panic.

stamp_lines_fill_in_the_name_date_and_time gains the review's case and
a name made only of a placeholder; both fail with the chained replaces.
The UI test checks the saved /PCByLine has no placeholder left (from
gabglus's commit f6760b37).

Co-Authored-By: gabglus <gabglus@hotmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed 2026-10-31 13:52:29 +08:00
1 parent 00566d74ea
commit f5575cf782
3 files changed
+37 -1

No files matched your search

+19 -1
View File
@@ -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()
}
+3
View File
@@ -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);
+15
View File
@@ -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]);