diff --git a/src/speech.rs b/src/speech.rs index 100d5ff3..1417000e 100644 --- a/src/speech.rs +++ b/src/speech.rs @@ -709,24 +709,38 @@ impl Intent { /// "lift" up the children any "TEMP_NAME" child -- could short circuit when only one child + /// + /// TEMP_NAME is only ever a transport wrapper created by `replace_nodes_tree`, so wrappers can nest: + /// a rule whose replacement is a bare `x:` (e.g., the mrow 'matrix' rule returns `x: "*[2]"`) yields + /// TEMP_NAME(matrix), and the parent intent's `x: "*[1]"` wraps that again. Lifting only one level left + /// `power(TEMP_NAME(matrix), 2)`, spoken as "TEMP NAME of the 2 by 2 matrix ... squared" (issue #762). + /// Nested wrappers whose children are all elements are flattened; a leaf wrapper (text from a Text or + /// Attribute node) is only unwrapped when it is a direct child, matching the previous behavior. fn lift_children(result: Element) -> Element { // debug!("lift_children:\n{}", mml_to_string(result)); // most likely there will be the same number of new children as result has, but there could be more let mut new_children = Vec::with_capacity(2*result.children().len()); for child_of_element in result.children() { - match child_of_element { - ChildOfElement::Element(child) => { - if name(child) == "TEMP_NAME" { - new_children.append(&mut child.children()); // almost always just one - } else { - new_children.push(child_of_element); - } - }, - _ => new_children.push(child_of_element), // text() - } + push_lifted(child_of_element, &mut new_children, true); } result.replace_children(new_children); return result; + + fn push_lifted<'a>(child_of_element: ChildOfElement<'a>, new_children: &mut Vec>, is_direct_child: bool) { + if let ChildOfElement::Element(child) = child_of_element { + if name(child) == "TEMP_NAME" { + let grandchildren = child.children(); + let is_leaf_wrapper = grandchildren.iter().any(|gc| matches!(gc, ChildOfElement::Text(_))); + if is_direct_child || !is_leaf_wrapper { + for grandchild in grandchildren { + push_lifted(grandchild, new_children, false); + } + return; + } + } + } + new_children.push(child_of_element); + } } } } diff --git a/tests/Languages/en/mtable.rs b/tests/Languages/en/mtable.rs index 8a670988..e2d06368 100644 --- a/tests/Languages/en/mtable.rs +++ b/tests/Languages/en/mtable.rs @@ -1390,3 +1390,47 @@ fn single_line_with_label() -> Result<()> { expr, "1 equation, with label 2; b equals 2")?; return Ok(()); } + +#[test] +fn matrix_raised_to_power() -> Result<()> { + // A parenthesized matrix as the base of msup must speak as "the matrix ... squared". + // Regression for #762: the mrow 'matrix' intent rule returns a bare `x:`, so the intent tree + // carried a nested TEMP_NAME wrapper into `power` and NVDA read "TEMP NAME of the 2 by 2 matrix ...". + let expr = " + ( + + 12 + 34 + + ) + 2 + "; + test("en", "ClearSpeak", expr, "the 2 by 2 matrix; row 1; 1, 2; row 2; 3, 4; squared") +} + +#[test] +fn matrix_raised_to_power_lualatex_spacing() -> Result<()> { + // The exact MathML from #762 (LuaLaTeX pmatrix output): negative-width mspace around the + // table and fence attributes on the parens. Canonicalization must fold the spacing away so + // the matrix is still recognized as the base of the power. + let expr = " + + + ( + + + + + 12 + 34 + + + + + ) + + 2 + + "; + test("en", "ClearSpeak", expr, "the 2 by 2 matrix; row 1; 1, 2; row 2; 3, 4; squared") +}