LRC-LIB-KEYWORDS: keyword list export names only what it loses

Export now leaves out only what it must, and its counts are exact:

- A name starting with a byte-order mark is left out. On the first
  line, the reader would take the mark for the file's own.
- A synonym in braces is written, since it reads back. An empty
  synonym, or one that repeats another in a different case, is
  skipped quietly, as the reader would drop it anyway.
- keyword.export counts and checks for Capture One the keywords it
  actually wrote, now returned with the text. It used to guess from
  the lines, so a name starting with a brace was missed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nathaniel Ledford
2026-10-09 17:02:07 -04:00
co-authored by Claude Opus 5.5
parent a970bdac51
commit 060da5bb5e
3 changed files with 69 additions and 28 deletions
+10 -9
View File
@@ -434,20 +434,21 @@ pub fn specs() -> Vec<CommandSpec> {
"{path?} — the keyword list as a file (Lightroom Classic's format, which Capture One, Photo Supreme and Bridge read) → {keywords, captureOneRefuses: keywords with ; , < > that Capture One's importer refuses, unwritable: keywords left out because the format can't hold them (a name in brackets or braces, a line break, deeper than 64 levels; a synonym as \"path {synonym}\"), text when there is no path}",
always,
|s, p| {
let (text, unwritable) = s.catalog.keyword_list_text();
let keywords = text.lines().filter(|l| !l.trim_start().starts_with('{')).count();
let refuses: Vec<String> = text
.lines()
.map(|l| l.trim().trim_start_matches('[').trim_end_matches(']').to_string())
.filter(|l| !l.starts_with('{') && l.contains(CAPTURE_ONE_REFUSES))
let file = s.catalog.keyword_list_text();
// Capture One's importer refuses ; , < > in a keyword's name
let refuses: Vec<&str> = file
.written
.iter()
.filter_map(|p| p.rsplit(lightcraft_catalog::keywords::SEP).next())
.filter(|n| n.contains(CAPTURE_ONE_REFUSES))
.collect();
let mut r = json!({"keywords": keywords, "captureOneRefuses": refuses, "unwritable": unwritable});
let mut r = json!({"keywords": file.written.len(), "captureOneRefuses": refuses, "unwritable": file.unwritable});
match str_param(p, "path").map(str::trim).filter(|x| !x.is_empty()) {
Some(path) => {
std::fs::write(path, &text).map_err(|e| crate::EngineError::Other(format!("{path}: {e}")))?;
std::fs::write(path, &file.text).map_err(|e| crate::EngineError::Other(format!("{path}: {e}")))?;
r["path"] = json!(path);
}
None => r["text"] = json!(text),
None => r["text"] = json!(file.text),
}
Ok(r)
}