Skip to content
Open
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
14 changes: 14 additions & 0 deletions Rules/Intent/general.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,20 @@
name: "system-of-equations"
children:
- x: "*"
-
name: mtable-array-property
tag: mtable
match: "count(*) > 0 and ((@frame='solid' or @frame='dashed') or child::*[@rowspan] or child::*/child::*[@rowspan or @colspan or @columnspan] or not(preceding-sibling::*[1][self::m:mo and text() != '\u2062']))"
replace:
- with:
variables:
- TableProperty: "'array'"
replace:
- intent:
name: "array"
children:
- x: "*"


-
name: mtable-lines-property
Expand Down
11 changes: 6 additions & 5 deletions Rules/Languages/en/SharedRules/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -419,22 +419,23 @@
- t: "end scripts" # phrase(At this point 'end scripts' occurs)

- name: default
tag: mtable
tag: [mtable, array]
variables:
- IsColumnSilent: "false()"
- NumColumns: "count(*[1]/*) - IfThenElse(*/self::m:mlabeledtr, 1, 0)"
- NumColumns: "CountTableColumns(.)"
- NumRows: "CountTableRows(.)"
match: "."
replace:
- t: "table with" # phrase(the 'table with' 3 rows)
- x: count(*)
- x: "$NumRows"
- test:
if: count(*)=1
if: "$NumRows=1"
then: [t: "row"] # phrase(the table with 1 'row')
else: [t: "rows"] # phrase(the table with 3 'rows')
- t: "and" # phrase(the table with 3 rows 'and' 4 columns)
- x: "$NumColumns"
- test:
if: "NumColumns=1"
if: "$NumColumns=1"
then: [t: "column"] # phrase(the table with 3 rows and 1 'column')
else: [t: "columns"] # phrase(the table with 3 rows and 4 'columns')
- pause: long
Expand Down
3 changes: 2 additions & 1 deletion src/speech.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::path::PathBuf;
use std::collections::HashMap;
use std::cell::{RefCell, RefMut};
use std::sync::LazyLock;
use std::fmt::Debug;
use sxd_document::dom::{ChildOfElement, Document, Element};
use sxd_document::{Package, QName};
use sxd_xpath::context::Evaluation;
Expand Down Expand Up @@ -311,7 +312,7 @@ pub fn process_include<F>(current_file: &Path, new_file_name: &str, mut read_new

/// As the name says, TreeOrString is either a Tree (Element) or a String
/// It is used to share code during pattern matching
pub trait TreeOrString<'c, 'm:'c, T> {
pub trait TreeOrString<'c, 'm:'c, T: Debug> : Debug {
fn from_element(e: Element<'m>) -> Result<T>;
fn from_string(s: String, doc: Document<'m>) -> Result<T>;
fn replace_tts<'s:'c, 'r>(tts: &TTS, command: &TTSCommandRule, prefs: &PreferenceManager, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T>;
Expand Down
195 changes: 192 additions & 3 deletions src/xpath_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use sxd_xpath::{Value, Context, context, function::*, nodeset::*};
use crate::definitions::{Definitions, SPEECH_DEFINITIONS, BRAILLE_DEFINITIONS};
use regex::Regex;
use crate::pretty_print::mml_to_string;
use std::cell::{Ref, RefCell};
use std::{cell::{Ref, RefCell}, collections::HashMap};
use log::{debug, error, warn};
use std::sync::LazyLock;
use std::thread::LocalKey;
Expand Down Expand Up @@ -333,7 +333,7 @@ static ALL_MATHML_ELEMENTS: phf::Set<&str> = phf_set!{
};

static MATHML_LEAF_NODES: phf::Set<&str> = phf_set! {
"mi", "mo", "mn", "mtext", "ms", "mspace", "mglyph",
"mi", "mo", "mn", "mtext", "ms", "mspace", "mglyph",
"none", "annotation", "ci", "cn", "csymbol", // content could be inside an annotation-xml (faster to allow here than to check lots of places)
};

Expand Down Expand Up @@ -1413,6 +1413,172 @@ impl Function for ReplaceAll {
}
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum CTDRowType {
Normal,
Labeled,
Implicit,
}

/// A single-use structure for computing the proper dimensions of an
/// `mtable`.
struct CountTableDims {
num_rows: usize,
num_cols: usize,
/// map from number of remaining in extra row-span to number of
/// columns with that value.
extended_cells: HashMap<usize, usize>,
/// rowspan=0 cells extend for the rest of the table, however
/// long that may be as determined by all other finite cells.
permanent_cols: usize,
}

impl CountTableDims {

fn new() -> CountTableDims {
Self { num_rows: 0, num_cols: 0, extended_cells: HashMap::new(), permanent_cols: 0 }
}

/// Returns the number of columns the cell contributes to the
/// current row. Also updates `extended_cells` as appropriate.
fn process_cell_in_row<'d>(&mut self, mtd: Element<'d>, is_first: bool, row_type: CTDRowType) -> usize {
// Rows can only contain `mtd`s. If this is not an `mtd`, we will just skip it.
if name(mtd) != "mtd" {
return 0;
}

// Add the contributing columns, taking colspan into account. Don't contribute if
// this is the first element of a labeled row.
let colspan = mtd.attribute_value("colspan")
.or_else(|| mtd.attribute_value("columnspan"))
.map_or(1, |e| e.parse::<usize>().unwrap_or(1));
if row_type == CTDRowType::Labeled && is_first {
// This is a label for the row and does not contibute to
// the size of the table. NOTE: Can this label have a
// non-trivial rowspan? If so, can it otherwise extend the
// size of the table?
return 0;
}

let rowspan = mtd.attribute_value("rowspan").map_or(1, |e| {
e.parse::<usize>().unwrap_or(1)
});

if rowspan > 1 {
*self.extended_cells.entry(rowspan).or_default() += colspan;
} else if rowspan == 0 {
self.permanent_cols += colspan;
}

colspan
}

/// Update the number of rows, and update the extended cells.
/// Returns the total number of columns accross all extended
/// cells.
fn next_row(&mut self) -> usize {
self.num_rows += 1;
let mut ext_cols = 0;
self.extended_cells = self.extended_cells.iter().filter(|&(k, _)| *k > 1).map(|(k, v)| {
ext_cols += *v;
(k-1, *v)
}).collect();
ext_cols
}

/// For an `mtable` element, count the number of rows and columns in the table.
///
/// 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> {
for child in e.children() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I missed it, but I don't think anywhere is checking
name(e) == "mtable"

let ChildOfElement::Element(row) = child else {
continue
};

// Each child of mtable should be an mtr or mlabeledtr. According to the spec, though,
// bare `mtd`s should also be treated as having an implicit wrapping `<mtr>`.
// Other elements should be ignored.
let row_name = name(row);

let row_type = if row_name == "mlabeledtr" {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if "if row_name== "..." then ... else if ... " or
let row_type = match row_name {
"mlabeledtr" => CTDRowType::Labeled,
"mtr" => CTDRowType::Normal,
"mtd" => CTDRowType::Implicit,
_ => continue,
};
is more idiomatic. It's fine to leave it as is, but I'm throwing that out as alternative as I think I've seen "match" used more often in Rust code.

CTDRowType::Labeled
} else if row_name == "mtr" {
CTDRowType::Normal
} else if row_name == "mtd" {
CTDRowType::Implicit
} else {
continue;
};

let ext_cols = self.next_row();

let mut num_cols_in_row = 0;
match row_type {
CTDRowType::Normal | CTDRowType::Labeled => {
let mut first_elem = true;
for row_child in row.children() {
let ChildOfElement::Element(mtd) = row_child else {
continue;
};

num_cols_in_row += self.process_cell_in_row(mtd, first_elem, row_type);
first_elem= false;
}
}
CTDRowType::Implicit => {
num_cols_in_row += self.process_cell_in_row(row, true, row_type)
}
}
// update the number of columns based on this row.
self.num_cols = self.num_cols.max(num_cols_in_row + ext_cols + self.permanent_cols);
}

// At this point, the number of columns is correct. If we have
// any leftover rows from rowspan extended cells, we need to
// account for them here.
//
// NOTE: It does not appear that renderers respect these extra
// 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)))
}

fn evaluate<'d>(self, fn_name: &str,
args: Vec<Value<'d>>) -> Result<(Value<'d>, Value<'d>), Error> {
let mut args = Args(args);
args.exactly(1)?;
let element = args.pop_nodeset()?;
let node = validate_one_node(element, fn_name)?;
if let Node::Element(e) = node {
return self.count_table_dims(e);
}

Err( Error::Other("Could not count dimensions of non-Element.".to_string()) )
}
}

struct CountTableRows;
impl Function for CountTableRows {
fn evaluate<'c, 'd>(&self,
_context: &context::Evaluation<'c, 'd>,
args: Vec<Value<'d>>) -> Result<Value<'d>, Error> {
CountTableDims::new().evaluate("CountTableRows", args).map(|a| a.0)
}
}

struct CountTableColumns;
impl Function for CountTableColumns {
fn evaluate<'c, 'd>(&self,
_context: &context::Evaluation<'c, 'd>,
args: Vec<Value<'d>>) -> Result<Value<'d>, Error> {
CountTableDims::new().evaluate("CountTableColumns", args).map(|a| a.1)
}
}


/// Add all the functions defined in this module to `context`.
pub fn add_builtin_functions(context: &mut Context) {
context.set_function("NestingChars", crate::braille::NemethNestingChars);
Expand All @@ -1432,6 +1598,8 @@ pub fn add_builtin_functions(context: &mut Context) {
context.set_function("SpeakIntentName", SpeakIntentName);
context.set_function("GetBracketingIntentName", GetBracketingIntentName);
context.set_function("GetNavigationPartName", GetNavigationPartName);
context.set_function("CountTableRows", CountTableRows);
context.set_function("CountTableColumns", CountTableColumns);
context.set_function("DEBUG", Debug);

// Not used: remove??
Expand Down Expand Up @@ -1606,6 +1774,27 @@ mod tests {

}

fn check_table_dims(mathml: &str, dims: (usize, usize)) {
let package = parser::parse(mathml).expect("failed to parse XML");
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))));
}

#[test]
fn table_dim() {
check_table_dims("<math><mtable><mtr><mtd>a</mtd></mtr></mtable></math>", (1, 1));
check_table_dims("<math><mtable><mtr><mtd colspan=\"3\">a</mtd><mtd>b</mtd></mtr><mtr><mtd></mtd></mtr></mtable></math>", (2, 4));

check_table_dims("<math><mtable><mlabeledtr><mtd>label</mtd><mtd>a</mtd><mtd>b</mtd></mlabeledtr><mtr><mtd>c</mtd><mtd>d</mtd></mtr></mtable></math>", (2, 2));
// extended rows beyond the `mtr`s do *not* count towards the row count.
check_table_dims("<math><mtable><mtr><mtd rowspan=\"3\">a</mtd></mtr></mtable></math>", (1, 1));

check_table_dims("<math><mtable><mtr><mtd rowspan=\"3\">a</mtd></mtr>
<mtr><mtd columnspan=\"2\">b</mtd></mtr></mtable></math>", (2, 3));

}

#[test]
fn at_left_edge() {
let mathml = "<math><mfrac><mrow><mn>30</mn><mi>x</mi></mrow><mn>4</mn></mfrac></math>";
Expand Down Expand Up @@ -1636,4 +1825,4 @@ mod tests {
let mn = as_element(as_element(fraction.children()[1]).children()[0]);
assert_eq!(EdgeNode::edge_node(mn, true, "2D"), None);
}
}
}
35 changes: 25 additions & 10 deletions tests/Languages/en/mtable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1028,9 +1028,8 @@ let expr = "<math><mrow><mrow><mo>(</mo><mrow>
test_ClearSpeak("en", "ClearSpeak_Matrix", "EndVector",
expr, "the 2 by 2 matrix; row 1; column 1; b sub 1 1; column 2; b sub 1 2; \
row 2; column 1; b sub 2 1; column 2; b sub 2 2; end matrix")?;
return Ok(());
}

return Ok(());
}


#[test]
Expand All @@ -1041,19 +1040,35 @@ fn matrix_binomial() -> Result<()> {
</mrow><mo>)</mo>
</math>";
test_ClearSpeak("en", "ClearSpeak_Matrix", "Combinatorics", expr, "3 choose 2")?;
return Ok(());
}
return Ok(());
}

#[test]
fn matrix_simple_table() {
let expr = "<math>
<mtable intent=\":array\"><mtr><mtd><mn>3</mn></mtd></mtr><mtr><mtd><mn>2</mn></mtd></mtr></mtable>
</math>";
let _ = test("en", "ClearSpeak", expr, "table with 2 rows and 1 column; row 1; column 1; 3; row 2; column 1; 2");
}

#[test]
fn matrix_span_table() {
let expr = "<math>
<mtable><mtr rowspan=\"1\"><mtd><mn>3</mn></mtd></mtr><mtr><mtd><mn>2</mn></mtd></mtr></mtable>
</math>";
let _ = test("en", "ClearSpeak", expr, "table with 2 rows and 1 column; row 1; column 1; 3; row 2; column 1; 2");
}


#[test]
fn matrix_times() -> Result<()> {
fn matrix_times() {
let expr = "<math>
<mfenced><mtable><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></mfenced>
<mfenced><mtable><mtr><mtd><mi>a</mi></mtd><mtd><mi>b</mi></mtd></mtr><mtr><mtd><mi>c</mi></mtd><mtd><mi>d</mi></mtd></mtr></mtable></mfenced>
</math>";
test("en", "SimpleSpeak", expr,
"the 2 by 2 matrix; row 1; 1, 2; row 2; 3, 4; times, the 2 by 2 matrix; row 1; eigh, b; row 2; c, d")?;
return Ok(());
}
let _ = test("en", "SimpleSpeak", expr,
"the 2 by 2 matrix; row 1; 1, 2; row 2; 3, 4; times, the 2 by 2 matrix; row 1; eigh, b; row 2; c, d");
}

#[test]
fn unknown_mtable_property() -> Result<()> {
Expand Down
Loading