diff --git a/Rules/Languages/en/SharedRules/default.yaml b/Rules/Languages/en/SharedRules/default.yaml index 21cfc256..db35793c 100644 --- a/Rules/Languages/en/SharedRules/default.yaml +++ b/Rules/Languages/en/SharedRules/default.yaml @@ -493,6 +493,9 @@ - x: "count(preceding-sibling::*)+IfThenElse(parent::m:mlabeledtr, 0, 1)" - pause: medium - x: "*" + - test: + if: "HasVisibleColumnLine(../.., count(preceding-sibling::*) + 1)" + then: [t: "separator"] - test: # short pause after each element; medium pause if last element in a row; long pause for last element in matrix - if: count(following-sibling::*) > 0 diff --git a/src/speech.rs b/src/speech.rs index faf4e69d..5dfaaee8 100644 --- a/src/speech.rs +++ b/src/speech.rs @@ -1758,7 +1758,7 @@ impl<'c, 'r> ContextStack<'c> { fn base_context(var_defs: PreferenceHashMap) -> sxd_xpath_no_unsafe::Context<'c> { let mut context = sxd_xpath_no_unsafe::Context::new(); context.set_namespace("m", "http://www.w3.org/1998/Math/MathML"); - crate::xpath_functions::add_builtin_functions(&mut context); + crate::xpath_functions::register_mathcat_xpath_functions(&mut context); for (key, value) in var_defs { context.set_variable(key.as_str(), yaml_to_value(&value)); // if let Some(str_value) = value.as_str() { diff --git a/src/xpath_functions.rs b/src/xpath_functions.rs index 47b9d6ab..cf0caaf7 100644 --- a/src/xpath_functions.rs +++ b/src/xpath_functions.rs @@ -1569,9 +1569,56 @@ impl Function for CountTableColumns { } } +/// Return whether a one-based mtable boundary has a visible column line. +/// +/// MathML repeats the final `columnlines` value for remaining boundaries. +/// Boundaries after the final column, and values other than `solid` and +/// `dashed`, do not describe a visible separator. +fn has_visible_column_line(table: Element, boundary: usize) -> bool { + if boundary == 0 || !is_tag(table, "mtable") { + return false; + } + + let Ok((_, Value::Number(column_count))) = CountTableDims::new().count_table_dims(table) else { + return false; + }; + if boundary as f64 >= column_count { + return false; + } + + return table + .attribute_value("columnlines") + .map(|values| { + matches!( + values.split_whitespace().take(boundary).last(), + Some("solid" | "dashed") + ) + }) + .unwrap_or(false); +} + +struct HasVisibleColumnLine; +impl Function for HasVisibleColumnLine { + fn evaluate<'c, 'd>(&self, + _context: &context::Evaluation<'c, 'd>, + args: Vec>) -> Result, Error> { + let mut args = Args(args); + args.exactly(2)?; + let boundary = args.pop_number()?; + let table = validate_one_node(args.pop_nodeset()?, "HasVisibleColumnLine")?; + let Node::Element(table) = table else { + return Err(Error::Other { what: "HasVisibleColumnLine requires an mtable element".to_string() }); + }; + if !boundary.is_finite() || boundary < 1.0 || boundary.fract() != 0.0 { + return Ok(Value::Boolean(false)); + } + return Ok(Value::Boolean(has_visible_column_line(table, boundary as usize))); + } +} + /// Add all the functions defined in this module to `context`. -pub fn add_builtin_functions(context: &mut Context) { +pub fn register_mathcat_xpath_functions(context: &mut Context) { context.set_function("NestingChars", crate::braille::NemethNestingChars); context.set_function("BrailleChars", crate::braille::BrailleChars); context.set_function("NeedsToBeGrouped", crate::braille::NeedsToBeGrouped); @@ -1591,6 +1638,7 @@ pub fn add_builtin_functions(context: &mut Context) { context.set_function("GetNavigationPartName", GetNavigationPartName); context.set_function("CountTableRows", CountTableRows); context.set_function("CountTableColumns", CountTableColumns); + context.set_function("HasVisibleColumnLine", HasVisibleColumnLine); context.set_function("DEBUG", Debug); // Not used: remove?? @@ -1796,7 +1844,10 @@ mod tests { let package = parser::parse(mathml).map_err(|e| anyhow::anyhow!("failed to parse XML: {e}"))?; let math_elem = get_element(&package); let child = as_element(math_elem.children()[0]); - assert!(CountTableDims::new().count_table_dims(child) == Ok((Value::Number(dims.0 as f64), Value::Number(dims.1 as f64)))); + assert_eq!( + CountTableDims::new().count_table_dims(child), + Ok((Value::Number(dims.0 as f64), Value::Number(dims.1 as f64))) + ); return Ok( () ); } @@ -1816,6 +1867,36 @@ mod tests { }); } + fn check_column_line(mathml: &str, boundary: usize, expected: bool) -> Result<()> { + let package = parser::parse(mathml).map_err(|e| anyhow::anyhow!("failed to parse XML: {e}"))?; + let math = get_element(&package); + let table = as_element(math.children()[0]); + assert_eq!(has_visible_column_line(table, boundary), expected); + return Ok(()); + } + + /// Verifies visible column-line styles, repeated styles, and boundaries outside the table. + #[test] + fn visible_column_lines() -> Result<()> { + return xpath_test(|| { + check_column_line("", 1, false)?; + check_column_line("", 2, true)?; + check_column_line("", 3, true)?; + + check_column_line("", 3, true)?; + + // No column-line style is specified. + check_column_line("", 1, false)?; + // The boundary is explicitly invisible. + check_column_line("", 1, false)?; + // Only `solid` and `dashed` describe visible column lines. + check_column_line("", 1, false)?; + // Boundary 2 is after the final column, not between two columns. + check_column_line("", 2, false)?; + return Ok(()); + }); + } + #[test] fn at_left_edge() -> Result<()> { return xpath_test(|| { diff --git a/tests/Languages/en/mtable.rs b/tests/Languages/en/mtable.rs index cbd2a527..be197013 100644 --- a/tests/Languages/en/mtable.rs +++ b/tests/Languages/en/mtable.rs @@ -271,8 +271,23 @@ fn augmented_matrix_2x3() -> Result<()> { ] "; - test("en", "ClearSpeak", expr, "the 2 by 3 augmented matrix; row 1; 3, 1, 4; row 2; 0, 2, 6")?; - test("en", "SimpleSpeak", expr, "the 2 by 3 augmented matrix; row 1; 3, 1, 4; row 2; 0, 2, 6")?; + test("en", "ClearSpeak", expr, "the 2 by 3 augmented matrix; row 1; 3, 1 separator, 4; row 2; 0, 2 separator, 6")?; + test("en", "SimpleSpeak", expr, "the 2 by 3 augmented matrix; row 1; 3, 1 separator, 4; row 2; 0, 2 separator, 6")?; + Ok(()) +} + +#[test] +fn dashed_augmented_matrix_separator() -> Result<()> { + let expr = " + + [ + + 123 + + ] + "; + test("en", "ClearSpeak", expr, "the 1 by 3 row matrix; 1 separator, 2 separator, 3")?; + test("en", "SimpleSpeak", expr, "the 1 by 3 row matrix; 1 separator, 2 separator, 3")?; Ok(()) } @@ -926,13 +941,13 @@ let expr = " "; test_ClearSpeak("en", "ClearSpeak_Matrix", "EndMatrix", - expr, "the 3 by 4 augmented matrix; row 1; column 1; 1, column 2; 2, column 3; negative 1, column 4; 3; \ - row 2; column 1; negative 3, column 2; 3, column 3; negative 1, column 4; 2; \ - row 3; column 1; 2, column 2; 3, column 3; 2, column 4; negative 1; end matrix")?; + expr, "the 3 by 4 augmented matrix; row 1; column 1; 1, column 2; 2, column 3; negative 1 separator, column 4; 3; \ + row 2; column 1; negative 3, column 2; 3, column 3; negative 1 separator, column 4; 2; \ + row 3; column 1; 2, column 2; 3, column 3; 2 separator, column 4; negative 1; end matrix")?; test("en", "SimpleSpeak", - expr, "the 3 by 4 augmented matrix; row 1; column 1; 1, column 2; 2, column 3; negative 1, column 4; 3; \ - row 2; column 1; negative 3, column 2; 3, column 3; negative 1, column 4; 2; \ - row 3; column 1; 2, column 2; 3, column 3; 2, column 4; negative 1; end matrix")?; + expr, "the 3 by 4 augmented matrix; row 1; column 1; 1, column 2; 2, column 3; negative 1 separator, column 4; 3; \ + row 2; column 1; negative 3, column 2; 3, column 3; negative 1 separator, column 4; 2; \ + row 3; column 1; 2, column 2; 3, column 3; 2 separator, column 4; negative 1; end matrix")?; Ok(()) }