diff --git a/Rules/Languages/hu/definitions.yaml b/Rules/Languages/hu/definitions.yaml
index f070f72fc..c72e0facc 100644
--- a/Rules/Languages/hu/definitions.yaml
+++ b/Rules/Languages/hu/definitions.yaml
@@ -351,6 +351,10 @@
## Other : Not tested, don't appear in https://w3c.github.io/mathml-docs/intent-core-concepts/
"modified-variable": "silent= "
}
+ # The connector word to say for functional 'IntentMappings' (in English, "of")
+ # This is likely the word used for U+2061 in unicode.yaml, but for technical reasons needs to be defined here
+ # vector of length=1 because scalars not supported
+- FunctionApplicationWord: ["a(z)"]
# Names of functions that in terse mode don't say "of" (or it's equivalent in other languages)
- TerseFunctionNames: {
@@ -359,30 +363,30 @@
- NavigationParts: {
# These are the parts of a formula that can be navigated to
- "large-op": "base; lower limit; upper limit",
- "mfrac": "numerator; denominator",
+ "large-op": "alap; alsó határ; felső határ",
+ "mfrac": "számláló; nevező",
"fraction": "számláló; nevező",
"msqrt": "gyök",
"square-root": "gyök",
- "mroot": "root; root index",
+ "mroot": "gyök; gyök index",
"root": "gyök; gyök index",
- "msub": "base; subscript",
- "sub": "base; subscript",
- "logarithm-with-base": "base",
- "indexed-by": "base; subscript",
- "msup": "base; superscript",
- "say-super": "base; superscript",
- "skip-super": "base; superscript",
- "power": "base; exponent",
- "msubsup": "base; subscript; superscript",
- "munder": "base; under limit",
- "mover": "base; upper limit",
- "munderover": "base; under limit; upper limit",
+ "msub": "alap; alsó index",
+ "sub": "alap; alsó index",
+ "logarithm-with-base": "alap logaritmus",
+ "indexed-by": "alap; alsó index",
+ "msup": "alap; felső index",
+ "say-super": "alap; felső index",
+ "skip-super": "alap; felső index",
+ "power": "alap; kitevő",
+ "msubsup": "alap; alsó index; felső index",
+ "munder": "alap; határérték alatt",
+ "mover": "alap; határérték felett",
+ "munderover": "alap; határérték alatt; határérték felett",
# words for moving into and out of one of the parts (e.g., "move right 'out of' numerator, 'in' denominator")
# it's a hack to put them here, but at least they are grouped with the other navigation parts
- "in": "in",
- "out": "out of",
+ "in": "ban/ben",
+ "out": "ki",
}
- KnownWords: {
diff --git a/Rules/Languages/hu/navigate.yaml b/Rules/Languages/hu/navigate.yaml
index 475e1f625..5a7636d9d 100644
--- a/Rules/Languages/hu/navigate.yaml
+++ b/Rules/Languages/hu/navigate.yaml
@@ -39,7 +39,7 @@
# as they would likely be much more efficient and also cleaner.
# Rules for announcing the command
-- name: say-command
+- name: say-command #audit-ignore
tag: "!*"
match: "$SayCommand != ''" # value should be '', 'true', or 'false'
variables: [Prefix: "''"]
@@ -48,7 +48,7 @@
- if: "$MatchCounter = 0 and $SayCommand = 'true'"
then_test:
- if: "self::m:math and starts-with($NavCommand, 'ZoomOut')"
- then: [T: "teljesen kinagyítva", pause: "medium"]
+ then: [T: "teljesen kicsinyítve", pause: "medium"] # phrase('zoomed out all of the way')
- else_if: "IsNode(., 'leaf') and starts-with($NavCommand, 'ZoomIn')"
then:
- test:
@@ -56,41 +56,51 @@
then: [T: "teljesen ráközelített"] # phrase('zoomed in all of the way')
- else_if: "$NavNodeOffset = 0"
then: [T: "ráközelített az első karakterre"] # phrase('zoomed in to first character')
- else: [T: "karakterre nagyítva"] # phrase('zoomed in to character')
+ else: [T: "ráközelített a karakterre"] # phrase('zoomed in to character')
- pause: "medium"
else:
- test:
# CommandOffset is 1 + length of the English NavCommand stem (Zoom/Move/Read/Describe),
# not the spoken Prefix length — so translations can use a different Prefix word.
- if: "starts-with($NavCommand, 'Zoom')"
- then: [set_variables: [Prefix: "'zoom'", CommandOffset: "5"]] # phrase('zoom' in to see more details)
+ then:
+ # the whole phrase is chosen here ("kicsinyítés" is the Hungarian for "zoom out"), so no suffix word is needed below
+ - test:
+ - if: "$NavCommand = 'ZoomOut'"
+ then: [set_variables: [Prefix: "'kicsinyítés'", CommandOffset: "5"]] # phrase('zoom out')
+ - else_if: "$NavCommand = 'ZoomOutAll'"
+ then: [set_variables: [Prefix: "'teljes kicsinyítés'", CommandOffset: "5"]] # phrase('zoom out all of the way')
+ - else_if: "$NavCommand = 'ZoomInAll'"
+ then: [set_variables: [Prefix: "'teljes nagyítás'", CommandOffset: "5"]] # phrase('zoom in all of the way')
+ - else: [set_variables: [Prefix: "'nagyítás'", CommandOffset: "5"]] # phrase('zoom' in to see more details)
- else_if: "starts-with($NavCommand, 'Move')"
- then: [set_variables: [Prefix: "'move'", CommandOffset: "5"]] # phrase('move' to next entry in table)
+ then: [set_variables: [Prefix: "'ugrás'", CommandOffset: "5"]] # phrase('move' to next entry in table)
- else_if: "starts-with($NavCommand, 'Read')"
- then: [set_variables: [Prefix: "'read'", CommandOffset: "5"]] # phrase('read' to next entry in table)
+ then:
+ # "olvasás jelenlegi" is not Hungarian; the whole phrase is chosen here (no suffix word needed for 'Current')
+ - test:
+ - if: "$NavCommand = 'ReadCurrent'"
+ then: [set_variables: [Prefix: "'az aktuális elem felolvasása'", CommandOffset: "5"]] # phrase('read current')
+ - else: [set_variables: [Prefix: "'olvasás'", CommandOffset: "5"]] # phrase('read' to next entry in table)
- else_if: "starts-with($NavCommand, 'Describe')"
- then: [set_variables: [Prefix: "'describe'", CommandOffset: "9"]] # phrase('describe' to next entry in table)
+ then:
+ - test:
+ - if: "$NavCommand = 'DescribeCurrent'"
+ then: [set_variables: [Prefix: "'az aktuális elem leírása'", CommandOffset: "9"]] # phrase('describe current')
+ - else: [set_variables: [Prefix: "'leírás'", CommandOffset: "9"]] # phrase('describe' to next entry in table)
- test:
if: "$Prefix != ''"
then:
- x: "$Prefix"
- test:
- if: "substring($NavCommand, $CommandOffset) = 'In'"
- then: [T: "ban"] # phrase(zoom 'in' to see more details)
- - else_if: "substring($NavCommand, $CommandOffset) = 'InAll'"
- # HACK: '\uF8FE' is used internally for the concatenation char by 'ct' -- this gets "ed" concatenated to "zoom"
- then: [T: "Teljesen kinagyítva"] # phrase(zoom 'out all of the way' to see more details)
- - else_if: "substring($NavCommand, $CommandOffset) = 'Out'"
- then: [T: "ki"] # phrase(zoom 'out' to see more details)
- - else_if: "substring($NavCommand, $CommandOffset) = 'OutAll'"
- # HACK: '\uF8FE' is used internally for the concatenation char by 'ct' -- this gets "ed" concatenated to "zoom"
- then: [T: "teljes táblázat kinagyítása"] # phrase(zoom 'out all of the way' to see more details)
+ then: [T: ""] # phrase(zoom 'in' to see more details)
+ # 'InAll', 'Out' and 'OutAll' are covered by the Prefix chosen above (nothing more to say)
- else_if: "substring($NavCommand, $CommandOffset) = 'Next'"
then: [T: "jobbra"] # phrase(move to the 'right')
- else_if: "substring($NavCommand, $CommandOffset) = 'Previous'"
then: [T: "balra"] # phrase(move to the 'left')
- - else_if: "substring($NavCommand, $CommandOffset) = 'Current'"
- then: [T: "jelenlegi"] # phrase(who is the 'current' president)
+ # 'Current' is covered by the Prefix chosen above (ReadCurrent / DescribeCurrent)
- else_if: "substring($NavCommand, $CommandOffset) = 'LineStart'"
then: [T: "a sor elejére"] # phrase(move 'to start of line')
- else_if: "substring($NavCommand, $CommandOffset) = 'LineEnd'"
@@ -122,7 +132,7 @@
- x: "count($Child2D/preceding-sibling::*)+1"
- pause: "medium"
-- name: into-or-out-of-mmultiscripts
+- name: into-or-out-of-mmultiscripts #audit-ignore
tag: "*"
match: "$Move2D != '' and (@data-from-mathml='mmultiscripts' or self::m:mmultiscripts)"
replace:
@@ -133,25 +143,34 @@
variables:
- NumPrecedingSiblings: "count($Child2D/preceding-sibling::*)"
replace:
- - x: "$Move2D"
- test:
- if: "$NumPrecedingSiblings=0"
- then: [T: "alapja"] # phrase(the 'base' of the power)
+ then: [T: "alap"] # phrase(the 'base' of the power)
- else_if: "$Child2D/preceding-sibling::*[self::m:mprescripts]" # are we before mprescripts and hence are postscripts
then:
- test: # in postscripts -- base shifts by one
if: "$NumPrecedingSiblings mod 2 = 0"
- then: [T: "alsó index kezdete"] # phrase(x with 'subscript' 2)
- else: [T: "Felső index kezdete"] # phrase(x with 'superscript' 2)
+ then: [T: "bal alsó index"] # phrase(x with 'pre-subscript' 2)
+ else: [T: "bal felső index"] # phrase(x with 'pre-superscript' 2)
else:
- test:
if: "$NumPrecedingSiblings mod 2 = 0"
- then: [T: "felső index"] # phrase(x with 'pre-superscript' 2)
- else: [T: "alsó index"] # phrase(x with 'pre-subscript' 2)
+ then: [T: "felső index"] # phrase(x with 'superscript' 2)
+ else: [T: "alsó index"] # phrase(x with 'subscript' 2)
+ # Hungarian puts the case suffix on the part name ("a számlálóban"), which cannot be built for arbitrary part names.
+ # So "in" is silent (the command word already says it) and the other words follow the part name as a
+ # possessive/genitive noun phrase that works for any name: "számláló elhagyása", "nevező eleje", "alsó index vége".
+ - test:
+ - if: "$Move2D = 'out of'"
+ then: [T: "elhagyása"] # phrase('out of' the denominator)
+ - else_if: "$Move2D = 'start of'"
+ then: [T: "eleje"] # phrase('start of' the denominator)
+ - else_if: "$Move2D = 'end of'"
+ then: [T: "vége"] # phrase('end of' the denominator)
- pause: "medium"
# Rules for speaking what happens when moving into or out of a notation
-- name: into-or-out-of-default
+- name: into-or-out-of-default #audit-ignore
tag: "*"
# saying "out of row n" is not very useful, so skip it
# match: "$Move2D != '' and @data-from-mathml and @data-from-mathml != name(.) and count(*)>1 and @data-from-mathml != 'mtable'"
@@ -162,7 +181,6 @@
- PartNumber: "count($Child2D/preceding-sibling::*)"
- PartName: "GetNavigationPartName(name(.), $PartNumber)"
replace:
- - x: "$Move2D"
- test:
- if: "$PartName != ''"
then: [x: "$PartName"]
@@ -185,8 +203,18 @@
then: [x: "SpeakIntentName(name(.), $Verbosity, 'silent')"]
- else: [x: "SpeakIntentName(name(.), $Verbosity, 'other')"]
else:
- - T: "része" # phrase(the 'part' of the expression)
+ - T: "rész" # phrase(the 'part' of the expression)
- x: "count($Child2D/preceding-sibling::*) + 1"
+ # Hungarian puts the case suffix on the part name ("a számlálóban"), which cannot be built for arbitrary part names.
+ # So "in" is silent (the command word already says it) and the other words follow the part name as a
+ # possessive/genitive noun phrase that works for any name: "számláló elhagyása", "nevező eleje", "alsó index vége".
+ - test:
+ - if: "$Move2D = 'out of'"
+ then: [T: "elhagyása"] # phrase('out of' the denominator)
+ - else_if: "$Move2D = 'start of'"
+ then: [T: "eleje"] # phrase('start of' the denominator)
+ - else_if: "$Move2D = 'end of'"
+ then: [T: "vége"] # phrase('end of' the denominator)
- pause: "medium"
- name: default-move
@@ -1205,16 +1233,15 @@
- test:
if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'"
then:
- - T: "nem tud" # phrase('cannot' move right in expression)
+ - T: "nem tud jobbra" # phrase('cannot' move 'right' in expression)
- test:
- if: "$NavCommand = 'MoveNext'"
then: [T: "mozogni"] # phrase('move' to next entry in table)
- else_if: "$NavCommand = 'ReadNext'"
then: [T: "olvasni"] # phrase('read' next entry in table)
else: [T: "leírni"] # phrase('describe' next entry in table)
- - T: "jobbra" # phrase(move 'right')
- pause: short
- - T: "matematematikai blokk vége" # phrase(move 'end of math')
+ - T: "matematikai blokk vége" # phrase(move 'end of math')
- pause: long
- set_variables: [SpeakExpression: "'false'"]
@@ -1406,7 +1433,7 @@
- pause: medium
- with:
variables:
- - Move2D: "'end of'"
+ - Move2D: "'start of'"
- Child2D: "$EdgeNode/*[1]"
- MatchCounter: $MatchCounter + 1
replace: [x: "$EdgeNode"]
@@ -1680,9 +1707,8 @@
then:
- test:
- if: "$NavCommand = 'ReadCurrent'"
- then: [T: "olvassa"] # phrase('read' next entry in table)
- else: [T: "írja le"] # phrase('describe' next entry in table)
- - T: "jelenlegi" # phrase('current' entry in table)
+ then: [T: "az aktuális elem felolvasása"] # phrase('read current' entry in table)
+ else: [T: "az aktuális elem leírása"] # phrase('describe current' entry in table)
- pause: long
- set_variables: [NavNode: "@id"]
diff --git a/tests/Languages/hu.rs b/tests/Languages/hu.rs
index ac24dd22e..b15d27050 100644
--- a/tests/Languages/hu.rs
+++ b/tests/Languages/hu.rs
@@ -32,4 +32,4 @@ mod alphabets;
mod definitions;
mod intent;
mod mtable;
-
+mod navigate;
diff --git a/tests/Languages/hu/mtable.rs b/tests/Languages/hu/mtable.rs
index f02050e47..8f33d176e 100644
--- a/tests/Languages/hu/mtable.rs
+++ b/tests/Languages/hu/mtable.rs
@@ -240,6 +240,105 @@ fn matrix_2x3() -> Result<()> {
}
+#[test]
+fn augmented_matrix_2x3() -> Result<()> {
+ let expr = "
+
+ ";
+ test("hu", "ClearSpeak", expr, "a(z) 2 x 3 mátrix; 1 . sor; 3, 1, elválasztó, 4; 2 . sor; 0, 2, elválasztó, 6")?;
+ test("hu", "SimpleSpeak", expr, "a(z) 2 x 3 mátrix; 1 . sor; 3, 1, elválasztó, 4; 2 . sor; 0, 2, elválasztó, 6")?;
+ Ok(())
+}
+
+#[test]
+fn dashed_augmented_matrix_separator() -> Result<()> {
+ let expr = "
+ ";
+ test("hu", "ClearSpeak", expr, "az 1-szer 3 -es sor mátrix; 1, elválasztó, 2, elválasztó, 3")?;
+ test("hu", "SimpleSpeak", expr, "az 1-szer 3 -es sor mátrix; 1, elválasztó, 2, elválasztó, 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 = "
+ ";
+ test("hu", "ClearSpeak", expr, "a(z) 2 x 2 mátrix; 1 . sor; 1, 2, sorelválasztó; 2 . sor; 3, 4")?;
+ test("hu", "SimpleSpeak", expr, "a(z) 2 x 2 mátrix; 1 . sor; 1, 2, sorelválasztó; 2 . sor; 3, 4")?;
+ Ok(())
+}
+
+/// Horizontal and vertical lines use distinct announcements at their respective boundaries.
+#[test]
+fn matrix_row_and_column_separators() -> Result<()> {
+ let expr = "
+ ";
+ test("hu", "ClearSpeak", expr, "a(z) 2 x 2 mátrix; 1 . sor; 1, elválasztó, 2, sorelválasztó; 2 . sor; 3, elválasztó, 4")?;
+ test("hu", "SimpleSpeak", expr, "a(z) 2 x 2 mátrix; 1 . sor; 1, elválasztó, 2, sorelválasztó; 2 . sor; 3, elválasztó, 4")?;
+ Ok(())
+}
+
// AI generated
#[test]
fn matrix_2x3_labeled() -> Result<()> {
@@ -860,6 +959,40 @@ test_ClearSpeak("hu", "ClearSpeak_Matrix", "EndMatrix",
return Ok(());
}
+#[test]
+fn augmented_matrix_3x4_end_matrix() -> Result<()> {
+let expr = "";
+test_ClearSpeak("hu", "ClearSpeak_Matrix", "EndMatrix",
+ expr, "a(z) 3 x 4 mátrix; 1 . sor; oszlop 1; 1, oszlop 2; 2, oszlop 3; negatív 1, elválasztó, oszlop 4; 3; 2 . sor; oszlop 1; negatív 3, oszlop 2; 3, oszlop 3; negatív 1, elválasztó, oszlop 4; 2; 3 . sor; oszlop 1; 2, oszlop 2; 3, oszlop 3; 2, elválasztó, oszlop 4; negatív 1; vége a mátrixnak")?;
+ test("hu", "SimpleSpeak",
+ expr, "a(z) 3 x 4 mátrix; 1 . sor; oszlop 1; 1, oszlop 2; 2, oszlop 3; negatív 1, elválasztó, oszlop 4; 3; 2 . sor; oszlop 1; negatív 3, oszlop 2; 3, oszlop 3; negatív 1, elválasztó, oszlop 4; 2; 3 . sor; oszlop 1; 2, oszlop 2; 3, oszlop 3; 2, elválasztó, oszlop 4; negatív 1; vége a mátrixnak")?;
+ Ok(())
+ }
// AI generated
#[test]
diff --git a/tests/Languages/hu/navigate.rs b/tests/Languages/hu/navigate.rs
new file mode 100644
index 000000000..892d69481
--- /dev/null
+++ b/tests/Languages/hu/navigate.rs
@@ -0,0 +1,599 @@
+//! Navigation ZoomIn speech vs NavigationParts and intent fixity.
+//!
+//! Uses only real intents / MathML from `definitions.yaml` and `Rules/Intent/*.yaml`
+//! (same shapes as other en language tests). Fixities covered are those that actually
+//! appear there: prefix, infix, postfix, function, silent, nofix.
+
+use crate::common::*;
+use anyhow::Result;
+use std::panic::{catch_unwind, AssertUnwindSafe};
+
+fn init_nav(mathml: &str) -> Result<()> {
+ init_nav_with_auto_zoom_out(mathml, false)
+}
+
+/// Same setup as `init_nav`, but lets a test choose the "AutoZoomOut" preference.
+///
+/// With AutoZoomOut == "False" the navigation rules stop at the edge of a notation and announce it
+/// ("nem tud jobbra mozogni" + the part). With "True" they leave the notation automatically and announce
+/// that with the "elhagyása" fragment ("nevező elhagyása").
+fn init_nav_with_auto_zoom_out(mathml: &str, auto_zoom_out: bool) -> Result<()> {
+ set_rules_dir(abs_rules_dir_path())?;
+ set_preference("Language", "hu")?;
+ set_preference("SpeechStyle", "SimpleSpeak")?;
+ set_preference("Verbosity", "Medium")?;
+ set_preference("NavMode", "Enhanced")?;
+ set_preference("NavVerbosity", "Verbose")?;
+ set_preference("AutoZoomOut", if auto_zoom_out { "True" } else { "False" })?;
+ set_preference("Overview", "False")?;
+ set_mathml(mathml)?;
+ Ok(())
+}
+
+fn assert_zoom_in(command: &str, mathml: &str, expected: &str) -> Result<()> {
+ init_panic_handler();
+ let result = catch_unwind(AssertUnwindSafe(|| {
+ init_nav(mathml)?;
+ let speech = do_navigate_command(command)?;
+ let trimmed_speech = speech.trim_end_matches([' ', ',', ';']).to_string();
+ assert_eq!(expected, trimmed_speech);
+ Ok(())
+ }));
+ report_any_panic(result)
+}
+
+// --- Intents in NavigationParts (prefix, infix, function, silent; no postfix/nofix) ---
+
+#[test]
+fn parts_prefix_logarithm_with_base() -> Result<()> {
+ // Intent/general.yaml log-with-base → logarithm-with-base:prefix; parts "base"
+ let expr = r#"
+
+ "#;
+ assert_zoom_in("ZoomIn", expr, "nagyítás; alap logaritmus; b")
+}
+
+#[test]
+fn parts_infix_power() -> Result<()> {
+ // power:infix; parts "base; exponent"
+ let expr = r#"
+
+ "#;
+ assert_zoom_in("ZoomIn", expr, "nagyítás; alap; x")
+}
+
+#[test]
+fn parts_infix_indexed_by() -> Result<()> {
+ // indexed-by:infix; parts "base; subscript"
+ let expr = r#"
+
+ "#;
+ assert_zoom_in("ZoomIn", expr, "nagyítás; alap; x")
+}
+
+#[test]
+fn parts_function_fraction() -> Result<()> {
+ // fraction (from mfrac); parts "numerator; denominator"
+ let expr = r#"
+
+ "#;
+ assert_zoom_in("ZoomIn", expr, "nagyítás; számláló; 1")
+}
+
+#[test]
+fn parts_function_square_root() -> Result<()> {
+ // square-root:function; parts "root"
+ let expr = r#"
+
+ "#;
+ assert_zoom_in("ZoomIn", expr, "nagyítás; gyök; x")
+}
+
+#[test]
+fn parts_silent_skip_super() -> Result<()> {
+ // skip-super:silent (degree); parts "base; superscript"
+ let expr = r#"
+
+ "#;
+ assert_zoom_in("ZoomIn", expr, "nagyítás; alap; x")
+}
+
+// --- Intents not in NavigationParts ---
+
+#[test]
+fn no_parts_prefix_unary_minus() -> Result<()> {
+ // minus:prefix (Intent/general.yaml positive-or-negative) — silent "in"
+ let expr = r#"
+
+ "#;
+ assert_zoom_in("ZoomIn", expr, "nagyítás; b")
+}
+
+#[test]
+fn no_parts_prefix_limit() -> Result<()> {
+ // limit:prefix — silent "in"
+ let expr = r#"
+
+ "#;
+ assert_zoom_in("ZoomIn", expr, "nagyítás; x")
+}
+
+#[test]
+fn no_parts_prefix_vector() -> Result<()> {
+ // vector:prefix from mover + arrow
+ let expr = r#"
+
+ "#;
+ assert_zoom_in("ZoomIn", expr, "nagyítás; v")
+}
+
+#[test]
+fn no_parts_infix_binomial() -> Result<()> {
+ // binomial:infix (choose); no NavigationParts → "part 1"
+ let expr = r#"
+
+ "#;
+ assert_zoom_in("ZoomIn", expr, "nagyítás; rész 1; 7")
+}
+
+#[test]
+fn no_parts_postfix_transpose() -> Result<()> {
+ // transpose:postfix — not prefix, so still announces its name
+ let expr = r#"
+
+ "#;
+ assert_zoom_in("ZoomIn", expr, "nagyítás; transzponált; nagy m")
+}
+
+#[test]
+fn no_parts_function_absolute_value() -> Result<()> {
+ // absolute-value:function
+ let expr = r#"
+
+ "#;
+ assert_zoom_in("ZoomIn", expr, "nagyítás; az abszolút érték; x")
+}
+
+#[test]
+fn no_parts_silent_modified_variable() -> Result<()> {
+ // modified-variable:silent (x-hat) — no NavigationParts → silent
+ let expr = r#"
+
+ "#;
+ assert_zoom_in("ZoomIn", expr, "nagyítás; x")
+}
+
+#[test]
+fn no_parts_nofix_set_of_reals() -> Result<()> {
+ // set-of-reals:nofix — leaf
+ let expr = r#"
+
+ "#;
+ assert_zoom_in("ZoomIn", expr, "teljesen ráközelített; az összes valós szám halmaza")
+}
+
+// --- No intent= attribute (native MathML only; may still be inferred) ---
+
+#[test]
+fn no_intent_sum_mrow() -> Result<()> {
+ let expr = r#"
+
+ "#;
+ assert_zoom_in("ZoomIn", expr, "nagyítás; x")
+}
+
+#[test]
+fn no_intent_times_mrow() -> Result<()> {
+ let expr = r#"
+
+ "#;
+ assert_zoom_in("ZoomIn", expr, "nagyítás; 2")
+}
+
+#[test]
+fn no_intent_mi() -> Result<()> {
+ let expr = r#"
+
+ "#;
+ assert_zoom_in("ZoomIn", expr, "teljesen ráközelített; x")
+}
+
+// --- Move2D words (out of / start of / end of) and the zoom out / zoom in all commands ---
+//
+// Hungarian puts a case suffix on the part name ("a számlálóban"), which cannot be built for arbitrary
+// part names. So "in" is silent and the other words follow the part name: "számláló elhagyása",
+// "nevező vége", "számláló eleje". These tests check the fragments in order (not the exact separators
+// or the speech of the node that is landed on) and that no English navigation word is left over.
+
+fn nav_speech(commands: &[&str], mathml: &str, auto_zoom_out: bool) -> Result {
+ init_nav_with_auto_zoom_out(mathml, auto_zoom_out)?;
+ let mut speech = String::new();
+ for command in commands {
+ speech = do_navigate_command(command)?;
+ }
+ Ok(speech)
+}
+
+/// Runs `commands` one after the other and checks the speech of the LAST one: every fragment must occur,
+/// in the given order, and no English navigation word may be left over (AutoZoomOut is off).
+fn assert_nav_fragments(commands: &[&str], mathml: &str, fragments: &[&str]) -> Result<()> {
+ assert_nav_fragments_with(commands, mathml, false, fragments)
+}
+
+/// Like `assert_nav_fragments`, but with AutoZoomOut switched on.
+fn assert_nav_fragments_auto_zoom_out(commands: &[&str], mathml: &str, fragments: &[&str]) -> Result<()> {
+ assert_nav_fragments_with(commands, mathml, true, fragments)
+}
+
+fn assert_nav_fragments_with(commands: &[&str], mathml: &str, auto_zoom_out: bool, fragments: &[&str]) -> Result<()> {
+ init_panic_handler();
+ let result = catch_unwind(AssertUnwindSafe(|| {
+ let speech = nav_speech(commands, mathml, auto_zoom_out)?;
+ let mut rest = speech.as_str();
+ for fragment in fragments {
+ match rest.find(fragment) {
+ Some(pos) => rest = &rest[pos + fragment.len()..],
+ None => panic!("'{}' not found (in this order) in speech '{}'", fragment, speech),
+ }
+ }
+ let english = ["in", "out", "of", "start", "end", "zoom", "move", "cannot", "read", "describe", "current"];
+ for word in speech.split(|c: char| !c.is_alphanumeric()) {
+ assert!(!english.contains(&word), "English word '{}' left in speech '{}'", word, speech);
+ }
+ Ok(())
+ }));
+ report_any_panic(result)
+}
+
+/// Runs `commands` and compares the speech of the LAST one exactly (trailing separators are ignored).
+fn assert_nav_exact(commands: &[&str], mathml: &str, auto_zoom_out: bool, expected: &str) -> Result<()> {
+ init_panic_handler();
+ let result = catch_unwind(AssertUnwindSafe(|| {
+ let speech = nav_speech(commands, mathml, auto_zoom_out)?;
+ assert_eq!(expected, speech.trim_end_matches([' ', ',', ';']));
+ Ok(())
+ }));
+ report_any_panic(result)
+}
+
+const FRACTION: &str = r#"
+
+ "#;
+
+const POWER: &str = r#"
+
+ "#;
+
+/// 1/2 + y: the fraction has material to its right, so it is not at the edge of the math.
+const FRACTION_PLUS_Y: &str = r#"
+
+ "#;
+
+/// y + 1/2: the fraction has material to its left.
+const Y_PLUS_FRACTION: &str = r#"
+
+ "#;
+
+/// Native mmultiscripts: base x, postscripts (subscript 1, superscript 2), prescripts (subscript 3, superscript 4).
+/// Children are: x, 1, 2, , 3, 4. Numbers are used for the scripts so the test does not depend
+/// on how single letters are spoken.
+const MULTISCRIPTS: &str = r#"
+
+ "#;
+
+// --- ZoomOut / ZoomOutAll / ZoomInAll command phrases ---
+
+#[test]
+fn zoom_out_of_numerator() -> Result<()> {
+ assert_nav_fragments(&["ZoomIn", "ZoomOut"], FRACTION, &["kicsinyítés", "számláló elhagyása"])
+}
+
+#[test]
+fn zoom_out_of_power_base() -> Result<()> {
+ assert_nav_fragments(&["ZoomIn", "ZoomOut"], POWER, &["kicsinyítés", "alap elhagyása"])
+}
+
+#[test]
+fn zoom_out_all_of_numerator() -> Result<()> {
+ // ZoomOutAll leaves every notation on the way up: the command phrase comes first, then the part that was left
+ assert_nav_fragments(&["ZoomIn", "ZoomOutAll"], FRACTION, &["teljes kicsinyítés", "számláló elhagyása"])
+}
+
+#[test]
+fn zoom_out_all_of_power_base() -> Result<()> {
+ assert_nav_fragments(&["ZoomIn", "ZoomOutAll"], POWER, &["teljes kicsinyítés", "alap elhagyása"])
+}
+
+#[test]
+fn zoom_out_at_top_of_math() -> Result<()> {
+ // first ZoomOut leaves the numerator, the second one reaches the top: "zoomed out all of the way"
+ assert_nav_fragments(&["ZoomIn", "ZoomOut", "ZoomOut"], FRACTION, &["teljesen kicsinyítve"])
+}
+
+#[test]
+fn zoom_in_all_command_phrase() -> Result<()> {
+ assert_nav_fragments(&["ZoomInAll"], FRACTION, &["teljes nagyítás"])
+}
+
+// --- MoveNext / MovePrevious inside a notation and at its edges (AutoZoomOut off) ---
+
+#[test]
+fn move_right_into_denominator() -> Result<()> {
+ assert_nav_fragments(&["ZoomIn", "MoveNext"], FRACTION, &["ugrás jobbra", "nevező", "2"])
+}
+
+#[test]
+fn move_right_at_end_of_denominator() -> Result<()> {
+ assert_nav_fragments(&["ZoomIn", "MoveNext", "MoveNext"], FRACTION, &["nem tud jobbra mozogni", "nevező vége"])
+}
+
+#[test]
+fn move_left_at_start_of_numerator() -> Result<()> {
+ assert_nav_fragments(&["ZoomIn", "MovePrevious"], FRACTION, &["nem tud balra mozogni", "számláló eleje"])
+}
+
+#[test]
+fn move_right_at_end_of_math_without_auto_zoom_out() -> Result<()> {
+ // With AutoZoomOut off, the "edge of the notation" rule fires first, even at the right end of the whole
+ // math: it says only "nem tud jobbra mozogni" (no part name, no "matematikai blokk vége").
+ let expr = r#"
+
+ "#;
+ assert_nav_exact(&["ZoomIn", "MoveNext"], expr, false, "nem tud jobbra mozogni")
+}
+
+#[test]
+fn move_left_at_start_of_math_without_auto_zoom_out() -> Result<()> {
+ let expr = r#"
+
+ "#;
+ assert_nav_fragments(&["ZoomIn", "MovePrevious"], expr, &["nem tud balra mozogni"])
+}
+
+// --- AutoZoomOut on: leaving a notation automatically ("out of" -> "elhagyása") ---
+//
+// With AutoZoomOut == "True" the edge-of-notation rule is skipped. If there is more math beyond the notation,
+// MoveNext/MovePrevious leave it automatically and say which part was left. Only at the very end of the math
+// the "cannot move" announcement is made, followed by "matematikai blokk vége" / "matematikai blokk kezdete".
+// Expected walk through 1/2 + y: ZoomIn (fraction), ZoomIn (numerator), MoveNext (denominator),
+// MoveNext (leaves the denominator, lands on "+").
+
+#[test]
+fn auto_zoom_out_move_right_leaves_denominator() -> Result<()> {
+ assert_nav_fragments_auto_zoom_out(
+ &["ZoomIn", "ZoomIn", "MoveNext", "MoveNext"],
+ FRACTION_PLUS_Y,
+ &["ugrás jobbra", "nevező elhagyása"],
+ )
+}
+
+#[test]
+fn auto_zoom_out_move_left_leaves_numerator() -> Result<()> {
+ // y + 1/2: ZoomIn (y), MoveNext (+), MoveNext (fraction), ZoomIn (numerator), MovePrevious (leaves it)
+ assert_nav_fragments_auto_zoom_out(
+ &["ZoomIn", "MoveNext", "MoveNext", "ZoomIn", "MovePrevious"],
+ Y_PLUS_FRACTION,
+ &["ugrás balra", "számláló elhagyása"],
+ )
+}
+
+#[test]
+fn auto_zoom_out_move_right_at_end_of_math() -> Result<()> {
+ let expr = r#"
+
+ "#;
+ assert_nav_fragments_auto_zoom_out(&["ZoomIn", "MoveNext"], expr, &["nem tud jobbra mozogni", "matematikai blokk vége"])
+}
+
+#[test]
+fn auto_zoom_out_read_and_describe_right_at_end_of_math() -> Result<()> {
+ let expr = r#"
+
+ "#;
+ assert_nav_fragments_auto_zoom_out(&["ZoomIn", "ReadNext"], expr, &["nem tud jobbra olvasni", "matematikai blokk vége"])?;
+ assert_nav_fragments_auto_zoom_out(&["ZoomIn", "DescribeNext"], expr, &["nem tud jobbra leírni", "matematikai blokk vége"])
+}
+
+#[test]
+fn auto_zoom_out_move_left_at_start_of_math() -> Result<()> {
+ let expr = r#"
+
+ "#;
+ assert_nav_fragments_auto_zoom_out(&["ZoomIn", "MovePrevious"], expr, &["matematikai blokk kezdete"])
+}
+
+// --- mmultiscripts part names ---
+//
+// Children of the mmultiscripts: base, subscript, superscript, , pre-subscript, pre-superscript.
+// The names are "alap", "alsó index", "felső index", "bal alsó index" and "bal felső index".
+
+#[test]
+fn multiscripts_zoom_in_base() -> Result<()> {
+ assert_nav_fragments(&["ZoomIn"], MULTISCRIPTS, &["nagyítás", "alap", "x"])
+}
+
+#[test]
+fn multiscripts_move_right_to_subscript() -> Result<()> {
+ assert_nav_fragments(&["ZoomIn", "MoveNext"], MULTISCRIPTS, &["ugrás jobbra", "alsó index", "1"])
+}
+
+#[test]
+fn multiscripts_move_right_to_superscript() -> Result<()> {
+ assert_nav_fragments(&["ZoomIn", "MoveNext", "MoveNext"], MULTISCRIPTS, &["ugrás jobbra", "felső index", "2"])
+}
+
+#[test]
+fn multiscripts_move_right_to_pre_subscript() -> Result<()> {
+ assert_nav_fragments(&["ZoomIn", "MoveNext", "MoveNext", "MoveNext"], MULTISCRIPTS, &["ugrás jobbra", "bal alsó index", "3"])
+}
+
+#[test]
+fn multiscripts_move_right_to_pre_superscript() -> Result<()> {
+ assert_nav_fragments(
+ &["ZoomIn", "MoveNext", "MoveNext", "MoveNext", "MoveNext"],
+ MULTISCRIPTS,
+ &["ugrás jobbra", "bal felső index", "4"],
+ )
+}
+
+#[test]
+fn multiscripts_move_left_to_base() -> Result<()> {
+ assert_nav_fragments(&["ZoomIn", "MoveNext", "MovePrevious"], MULTISCRIPTS, &["ugrás balra", "alap", "x"])
+}
+
+#[test]
+fn multiscripts_zoom_out_of_base() -> Result<()> {
+ assert_nav_fragments(&["ZoomIn", "ZoomOut"], MULTISCRIPTS, &["kicsinyítés", "alap elhagyása"])
+}
+
+#[test]
+fn multiscripts_zoom_out_of_pre_superscript() -> Result<()> {
+ assert_nav_fragments(
+ &["ZoomIn", "MoveNext", "MoveNext", "MoveNext", "MoveNext", "ZoomOut"],
+ MULTISCRIPTS,
+ &["kicsinyítés", "bal felső index elhagyása"],
+ )
+}
+
+// --- ReadCurrent / DescribeCurrent ---
+
+#[test]
+fn read_current() -> Result<()> {
+ assert_nav_fragments(&["ZoomIn", "ReadCurrent"], FRACTION, &["az aktuális elem felolvasása"])
+}
+
+#[test]
+fn describe_current() -> Result<()> {
+ assert_nav_fragments(&["ZoomIn", "DescribeCurrent"], FRACTION, &["az aktuális elem leírása"])
+}