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..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,9 +123,11 @@
### 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
+ # 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",
@@ -228,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
@@ -251,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
@@ -362,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",
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 = "