Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Rules/Languages/en/SharedRules/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,17 @@
- x: "count(preceding-sibling::*)+IfThenElse(parent::m:mlabeledtr, 0, 1)"
- pause: medium
- x: "*"
- test:
if: "HasVisibleColumnLine(../.., count(preceding-sibling::*) + 1)"
then:
- 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: 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
- if: count(following-sibling::*) > 0
Expand Down
2 changes: 1 addition & 1 deletion src/speech.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1761,7 +1761,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() {
Expand Down
190 changes: 188 additions & 2 deletions src/xpath_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1646,9 +1646,86 @@ impl Function for CountTableColumns {
}
}

#[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 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(row_count), Value::Number(column_count))) = CountTableDims::new().count_table_dims(table) else {
return false;
};
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(attribute_name)
.map(|values| {
matches!(
values.split_whitespace().take(boundary).last(),
Some("solid" | "dashed")
)
})
.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<Value<'d>>,
function_name: &str,
axis: TableLineAxis,
) -> Result<Value<'d>, 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<Value<'d>>) -> Result<Value<'d>, Error> {
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<Value<'d>>) -> Result<Value<'d>, Error> {
evaluate_has_visible_table_line(args, "HasVisibleRowLine", TableLineAxis::Row)
}
}


/// 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);
Expand All @@ -1671,6 +1748,8 @@ 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("HasVisibleRowLine", HasVisibleRowLine);
context.set_function("DEBUG", Debug);

// Not used: remove??
Expand Down Expand Up @@ -1876,7 +1955,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( () );
}

Expand All @@ -1896,6 +1978,110 @@ 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 = 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(());
}

/// Verifies visible table-line styles, repeated styles, and boundaries outside the table.
#[test]
fn visible_table_lines() -> Result<()> {
return xpath_test(|| {
// The three values map in order to the three boundaries between four columns.
let mixed_column_lines: &str = "<math>
<mtable columnlines=' none solid dashed '>
<mtr>
<mtd>column 1</mtd>
<mtd>column 2</mtd>
<mtd>column 3</mtd>
<mtd>column 4</mtd>
</mtr>
</mtable></math>";
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 = "<math>
<mtable columnlines='solid dashed'>
<mtr>
<mtd>column 1</mtd>
<mtd>column 2</mtd>
<mtd>column 3</mtd>
<mtd>column 4</mtd>
</mtr>
</mtable></math>";
check_table_line(repeated_column_line, 3, TableLineAxis::Column, true)?;

// A table without `columnlines` has no visible column boundary.
let no_column_lines: &str = "<math>
<mtable>
<mtr>
<mtd>column 1</mtd>
<mtd>column 2</mtd>
</mtr>
</mtable></math>";
check_table_line(no_column_lines, 1, TableLineAxis::Column, false)?;

// `none` explicitly makes the column boundary invisible.
let invisible_column_line: &str = "<math>
<mtable columnlines='none'>
<mtr>
<mtd>column 1</mtd>
<mtd>column 2</mtd>
</mtr>
</mtable></math>";
check_table_line(invisible_column_line, 1, TableLineAxis::Column, false)?;

// Only `solid` and `dashed` describe visible table lines.
let unsupported_column_line: &str = "<math>
<mtable columnlines='double'>
<mtr>
<mtd>column 1</mtd>
<mtd>column 2</mtd>
</mtr>
</mtable></math>";
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 = "<math><mtable columnlines='solid'>
<mtr>
<mtd>column 1</mtd>
<mtd>column 2</mtd>
</mtr>
</mtable></math>";
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 = "<math>
<mtable rowlines='none dashed'>
<mtr><mtd>row 1</mtd></mtr>
<mtr><mtd>row 2</mtd></mtr>
<mtr><mtd>row 3</mtd></mtr>
<mtr><mtd>row 4</mtd></mtr>
</mtable></math>";
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(());
});
}

#[test]
fn at_left_edge() -> Result<()> {
return xpath_test(|| {
Expand Down
77 changes: 69 additions & 8 deletions tests/Languages/en/mtable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,69 @@ fn augmented_matrix_2x3() -> Result<()> {
<mo>]</mo></mrow></mrow>
</math>
";
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 = "
<math xmlns='http://www.w3.org/1998/Math/MathML'>
<mrow><mo>[</mo>
<mtable columnlines='dashed'>
<mtr><mtd><mn>1</mn></mtd><mtd><mn>2</mn></mtd><mtd><mn>3</mn></mtd></mtr>
</mtable>
<mo>]</mo></mrow>
</math>";
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(())
}

/// A horizontal line is announced once, after the row it separates from the next row.
#[test]
fn matrix_row_separator() -> Result<()> {
let expr = "
<math xmlns='http://www.w3.org/1998/Math/MathML'>
<mrow><mo>[</mo>
<mtable rowlines='solid'>
<mtr>
<mtd><mn>1</mn></mtd>
<mtd><mn>2</mn></mtd>
</mtr>
<mtr>
<mtd><mn>3</mn></mtd>
<mtd><mn>4</mn></mtd>
</mtr>
</mtable>
<mo>]</mo></mrow>
</math>";
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 = "
<math xmlns='http://www.w3.org/1998/Math/MathML'>
<mrow><mo>[</mo>
<mtable rowlines='dashed' columnlines='solid'>
<mtr>
<mtd><mn>1</mn></mtd>
<mtd><mn>2</mn></mtd>
</mtr>
<mtr>
<mtd><mn>3</mn></mtd>
<mtd><mn>4</mn></mtd>
</mtr>
</mtable>
<mo>]</mo></mrow>
</math>";
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(())
}

Expand Down Expand Up @@ -926,13 +987,13 @@ let expr = "<math display='block' xmlns='http://www.w3.org/1998/Math/MathML'>
</mrow>
</math>";
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(())
}

Expand Down