From dff37bf08d3da2aeeb4edbd0d457189f6b9d725f Mon Sep 17 00:00:00 2001 From: mgros Date: Wed, 12 Aug 2026 00:20:06 +0200 Subject: [PATCH 01/12] Add `HasVisibleColumnLine` XPath function to handle table separators in speech output - Replace `count_table_dims` return type with `usize` values for clarity. - Update function registration to include `HasVisibleColumnLine`. - Modify speech tests to reflect separator usage in matrix descriptions. --- Rules/Languages/en/SharedRules/default.yaml | 3 + src/speech.rs | 2 +- src/xpath_functions.rs | 77 +++++++++++++++++++-- tests/Languages/en/mtable.rs | 31 ++++++--- 4 files changed, 99 insertions(+), 14 deletions(-) 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 0c956fe9..e2663f6a 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..6a4e1311 100644 --- a/src/xpath_functions.rs +++ b/src/xpath_functions.rs @@ -1480,7 +1480,7 @@ impl CountTableDims { /// This function is relatively permissive. Non-`mtr` rows are /// ignored. The number of columns is determined only from the first /// row, if it exists. Within that row, non-`mtd` elements are ignored. - fn count_table_dims<'d>(mut self, e: Element<'_>) -> Result<(Value<'d>, Value<'d>), Error> { + fn count_table_dims(mut self, e: Element<'_>) -> (usize, usize) { for child in e.children() { let ChildOfElement::Element(row) = child else { continue @@ -1529,7 +1529,7 @@ impl CountTableDims { // columns, so we will not use them. let _extra_rows = self.extended_cells.keys().max().map(|k| k-1).unwrap_or(0); - Ok((Value::Number(self.num_rows as f64), Value::Number(self.num_cols as f64))) + (self.num_rows, self.num_cols) } fn evaluate<'d>(self, fn_name: &str, @@ -1540,7 +1540,8 @@ impl CountTableDims { let node = validate_one_node(element, fn_name)?; if let Node::Element(e) = node { if is_tag(e, "mtable") { - return self.count_table_dims(e); + let (rows, columns) = self.count_table_dims(e); + return Ok((Value::Number(rows as f64), Value::Number(columns as f64))); } else { return Err(Error::Other { what: format!("Input element was a <{}>, not an ", as_qname!(e.name()).local_part()) }); @@ -1569,9 +1570,49 @@ 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 (_, column_count) = CountTableDims::new().count_table_dims(table); + if boundary >= column_count { + return false; + } + + let line_style = table + .attribute_value("columnlines") + .and_then(|values| values.split_whitespace().take(boundary).last()); + return matches!(line_style, Some("solid" | "dashed")); +} + +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 +1632,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 +1838,7 @@ 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), dims); return Ok( () ); } @@ -1816,6 +1858,31 @@ 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(()); + } + + #[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)?; + + check_column_line("", 1, false)?; + check_column_line("", 1, false)?; + check_column_line("", 1, false)?; + 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(()) } From 9fb3060b62cbe74c87b4976bb7e139b8b047a514 Mon Sep 17 00:00:00 2001 From: mgros Date: Fri, 14 Aug 2026 17:04:06 +0200 Subject: [PATCH 02/12] Refactor `count_table_dims` to return `Result` type and update usage sites --- src/xpath_functions.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/xpath_functions.rs b/src/xpath_functions.rs index 6a4e1311..a17a93c1 100644 --- a/src/xpath_functions.rs +++ b/src/xpath_functions.rs @@ -1480,7 +1480,7 @@ impl CountTableDims { /// This function is relatively permissive. Non-`mtr` rows are /// ignored. The number of columns is determined only from the first /// row, if it exists. Within that row, non-`mtd` elements are ignored. - fn count_table_dims(mut self, e: Element<'_>) -> (usize, usize) { + fn count_table_dims<'d>(mut self, e: Element<'_>) -> Result<(Value<'d>, Value<'d>), Error> { for child in e.children() { let ChildOfElement::Element(row) = child else { continue @@ -1529,7 +1529,7 @@ impl CountTableDims { // columns, so we will not use them. let _extra_rows = self.extended_cells.keys().max().map(|k| k-1).unwrap_or(0); - (self.num_rows, self.num_cols) + Ok((Value::Number(self.num_rows as f64), Value::Number(self.num_cols as f64))) } fn evaluate<'d>(self, fn_name: &str, @@ -1540,8 +1540,7 @@ impl CountTableDims { let node = validate_one_node(element, fn_name)?; if let Node::Element(e) = node { if is_tag(e, "mtable") { - let (rows, columns) = self.count_table_dims(e); - return Ok((Value::Number(rows as f64), Value::Number(columns as f64))); + return self.count_table_dims(e); } else { return Err(Error::Other { what: format!("Input element was a <{}>, not an ", as_qname!(e.name()).local_part()) }); @@ -1580,8 +1579,10 @@ fn has_visible_column_line(table: Element, boundary: usize) -> bool { return false; } - let (_, column_count) = CountTableDims::new().count_table_dims(table); - if boundary >= column_count { + let Ok((_, Value::Number(column_count))) = CountTableDims::new().count_table_dims(table) else { + return false; + }; + if boundary as f64 >= column_count { return false; } @@ -1838,7 +1839,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_eq!(CountTableDims::new().count_table_dims(child), dims); + assert_eq!( + CountTableDims::new().count_table_dims(child), + Ok((Value::Number(dims.0 as f64), Value::Number(dims.1 as f64))) + ); return Ok( () ); } From 5a8c76cb5bdd5c3d3573437af77db922032a66e8 Mon Sep 17 00:00:00 2001 From: mgros Date: Fri, 14 Aug 2026 17:04:52 +0200 Subject: [PATCH 03/12] undo whitespace change --- src/xpath_functions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xpath_functions.rs b/src/xpath_functions.rs index a17a93c1..63318936 100644 --- a/src/xpath_functions.rs +++ b/src/xpath_functions.rs @@ -1529,7 +1529,7 @@ impl CountTableDims { // columns, so we will not use them. let _extra_rows = self.extended_cells.keys().max().map(|k| k-1).unwrap_or(0); - Ok((Value::Number(self.num_rows as f64), Value::Number(self.num_cols as f64))) + Ok((Value::Number(self.num_rows as f64), Value::Number(self.num_cols as f64))) } fn evaluate<'d>(self, fn_name: &str, From a43117b1c66dd949bcb18c4ef69ffea172d2d6ad Mon Sep 17 00:00:00 2001 From: mgros Date: Fri, 14 Aug 2026 17:04:52 +0200 Subject: [PATCH 04/12] . --- src/xpath_functions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xpath_functions.rs b/src/xpath_functions.rs index a17a93c1..63318936 100644 --- a/src/xpath_functions.rs +++ b/src/xpath_functions.rs @@ -1529,7 +1529,7 @@ impl CountTableDims { // columns, so we will not use them. let _extra_rows = self.extended_cells.keys().max().map(|k| k-1).unwrap_or(0); - Ok((Value::Number(self.num_rows as f64), Value::Number(self.num_cols as f64))) + Ok((Value::Number(self.num_rows as f64), Value::Number(self.num_cols as f64))) } fn evaluate<'d>(self, fn_name: &str, From 15f1d5bafeef5f41ab7b6ba9fcdef8ff8dee47ee Mon Sep 17 00:00:00 2001 From: mgros Date: Fri, 14 Aug 2026 17:06:45 +0200 Subject: [PATCH 05/12] . --- src/xpath_functions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xpath_functions.rs b/src/xpath_functions.rs index 63318936..2412777b 100644 --- a/src/xpath_functions.rs +++ b/src/xpath_functions.rs @@ -1529,7 +1529,7 @@ impl CountTableDims { // columns, so we will not use them. let _extra_rows = self.extended_cells.keys().max().map(|k| k-1).unwrap_or(0); - Ok((Value::Number(self.num_rows as f64), Value::Number(self.num_cols as f64))) + Ok((Value::Number(self.num_rows as f64), Value::Number(self.num_cols as f64))) } fn evaluate<'d>(self, fn_name: &str, From 4a453e737416a4796c1fac15f96091cd3c755774 Mon Sep 17 00:00:00 2001 From: mgros Date: Fri, 14 Aug 2026 17:18:03 +0200 Subject: [PATCH 06/12] explain tests better in comments --- src/xpath_functions.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/xpath_functions.rs b/src/xpath_functions.rs index 2412777b..907117ad 100644 --- a/src/xpath_functions.rs +++ b/src/xpath_functions.rs @@ -1870,6 +1870,7 @@ mod tests { return Ok(()); } + /// Verifies visible column-line styles, repeated styles, and boundaries outside the table. #[test] fn visible_column_lines() -> Result<()> { return xpath_test(|| { @@ -1879,9 +1880,13 @@ mod tests { 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(()); }); From 463b47000d536ee340b8b6eca64a165ae9385f68 Mon Sep 17 00:00:00 2001 From: mgros Date: Fri, 14 Aug 2026 17:24:24 +0200 Subject: [PATCH 07/12] explain tests better in comments --- src/xpath_functions.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/xpath_functions.rs b/src/xpath_functions.rs index 907117ad..cf0caaf7 100644 --- a/src/xpath_functions.rs +++ b/src/xpath_functions.rs @@ -1586,10 +1586,15 @@ fn has_visible_column_line(table: Element, boundary: usize) -> bool { return false; } - let line_style = table + return table .attribute_value("columnlines") - .and_then(|values| values.split_whitespace().take(boundary).last()); - return matches!(line_style, Some("solid" | "dashed")); + .map(|values| { + matches!( + values.split_whitespace().take(boundary).last(), + Some("solid" | "dashed") + ) + }) + .unwrap_or(false); } struct HasVisibleColumnLine; From a3364da099bf44649d2adebb8f7633c51dc24552 Mon Sep 17 00:00:00 2001 From: mgros Date: Sun, 16 Aug 2026 02:07:10 +0200 Subject: [PATCH 08/12] Add support for row separators and unify boundary line logic --- Rules/Languages/en/SharedRules/default.yaml | 4 + src/xpath_functions.rs | 174 +++++++++++++++----- tests/Languages/en/mtable.rs | 46 ++++++ 3 files changed, 186 insertions(+), 38 deletions(-) diff --git a/Rules/Languages/en/SharedRules/default.yaml b/Rules/Languages/en/SharedRules/default.yaml index 142a8faf..b6731845 100644 --- a/Rules/Languages/en/SharedRules/default.yaml +++ b/Rules/Languages/en/SharedRules/default.yaml @@ -481,6 +481,10 @@ - test: if: "HasVisibleColumnLine(../.., count(preceding-sibling::*) + 1)" then: [t: "separator"] + - test: + # Announce a row separator only after the row's final cell, so it is spoken once per horizontal boundary. + if: "count(following-sibling::*) = 0 and HasVisibleRowLine(../.., count(../preceding-sibling::*) + 1)" + then: [t: "row 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/xpath_functions.rs b/src/xpath_functions.rs index f4c2db52..5209e353 100644 --- a/src/xpath_functions.rs +++ b/src/xpath_functions.rs @@ -1646,25 +1646,35 @@ impl Function for CountTableColumns { } } -/// Return whether a one-based mtable boundary has a visible column line. +#[derive(Clone, Copy)] +enum TableLineAxis { + Row, + Column, +} + +/// Return whether a one-based mtable boundary has a visible line on the given axis. /// -/// 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 { +/// MathML repeats the final line style for remaining boundaries. Boundaries after +/// the final row or column, and values other than `solid` and `dashed`, do not +/// describe a visible separator. +fn has_visible_table_line(table: Element, boundary: usize, axis: TableLineAxis) -> bool { if boundary == 0 || !is_tag(table, "mtable") { return false; } - let Ok((_, Value::Number(column_count))) = CountTableDims::new().count_table_dims(table) else { + let Ok((Value::Number(row_count), Value::Number(column_count))) = CountTableDims::new().count_table_dims(table) else { return false; }; - if boundary as f64 >= column_count { + let (line_count, attribute_name) = match axis { + TableLineAxis::Row => (row_count, "rowlines"), + TableLineAxis::Column => (column_count, "columnlines"), + }; + if boundary as f64 >= line_count { return false; } return table - .attribute_value("columnlines") + .attribute_value(attribute_name) .map(|values| { matches!( values.split_whitespace().take(boundary).last(), @@ -1674,22 +1684,42 @@ fn has_visible_column_line(table: Element, boundary: usize) -> bool { .unwrap_or(false); } +/// Validate and convert XPath arguments before delegating to the typed table-line helper. +fn evaluate_has_visible_table_line<'d>( + args: Vec>, + function_name: &str, + axis: TableLineAxis, +) -> Result, Error> { + let mut args = Args(args); + args.exactly(2)?; + let boundary = args.pop_number()?; + let table = validate_one_node(args.pop_nodeset()?, function_name)?; + let Node::Element(table) = table else { + return Err(Error::Other { what: format!("{function_name} requires an mtable element") }); + }; + if !boundary.is_finite() || boundary < 1.0 || boundary.fract() != 0.0 { + return Ok(Value::Boolean(false)); + } + return Ok(Value::Boolean(has_visible_table_line(table, boundary as usize, axis))); +} + +/// XPath function reporting whether an mtable column boundary has a visible line. 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))); + evaluate_has_visible_table_line(args, "HasVisibleColumnLine", TableLineAxis::Column) + } +} + +/// XPath function reporting whether an mtable row boundary has a visible line. +struct HasVisibleRowLine; +impl Function for HasVisibleRowLine { + fn evaluate<'c, 'd>(&self, + _context: &context::Evaluation<'c, 'd>, + args: Vec>) -> Result, Error> { + evaluate_has_visible_table_line(args, "HasVisibleRowLine", TableLineAxis::Row) } } @@ -1719,6 +1749,7 @@ pub fn register_mathcat_xpath_functions(context: &mut Context) { context.set_function("CountTableRows", CountTableRows); context.set_function("CountTableColumns", CountTableColumns); context.set_function("HasVisibleColumnLine", HasVisibleColumnLine); + context.set_function("HasVisibleRowLine", HasVisibleRowLine); context.set_function("DEBUG", Debug); // Not used: remove?? @@ -1947,33 +1978,100 @@ mod tests { }); } - fn check_column_line(mathml: &str, boundary: usize, expected: bool) -> Result<()> { + fn check_table_line(mathml: &str, boundary: usize, axis: TableLineAxis, 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); + assert_eq!(has_visible_table_line(table, boundary, axis), expected); return Ok(()); } - /// Verifies visible column-line styles, repeated styles, and boundaries outside the table. + /// Verifies visible table-line styles, repeated styles, and boundaries outside the table. #[test] - fn visible_column_lines() -> Result<()> { + fn visible_table_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(()); + // The three values map in order to the three boundaries between four columns. + let mixed_column_lines: &str = " + + + column 1 + column 2 + column 3 + column 4 + + "; + check_table_line(mixed_column_lines, 1, TableLineAxis::Column, false)?; + check_table_line(mixed_column_lines, 2, TableLineAxis::Column, true)?; + check_table_line(mixed_column_lines, 3, TableLineAxis::Column, true)?; + + // The final `dashed` value repeats for the third column boundary. + let repeated_column_line: &str = " + + + column 1 + column 2 + column 3 + column 4 + + "; + check_table_line(repeated_column_line, 3, TableLineAxis::Column, true)?; + + // A table without `columnlines` has no visible column boundary. + let no_column_lines: &str = " + + + column 1 + column 2 + + "; + check_table_line(no_column_lines, 1, TableLineAxis::Column, false)?; + + // `none` explicitly makes the column boundary invisible. + let invisible_column_line: &str = " + + + column 1 + column 2 + + "; + check_table_line(invisible_column_line, 1, TableLineAxis::Column, false)?; + + // Only `solid` and `dashed` describe visible table lines. + let unsupported_column_line: &str = " + + + column 1 + column 2 + + "; + check_table_line(unsupported_column_line, 1, TableLineAxis::Column, false)?; + + // Boundary 2 is after the final column, not between two columns. + let two_column_table: &str = " + + column 1 + column 2 + + "; + check_table_line(two_column_table, 2, TableLineAxis::Column, false)?; + + // Four rows have three interior boundaries. `none` applies after row 1, + // `dashed` applies after row 2, and the final `dashed` repeats after row 3. + let mixed_row_lines: &str = " + + row 1 + row 2 + row 3 + row 4 + "; + check_table_line(mixed_row_lines, 1, TableLineAxis::Row, false)?; + check_table_line(mixed_row_lines, 2, TableLineAxis::Row, true)?; + check_table_line(mixed_row_lines, 3, TableLineAxis::Row, true)?; + // Boundary 4 is after the final row, not between two rows. + check_table_line(mixed_row_lines, 4, TableLineAxis::Row, false)?; + // Boundary zero is invalid for both axes. + check_table_line(mixed_row_lines, 0, TableLineAxis::Row, false)?; + return Ok(()); }); } diff --git a/tests/Languages/en/mtable.rs b/tests/Languages/en/mtable.rs index 22435368..76e75336 100644 --- a/tests/Languages/en/mtable.rs +++ b/tests/Languages/en/mtable.rs @@ -291,6 +291,52 @@ fn dashed_augmented_matrix_separator() -> Result<()> { Ok(()) } +/// A horizontal line is announced once, after the row it separates from the next row. +#[test] +fn matrix_row_separator() -> Result<()> { + let expr = " + + [ + + + 1 + 2 + + + 3 + 4 + + + ] + "; + test("en", "ClearSpeak", expr, "the 2 by 2 matrix; row 1; 1, 2 row separator; row 2; 3, 4")?; + test("en", "SimpleSpeak", expr, "the 2 by 2 matrix; row 1; 1, 2 row separator; row 2; 3, 4")?; + Ok(()) +} + +/// Horizontal and vertical lines use distinct announcements at their respective boundaries. +#[test] +fn matrix_row_and_column_separators() -> Result<()> { + let expr = " + + [ + + + 1 + 2 + + + 3 + 4 + + + ] + "; + test("en", "ClearSpeak", expr, "the 2 by 2 augmented matrix; row 1; 1 separator, 2 row separator; row 2; 3 separator, 4")?; + test("en", "SimpleSpeak", expr, "the 2 by 2 augmented matrix; row 1; 1 separator, 2 row separator; row 2; 3 separator, 4")?; + Ok(()) +} + #[test] fn matrix_2x3_labeled() -> Result<()> { let expr = " From cced25c7b752d0f2e79fdfb75854e779e750b9cf Mon Sep 17 00:00:00 2001 From: mgros Date: Sun, 16 Aug 2026 02:19:10 +0200 Subject: [PATCH 09/12] fix check_table_line helper function --- src/xpath_functions.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/xpath_functions.rs b/src/xpath_functions.rs index 5209e353..587206f4 100644 --- a/src/xpath_functions.rs +++ b/src/xpath_functions.rs @@ -1981,7 +1981,14 @@ mod tests { fn check_table_line(mathml: &str, boundary: usize, axis: TableLineAxis, 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]); + let table = math + .children() + .iter() + .find_map(|child| match child { + ChildOfElement::Element(table) => Some(*table), + _ => None, + }) + .expect("test MathML should contain an mtable element"); assert_eq!(has_visible_table_line(table, boundary, axis), expected); return Ok(()); } From 8e3bfc99d567af6d491e72d16a2785996545262a Mon Sep 17 00:00:00 2001 From: NSoiffer Date: Sat, 15 Aug 2026 23:12:31 -0700 Subject: [PATCH 10/12] added a pause before separator announcements --- Rules/Languages/en/SharedRules/default.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Rules/Languages/en/SharedRules/default.yaml b/Rules/Languages/en/SharedRules/default.yaml index b6731845..b741a981 100644 --- a/Rules/Languages/en/SharedRules/default.yaml +++ b/Rules/Languages/en/SharedRules/default.yaml @@ -480,11 +480,15 @@ - x: "*" - test: if: "HasVisibleColumnLine(../.., count(preceding-sibling::*) + 1)" - then: [t: "separator"] + then: + - pause: medium + - t: "separator" - test: # Announce a row separator only after the row's final cell, so it is spoken once per horizontal boundary. if: "count(following-sibling::*) = 0 and HasVisibleRowLine(../.., count(../preceding-sibling::*) + 1)" - then: [t: "row separator"] + then: + - pause: medium + - t: "row 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 From 40a05bd3f78a8aeceffb994c3ece581175052f74 Mon Sep 17 00:00:00 2001 From: NSoiffer Date: Sat, 15 Aug 2026 23:18:16 -0700 Subject: [PATCH 11/12] Change pause duration from medium to short --- Rules/Languages/en/SharedRules/default.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Rules/Languages/en/SharedRules/default.yaml b/Rules/Languages/en/SharedRules/default.yaml index b741a981..264c1454 100644 --- a/Rules/Languages/en/SharedRules/default.yaml +++ b/Rules/Languages/en/SharedRules/default.yaml @@ -481,13 +481,13 @@ - test: if: "HasVisibleColumnLine(../.., count(preceding-sibling::*) + 1)" then: - - pause: medium + - pause: short - t: "separator" - test: # Announce a row separator only after the row's final cell, so it is spoken once per horizontal boundary. if: "count(following-sibling::*) = 0 and HasVisibleRowLine(../.., count(../preceding-sibling::*) + 1)" then: - - pause: medium + - pause: short - t: "row separator" - test: # short pause after each element; medium pause if last element in a row; long pause for last element in matrix From 249217f4c333d624585fe21475754aa4b97732e8 Mon Sep 17 00:00:00 2001 From: NSoiffer Date: Sat, 15 Aug 2026 23:20:34 -0700 Subject: [PATCH 12/12] Update test cases for matrix representations to add pause before separator --- tests/Languages/en/mtable.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/Languages/en/mtable.rs b/tests/Languages/en/mtable.rs index 76e75336..8a670988 100644 --- a/tests/Languages/en/mtable.rs +++ b/tests/Languages/en/mtable.rs @@ -271,8 +271,8 @@ fn augmented_matrix_2x3() -> Result<()> { ] "; - 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")?; + 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(()) } @@ -286,8 +286,8 @@ fn dashed_augmented_matrix_separator() -> Result<()> { ] "; - 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")?; + 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(()) } @@ -309,8 +309,8 @@ fn matrix_row_separator() -> Result<()> { ] "; - test("en", "ClearSpeak", expr, "the 2 by 2 matrix; row 1; 1, 2 row separator; row 2; 3, 4")?; - test("en", "SimpleSpeak", expr, "the 2 by 2 matrix; row 1; 1, 2 row separator; row 2; 3, 4")?; + test("en", "ClearSpeak", expr, "the 2 by 2 matrix; row 1; 1, 2, row separator; row 2; 3, 4")?; + test("en", "SimpleSpeak", expr, "the 2 by 2 matrix; row 1; 1, 2, row separator; row 2; 3, 4")?; Ok(()) } @@ -332,8 +332,8 @@ fn matrix_row_and_column_separators() -> Result<()> { ] "; - test("en", "ClearSpeak", expr, "the 2 by 2 augmented matrix; row 1; 1 separator, 2 row separator; row 2; 3 separator, 4")?; - test("en", "SimpleSpeak", expr, "the 2 by 2 augmented matrix; row 1; 1 separator, 2 row separator; row 2; 3 separator, 4")?; + test("en", "ClearSpeak", expr, "the 2 by 2 augmented matrix; row 1; 1, separator, 2, row separator; row 2; 3, separator, 4")?; + test("en", "SimpleSpeak", expr, "the 2 by 2 augmented matrix; row 1; 1, separator, 2, row separator; row 2; 3, separator, 4")?; Ok(()) } @@ -987,13 +987,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 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")?; + 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 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")?; + 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(()) }