diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index 879ebc6c66..56755d1b94 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -5218,3 +5218,24 @@ fn a_clipping_copy_on_the_same_baseline_is_refused() { assert!(matches!(&block, Err(EditError::Invalid(m)) if m.contains("clipping path")), "{block:?}"); assert_eq!(text::text_lines(&doc, 0).unwrap().len(), 2, "refused means untouched"); } + +/// #3573: past the 256-level marked-content cap every `EMC` must still close its own sequence, +/// so `/ActualText` nested that deep is neither dropped nor applied to too little text. +#[test] +fn actual_text_pairs_past_the_marked_content_cap() { + let span = "/Span <> BDC BT /F1 24 Tf 72 700 Td (abc) Tj ET EMC"; + let outer = "/Span <> BDC BT /F1 24 Tf 72 700 Td /Y BMC (abc) Tj EMC (def) Tj ET EMC"; + for (content, expected) in [ + (span.to_string(), "REPLACED"), + (format!("{}{span}{}", "/X BMC ".repeat(256), " EMC".repeat(256)), "REPLACED"), + (format!("{}{span}{}", "/X BMC ".repeat(1000), " EMC".repeat(1000)), "REPLACED"), + (outer.to_string(), "OUTER"), + (format!("{}{outer}{}", "/X BMC ".repeat(255), " EMC".repeat(255)), "OUTER"), + ] { + let doc = text_page(&content); + let lines = text::text_lines(&doc, 0).unwrap(); + assert_eq!(lines.iter().map(|l| l.text.as_str()).collect::>(), [expected]); + let reading: Vec = text::reading_blocks(&doc, 0).unwrap().into_iter().map(|b| b.text).collect(); + assert_eq!(reading, [expected]); + } +} diff --git a/crates/edit/src/text.rs b/crates/edit/src/text.rs index 2672592178..8ddef552dc 100644 --- a/crates/edit/src/text.rs +++ b/crates/edit/src/text.rs @@ -829,7 +829,9 @@ fn interpret( let mut actual: Vec> = Vec::new(); for (i, op) in ops.iter().enumerate() { match op.op.as_slice() { - b"BMC" | b"BDC" if marked.len() < 256 => { + // No depth cap: every `EMC` pops, so a capped push would pair it with the enclosing + // sequence (#3573). Both stacks stay bounded by the number of operators. + b"BMC" | b"BDC" => { // Marked text paragraphs/headings are authoritative boundaries. Ignore // artifacts and inner spans, which inherit their enclosing region. let region = matches!(op.name(0), Some(b"P" | b"H" | b"H1" | b"H2" | b"H3" | b"H4" | b"H5" | b"H6" | b"LI" | b"TD" | b"TH"));