Skip to content

Commit b5b5490

Browse files
committed
fix(note): stop trimming Unicode whitespace off the last frontmatter field
to_markdown() removed serde_yaml's trailing newline with trim_end(), which trims any Unicode whitespace. A note whose last serialized field ended in U+00A0 (or U+2007, U+3000, ...) lost that character on every write, so the value read back off disk differed from the one stored. Strip newlines only. Found by the note round-trip proptest; its failing seed is checked in, plus an explicit regression test over three Unicode space characters.
1 parent 0deba11 commit b5b5490

2 files changed

Lines changed: 44 additions & 2 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Seeds for failure cases proptest has generated in the past. It is
2+
# automatically read and these particular cases re-run before any
3+
# novel cases are generated.
4+
#
5+
# It is recommended to check this file in to source control so that
6+
# everyone who runs the test benefits from these saved cases.
7+
cc f41cf5be61fda18330784477bb1c1fa0867b4589e96f1d5dc65d07957072d4d6 # shrinks to note = Note { frontmatter: NoteFrontmatter { title: "0", aliases: ["¡", "!"], tags: [], note_type: Fact, created: 2000-01-01, updated: 2000-01-01, expires: None, source: Some("é\u{a0}") }, body: "" }

‎crates/memory-core/src/note.rs‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,10 @@ impl Note {
150150
pub fn to_markdown(&self) -> Result<String> {
151151
self.frontmatter.validate()?;
152152
let yaml = serde_yaml::to_string(&self.frontmatter)?;
153-
// serde_yaml adds a trailing newline; keep a single blank line after ---
154-
let yaml = yaml.trim_end();
153+
// serde_yaml adds a trailing newline; strip newlines only. `trim_end` would
154+
// also eat Unicode whitespace belonging to the last field's value (U+00A0
155+
// and friends), silently corrupting it on the way to disk.
156+
let yaml = yaml.trim_end_matches('\n');
155157
let mut out = String::with_capacity(yaml.len() + self.body.len() + 16);
156158
out.push_str("---\n");
157159
out.push_str(yaml);
@@ -382,6 +384,39 @@ Related: [[docker-registry-auth]]
382384
}
383385
}
384386

387+
/// Regression: `to_markdown` used to `trim_end()` serde_yaml's trailing
388+
/// newline. That trims *Unicode* whitespace, so a last field ending in
389+
/// U+00A0 (or U+2007, U+3000, …) lost that character on every write.
390+
#[test]
391+
fn trailing_unicode_whitespace_in_the_last_field_survives_a_round_trip() {
392+
for suffix in ['\u{a0}', '\u{2007}', '\u{3000}'] {
393+
let source = format!("session{suffix}");
394+
let note = Note {
395+
frontmatter: NoteFrontmatter {
396+
title: "Trailing whitespace".into(),
397+
aliases: vec!["nbsp".into(), "unicode space".into()],
398+
tags: vec![],
399+
note_type: NoteType::Fact,
400+
created: date(2026, 8, 15),
401+
updated: date(2026, 8, 15),
402+
expires: None,
403+
source: Some(source.clone()),
404+
},
405+
body: String::new(),
406+
};
407+
408+
let markdown = note.to_markdown().unwrap();
409+
let parsed = Note::parse(&markdown).unwrap();
410+
assert_eq!(
411+
parsed.frontmatter.source.as_deref(),
412+
Some(source.as_str()),
413+
"U+{:04X} was stripped from the last frontmatter field",
414+
suffix as u32
415+
);
416+
assert_eq!(parsed, note);
417+
}
418+
}
419+
385420
#[test]
386421
fn parse_spec_example_round_trip() {
387422
let note = Note::parse(SPEC_EXAMPLE).unwrap();

0 commit comments

Comments
 (0)