From 1d8d57ce9a01dc59a5d2e1d06c3238a225fce604 Mon Sep 17 00:00:00 2001 From: Nathaniel Ledford Date: Fri, 9 Oct 2026 16:27:16 -0400 Subject: [PATCH] LRC-LIB-KEYWORDS: Photo Supreme's vocabulary files read as keyword lists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Photo Supreme's Formatted Vocabulary File (Data ▸ Export to Formatted Vocabulary File) turns out to be Lightroom Classic's keyword list format: plain text with Windows line ends, a top-level category in square brackets, keywords a tab further in at each level. A test with a file of that shape pins it down; a real export of 118 keywords read and wrote back line for line. (Photo Supreme reads Lightroom keyword files too: Data ▸ Import from Lightroom Keywords file.) Co-Authored-By: Claude Opus 5.5 --- crates/catalog/src/keywords.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/catalog/src/keywords.rs b/crates/catalog/src/keywords.rs index b1c22e2dd..9c27db54e 100644 --- a/crates/catalog/src/keywords.rs +++ b/crates/catalog/src/keywords.rs @@ -1196,6 +1196,23 @@ mod tests { ); } + /// Photo Supreme's Formatted Vocabulary File is the same format: plain text with Windows line + /// ends and no byte-order mark, a top-level category in square brackets (an organizing keyword, + /// left out of export), keywords a tab further in at each level. + #[test] + fn a_photo_supreme_vocabulary_file_reads() { + let file = "[Places]\r\n\tPortugal\r\n\t\tLisbon\r\n\t\t\tAlfama\r\n\t\tPorto\r\n\tSpain\r\n\t\tSeville\r\n"; + let read = parse_keyword_list(file).unwrap(); + let paths: Vec<&str> = read.iter().map(|(p, _)| p.as_str()).collect(); + assert_eq!( + paths, + ["Places", "Portugal", "Portugal|Lisbon", "Portugal|Lisbon|Alfama", "Portugal|Porto", "Spain", "Spain|Seville"] + .map(|p| if p == "Places" { p.to_string() } else { format!("Places|{p}") }) + ); + assert!(!read[0].1.include_on_export, "the category organizes: it isn't exported"); + assert!(read[1..].iter().all(|(_, i)| i.include_on_export)); + } + /// A list that can't be read says which line and why, and nothing of it is taken. #[test] fn a_bad_keyword_list_says_which_line() {