From d51f2b1c5e65732d1d4a1206580c692d0e18e8b7 Mon Sep 17 00:00:00 2001
From: Danil <81031453+Kostenkov-2021@users.noreply.github.com>
Date: Fri, 30 Oct 2026 11:29:34 +0300
Subject: [PATCH 1/5] Preserve tokens and test W3C intent speech
Prevent wrapper cleanup from dropping split function names, correct the `re` definition, and add bilingual speech and navigation coverage for W3C calculus and general intents.
---
Rules/definitions.yaml | 2 +-
src/canonicalize.rs | 27 ++++++++
tests/Languages/intent.rs | 14 +++-
tests/Languages/intent/calculus.rs | 64 +++++++++++++++++
tests/Languages/intent/general.rs | 106 ++++++++++++++++++-----------
5 files changed, 173 insertions(+), 40 deletions(-)
diff --git a/Rules/definitions.yaml b/Rules/definitions.yaml
index 07681390c..307395601 100644
--- a/Rules/definitions.yaml
+++ b/Rules/definitions.yaml
@@ -36,7 +36,7 @@
"arccot", "arccotan", "arccotanh", "arccoth",
"arccsc", "arccsch", "arcsec", "arcsech", "arcsin", "arcsinh",
"arctan", "arctang", "arctanh",
- "exp", "Exp", "expt", "arg", "im", "Im", "ℑ", re", "Re", "ℜ", "Pr", "cis",
+ "exp", "Exp", "expt", "arg", "im", "Im", "ℑ", "re", "Re", "ℜ", "Pr", "cis",
"gcd", "lcm",
"min", "max", "deg", "det", "rank", "rg", "sgn",
"dim", "hom", "ker", "tr", "rot", "div",
diff --git a/src/canonicalize.rs b/src/canonicalize.rs
index 1e3936f8d..560a7067a 100644
--- a/src/canonicalize.rs
+++ b/src/canonicalize.rs
@@ -1254,6 +1254,11 @@ impl CanonicalizeContext {
if element_name == "mrow" && children.len() == 1 && CanonicalizeContext::is_ok_to_merge_mrow_child(mathml) {
// "lift" the child up so all the links (e.g., siblings) are correct
let child = as_element(children[0]);
+ // The synthesized wrapper can disappear, but its original token must
+ // not inherit a marker that chemistry cleanup uses to delete added leaves.
+ if is_leaf(child) && mathml.attribute_value(CHANGED_ATTR).as_deref() == Some(ADDED_ATTR_VALUE) {
+ mathml.remove_attribute(CHANGED_ATTR);
+ }
mathml.replace_children(child.children());
set_mathml_name(mathml, as_str!(name(child)));
add_attrs(mathml, &child.attributes());
@@ -4719,6 +4724,28 @@ mod canonicalize_tests {
are_strs_canonically_equal_result(target_str, target_str, &[])
}
+ #[test]
+ fn mstyle_split_function_preserves_content() -> Result<()> {
+ // A style wrapper must preserve a split function name when its mi children merge.
+ crate::interface::set_rules_dir(abs_rules_dir_path())?;
+ crate::interface::set_preference("Language", "ru")?;
+ let canonical = crate::interface::set_mathml("")?;
+ assert!(canonical.contains(">Re"), "{canonical}");
+ assert!(canonical.contains(">Im"), "{canonical}");
+ Ok(())
+ }
+
+ #[test]
+ fn mpadded_split_function_preserves_content() -> Result<()> {
+ // mpadded follows the same wrapper cleanup path; chemistry rollback must keep both names.
+ crate::interface::set_rules_dir(abs_rules_dir_path())?;
+ crate::interface::set_preference("Language", "ru")?;
+ let canonical = crate::interface::set_mathml("")?;
+ assert!(canonical.contains(">Re"), "{canonical}");
+ assert!(canonical.contains(">Im"), "{canonical}");
+ Ok(())
+ }
+
#[test]
fn plane1_common() -> Result<()> {
let test_str = "";
test_intent(mathml, intent, vec![])?;
+ // The W3C alias reuses binomial speech without changing inferred binomial semantics.
+ test_core_speech("73", "7 choose 3", "число сочетаний из 7 по 3")?;
return Ok(());
}
@@ -115,52 +117,80 @@ fn evaluated_at() -> Result<()> {
}
+// W3C core speech cases complement the inference checks above.
+use super::test_core_speech;
+
#[test]
-fn child_intent_blocks_ratio() -> Result<()> {
- // The intent on the ∶ says it is not a ratio colon, so the mrow must not be inferred as a ratio.
- let expr = "
-
- 20
- ∶
- 5
-
- ";
- test("en", "SimpleSpeak", expr, "20 divides 5")?;
- return Ok(());
+fn w3c_repeating_decimal_one_argument() -> Result<()> {
+ // W3C supplies the period alone; a missing second child must never cause an XPath error.
+ test_core_speech("45_", "repeating decimal 4 5", "период 4 5")?;
+ test_core_speech("", "repeating decimal 4 5", "период 4 5")
}
#[test]
-fn child_intent_supplies_ratio() -> Result<()> {
- // The intent on the operator says it is a ∶, so the mrow is inferred as a ratio as if ∶ had been written.
- let expr = "
-
- 20
- ÷
- 5
-
- ";
- let target = "
-
- 20
- 5
-
- ";
- test_intent(expr, target, vec![])?;
- return Ok(());
+fn w3c_repeating_decimal_leading_zeroes() -> Result<()> {
+ // A period is a digit sequence: 003 must retain both leading zeroes rather than become 3.
+ test_core_speech("003¯", "repeating decimal 0 0 3", "период 0 0 3")
}
#[test]
-fn child_intent_supplies_number_set() -> Result<()> {
- // The intent on the base says it is ℝ, so the msup is inferred as a number set as if ℝ had been written.
- let expr = "R+";
- test("en", "SimpleSpeak", expr, "the positive double-struck cap r")?;
- return Ok(());
+fn w3c_repeating_decimal_legacy_arguments() -> Result<()> {
+ // Keep the existing prefix/period extension used by automatic inference.
+ test_core_speech("12003", "12 with repeating digits 0 0 3", "12 с повторяющимися цифрами 0 0 3")?;
+ test_core_speech("1245¯", "12 with repeating digits 4 5", "12 с повторяющимися цифрами 4 5")
}
#[test]
-fn child_intent_blocks_number_set() -> Result<()> {
- // The intent on the base says it is not ℝ, so the msup must not be inferred as a number set.
- let expr = "ℝ+";
- test("en", "SimpleSpeak", expr, "foo superscript plus")?;
- return Ok(());
+fn w3c_repeating_decimal_complete_w3c_example() -> Result<()> {
+ // The W3C surrounding decimal must retain its prefix as well as both period digits.
+ let mathml="0.045_";
+ for (language,expected) in [("en","0.0 with repeating digits 4 5"),("ru","0.0 с повторяющимися цифрами 4 5")] {
+ for style in ["ClearSpeak","SimpleSpeak"] {
+ for verbosity in ["Terse","Medium","Verbose"] {
+ test_prefs(language,style,vec![("Verbosity",verbosity),("IntentErrorRecovery","Error"),("DecimalSeparators","."),("BlockSeparators",", ")],mathml,expected)?;
+ }
+ }
+ }
+ Ok(())
+}
+
+#[test]
+fn w3c_repeating_decimal_semantic_navigation() -> Result<()> {
+ // Semantic reading must use the same one-argument rule when revisiting the current expression.
+ for (language,expected) in [("en","repeating decimal 4 5"),("ru","период 4 5")] {
+ for style in ["ClearSpeak","SimpleSpeak"] {
+ init_nav(language,"")?;
+ set_preference("SpeechStyle",style)?;
+ let speech=do_navigate_command("ReadCurrent")?;
+ assert!(speech.contains(expected),"{language}/{style}: {speech}");
+ }
+ }
+ Ok(())
+}
+
+#[test]
+fn w3c_blank_intent_representations() -> Result<()> {
+ // The semantic blank is independent of its visible spelling, including a space-only blank.
+ test_core_speech("__", "blank", "пустое место")?;
+ test_core_speech("?", "blank", "пустое место")?;
+ test_core_speech("", "blank", "пустое место")
+}
+
+#[test]
+fn w3c_invisible_times_keeps_operands() -> Result<()> {
+ // W3C assigns silent fixity: suppress the operator name, never its operands.
+ test_core_speech("xy", "x y", "икс игрек")
+}
+
+#[test]
+fn w3c_invisible_separator_keeps_operands() -> Result<()> {
+ // A silent separator must not add the word 'comma' or drop any listed operand.
+ test_core_speech("xyz", "x y z", "икс игрек зэт")
+}
+
+#[test]
+fn w3c_invisible_operator_leaves_are_silent() -> Result<()> {
+ // Explicit annotations on the Unicode operator itself have no spoken name or argument.
+ test_core_speech("", "", "")?;
+ test_core_speech("", "", "")
}
From cb36a11b2b65c3389d830522023be9a2d3a90c97 Mon Sep 17 00:00:00 2001
From: Danil <81031453+Kostenkov-2021@users.noreply.github.com>
Date: Fri, 30 Oct 2026 11:45:09 +0300
Subject: [PATCH 2/5] Add English rules for W3C intents
Map definite integrals, derivatives, blanks, and invisible operators to English speech, and support both W3C and inferred repeating-decimal intent forms.
---
Rules/Languages/en/SharedRules/general.yaml | 55 +++++++++++++++++++--
Rules/Languages/en/definitions.yaml | 5 ++
2 files changed, 56 insertions(+), 4 deletions(-)
diff --git a/Rules/Languages/en/SharedRules/general.yaml b/Rules/Languages/en/SharedRules/general.yaml
index 832384da4..d256e8846 100644
--- a/Rules/Languages/en/SharedRules/general.yaml
+++ b/Rules/Languages/en/SharedRules/general.yaml
@@ -1,5 +1,49 @@
---
+# W3C definite-integral arguments are the integration domain or the two bounds, not the sign.
+- name: definite-integral-intent
+ tag: definite-integral
+ match: "(count(*)=1 or count(*)=2) and not(contains(@data-intent-property, ':silent:'))"
+ replace:
+ - bookmark: "@id"
+ - t: "integral"
+ - test:
+ if: "count(*)=1"
+ then: [t: "over", x: "*[1]"]
+ else: [t: "from", x: "*[1]", t: "to", x: "*[2]"]
+
+# A semantic reading also works for prime notation; do not assume a Leibniz fraction.
+- name: derivative-intent
+ tag: derivative
+ match: "count(*)=3 and not(contains(@data-intent-property, ':silent:'))"
+ replace:
+ - bookmark: "@id"
+ - t: "the"
+ - test:
+ - if: "*[3][self::m:mn and .='1']"
+ then: [t: "derivative of"]
+ - else_if: "*[3][self::m:mn and string-length(.)>0 and translate(., '0123456789', '')='']"
+ then: [x: "ToOrdinal(*[3])", t: "derivative of"]
+ else: [t: "derivative of order", x: "*[3]", t: "of"]
+ - x: "*[1]"
+ - t: "with respect to"
+ - x: "*[2]"
+
+- name: blank-intent
+ tag: blank
+ match: "count(*)=0"
+ replace: [bookmark: "@id", t: "blank"]
+
+# Preserve operands while suppressing the default-silent operator, including a bare operator leaf.
+- name: invisible-operator-intent
+ tag: [invisible-times, invisible-separator]
+ match: "not(@data-intent-property) or contains(@data-intent-property, ':silent:')"
+ replace:
+ - test:
+ if: "*"
+ then: [x: "*"]
+ else: [t: ""]
+
# number-sets are a little messy in that the base was converted to a number-set, so we have to match that (simple) case last
- name: pos-neg-number-sets
tag: number-sets
@@ -190,13 +234,16 @@
- t: "of" # phrase(the square root 'of' 25 equals 5)
- x: "*[2]"
+# W3C supplies the period alone; inferred MathCAT intents also carry a non-repeating prefix.
- name: repeating-decimal
tag: repeating-decimal
- match: "."
+ match: "(count(*)=1 or count(*)=2) and not(contains(@data-intent-property, ':silent:'))"
replace:
- - x: "*[1]"
- - t: "with repeating digits" # phrase('with repeating digits')
- - spell: "*[2]"
+ - test:
+ if: "count(*)=2"
+ then: [x: "*[1]", t: "with repeating digits"]
+ else: [t: "repeating decimal"]
+ - spell: "*[last()]"
- name: msubsup-skip-super
# handles single, double, etc., prime
diff --git a/Rules/Languages/en/definitions.yaml b/Rules/Languages/en/definitions.yaml
index fc18be07f..671decd06 100644
--- a/Rules/Languages/en/definitions.yaml
+++ b/Rules/Languages/en/definitions.yaml
@@ -79,6 +79,11 @@
# "ratio": "function=ratio : to | the ratio : to | the ratio : to" # verbosity of name:glue pairs
#
- IntentMappings: {
+ "blank": "nofix=blank",
+ "definite-integral": "other=integral",
+ "derivative": "other=derivative",
+ "invisible-separator": "silent=",
+ "invisible-times": "silent=",
"binomial": "infix=binomial; choose; end binomial",
### Functions and Inverses
From 7b93b936ff646e37215358a72b21da5b9d9beb30 Mon Sep 17 00:00:00 2001
From: Danil <81031453+Kostenkov-2021@users.noreply.github.com>
Date: Mon, 2 Nov 2026 01:48:21 +0300
Subject: [PATCH 3/5] Remove redundant intent mappings
Drop explicit mappings for blank, integral, derivative, and invisible MathML operators from the English definitions, along with obsolete commented calculus mappings.
---
Rules/Languages/en/definitions.yaml | 7 -------
1 file changed, 7 deletions(-)
diff --git a/Rules/Languages/en/definitions.yaml b/Rules/Languages/en/definitions.yaml
index 671decd06..d40cba061 100644
--- a/Rules/Languages/en/definitions.yaml
+++ b/Rules/Languages/en/definitions.yaml
@@ -79,11 +79,6 @@
# "ratio": "function=ratio : to | the ratio : to | the ratio : to" # verbosity of name:glue pairs
#
- IntentMappings: {
- "blank": "nofix=blank",
- "definite-integral": "other=integral",
- "derivative": "other=derivative",
- "invisible-separator": "silent=",
- "invisible-times": "silent=",
"binomial": "infix=binomial; choose; end binomial",
### Functions and Inverses
@@ -126,8 +121,6 @@
### Calculus
- # "definite-integral":"function=integral over || function=integral from; to", # Property ???
- # "derivative":"function=the derivative; with respect to", # Property ???
# "partial-derivative":"function=partial", ## Note, included with infix, but separately under calculus tab has ??? for functionality
"limit": "prefix=limit as|the limit as",
From 8521388d13049e2cb82a3c203d94989818f6f221 Mon Sep 17 00:00:00 2001
From: Danil <81031453+Kostenkov-2021@users.noreply.github.com>
Date: Mon, 2 Nov 2026 01:55:53 +0300
Subject: [PATCH 4/5] Clarify delegated speech intent mappings
Document which speech intents are handled by shared or style-specific rules, and clarify why their duplicate definitions remain commented out.
---
Rules/Languages/en/definitions.yaml | 25 ++++++++++++++++++++-----
1 file changed, 20 insertions(+), 5 deletions(-)
diff --git a/Rules/Languages/en/definitions.yaml b/Rules/Languages/en/definitions.yaml
index d40cba061..9bc650629 100644
--- a/Rules/Languages/en/definitions.yaml
+++ b/Rules/Languages/en/definitions.yaml
@@ -82,14 +82,16 @@
"binomial": "infix=binomial; choose; end binomial",
### Functions and Inverses
- # Interval intents (open-/closed-interval, etc.) are handled by rules due to special cases involving ∞.
+ # Speech rules for open-interval, open-closed-interval, closed-interval, and closed-open-interval
+ # are defined elsewhere: SharedRules/general.yaml (intervals).
"inverse":"function=inverse || postfix=inverse",
"domain": "function= ; domain",
"codomain": "function= ; codomain",
"image":"function=image",
- #"fraction":"function=fraction; over; end fraction", # NOTE: Fails
+ # Speech rules defined elsewhere: ClearSpeak_Rules.yaml and SimpleSpeak_Rules.yaml.
+ # "fraction":"function=fraction; over; end fraction",
"mixed-fraction":"infix=and", # NOTE: in website says function, but follow infix speech pattern.
"quotient":"function=integer part: divided by",
"evaluated-at":"infix=evaluated at",
@@ -121,7 +123,11 @@
### Calculus
- # "partial-derivative":"function=partial", ## Note, included with infix, but separately under calculus tab has ??? for functionality
+ # Speech rules defined elsewhere: SharedRules/general.yaml (definite-integral-intent, derivative-intent).
+ # "definite-integral":"function=integral over || function=integral from; to",
+ # "derivative":"function=the derivative; with respect to",
+ # The active prefix mapping for partial-derivative is in Other below.
+ # "partial-derivative":"function=partial",
"limit": "prefix=limit as|the limit as",
"tends-to":"infix=tends to",
@@ -226,9 +232,14 @@
"point":"prefix=point", ## NOTE: Has ??? for property in site. Should it be prefix? Or something else.
### Separators
+ # Speech rules defined elsewhere: SharedRules/general.yaml (invisible-operator-intent).
+ # "invisible-separator": "silent=",
+ # "invisible-times": "silent=",
"time-separator":"infix=",
### General Concepts
+ # Speech rules defined elsewhere: SharedRules/general.yaml (blank-intent).
+ # "blank": "nofix=blank",
"fenced-group":"function=fenced-group", # appears both under general concepts and grouping
# NOTE: in site mentions "the pair x and y", due to being defined as function, needs the "of" keyword
"ordered-pair": "function=the pair: and", # Needs to be tested
@@ -249,7 +260,8 @@
### Grouping
"annotation":"infix= ; which is ;", # NOTE: Follows the same order as indexed-by, in site listed as function. Should it be infix?
"braced-group":"function=grouped; end grouped", # NOTE: not tested, site is missing "of" keyword function introduces
- #"repeating-decimal":"function=repeating decimal", # NOTE: BREAKS TEST. Check again. Site is missing "of" keyword function introduces.
+ # Speech rules defined elsewhere: SharedRules/general.yaml (repeating-decimal).
+ # "repeating-decimal":"function=repeating decimal",
### Other
@@ -360,11 +372,14 @@
"modified-variable": "silent= ",
"say-super": "infix=super| superscript| superscript", # used with 'mo' for superscripts (e.g, "<")
"skip-super": "silent=", # used with 'mo' for superscripts (e.g, "*")
+ # Speech rules defined elsewhere: SharedRules/general.yaml (bigop-both, bigop-under).
# "large-op": "infix=over || other=from,to",
"lim-sup": "prefix=lim sup as| the limit superior as| the limit superior as",
"lim-inf": "prefix=lim inf as| the limit inferior as| the limit inferior as",
"logarithm-with-base": "prefix=log base| the log base| the log base",
- # "pochhammer": "infix=permutations of", # arguments are in reverse order, so can't work here
+ # Infix speech rules defined elsewhere: SharedRules/general.yaml (permutation) reverses the arguments.
+ # The active function mapping above handles the other fixity.
+ # "pochhammer": "infix=permutations of",
"trace": "function= ; trace| trace| the trace; end trace",
"dimension": "function=; dimension| dimension| the dimension; end dimension",
"homomorphism": "function= ; homomorphism| homomorphism| the homomorphism; end homomorphism",
From eb8b44b430b9c2fdf5793cf2fb8e68087448ce33 Mon Sep 17 00:00:00 2001
From: Danil <81031453+Kostenkov-2021@users.noreply.github.com>
Date: Mon, 2 Nov 2026 12:30:20 +0300
Subject: [PATCH 5/5] Expand W3C intent speech tests by language
Move W3C intent speech coverage into English and Russian test suites, exercising both speech styles, all verbosity levels, and semantic navigation. Set a stable English language baseline in intent tests to avoid host-locale-dependent fixities.
---
tests/Languages/en/intent.rs | 2 +
tests/Languages/en/intent/w3c.rs | 148 +++++++++++++++++++++++++++++
tests/Languages/intent.rs | 12 ---
tests/Languages/intent/calculus.rs | 64 -------------
tests/Languages/intent/general.rs | 80 ----------------
tests/Languages/ru/intent.rs | 2 +
tests/Languages/ru/intent/w3c.rs | 148 +++++++++++++++++++++++++++++
tests/common/mod.rs | 3 +
8 files changed, 303 insertions(+), 156 deletions(-)
create mode 100644 tests/Languages/en/intent/w3c.rs
create mode 100644 tests/Languages/ru/intent/w3c.rs
diff --git a/tests/Languages/en/intent.rs b/tests/Languages/en/intent.rs
index 795854781..10f93494a 100644
--- a/tests/Languages/en/intent.rs
+++ b/tests/Languages/en/intent.rs
@@ -142,3 +142,5 @@ fn intent_prob_x() -> Result<()> {
return Ok(());
}
+
+mod w3c;
diff --git a/tests/Languages/en/intent/w3c.rs b/tests/Languages/en/intent/w3c.rs
new file mode 100644
index 000000000..0f4d8190f
--- /dev/null
+++ b/tests/Languages/en/intent/w3c.rs
@@ -0,0 +1,148 @@
+use crate::common::*;
+use anyhow::Result;
+
+// Check this language's W3C speech in both styles and at every verbosity.
+fn test_w3c_speech(mathml: &str, expected: &str) -> Result<()> {
+ for style in ["ClearSpeak", "SimpleSpeak"] {
+ for verbosity in ["Terse", "Medium", "Verbose"] {
+ test_prefs("en", style, vec![("Verbosity", verbosity), ("IntentErrorRecovery", "Error")], mathml, expected)?;
+ }
+ }
+ Ok(())
+}
+
+#[test]
+fn w3c_repeating_decimal_one_argument() -> Result<()> {
+ // W3C supplies the period alone; a missing second child must never cause an XPath error.
+ test_w3c_speech("45_", "repeating decimal 4 5")?;
+ test_w3c_speech("", "repeating decimal 4 5")
+}
+
+#[test]
+fn w3c_repeating_decimal_leading_zeroes() -> Result<()> {
+ // A period is a digit sequence: 003 must retain both leading zeroes rather than become 3.
+ test_w3c_speech("003¯", "repeating decimal 0 0 3")
+}
+
+#[test]
+fn w3c_repeating_decimal_legacy_arguments() -> Result<()> {
+ // Keep the existing prefix/period extension used by automatic inference.
+ test_w3c_speech("12003", "12 with repeating digits 0 0 3")?;
+ test_w3c_speech("1245¯", "12 with repeating digits 4 5")
+}
+
+#[test]
+fn w3c_repeating_decimal_complete_w3c_example() -> Result<()> {
+ // The W3C surrounding decimal must retain its prefix as well as both period digits.
+ let mathml="0.045_";
+ let (language,expected) = ("en","0.0 with repeating digits 4 5");
+ for style in ["ClearSpeak","SimpleSpeak"] {
+ for verbosity in ["Terse","Medium","Verbose"] {
+ test_prefs(language,style,vec![("Verbosity",verbosity),("IntentErrorRecovery","Error"),("DecimalSeparators","."),("BlockSeparators",", ")],mathml,expected)?;
+ }
+ }
+ Ok(())
+}
+
+#[test]
+fn w3c_repeating_decimal_semantic_navigation() -> Result<()> {
+ // Semantic reading must use the same one-argument rule when revisiting the current expression.
+ let (language,expected) = ("en","repeating decimal 4 5");
+ for style in ["ClearSpeak","SimpleSpeak"] {
+ init_nav(language,"")?;
+ set_preference("SpeechStyle",style)?;
+ let speech=do_navigate_command("ReadCurrent")?;
+ assert!(speech.contains(expected),"{language}/{style}: {speech}");
+ }
+ Ok(())
+}
+
+#[test]
+fn w3c_blank_intent_representations() -> Result<()> {
+ // The semantic blank is independent of its visible spelling, including a space-only blank.
+ test_w3c_speech("__", "blank")?;
+ test_w3c_speech("?", "blank")?;
+ test_w3c_speech("", "blank")
+}
+
+#[test]
+fn w3c_invisible_times_keeps_operands() -> Result<()> {
+ // W3C assigns silent fixity: suppress the operator name, never its operands.
+ test_w3c_speech("xy", "x y")
+}
+
+#[test]
+fn w3c_invisible_separator_keeps_operands() -> Result<()> {
+ // A silent separator must not add the word 'comma' or drop any listed operand.
+ test_w3c_speech("xyz", "x y z")
+}
+
+#[test]
+fn w3c_invisible_operator_leaves_are_silent() -> Result<()> {
+ // Explicit annotations on the Unicode operator itself have no spoken name or argument.
+ test_w3c_speech("", "")?;
+ test_w3c_speech("", "")
+}
+
+#[test]
+fn w3c_definite_integral_domain() -> Result<()> {
+ // The single W3C argument is an integration domain, not an integrand or integral sign.
+ test_w3c_speech("∫x", "integral over x")
+}
+
+#[test]
+fn w3c_definite_integral_bounds() -> Result<()> {
+ // Two W3C arguments are lower and upper bounds, in that order; neither may disappear.
+ test_w3c_speech("∫01", "integral from 0 to 1")
+}
+
+#[test]
+fn w3c_definite_integral_infinite_bounds() -> Result<()> {
+ // Infinite bounds must retain both signs and endpoint values.
+ let mathml="∫−∞∞";
+ test_w3c_speech(mathml, "integral from negative infinity to infinity")
+}
+
+#[test]
+fn w3c_derivative_leibniz_second_order() -> Result<()> {
+ // The three arguments denote function, variable, and order, regardless of fraction layout.
+ let mathml = "d2ydx2";
+ test_w3c_speech(mathml, "the second derivative of y with respect to x")
+}
+
+#[test]
+fn w3c_derivative_prime_first_order() -> Result<()> {
+ // Prime notation needs the same semantic meaning, without assuming a spoken Leibniz fraction.
+ let mathml = "f′(x)";
+ test_w3c_speech(mathml, "the derivative of f with respect to x")
+}
+
+#[test]
+fn w3c_derivative_symbolic_order() -> Result<()> {
+ // Symbolic orders need an order phrase, not a numeric-ordinal conversion of the letter n.
+ let mathml="fxn";
+ test_w3c_speech(mathml, "the derivative of order n of f with respect to x")
+}
+
+#[test]
+fn w3c_calculus_intent_read_current() -> Result<()> {
+ // Semantic navigation must use the same integral and derivative readings as full speech.
+ let (language, integral, derivative) = ("en", "integral from 0 to 1", "the derivative of f with respect to x");
+ for style in ["ClearSpeak", "SimpleSpeak"] {
+ init_nav(language, "∫01")?;
+ set_preference("SpeechStyle", style)?;
+ let speech=do_navigate_command("ReadCurrent")?.split_whitespace().collect::>().join(" ");
+ assert!(speech.contains(integral), "{language}/{style}: {speech}");
+ init_nav(language, "f′")?;
+ set_preference("SpeechStyle", style)?;
+ let speech=do_navigate_command("ReadCurrent")?.split_whitespace().collect::>().join(" ");
+ assert!(speech.contains(derivative), "{language}/{style}: {speech}");
+ }
+ Ok(())
+}
+
+#[test]
+fn w3c_binomial_coefficient() -> Result<()> {
+ // The W3C alias reuses binomial speech without changing inferred semantics.
+ test_w3c_speech("73", "7 choose 3")
+}
diff --git a/tests/Languages/intent.rs b/tests/Languages/intent.rs
index d6c097a4d..0d4496c21 100644
--- a/tests/Languages/intent.rs
+++ b/tests/Languages/intent.rs
@@ -4,15 +4,3 @@ mod general;
mod calculus;
mod linear_algebra;
mod tables;
-// Reviewed core concept speech, shared across language and verbosity variants.
-fn test_core_speech(mathml: &str, english: &str, russian: &str) -> anyhow::Result<()> {
- for (language, expected) in [("en", english), ("ru", russian)] {
- for style in ["ClearSpeak", "SimpleSpeak"] {
- for verbosity in ["Terse", "Medium", "Verbose"] {
- crate::common::test_prefs(language, style, vec![("Verbosity", verbosity), ("IntentErrorRecovery", "Error")], mathml, expected)?;
- }
- }
- }
- Ok(())
-}
-
diff --git a/tests/Languages/intent/calculus.rs b/tests/Languages/intent/calculus.rs
index 21eb4e759..515eb77aa 100644
--- a/tests/Languages/intent/calculus.rs
+++ b/tests/Languages/intent/calculus.rs
@@ -182,67 +182,3 @@ fn curl_vector() -> Result<()> {
return Ok(());
}
-
-// W3C core speech cases complement the inference checks above.
-use super::test_core_speech;
-
-#[test]
-fn w3c_definite_integral_domain() -> Result<()> {
- // The single W3C argument is an integration domain, not an integrand or integral sign.
- test_core_speech("∫x", "integral over x", "интеграл по икс")
-}
-
-#[test]
-fn w3c_definite_integral_bounds() -> Result<()> {
- // Two W3C arguments are lower and upper bounds, in that order; neither may disappear.
- test_core_speech("∫01", "integral from 0 to 1", "интеграл от 0 до 1")
-}
-
-#[test]
-fn w3c_definite_integral_infinite_bounds() -> Result<()> {
- // Infinite bounds need the Russian genitive after both 'от' and 'до'.
- let mathml="∫−∞∞";
- test_core_speech(mathml, "integral from negative infinity to infinity", "интеграл от минус бесконечности до бесконечности")
-}
-
-#[test]
-fn w3c_derivative_leibniz_second_order() -> Result<()> {
- // The three arguments denote function, variable, and order, regardless of fraction layout.
- let mathml = "d2ydx2";
- test_core_speech(mathml, "the second derivative of y with respect to x", "производная порядка 2 игрек по икс")
-}
-
-#[test]
-fn w3c_derivative_prime_first_order() -> Result<()> {
- // Prime notation needs the same semantic meaning, without assuming a spoken Leibniz fraction.
- let mathml = "f′(x)";
- test_core_speech(mathml, "the derivative of f with respect to x", "производная эф по икс")
-}
-
-#[test]
-fn w3c_derivative_symbolic_order() -> Result<()> {
- // Symbolic orders need an order phrase, not a numeric-ordinal conversion of the letter n.
- let mathml="fxn";
- test_core_speech(mathml, "the derivative of order n of f with respect to x", "производная порядка эн эф по икс")
-}
-
-#[test]
-fn w3c_calculus_intent_read_current() -> Result<()> {
- // Semantic navigation must use the same integral and derivative readings as full speech.
- for (language, integral, derivative) in [
- ("en", "integral from 0 to 1", "the derivative of f with respect to x"),
- ("ru", "интеграл от 0 до 1", "производная эф по икс"),
- ] {
- for style in ["ClearSpeak", "SimpleSpeak"] {
- init_nav(language, "∫01")?;
- set_preference("SpeechStyle", style)?;
- let speech=do_navigate_command("ReadCurrent")?.split_whitespace().collect::>().join(" ");
- assert!(speech.contains(integral), "{language}/{style}: {speech}");
- init_nav(language, "f′")?;
- set_preference("SpeechStyle", style)?;
- let speech=do_navigate_command("ReadCurrent")?.split_whitespace().collect::>().join(" ");
- assert!(speech.contains(derivative), "{language}/{style}: {speech}");
- }
- }
- Ok(())
-}
diff --git a/tests/Languages/intent/general.rs b/tests/Languages/intent/general.rs
index e074e3063..dd7f3d4be 100644
--- a/tests/Languages/intent/general.rs
+++ b/tests/Languages/intent/general.rs
@@ -20,8 +20,6 @@ fn binomial() -> Result<()> {
";
test_intent(mathml, intent, vec![])?;
- // The W3C alias reuses binomial speech without changing inferred binomial semantics.
- test_core_speech("73", "7 choose 3", "число сочетаний из 7 по 3")?;
return Ok(());
}
@@ -116,81 +114,3 @@ fn evaluated_at() -> Result<()> {
return Ok(());
}
-
-// W3C core speech cases complement the inference checks above.
-use super::test_core_speech;
-
-#[test]
-fn w3c_repeating_decimal_one_argument() -> Result<()> {
- // W3C supplies the period alone; a missing second child must never cause an XPath error.
- test_core_speech("45_", "repeating decimal 4 5", "период 4 5")?;
- test_core_speech("", "repeating decimal 4 5", "период 4 5")
-}
-
-#[test]
-fn w3c_repeating_decimal_leading_zeroes() -> Result<()> {
- // A period is a digit sequence: 003 must retain both leading zeroes rather than become 3.
- test_core_speech("003¯", "repeating decimal 0 0 3", "период 0 0 3")
-}
-
-#[test]
-fn w3c_repeating_decimal_legacy_arguments() -> Result<()> {
- // Keep the existing prefix/period extension used by automatic inference.
- test_core_speech("12003", "12 with repeating digits 0 0 3", "12 с повторяющимися цифрами 0 0 3")?;
- test_core_speech("1245¯", "12 with repeating digits 4 5", "12 с повторяющимися цифрами 4 5")
-}
-
-#[test]
-fn w3c_repeating_decimal_complete_w3c_example() -> Result<()> {
- // The W3C surrounding decimal must retain its prefix as well as both period digits.
- let mathml="0.045_";
- for (language,expected) in [("en","0.0 with repeating digits 4 5"),("ru","0.0 с повторяющимися цифрами 4 5")] {
- for style in ["ClearSpeak","SimpleSpeak"] {
- for verbosity in ["Terse","Medium","Verbose"] {
- test_prefs(language,style,vec![("Verbosity",verbosity),("IntentErrorRecovery","Error"),("DecimalSeparators","."),("BlockSeparators",", ")],mathml,expected)?;
- }
- }
- }
- Ok(())
-}
-
-#[test]
-fn w3c_repeating_decimal_semantic_navigation() -> Result<()> {
- // Semantic reading must use the same one-argument rule when revisiting the current expression.
- for (language,expected) in [("en","repeating decimal 4 5"),("ru","период 4 5")] {
- for style in ["ClearSpeak","SimpleSpeak"] {
- init_nav(language,"")?;
- set_preference("SpeechStyle",style)?;
- let speech=do_navigate_command("ReadCurrent")?;
- assert!(speech.contains(expected),"{language}/{style}: {speech}");
- }
- }
- Ok(())
-}
-
-#[test]
-fn w3c_blank_intent_representations() -> Result<()> {
- // The semantic blank is independent of its visible spelling, including a space-only blank.
- test_core_speech("__", "blank", "пустое место")?;
- test_core_speech("?", "blank", "пустое место")?;
- test_core_speech("", "blank", "пустое место")
-}
-
-#[test]
-fn w3c_invisible_times_keeps_operands() -> Result<()> {
- // W3C assigns silent fixity: suppress the operator name, never its operands.
- test_core_speech("xy", "x y", "икс игрек")
-}
-
-#[test]
-fn w3c_invisible_separator_keeps_operands() -> Result<()> {
- // A silent separator must not add the word 'comma' or drop any listed operand.
- test_core_speech("xyz", "x y z", "икс игрек зэт")
-}
-
-#[test]
-fn w3c_invisible_operator_leaves_are_silent() -> Result<()> {
- // Explicit annotations on the Unicode operator itself have no spoken name or argument.
- test_core_speech("", "", "")?;
- test_core_speech("", "", "")
-}
diff --git a/tests/Languages/ru/intent.rs b/tests/Languages/ru/intent.rs
index 40dd2e1c5..9fbcadf96 100644
--- a/tests/Languages/ru/intent.rs
+++ b/tests/Languages/ru/intent.rs
@@ -138,3 +138,5 @@ fn intent_prob_x() -> Result<()> {
test("ru", "ClearSpeak", expr, "probability от икс")?;
return Ok(());
}
+
+mod w3c;
diff --git a/tests/Languages/ru/intent/w3c.rs b/tests/Languages/ru/intent/w3c.rs
new file mode 100644
index 000000000..9db592184
--- /dev/null
+++ b/tests/Languages/ru/intent/w3c.rs
@@ -0,0 +1,148 @@
+use crate::common::*;
+use anyhow::Result;
+
+// Check this language's W3C speech in both styles and at every verbosity.
+fn test_w3c_speech(mathml: &str, expected: &str) -> Result<()> {
+ for style in ["ClearSpeak", "SimpleSpeak"] {
+ for verbosity in ["Terse", "Medium", "Verbose"] {
+ test_prefs("ru", style, vec![("Verbosity", verbosity), ("IntentErrorRecovery", "Error")], mathml, expected)?;
+ }
+ }
+ Ok(())
+}
+
+#[test]
+fn w3c_repeating_decimal_one_argument() -> Result<()> {
+ // W3C supplies the period alone; a missing second child must never cause an XPath error.
+ test_w3c_speech("45_", "период 4 5")?;
+ test_w3c_speech("", "период 4 5")
+}
+
+#[test]
+fn w3c_repeating_decimal_leading_zeroes() -> Result<()> {
+ // A period is a digit sequence: 003 must retain both leading zeroes rather than become 3.
+ test_w3c_speech("003¯", "период 0 0 3")
+}
+
+#[test]
+fn w3c_repeating_decimal_legacy_arguments() -> Result<()> {
+ // Keep the existing prefix/period extension used by automatic inference.
+ test_w3c_speech("12003", "12 с повторяющимися цифрами 0 0 3")?;
+ test_w3c_speech("1245¯", "12 с повторяющимися цифрами 4 5")
+}
+
+#[test]
+fn w3c_repeating_decimal_complete_w3c_example() -> Result<()> {
+ // The W3C surrounding decimal must retain its prefix as well as both period digits.
+ let mathml="0.045_";
+ let (language,expected) = ("ru","0.0 с повторяющимися цифрами 4 5");
+ for style in ["ClearSpeak","SimpleSpeak"] {
+ for verbosity in ["Terse","Medium","Verbose"] {
+ test_prefs(language,style,vec![("Verbosity",verbosity),("IntentErrorRecovery","Error"),("DecimalSeparators","."),("BlockSeparators",", ")],mathml,expected)?;
+ }
+ }
+ Ok(())
+}
+
+#[test]
+fn w3c_repeating_decimal_semantic_navigation() -> Result<()> {
+ // Semantic reading must use the same one-argument rule when revisiting the current expression.
+ let (language,expected) = ("ru","период 4 5");
+ for style in ["ClearSpeak","SimpleSpeak"] {
+ init_nav(language,"")?;
+ set_preference("SpeechStyle",style)?;
+ let speech=do_navigate_command("ReadCurrent")?;
+ assert!(speech.contains(expected),"{language}/{style}: {speech}");
+ }
+ Ok(())
+}
+
+#[test]
+fn w3c_blank_intent_representations() -> Result<()> {
+ // The semantic blank is independent of its visible spelling, including a space-only blank.
+ test_w3c_speech("__", "пустое место")?;
+ test_w3c_speech("?", "пустое место")?;
+ test_w3c_speech("", "пустое место")
+}
+
+#[test]
+fn w3c_invisible_times_keeps_operands() -> Result<()> {
+ // W3C assigns silent fixity: suppress the operator name, never its operands.
+ test_w3c_speech("xy", "икс игрек")
+}
+
+#[test]
+fn w3c_invisible_separator_keeps_operands() -> Result<()> {
+ // A silent separator must not add the word 'comma' or drop any listed operand.
+ test_w3c_speech("xyz", "икс игрек зэт")
+}
+
+#[test]
+fn w3c_invisible_operator_leaves_are_silent() -> Result<()> {
+ // Explicit annotations on the Unicode operator itself have no spoken name or argument.
+ test_w3c_speech("", "")?;
+ test_w3c_speech("", "")
+}
+
+#[test]
+fn w3c_definite_integral_domain() -> Result<()> {
+ // The single W3C argument is an integration domain, not an integrand or integral sign.
+ test_w3c_speech("∫x", "интеграл по икс")
+}
+
+#[test]
+fn w3c_definite_integral_bounds() -> Result<()> {
+ // Two W3C arguments are lower and upper bounds, in that order; neither may disappear.
+ test_w3c_speech("∫01", "интеграл от 0 до 1")
+}
+
+#[test]
+fn w3c_definite_integral_infinite_bounds() -> Result<()> {
+ // Infinite bounds need the Russian genitive after both 'от' and 'до'.
+ let mathml="∫−∞∞";
+ test_w3c_speech(mathml, "интеграл от минус бесконечности до бесконечности")
+}
+
+#[test]
+fn w3c_derivative_leibniz_second_order() -> Result<()> {
+ // The three arguments denote function, variable, and order, regardless of fraction layout.
+ let mathml = "d2ydx2";
+ test_w3c_speech(mathml, "производная порядка 2 игрек по икс")
+}
+
+#[test]
+fn w3c_derivative_prime_first_order() -> Result<()> {
+ // Prime notation needs the same semantic meaning, without assuming a spoken Leibniz fraction.
+ let mathml = "f′(x)";
+ test_w3c_speech(mathml, "производная эф по икс")
+}
+
+#[test]
+fn w3c_derivative_symbolic_order() -> Result<()> {
+ // Symbolic orders need an order phrase, not a numeric-ordinal conversion of the letter n.
+ let mathml="fxn";
+ test_w3c_speech(mathml, "производная порядка эн эф по икс")
+}
+
+#[test]
+fn w3c_calculus_intent_read_current() -> Result<()> {
+ // Semantic navigation must use the same integral and derivative readings as full speech.
+ let (language, integral, derivative) = ("ru", "интеграл от 0 до 1", "производная эф по икс");
+ for style in ["ClearSpeak", "SimpleSpeak"] {
+ init_nav(language, "∫01")?;
+ set_preference("SpeechStyle", style)?;
+ let speech=do_navigate_command("ReadCurrent")?.split_whitespace().collect::>().join(" ");
+ assert!(speech.contains(integral), "{language}/{style}: {speech}");
+ init_nav(language, "f′")?;
+ set_preference("SpeechStyle", style)?;
+ let speech=do_navigate_command("ReadCurrent")?.split_whitespace().collect::>().join(" ");
+ assert!(speech.contains(derivative), "{language}/{style}: {speech}");
+ }
+ Ok(())
+}
+
+#[test]
+fn w3c_binomial_coefficient() -> Result<()> {
+ // The W3C alias reuses binomial speech without changing inferred semantics.
+ test_w3c_speech("73", "число сочетаний из 7 по 3")
+}
diff --git a/tests/common/mod.rs b/tests/common/mod.rs
index 837fca05b..c93766d32 100644
--- a/tests/common/mod.rs
+++ b/tests/common/mod.rs
@@ -252,6 +252,9 @@ pub fn test_intent(mathml: &str, target: &str, test_prefs: Vec<(&str, &str)>) ->
prefs.set_user_prefs("BlockSeparators", ", ").unwrap();
});
+ // Default fixities come from language definitions; use a stable baseline instead of the OS locale.
+ // Explicit test preferences below can still select another language.
+ set_preference("Language", "en").unwrap();
set_preference("IntentErrorRecovery", "Error").unwrap();
set_preference("SpeechStyle", "SimpleSpeak").unwrap(); // avoids possibility of "LiteralSpeak"
for &(pref_name, pref_value) in &test_prefs {