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 = " + + + [ + + + + 3 + + + 1 + + + 4 + + + + + 0 + + + 2 + + + 6 + + + + ] + + "; + 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 = " + + [ + + 123 + + ] + "; + 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 = " + + [ + + + 1 + 2 + + + 3 + 4 + + + ] + "; + 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 = " + + [ + + + 1 + 2 + + + 3 + 4 + + + ] + "; + 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 = " + + [ + + + 1 + 2 + -1 + 3 + + + -3 + 3 + -1 + 2 + + + 2 + 3 + 2 + -1 + + + ] + +"; +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#" + + + log + b + + + "#; + 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#" + + + x + 2 + + + "#; + 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#" + + + x + 1 + + + "#; + 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#" + + + 1 + 2 + + + "#; + 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#" + + + x + + + "#; + 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#" + + + x + ° + + + "#; + 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#" + + + - + b + + + "#; + assert_zoom_in("ZoomIn", expr, "nagyítás; b") +} + +#[test] +fn no_parts_prefix_limit() -> Result<()> { + // limit:prefix — silent "in" + let expr = r#" + + + x + + + "#; + assert_zoom_in("ZoomIn", expr, "nagyítás; x") +} + +#[test] +fn no_parts_prefix_vector() -> Result<()> { + // vector:prefix from mover + arrow + let expr = r#" + + + v + → + + + "#; + 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#" + + + ( + + 7 + 3 + + ) + + + "#; + 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#" + + + M + T + + + "#; + 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#" + + + | + x + | + + + "#; + 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#" + + + x + ^ + + + "#; + 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#" + + 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#" + + + x + + + y + + + "#; + assert_zoom_in("ZoomIn", expr, "nagyítás; x") +} + +#[test] +fn no_intent_times_mrow() -> Result<()> { + let expr = r#" + + + 2 + ⁢ + a + + + "#; + assert_zoom_in("ZoomIn", expr, "nagyítás; 2") +} + +#[test] +fn no_intent_mi() -> Result<()> { + let expr = r#" + + x + + "#; + 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#" + + + 1 + 2 + + + "#; + +const POWER: &str = r#" + + + x + 2 + + + "#; + +/// 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#" + + + + 1 + 2 + + + + y + + + "#; + +/// y + 1/2: the fraction has material to its left. +const Y_PLUS_FRACTION: &str = r#" + + + y + + + + 1 + 2 + + + + "#; + +/// 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#" + + + x + 1 + 2 + + 3 + 4 + + + "#; + +// --- 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#" + + x + + "#; + 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#" + + x + + "#; + 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#" + + x + + "#; + 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#" + + x + + "#; + 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#" + + x + + "#; + 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"]) +}