From 76f28a41e51e7577315cfeb64145b3b6fd4bb62b Mon Sep 17 00:00:00 2001
From: magdale76 <16279778+magdale76@users.noreply.github.com>
Date: Sun, 18 Oct 2026 00:36:35 +0200
Subject: [PATCH] M3: an empty XMP alternative or list reads as no value, not
as its markup (#2373)
`get_property` returned the raw child markup when a property had no
`rdf:li` items, so `` with no /Info title
showed "" in Document Properties. Character data that still
holds a `<` is markup, never a value: an empty Alt/Seq/Bag, or an item
holding a nested structure, now reads as none.
Co-Authored-By: Claude Opus 5.5
---
crates/organize/src/xmp.rs | 20 +++++++++++++++++---
1 file changed, 17 insertions(+), 3 deletions(-)
diff --git a/crates/organize/src/xmp.rs b/crates/organize/src/xmp.rs
index 21cb282c21..9ed9c3320c 100644
--- a/crates/organize/src/xmp.rs
+++ b/crates/organize/src/xmp.rs
@@ -119,12 +119,13 @@ fn get_property(xmp: &str, name: &str, shape: Shape) -> Option {
let inner = element.get(open_end + 1..)?.strip_suffix(&format!("{name}>"))?;
let items = list_items(inner);
if items.is_empty() {
- unescape(inner.trim())
+ // An empty `` or `` has no value, not its markup.
+ text_value(inner)?
} else if shape == Shape::LangAlt {
let (_, text) = items.iter().find(|(lang, _)| lang.as_deref() == Some("x-default")).or(items.first())?;
- unescape(text.trim())
+ text_value(text)?
} else {
- items.iter().map(|(_, text)| unescape(text.trim())).filter(|t| !t.is_empty()).collect::>().join("; ")
+ items.iter().filter_map(|(_, text)| text_value(text)).filter(|t| !t.is_empty()).collect::>().join("; ")
}
} else {
let (_, value_start, value_end, _) = attribute_span(xmp, name)?;
@@ -133,6 +134,13 @@ fn get_property(xmp: &str, name: &str, shape: Shape) -> Option {
Some(value).filter(|v| !v.trim().is_empty())
}
+/// Character data `text` as a value, unescaped; `None` when it still holds markup (a `<`, which
+/// a value escapes as `<`), so a structure the reader doesn't take apart never shows as text.
+fn text_value(text: &str) -> Option {
+ let text = text.trim();
+ (!text.contains('<')).then(|| unescape(text))
+}
+
/// The `rdf:li` items of a property's content: each one's `xml:lang` (if any) and its text.
fn list_items(inner: &str) -> Vec<(Option, &str)> {
let mut items = Vec::new();
@@ -334,6 +342,12 @@ mod tests {
assert_eq!(get_property(seq, "dc:creator", Shape::Seq).as_deref(), Some("Ada"));
// Unfinished markup reads as nothing rather than as a wrong value.
assert_eq!(get_property("Cut", "dc:title", Shape::LangAlt), None);
+ // An empty alternative or list, or an item holding markup, has no value: never the markup.
+ assert_eq!(get_property("", "dc:title", Shape::LangAlt), None);
+ assert_eq!(get_property("", "dc:title", Shape::LangAlt), None);
+ assert_eq!(get_property("", "dc:creator", Shape::Seq), None);
+ let nested = "Ada";
+ assert_eq!(get_property(nested, "dc:creator", Shape::Seq).as_deref(), Some("Ada"));
}
#[test]