From 688058b3e8b8e3f780d4ee230ebbdfc6d28760b9 Mon Sep 17 00:00:00 2001 From: MattJackson <1085847+MattJackson@users.noreply.github.com> Date: Sun, 10 May 2026 22:12:59 -0700 Subject: [PATCH] labels/bdmt: drop description fields that are just XML child elements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Disc-04 (Top Gun: Maverick) re-test 2026-05-11 surfaced a real-world bdmt_eng.xml where contained no prose, only nested elements. The previous parser surfaced the raw XML fragment as the description string ("\\r\\n "). Worse than no description. Fix: filter description candidates that begin with `<` after trimming. Real prose never starts with an angle bracket; XML-only content always does. Net: title extraction unaffected (it uses its own element-priority path); description field drops when it would otherwise carry XML noise. Two new bdmt tests, 12 of 12 passing. --- src/labels/bdmt.rs | 54 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/src/labels/bdmt.rs b/src/labels/bdmt.rs index b0b77b2..b218e47 100644 --- a/src/labels/bdmt.rs +++ b/src/labels/bdmt.rs @@ -136,11 +136,25 @@ pub(crate) type BdmtFields = (String, Option, Option<(u32, u32)>); /// authoring-tool conventions documented at the module level). pub(crate) fn parse_bdmt_xml(_lang_code: &str, xml_text: &str) -> Option { let title = extract_title(xml_text)?; - let description = xml::text(xml_text, "description").filter(|s| !s.is_empty()); + let description = xml::text(xml_text, "description") + .filter(|s| !s.is_empty()) + .filter(|s| !looks_like_xml(s)) + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()); let disc_set = extract_disc_set(xml_text); Some((title, description, disc_set)) } +/// Reject candidate description strings that are themselves XML +/// fragments — observed on disc-04 (Top Gun: Maverick), where +/// `` contained `` child +/// elements and no actual prose. Surfacing that raw to the JSON +/// output is worse than dropping the field entirely. +fn looks_like_xml(s: &str) -> bool { + let t = s.trim_start(); + t.starts_with('<') +} + /// Try title-bearing element variants in priority order. The `xml` /// helpers are case- and namespace-insensitive, so callers pass the /// bare local name (no `di:` prefix). @@ -326,6 +340,44 @@ mod tests { assert!(parse_bdmt_xml("eng", truncated).is_none()); } + #[test] + fn description_with_only_child_xml_is_dropped() { + // Real-world bug from disc-04 (Top Gun: Maverick, 2026-05-11 + // capture): contained only + // child elements with no actual prose. The previous parser + // surfaced the raw XML fragment as the description string. + // Now we reject candidates that begin with `<`. + let xml = r#" + Top Gun: Maverick + + + + + "#; + let (title, description, _) = + parse_bdmt_xml("eng", xml).expect("title is present so parse must succeed"); + assert_eq!(title, "Top Gun: Maverick"); + assert!( + description.is_none(), + "description containing only XML children must be dropped, got {description:?}" + ); + } + + #[test] + fn description_with_plain_text_passes_through() { + // The legitimate case still works: a description with actual + // prose survives the looks_like_xml filter. + let xml = r#" + Some Movie + An epic tale of one man's quest for tea. + "#; + let (_, description, _) = parse_bdmt_xml("eng", xml).expect("must parse"); + assert_eq!( + description.as_deref(), + Some("An epic tale of one man's quest for tea.") + ); + } + #[test] fn whitespace_in_title_is_trimmed() { let xml = r#"