diff --git a/crates/idml/src/export/designmap.rs b/crates/idml/src/export/designmap.rs
--- a/crates/idml/src/export/designmap.rs
+++ b/crates/idml/src/export/designmap.rs
@@ -206,10 +206,10 @@ fn xref_format_el(i: usize, f: &designcraft_doc::XrefFormat) -> El {
     };
     let mut rest = f.definition.as_str();
     while let Some(a) = rest.find('<') {
+        let Some(b) = rest[a..].find('>') else { break };
         if a > 0 {
             block(&mut el, "CustomStringBuildingBlock", &rest[..a], "", false);
         }
-        let Some(b) = rest[a..].find('>') else { break };
         let tag = rest[a + 1..a + b].trim().trim_end_matches('/').trim();
         let attr = |k: &str| tag.find(&format!("{k}=\"")).and_then(|i| tag[i + k.len() + 2..].split('"').next()).unwrap_or("").to_string();
         match tag.split_whitespace().next().unwrap_or("") {
@@ -350,3 +350,31 @@ fn text_variable_el(v: &designcraft_doc::vars::TextVariable) -> El {
     }
     e
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::xml::Node;
+
+    fn custom_text(definition: &str) -> String {
+        let f = designcraft_doc::XrefFormat { name: "x".into(), definition: definition.into() };
+        xref_format_el(0, &f)
+            .children
+            .iter()
+            .filter_map(|n| match n {
+                Node::El(e) if e.get("BlockType") == Some("CustomStringBuildingBlock") => e.get("CustomText").map(str::to_string),
+                _ => None,
+            })
+            .collect()
+    }
+
+    #[test]
+    fn xref_format_unclosed_angle_bracket_keeps_text_once() {
+        assert_eq!(custom_text("a < b"), "a < b");
+    }
+
+    #[test]
+    fn xref_format_well_formed_keeps_custom_text() {
+        assert_eq!(custom_text("p <pageNum /> of <chapNum />"), "p  of ");
+    }
+}
