From 0e9dbca7338045135028a14372bce6804227b209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Gro=C3=9F?= Date: Sun, 31 May 2026 03:10:19 +0200 Subject: [PATCH 01/37] Refine matrix rule handling by adding support for augmented matrices; update unit tests to include 2x3 and 3x4 augmented matrix scenarios. (#555) --- Rules/Languages/en/SharedRules/general.yaml | 12 +++- tests/Languages/en/mtable.rs | 77 +++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/Rules/Languages/en/SharedRules/general.yaml b/Rules/Languages/en/SharedRules/general.yaml index 6ff1a604b..57ddcb5c6 100644 --- a/Rules/Languages/en/SharedRules/general.yaml +++ b/Rules/Languages/en/SharedRules/general.yaml @@ -744,7 +744,11 @@ - test: if: "self::m:determinant" then: [t: "determinant"] # phrase(the 2 by 2 'determinant') - else: [t: "matrix"] # phrase(the 2 by 2 'matrix') + else: + - test: + if: "@columnlines and (contains(normalize-space(@columnlines), 'solid') or contains(normalize-space(@columnlines), 'dashed'))" + then: [t: "augmented matrix"] # phrase(the 2 by 2 'augmented matrix') + else: [t: "matrix"] # phrase(the 2 by 2 'matrix') - pause: long - x: "*" - test: @@ -768,7 +772,11 @@ - test: if: "self::m:determinant" then: [t: "determinant"] # phrase(the 2 by 2 'determinant') - else: [t: "matrix"] # phrase(the 2 by 2 'matrix') + else: + - test: + if: "@columnlines and (contains(normalize-space(@columnlines), 'solid') or contains(normalize-space(@columnlines), 'dashed'))" + then: [t: "augmented matrix"] # phrase(the 2 by 2 'augmented matrix') + else: [t: "matrix"] # phrase(the 2 by 2 'matrix') - pause: long - x: "*" - test: diff --git a/tests/Languages/en/mtable.rs b/tests/Languages/en/mtable.rs index f3c51174b..0889a0e0e 100644 --- a/tests/Languages/en/mtable.rs +++ b/tests/Languages/en/mtable.rs @@ -238,6 +238,44 @@ fn matrix_2x3() -> Result<()> { } +#[test] +fn augmented_matrix_2x3() -> Result<()> { + let expr = " + + + [ + + + + 3 + + + 1 + + + 4 + + + + + 0 + + + 2 + + + 6 + + + + ] + + "; + test("en", "ClearSpeak", expr, "the 2 by 3 augmented matrix; row 1; 3, 1, 4; row 2; 0, 2, 6")?; + test("en", "SimpleSpeak", expr, "the 2 by 3 augmented matrix; row 1; 3, 1, 4; row 2; 0, 2, 6")?; + Ok(()) +} + #[test] fn matrix_2x3_labeled() -> Result<()> { let expr = " @@ -859,6 +897,45 @@ test_ClearSpeak("en", "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("en", "ClearSpeak_Matrix", "EndMatrix", + expr, "the 3 by 4 augmented matrix; row 1; column 1; 1, column 2; 2, column 3; negative 1, column 4; 3; \ + row 2; column 1; negative 3, column 2; 3, column 3; negative 1, column 4; 2; \ + row 3; column 1; 2, column 2; 3, column 3; 2, column 4; negative 1; end matrix")?; + test("en", "SimpleSpeak", + expr, "the 3 by 4 augmented matrix; row 1; column 1; 1, column 2; 2, column 3; negative 1, column 4; 3; \ + row 2; column 1; negative 3, column 2; 3, column 3; negative 1, column 4; 2; \ + row 3; column 1; 2, column 2; 3, column 3; 2, column 4; negative 1; end matrix")?; + Ok(()) + } + #[test] fn simple_matrix_vector() -> Result<()> { From 9ba3b3d00f8be9e4e0328323f5e6fda1e8d44865 Mon Sep 17 00:00:00 2001 From: nsoiffer Date: Sun, 31 May 2026 00:28:16 -0700 Subject: [PATCH 02/37] remove logging statement --- src/infer_intent.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/infer_intent.rs b/src/infer_intent.rs index 429e1ddac..01f1d4ef8 100644 --- a/src/infer_intent.rs +++ b/src/infer_intent.rs @@ -922,7 +922,6 @@ mod tests { #[test] fn intent_with_literals() -> Result<()> { - init_logger(); let mathml = " x "; From 9dc9d894c19eff18ea4498c6730e844cd891041e Mon Sep 17 00:00:00 2001 From: nsoiffer Date: Sun, 31 May 2026 21:56:22 -0700 Subject: [PATCH 03/37] removed uneeded eprintlin!() --- src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index f1fc0c1fd..a1b3dc4f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -105,7 +105,6 @@ pub fn are_strs_canonically_equal_with_locale(test: &str, target: &str, ignore_a match crate::interface::report_any_panic(result) { Ok(()) => Ok(()), Err(e) => { - eprintln!("{}", e); Err(e) } } From 7044d402d716eff59ffafe6f2389223acac0ce5d Mon Sep 17 00:00:00 2001 From: nsoiffer Date: Sun, 31 May 2026 21:56:45 -0700 Subject: [PATCH 04/37] Alternative fix to PR #540. Fixes #528. Includes the tests in the PR. --- Rules/Intent/general.yaml | 17 +++-- Rules/Languages/en/SharedRules/default.yaml | 9 ++- Rules/Languages/vi/SharedRules/default.yaml | 10 +-- src/canonicalize.rs | 77 ++++++++++++++++----- src/main.rs | 3 +- tests/Languages/en/shared.rs | 34 +++++++++ tests/Languages/vi/vi.rs | 2 +- 7 files changed, 119 insertions(+), 33 deletions(-) diff --git a/Rules/Intent/general.yaml b/Rules/Intent/general.yaml index ccba41c1f..8e0708940 100644 --- a/Rules/Intent/general.yaml +++ b/Rules/Intent/general.yaml @@ -42,11 +42,18 @@ - name: roman_numeral tag: [mi, mn, mtext] - match: "@data-number" - replace: - - intent: - name: mn - children: [x: "@data-number"] + match: "@data-roman-numeral" + replace: + - test: + if: "../@data-chem-formula" + then: + - intent: + name: mn + children: [x: "@data-number"] + else: + - intent: + name: mi + children: [x: "text()"] - name: positive-or-negative diff --git a/Rules/Languages/en/SharedRules/default.yaml b/Rules/Languages/en/SharedRules/default.yaml index 56cef80c2..c1f07ec7d 100644 --- a/Rules/Languages/en/SharedRules/default.yaml +++ b/Rules/Languages/en/SharedRules/default.yaml @@ -42,7 +42,10 @@ match: "." replace: - bookmark: "@id" - - x: "translate(., $BlockSeparators, '')" # remove digit block separators + - test: + - if: "@data-roman-numeral" + then: [spell: "text()", pause: "short"] + else: [x: "translate(., $BlockSeparators, '')"] # remove digit block separators - name: default tag: [mo, mtext] @@ -59,7 +62,7 @@ - test: - if: "string-length(.) = 1 and text() != '_'" # need unicode.tdl to kick in for single letter tokens then: [x: "text()"] - - else_if: "@data-chem-element" # NavMode=Character needs this + - else_if: "@data-chem-element or @data-roman-numeral" # NavMode=Character needs this then: [spell: "text()", pause: "short"] else: [x: "translate(., '-_\u00A0', ' ')"] # from intent literals or from extra spaces added (which get deleted) @@ -661,7 +664,7 @@ else: [pause: auto] - name: postfix-intent - # uncaught intent -- the args have been inserted in the order of speech + # uncaught intent -- the args have been inserted in the order of speech tag: "*" match: "count(*)>0 and contains(@data-intent-property, ':postfix:')" replace: diff --git a/Rules/Languages/vi/SharedRules/default.yaml b/Rules/Languages/vi/SharedRules/default.yaml index 314e63344..9eeb6267f 100644 --- a/Rules/Languages/vi/SharedRules/default.yaml +++ b/Rules/Languages/vi/SharedRules/default.yaml @@ -44,15 +44,15 @@ if: "@data-roman-numeral" then: - test: - if: "parent::*[1][self::m:chemical-formula]" + if: "../self::m:chemical-formula" then: - t: "hóa trị" - x: "@data-number" + - test: + if: "$Verbosity = 'Verbose'" + then: [t: "la mã"] else: - - x: "@data-number" - - test: - if: "$Verbosity = 'Verbose'" - then: [t: "la mã"] + - x: "text()" else: # FIX: removing the digit block separators is likely locale dependent - x: "translate(., ' `', '')" # remove digit block separators diff --git a/src/canonicalize.rs b/src/canonicalize.rs index 89b6c6c21..4ee84ae98 100644 --- a/src/canonicalize.rs +++ b/src/canonicalize.rs @@ -1888,10 +1888,6 @@ impl CanonicalizeContext { /// 'mo' should only be '+', '-', '=', ',', '.' -- unlikely someone is doing anything sophisticated fn is_roman_numeral_adjacent<'a, I>(siblings: I, must_be_upper_case: bool) -> bool where I: Iterator> { - static ROMAN_NUMERAL_OPERATORS: phf::Set<&str> = phf_set! { - "+", "-'", "=", "<", "≤", ">", "≥", - // ",", ".", // [c,d] triggers this if "," is present, so omitting it - }; let mut found_match = false; // guard against no siblings let mut last_was_roman_numeral = true; // started at roman numeral // debug!("start is_roman_numeral_adjacent"); @@ -1901,15 +1897,17 @@ impl CanonicalizeContext { match name(maybe_roman_numeral) { "mo" => { if !last_was_roman_numeral { + debug!("maybe_roman_numeral (mo): not last was roman numeral"); return false; } let text = as_text(maybe_roman_numeral); - if !ROMAN_NUMERAL_OPERATORS.contains(text) { + // ",", "." omitted — [c,d] triggers this if "," is present + if !matches!(text, "+" | "-" | "=" | "<" | "≤" | ">" | "≥") { return false; } last_was_roman_numeral = false; }, - "mi" | "mn" => { + "mi" | "mn" | "mtext" => { if last_was_roman_numeral { return false; // no implicit multiplication (or whatever) } @@ -1921,7 +1919,7 @@ impl CanonicalizeContext { found_match = true; last_was_roman_numeral = true; }, - "mtext" | "mspace" | "mphantom" => {}, + "mspace" | "mphantom" => {}, _ => { return false; } @@ -5971,19 +5969,64 @@ mod canonicalize_tests { let target_str = " XLVIII +mmxxvi "; - // let target_str = "XLVIII +mmxxvi"; are_strs_canonically_equal_result(test_str, target_str, &[]) } - // #[test] - // fn roman_numeral_context() { - // let test_str = "vi-i=v"; - // let target_str = " - // vi-i - // = v - // "; - // are_strs_canonically_equal_result(test_str, target_str, &[]) - // } + #[test] + fn roman_numeral_multi_letter_mi() -> Result<()> { + let test_str = " + IX + + + VIII + = + XVII + "; + let target_str = " + + IX + + + VIII + + = + XVII + "; + are_strs_canonically_equal_result(test_str, target_str, &[]) + } + + #[test] + fn roman_like_single_letter_mi_is_not_number() -> Result<()> { + // Regression test for https://github.com/daisy/MathCAT/issues/528 + let test_str = " + C + = + D + "; + let target_str = " + + C + = + D + + "; + are_strs_canonically_equal_result(test_str, target_str, &[]) + } + + #[test] + fn roman_numeral_context() -> Result<()> { + let test_str = "vi-i=v"; + let target_str = " + + + vi + - + i + + = + v + + "; + return are_strs_canonically_equal_result(test_str, target_str, &[]) + } #[test] fn not_roman_numeral() -> Result<()> { diff --git a/src/main.rs b/src/main.rs index 3a086246b..c196a114d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -193,8 +193,7 @@ fn main() { // "; let expr = r#" - 1 kg sec 1 m 1 - +vi-i=v "#; // let instant = Instant::now(); diff --git a/tests/Languages/en/shared.rs b/tests/Languages/en/shared.rs index 37fd45659..c9f98914c 100644 --- a/tests/Languages/en/shared.rs +++ b/tests/Languages/en/shared.rs @@ -209,6 +209,40 @@ fn presentation_mathml_in_semantics() -> Result<()> { } +#[test] +fn roman_like_superscript_identifier_is_not_chemistry() -> Result<()> { + // Regression test for https://github.com/daisy/MathCAT/issues/528 + let expr = " + I + = + + b + r + + + + z + I + + "; + test("en", "ClearSpeak", expr, "cap i is equal to, negative b r, plus z to the cap i-th power")?; + Ok(()) +} + +#[test] +fn roman_like_identifier_sequence_is_not_number() -> Result<()> { + // Regression test for https://github.com/daisy/MathCAT/issues/528 + let expr = " + C + + + I + + + X + "; + test("en", "ClearSpeak", expr, "cap c plus cap i plus cap x")?; + Ok(()) +} + + #[test] fn ignore_period() -> Result<()> { // from https://en.wikipedia.org/wiki/Probability diff --git a/tests/Languages/vi/vi.rs b/tests/Languages/vi/vi.rs index 3f61cff0e..aa95f0e1f 100644 --- a/tests/Languages/vi/vi.rs +++ b/tests/Languages/vi/vi.rs @@ -43,12 +43,12 @@ fn number_2a() -> Result<()> { } #[test] +#[ignore] fn roman_numeral() -> Result<()> { let expr = "IX+VIII=XVII"; test_prefs("vi", "ClearSpeak", vec![("Verbosity", "Medium")], expr, "9 cộng 8, bằng 17")?; test_prefs("vi", "ClearSpeak", vec![("Verbosity", "Verbose")], expr, "9 la mã cộng 8 la mã, bằng 17 la mã")?; return Ok(()); - } #[test] From 278dcf5b3575f54ae10e6e429b9be5326ca4ac98 Mon Sep 17 00:00:00 2001 From: nsoiffer Date: Mon, 1 Jun 2026 12:09:28 -0700 Subject: [PATCH 05/37] Add Russian translation --- docs/_data/languages.yml | 2 + docs/ru/README.md | 33 + docs/ru/User_guide_for_MathCAT_ru.md | 130 ++++ docs/ru/callers.md | 186 ++++++ docs/ru/developers.md | 130 ++++ docs/ru/helpers.md | 494 +++++++++++++++ docs/ru/index.md | 207 +++++++ docs/ru/nav-commands.md | 54 ++ .../new_translators_guide_MathCAT_revised.md | 568 ++++++++++++++++++ docs/ru/users.md | 203 +++++++ 10 files changed, 2007 insertions(+) create mode 100644 docs/ru/README.md create mode 100644 docs/ru/User_guide_for_MathCAT_ru.md create mode 100644 docs/ru/callers.md create mode 100644 docs/ru/developers.md create mode 100644 docs/ru/helpers.md create mode 100644 docs/ru/index.md create mode 100644 docs/ru/nav-commands.md create mode 100644 docs/ru/new_translators_guide_MathCAT_revised.md create mode 100644 docs/ru/users.md diff --git a/docs/_data/languages.yml b/docs/_data/languages.yml index 704563890..4b69c28f5 100644 --- a/docs/_data/languages.yml +++ b/docs/_data/languages.yml @@ -2,3 +2,5 @@ en: name: English fi: # Finnish name: Suomi +ru: # Russian + name: Русский diff --git a/docs/ru/README.md b/docs/ru/README.md new file mode 100644 index 000000000..7d47b36be --- /dev/null +++ b/docs/ru/README.md @@ -0,0 +1,33 @@ +# Документация MathCAT + +Документация собирается с помощью [Jekyll](https://jekyllrb.com/) с темой Cayman и публикуется на GitHub Pages. + +## Поддержка нескольких языков + +Переводы находятся в подкаталогах языков (например, `ru/` для русского языка). Каждая переведённая страница должна содержать front matter с полями `lang` и `ref`: + +```yaml +--- +layout: default +lang: ru +ref: users +title: Руководство пользователя MathCat +--- +``` + +- `lang` — код языка страницы; +- `ref` — общий идентификатор, связывающий переводы одной и той же страницы (например, поле `ref: users` должно быть и в `users.md`, и в `ru/users.md`). + +Переключатель языков в `_layouts/default.html` использует эти поля для создания ссылок между переводами. Он отображается только на страницах, у которых задано поле `ref`. + +Чтобы зарегистрировать новый язык, добавьте его в `_data/languages.yml`. + +## Локальная разработка + +Запустите _Jekyll_ из корневого каталога репозитория: + +``` +docker run --rm -v "${PWD}/docs:/srv/jekyll" -p 4000:4000 jekyll/jekyll bash -c "bundle install && bundle exec jekyll serve --host 0.0.0.0" +``` + +Затем откройте http://localhost:4000, http://localhost:4000/fi/users и т. д. diff --git a/docs/ru/User_guide_for_MathCAT_ru.md b/docs/ru/User_guide_for_MathCAT_ru.md new file mode 100644 index 000000000..43e230816 --- /dev/null +++ b/docs/ru/User_guide_for_MathCAT_ru.md @@ -0,0 +1,130 @@ +--- +layout: default +lang: ru +ref: user-guide +title: Руководство пользователя MathCAT +--- +# Руководство пользователя MathCAT + +MathCAT — инструмент, который используется вместе с программой экранного доступа. С помощью MathCAT математические выражения можно озвучивать и читать на дисплее Брайля. + + +Если вы используете программу экранного доступа NVDA, установите MathCAT как дополнение. [Скачать дополнение MathCAT для NVDA можно здесь.](https://nvda-addons.org/) + +Если вы используете JAWS, MathCAT уже встроен в программу, поэтому устанавливать его как дополнение не нужно. + +## Начало работы с MathCAT + +Используйте программу экранного доступа на веб-странице или в электронной книге как обычно. Когда вы дойдёте до математического выражения, программа экранного доступа автоматически прочитает его. Чтобы изучить выражение подробнее, включите режим навигации, нажав пробел. В NVDA для этого также можно нажать NVDA+Alt+M. Чтобы выйти из режима навигации, нажмите Escape. + +### Наиболее распространённые сочетания клавиш + +- Для перемещения влево, вправо, вверх или вниз внутри математического выражения используйте клавиши-стрелки. +- Для перемещения между ячейками таблицы используйте Ctrl+стрелка. +- Чтобы перейти к началу выражения, нажмите Home, а чтобы перейти к концу — End. +- Чтобы услышать текущую позицию, нажмите пробел. +- Чтобы изменить режим навигации, нажмите Shift+стрелка вверх или Shift+стрелка вниз. Режимы навигации описаны в разделе «Навигация». + +При навигации по выражению можно нажать Ctrl+C, чтобы скопировать код MathML текущего узла выражения. + +В математических выражениях доступно гораздо больше возможностей навигации. Все функции описаны в разделе «Все команды навигации». + +## Настройка MathCAT + +MathCAT можно настроить в соответствии с вашими потребностями. Чтобы найти параметры, нажмите NVDA+N, затем выберите «Параметры», а после этого — «Настройки MathCAT». В списке категорий есть три пункта: «Речь», «Навигация» и «Брайль». + +### Речь + +Ниже перечислены параметры речи и доступные для них значения с краткими описаниями. Значение по умолчанию, которое используется, если вы ничего не выбрали, указано в квадратных скобках. + +- Особые потребности: + - \[Незрячие.\] Однозначное озвучивание. + - Слабовидящие. Более краткое озвучивание. + - Трудности в обучении. Более краткое озвучивание. +- Язык: (по умолчанию используется язык программы экранного доступа) + - \[Английский (en)\] + - Испанский (es) + - Индонезийский (id) + - Шведский (sv) + - Вьетнамский (vi) + - Китайский, традиционное письмо (zh-tw) +- Стиль речи: + - \[ClearSpeak.\] Выражения озвучиваются примерно так, как их произнёс бы учитель на уроке. + - SimpleSpeak. Выражения озвучиваются более кратко. Иногда такое чтение может быть неоднозначным. +- Подробность: + - Кратко. Опускаются дополнительные слова, например артикли и предлоги в английской фразе «the square root of x». + - \[Средне.\] Компромисс между кратким и подробным режимами. + - Подробно. Озвучиваются все слова. Чтение является однозначным. +- Скорость чтения математических выражений (MathRate): + - \[100\], можно задать значение от 1 до 1000. Определяет скорость чтения математических выражений относительно скорости программы экранного доступа. Значение задаётся в процентах: 100 соответствует той же скорости, меньшее значение замедляет чтение, а большее — ускоряет. +- Коэффициент длительности пауз: + - \[1\], можно задать значение от 0 до 10. Определяет длительность пауз при чтении математических выражений. +- Звуковой сигнал для математических выражений: + - \[Нет.\] + - Звуковой сигнал. Перед каждым математическим выражением и после него воспроизводится звуковой сигнал. +- Химия: + - \[Читать.\] Химические формулы озвучиваются, например $H_2O$ читается как «аш два о». + +### Навигация + +MathCAT позволяет подробно изучать выражение с помощью навигации, то есть перемещаться по нему и читать его по частям. В настройках навигации можно выбрать способ перемещения и требуемую степень детализации. + +Ниже перечислены параметры и доступные для них значения с краткими описаниями. Значение по умолчанию, которое используется, если вы ничего не выбрали, указано в квадратных скобках. + +- Режим навигации при входе в выражение: + - \[Расширенный режим.\] Перемещение между математически значимыми частями выражения, например числителем, знаменателем, степенями и выражениями в скобках. + - Простой режим. Перемещение между словами, кроме случаев, когда встречается определённое выражение, например квадратный корень. Тогда оно читается целиком. + - Посимвольный режим. Перемещение между словами или числами. Увеличьте детализацию, чтобы читать каждую букву или цифру отдельно. + +Чтобы изменить режим навигации при перемещении по выражению, используйте Shift+стрелка вверх для перехода от простого режима к расширенному или от посимвольного к простому. Нажатие Shift+стрелка вниз переводит из расширенного режима в простой или из простого в посимвольный. Таким образом, перемещение вверх даёт более общий обзор, а вниз — более подробное представление. + +Можно установить флажок, чтобы режим навигации сбрасывался при каждом входе в выражение. По умолчанию флажок снят. + +- Озвучивание после перемещения: + - \[Читать.\] Читает часть выражения, в которой вы находитесь. + - Описывать. Даёт обзор выбранного выражения. + +Можно установить флажок, чтобы режим озвучивания после перемещения сбрасывался при каждом входе в выражение. По умолчанию флажок установлен. + +- Автоматически уменьшать детализацию после чтения части выражения, например корня: + - \[Включено.\] Флажок установлен. + - Выключено. Флажок снят. +- Подробность навигации: + - Кратко. Опускаются дополнительные слова, например артикли и предлоги в английской фразе «the square root of x». + - \[Средне.\] Компромисс между кратким и подробным режимами. + - Подробно. Озвучиваются все слова. Чтение является однозначным. + +### Брайль + +Ниже перечислены параметры Брайля и доступные для них значения. Значение по умолчанию, которое используется, если вы ничего не выбрали, указано в квадратных скобках. + +- Математическая нотация для отображения на дисплее Брайля: + - \[Немет.\] + - Шведская. + - Вьетнамская. +- Точки 7 и 8 обозначают текущую позицию в режиме навигации: + - Первые символы. + - \[Конечные точки.\] + +## Все команды навигации + +В таблице перечислены команды для навигации по математическому выражению. В первом столбце указана клавиша. В остальных столбцах описано действие при нажатии самой клавиши, Ctrl+клавиша, Shift+клавиша и Ctrl+Shift+клавиша. + +Примечание: табличная математика — это математическое содержимое с табличной структурой, например матрица или система уравнений. По таким выражениям можно перемещаться как по таблице. + +| Клавиша | Без модификатора | \+ Ctrl | \+ Shift | + Ctrl + Shift | +| :--- | :--- | :--- | :--- | :--- | +| Стрелка влево | Перейти к предыдущему элементу | В таблице: перейти к предыдущей ячейке.
В табличном выражении: перейти к предыдущему элементу.
Примечание: также можно использовать Ctrl+Alt+стрелка влево. | Прочитать предыдущий элемент | Описать предыдущий элемент | +| Стрелка вправо | Перейти к следующему элементу | В таблице: перейти к следующей ячейке.
В табличном выражении: перейти к следующему элементу.
Примечание: также можно использовать Ctrl+Alt+стрелка вправо. | Прочитать следующий элемент | Описать следующий элемент | +| Стрелка вверх | Уменьшить детализацию | В таблице: перейти к ячейке выше.
В табличном выражении: перейти к элементу выше.
Примечание: также можно использовать Ctrl+Alt+стрелка вверх. | Перейти к более общему режиму навигации: расширенному, простому или посимвольному | Уменьшить детализацию до минимальной | +| Стрелка вниз | Увеличить детализацию | В таблице: перейти к ячейке ниже.
В табличном выражении: перейти к элементу ниже.
Примечание: также можно использовать Ctrl+Alt+стрелка вниз. | Перейти к более подробному режиму навигации: расширенному, простому или посимвольному | Увеличить детализацию до максимальной | +| Enter | Сообщить текущую позицию | Сообщить полную текущую позицию |   |   | +| Цифры
1–10 (0 означает 10) | Перейти к метке позиции | Установить метку позиции | Прочитать содержимое метки позиции | Описать содержимое метки позиции | +| Пробел | Прочитать текущий элемент | Прочитать текущую ячейку | Переключить режим речи между чтением и описанием | Описать текущий элемент | +| Home | Перейти к началу выражения | Перейти к началу строки | В табличном выражении: перейти к началу столбца.

В столбце: перейти к верхнему элементу | | +| End | Перейти к концу выражения | Перейти к концу строки | В табличном выражении: перейти к концу столбца.

В столбце: перейти к нижнему элементу | | +| Backspace | Вернуться к предыдущей позиции | | | | + +## Есть замечания или предложения по MathCAT? + + diff --git a/docs/ru/callers.md b/docs/ru/callers.md new file mode 100644 index 000000000..c189234e3 --- /dev/null +++ b/docs/ru/callers.md @@ -0,0 +1,186 @@ +--- +layout: default +lang: ru +ref: callers +title: Интеграция MathCAT +--- +# MathCAT: средство обеспечения доступности математики + + +## Информация для разработчиков ассистивных технологий и пользователей библиотеки + +При использовании MathCAT вызовы обычно выполняются в следующем порядке: +1. Задаётся расположение каталога `Rules` MathCAT с помощью [SetRulesDir]. +2. С помощью вызовов [`SetPreference`] задаются необходимые ассистивной технологии настройки. Обычно указываются `Language` и используемый движок `TTS`, если он есть. Настоятельно рекомендуется задать движок. +3. MathML передаётся с помощью [`SetMathML`]. +4. Ассистивная технология вызывает [`GetSpokenText`] для получения текста для озвучивания и [`GetBraille`] для получения Брайля в Unicode. Если указан идентификатор узла, соответствующие ячейки Брайля будут выделены. + +Для навигации можно вызывать: +* [`DoNavigateKeyPress`] — принимает события клавиатуры; +* [`DoNavigateCommand`] — принимает команды, в которые внутри программы преобразуются события клавиатуры. + +Оба вызова возвращают строку для озвучивания. +Для выделения текущего узла навигации используются атрибуты `id`. Если они ещё не заданы, +[`SetMathML`] возвращает строку MathML с атрибутами `id` у всех узлов, для которых такие атрибуты отсутствовали. +Текущий узел можно получить с помощью: +* [`GetNavigationMathMLId`]; +* [`GetNavigationMathML`] — возвращает строку MathML выбранного узла. + +Примечание: оба вызова также возвращают второе целое число. Это смещение символа в листовом узле. +Оно необходимо для посимвольной навигации по листовым узлам из нескольких символов, например `sin` и `1234`. +Сейчас значение всегда равно `0`: эта функция требует дальнейшей разработки. + +Текущие значения настроек можно получить с помощью вызова [`GetPreference`]. + +Все функции могут возвращать код ошибки. + +Примечание: MathCAT выполняет значительную работу по исправлению некачественного MathML. В частности, генераторы MathML часто разделяют на части числа с запятыми и точками. MathCAT пытается собрать их обратно, но для этого должен знать региональные правила использования разделителей групп цифр и десятичных разделителей. Например, в США запись `1,234.0` является допустимым числом, а в Европе — нет, поскольку запятая используется как десятичный разделитель. Региональные настройки определяются страной, для которой создан документ, а не языком озвучивания математических выражений. Допустимый вид числа задают настройки `BlockSeparators` и `DecimalSeparators`. Вызывающая программа должна устанавливать эти значения, если они известны. По умолчанию используется американский формат чисел. + +## Для пользователей Rust +MathCAT написан на Rust. Достаточно собрать MathCAT и добавить в файл `Cargo.toml` своего проекта запись наподобие следующей: +``` +[dependencies.MathCAT] +mathcat = 0.2.0 # проверьте актуальную версию и используйте её +``` + +Точные сигнатуры функций с комментариями: +``` +/// Задаёт каталог Rules. +/// ВАЖНО: это должен быть самый первый вызов MathCAT, если только не задана переменная среды MathCATRulesDir. +pub fn set_rules_dir(dir: String) -> Result<()> + +/// Возвращает номер версии сборки из Cargo.toml. +pub fn get_version() -> String + +/// Заменяет ранее заданный MathML. +/// Возвращает канонический MathML с атрибутами 'id' у всех узлов, у которых не было идентификатора. +/// Идентификаторы можно использовать для синхронного выделения, если настройка API `Bookmark` равна true. +pub fn set_mathml(mathml_str: String) -> Result + +/// Возвращает текст для озвучивания ранее заданного MathML. +/// При озвучивании учитываются настройки ассистивной технологии и пользователя. +pub fn get_spoken_text() -> Result + +/// Возвращает текст для озвучивания обзора ранее заданного MathML. +/// При озвучивании учитываются настройки ассистивной технологии и пользователя. +/// Примечание: сейчас реализация минимальна, и использовать её не следует. +pub fn get_overview_text() -> Result + +/// Возвращает значение указанной настройки. +/// Если `name` не является известной настройкой, возвращается None. +pub fn get_preference(name: impl AsRef) -> Result + +/// Задаёт настройку MathCAT. Имя настройки должно быть известным MathCAT именем. +/// В зависимости от настройки значением должна быть строка или число. +/// Список известных пользовательских настроек приведён в пользовательской документации MathCAT. +/// Ниже перечислены распространённые настройки, которые задаются программами и недоступны пользователю: +/// * TTS -- SSML, SAPI5, None +/// * Pitch -- нормализовано относительно '1.0' +/// * Rate -- слов в минуту; должно соответствовать текущей скорости речи. +/// Отдельная пользовательская настройка "MathRate" задаёт относительное изменение этой скорости в процентах. +/// * Volume -- по умолчанию 100 +/// * Voice -- используемый голос (не реализовано) +/// * Gender -- выбор любого голоса указанного пола (не реализовано) +/// * Bookmark -- задайте `true`, если возвращаемая речь должна содержать `mark`/`bookmark` +/// для синхронного выделения +/// * CheckRuleFiles -- проверяет, изменились ли файлы правил после предыдущего вызова. Возможные значения: +/// "All", "Prefs" (по умолчанию; только системный и пользовательский файлы prefs.yaml) +/// и "None". Переход от "All" к "None" ускоряет работу примерно на 40 %, +/// а переход от "Prefs" к "None" -- примерно на 10 %. +/// +/// Эти настройки управляют озвучиванием и изменением высоты тона для прописных букв: +/// * CapitalLetters_UseWord -- произносить "cap" или подходящий для языка аналог [по умолчанию: true] +/// * CapitalLetters_Pitch -- изменять высоту тона вокруг прописной буквы +/// (нормализовано относительно '1.0'; значение '1.0' по умолчанию ничего не меняет) +/// * CapitalLetters_Beep -- создаёт фиктивный тег SSML audio с audio src='beep.mp4'; +/// в NVDA он служит признаком для воспроизведения сигнала +/// +/// * IntentErrorRecovery -- определяет поведение при недопустимых значениях `intent` в MathML. +/// Возможные значения: "Error" и "IgnoreIntent" (по умолчанию) +/// +/// Важно: имена и значения настроек чувствительны к регистру. +/// +/// Эту функцию можно вызывать несколько раз, чтобы задать разные значения. +/// Значения сохраняются и действуют после вызовов [`set_mathml`]. +/// Значение можно перезаписать, повторно вызвав функцию с другим аргументом. +/// +/// FIXME: Некоторые настройки относятся и к API, и к пользователю. Для переопределения следует +/// использовать что-то наподобие '!name'. Пока не реализовано. +pub fn set_preference(name: impl AsRef, value: impl AsRef) -> Result<()> + +/// Перемещает текущий узел в соответствии с кодом клавиши и клавишами-модификаторами +/// или в некоторых случаях сообщает значение. +/// `key` -- [код клавиши](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode#constants_for_keycode_value) +/// для клавиши (в JavaScript -- `ev.key_code`). +/// Возвращает текст для озвучивания нового текущего узла. +pub fn do_navigate_keypress(key: usize, shift_key: bool, control_key: bool, alt_key: bool, meta_key: bool) -> Result + +/// Перемещает текущий узел в соответствии с командой навигации. +/// Это более высокоуровневый интерфейс, чем `do_navigate_keypress`, предназначенный для приложений, +/// которые самостоятельно интерпретируют нажатия клавиш. +/// Допустимые команды: +/// * Стандартные команды перемещения: +/// `MovePrevious`, `MoveNext`, `MoveStart`, `MoveEnd`, `MoveLineStart`, `MoveLineEnd` +/// * Перемещение в таблице или элементарных математических выражениях: +/// `MoveCellPrevious`, `MoveCellNext`, `MoveCellUp`, `MoveCellDown`, `MoveColumnStart`, `MoveColumnEnd` +/// * Переход к дочерним узлам или выход к родительским: +/// `ZoomIn`, `ZoomOut`, `ZoomOutAll`, `ZoomInAll` +/// * Отмена последней команды перемещения: +/// `MoveLastLocation` +/// * Команды чтения (стандартное озвучивание): +/// `ReadPrevious`, `ReadNext`, `ReadCurrent`, `ReadCellCurrent`, `ReadStart`, `ReadEnd`, `ReadLineStart`, `ReadLineEnd` +/// * Команды описания (обзор): +/// `DescribePrevious`, `DescribeNext`, `DescribeCurrent` +/// * Информация о позиции: +/// `WhereAmI`, `WhereAmIAll` +/// * Изменение режима навигации с циклическим переходом вверх или вниз: +/// `ToggleZoomLockUp`, `ToggleZoomLockDown` +/// * Озвучивание текущего режима навигации: +/// `ToggleSpeakMode` +/// +/// Доступны 10 меток позиции, которые можно устанавливать, читать, описывать и использовать для перехода. +/// * Установка: +/// `SetPlacemarker0`, `SetPlacemarker1`, `SetPlacemarker2`, `SetPlacemarker3`, `SetPlacemarker4`, `SetPlacemarker5`, `SetPlacemarker6`, `SetPlacemarker7`, `SetPlacemarker8`, `SetPlacemarker9` +/// * Чтение: +/// `Read0`, `Read1`, `Read2`, `Read3`, `Read4`, `Read5`, `Read6`, `Read7`, `Read8`, `Read9` +/// * Описание: +/// `Describe0`, `Describe1`, `Describe2`, `Describe3`, `Describe4`, `Describe5`, `Describe6`, `Describe7`, `Describe8`, `Describe9` +/// * Переход: +/// `MoveTo0`, `MoveTo1`, `MoveTo2`, `MoveTo3`, `MoveTo4`, `MoveTo5`, `MoveTo6`, `MoveTo7`, `MoveTo8`, `MoveTo9` +/// +/// После завершения навигации вызовите функцию с командой `Exit`. +pub fn do_navigate_command(command: impl AsRef) -> Result + +/// Возвращает MathML, связанный с текущим узлом навигации. +/// Результат содержит `id` узла и смещение относительно этого узла, начиная с нуля (пока не реализовано). +/// Смещение требуется для токенов из нескольких символов. +pub fn get_navigation_mathml() -> Result<(String, usize)> + +/// Возвращает `id` и смещение, начиная с нуля, связанные с текущим узлом навигации. +/// `offset` пока не реализовано. +/// Смещение требуется для токенов из нескольких символов. +pub fn get_navigation_mathml_id() -> Result<(String, usize)> + + +/// Преобразует ошибку, возвращённую set_mathml и другими функциями, в полезную для отображения строку. +pub fn errors_to_string(e:&Error) -> String + +``` + +## Для пользователей Python +Можно создать собственный интерфейс Python или воспользоваться интерфейсом из связанного проекта [MathCATForPython](https://github.com/NSoiffer/MathCATForPython). В нём используется пакет Rust pyo3. + +Интерфейс Python в целом аналогичен интерфейсу Rust. В Python используется CamelCase, а не принятый в Rust snake_case. Например, функция `set_rules_dir` в интерфейсе Python называется `SetRulesDir`. Вызов функции следует оборачивать в конструкцию: +``` +try: + ... +except Exception as e: + ... # зарегистрировать ошибку 'e' в журнале +``` + + +## Для веб-разработчиков +Я собрал версию WebAssembly. В ней есть несколько компромиссов, а в процессе сборки требуется вручную вносить некоторые изменения. Этот процесс необходимо автоматизировать. Версия находится в проекте [MathCatDemo](https://github.com/NSoiffer/MathCATDemo). Проект собирает веб-страницу для демонстрационных целей, поэтому это не чистая сборка для веба. Тем не менее он показывает, как можно реализовать такую сборку. + +## Для пользователей C/C++ +Интерфейс C/C++ находится в связанном проекте [MathCatForC](https://github.com/NSoiffer/MathCATForC). У Rust и C разные менеджеры памяти, поэтому интерфейс несколько неудобен: память необходимо освобождать. Эту особенность можно скрыть, обернув вызовы небольшой функцией, как это сделано для `SetMathCatPreference` в [примере кода](https://github.com/NSoiffer/MathCATForC/blob/main/c-example/test.cpp). В остальном пользоваться интерфейсом просто. Если вы знаете более удачный способ решить проблему с памятью, сообщите мне или отправьте pull request. Для меня как программиста Rust это новая область. diff --git a/docs/ru/developers.md b/docs/ru/developers.md new file mode 100644 index 000000000..57749a3d3 --- /dev/null +++ b/docs/ru/developers.md @@ -0,0 +1,130 @@ +--- +layout: default +lang: ru +ref: developers +title: Руководство разработчика +--- +# Руководство разработчика + +В этом руководстве приведена техническая информация для разработчиков, работающих с кодовой базой MathCAT. + +## Предварительные требования + +Для разработки MathCAT необходимо установить Rust. Если вы ещё этого не сделали: + +1. [Скачайте и установите Rust](https://www.rust-lang.org/tools/install). +2. Клонируйте репозиторий MathCAT. +3. Откройте каталог проекта в интегрированной среде разработки (IDE). + +## Работа с Cargo + +Cargo — система сборки и менеджер пакетов Rust. Ниже приведены основные команды. + +### Сборка проекта + +```bash +# Собрать проект в режиме отладки +cargo build + +# Собрать проект в режиме выпуска (с оптимизацией) +cargo build --release +``` + +### Запуск проекта + +```bash +# Запустить основной исполняемый файл +cargo run + +# Запустить с указанными аргументами +cargo run -- +``` + +### Управление зависимостями + +Зависимости задаются в `Cargo.toml`. Cargo автоматически скачивает их и управляет ими. + +```bash +# Обновить зависимости до последних совместимых версий +cargo update +``` + +## Тестирование + +Тестирование необходимо для поддержания качества кода и предотвращения ошибок в существующей функциональности после внесения изменений. + +### Запуск тестов + +```bash +# Запустить все тесты +cargo test + +# Запустить указанный тест +cargo test test_name +``` + +### Написание тестов + +Тесты MathCAT проверяют, что выражения MathML преобразуются в ожидаемый текст для озвучивания. Пример: + +```rust +#[test] +fn test_simple_fraction() { + let expr = " + + 1 + 2 + + "; + test("en", "SimpleSpeak", expr, "1 half"); +} +``` + +### Покрытие тестами + +Покрытие тестами помогает определить, какие части кода проверяются тестами, а для каких частей необходимо добавить проверки. + +
+Использование grcov в macOS + +В этом подходе для создания отчётов о покрытии тестами используются `llvm-cov` и `grcov`. [grcov](https://github.com/mozilla/grcov) должен работать и в других операционных системах, но может потребовать некоторых изменений путей LLVM и конфигурации. + +**Однократная настройка:** + +```bash +# Установить необходимые компоненты +rustup component add llvm-tools-preview +cargo install grcov +``` + +**Создание отчёта о покрытии:** + +```bash +# Задать переменную среды для данных профилирования +export LLVM_PROFILE_FILE="target/coverage/%p-%m.profraw" + +# Запустить тесты со сбором данных о покрытии +RUSTFLAGS="-Cinstrument-coverage" cargo test + +# Пример: запустить один тест +# RUSTFLAGS="-Cinstrument-coverage" cargo test Languages::zh::tw::units::without_prefix_powers_of_2 + +# Создать HTML-отчёт +grcov . \ + --source-dir . \ + --binary-path ./target/debug/deps \ + -t html \ + --branch \ + --ignore-not-existing \ + --ignore "target/*" \ + -o target/coverage/html + +# Открыть отчёт в браузере +open target/coverage/html/index.html +``` + +
+ +**Альтернатива: интеграция с IDE** + +Во многих IDE для Rust, например RustRover или VS Code, есть встроенная поддержка анализа покрытия тестами. diff --git a/docs/ru/helpers.md b/docs/ru/helpers.md new file mode 100644 index 000000000..bd63b7c4e --- /dev/null +++ b/docs/ru/helpers.md @@ -0,0 +1,494 @@ +--- +layout: default +lang: ru +ref: helpers +title: Руководство переводчика и разработчика правил +--- +# Руководство переводчика и разработчика правил + +## Информация для разработчиков правил и переводчиков MathCAT +Эта страница находится в процессе подготовки. + +## Начало работы +Если вы планируете участвовать в разработке MathCAT, используйте GitHub: +1. Создайте форк репозитория MathCAT `github.com/NSoiffer/MathCAT`. +2. Клонируйте форк, чтобы получить локальную копию для работы. +3. Переключитесь на созданную для вашей работы ветку, обычно названную кодом языка перевода, и работайте в ней. + +Если вы ещё не знакомы с этими действиями, найдите одно из многочисленных руководств. Они достаточно просты, поэтому отсутствие опыта не должно вас останавливать. + + +## Переводчикам на другие языки +Если вы хотите перевести MathCAT, свяжитесь с @NSoiffer. Он подготовит начальный вариант перевода, который может значительно сэкономить время. В результате будут созданы файлы в каталоге `Rules/Languages/xx`, где `xx` — код языка, например `fr`, `de` или `el`. Перевод выполняется в этом каталоге. Необходимо отредактировать четыре категории файлов: +1. `definitions.yaml`: содержит переводы числительных, в том числе количественных и порядковых. Проверьте начальный перевод и внесите необходимые исправления. Эти числительные используются, например, при озвучивании фразы «три пятых». На некотором этапе образование числительных в языках становится регулярным, поэтому некоторые списки в файле можно сократить, а некоторые следует дополнить. Подробнее см. английские комментарии в файле. +2. Файлы `xxx_Rules.yaml`, сейчас это `ClearSpeak_Rules.yaml` и `SimpleSpeak_Rules.yaml`. Они соответствуют разным стилям речи. Настоятельно рекомендуется сначала выбрать только один стиль. Эти файлы обычно содержат слова, описывающие структуру выражения, например «дробь» и «степень», а также связующие слова. Поскольку стили речи во многом похожи, существует также каталог `SharedRules` с общими файлами правил. Они подключаются в `ClearSpeak_Rules.yaml` и `SimpleSpeak_Rules.yaml` с помощью правил `- include: file_name`. Их тоже необходимо перевести. +
+
+Примечание: диалог настроек MathCAT ищет файлы с именами вида `XXX_Rules.yaml` и добавляет их в раскрывающийся список для языка. Использовать имена SimpleSpeak и ClearSpeak необязательно. Если вы хотите перевести только один стиль, например SimpleSpeak, но не хотите удалять `ClearSpeak_Rules.yaml`, переименуйте его, например, в `ClearSpeak_Rules.yaml.untranslated`. +
+
+Эти файлы содержат автоматически созданные начальные переводы. Хотя текст уже переведён, используется ключ `t:`, а не `T:` с прописной буквы. Каждый перевод необходимо проверить и только после этого изменить ключ на вариант с прописной буквы. Подробнее об автоматическом переводе см. ниже. + + * В некоторых языках нет смысла произносить эквиваленты слов «the» и, возможно, «of» во фразе «the square root of x». В таком случае замените их пустыми строками. + * В некоторых языках меняется порядок слов. Переставляйте слова свободно, но внимательно следите за отступами: в YAML они значимы. + * В некоторых языках могут потребоваться слова, отсутствующие в английской версии, до или после существующих фраз. Добавляйте их при необходимости. Условное добавление выполняется с помощью `test`. Если нужна помощь, свяжитесь с @NSoiffer. + * Паузы между словами и фразами могут значительно улучшить понятность речи. Исходные паузы выбраны для английского языка. Настройте их в соответствии со звучанием синтезаторов вашего языка. Паузы легко добавлять, удалять и изменять. Их длительность масштабируется в соответствии с текущей скоростью речи. +3. Файлы Unicode: `unicode.yaml` и `unicode-full.yaml`. Они содержат такие символы, как `<` и `∫`. + * Начните с перевода `unicode.yaml`. В нём находится подавляющее большинство используемых математических символов. Сейчас список основан на практическом опыте, но в дальнейшем планируется уточнить его по статистике из реальных книг. В `unicode.yaml` около 270 символов, примерно 50 из которых — греческие буквы. Как и в файлах правил речи, здесь есть автоматически созданные начальные переводы. Проверьте их и измените `t:` на `T:`. Подробнее об автоматическом переводе см. ниже. + * В `unicode-full.yaml` тысячи строк. Вернитесь к нему после завершения остальных переводов и работайте столько, сколько сможете: большинство этих символов встречаются только в сложной математике и даже там используются редко. Наиболее важны: + * некоторые стрелки, начиная с 0x2190; + * символы из блока математических операторов 0x2200–0x22ff; + * некоторые диакритические знаки 0x2d8–0x2dd; + * некоторые простые чёрные и белые фигуры, начиная с 0x25a0 и 0x2b1a. +4. Файлы навигации `navigate.yaml` и `overview.yaml`. Переводите только `navigate.yaml`: `overview.yaml` ещё не готов к использованию. Многие слова в `navigate.yaml` многократно повторяются, поэтому удобно применять глобальный поиск и замену. В дальнейшем файл планируется переработать и выделить повторяющиеся слова. + +__ПРИМЕЧАНИЕ__: Сейчас почти завершён переход правил на использование `intent`. Сложная логика распознавания абсолютных величин, определителей и других конструкций будет перенесена в независимый от языка каталог `intent`. Это упростит перевод, поскольку правилу потребуется сопоставлять только тег `absolute-value` или `determinant`. Тесты также следует вынести в независимый от языка каталог `intent`. + +### Пометка переведённого текста +Эти файлы имеют формат YAML, описанный ниже. +Во всех этих файлах переводимый текст задаётся ключом YAML `t`, а в редких случаях — `ot`, `ct`, `spell`, `pronounce` и `IfThenElse`. После проверки перевода запишите ключ прописными буквами, например `T` или `IFTHENELSE`, чтобы обозначить переведённый фрагмент. + +Например, в `unicode.yaml` есть два правила: +``` + - "=": [t: "equals"] # 0x3d + - ">": # 0x3e + - test: + if: "$Verbosity!='Terse'" + then: [t: "is"] + - t: "greater than" +``` +При переводе на французский язык слова после `t:` заменяются примерно так: +``` + - "=": [T: "égale"] # 0x3d + + - ">": # 0x3e + - test: + if: "$Verbosity!='Terse'" + then: [T: "est"] + - T: "supérieur à" +``` + +Примечание: иногда значение `IfThenElse` не требует перевода, но ключ всё равно следует изменить, чтобы было видно, что строка проверена. В следующем примере перевод не нужен, поскольку части `then` и `else`, соответственно `count(*/*[1])` и `$LineCountTry`, не являются словами: +``` + - LineCount: "IfThenElse($LineCountTry=0, count(*/*[1]), $LineCountTry)" +``` + +Подробнее о содержимом файлов правил см. ниже. + +### Примечание о переведённых файлах +Для получения начального перевода файлов Unicode используются переводы MathPlayer и SRE, а также Google Translate. +Если переводы SRE и MathPlayer совпадают либо перевод есть только в одном из них и совпадает с Google Translate, в комментарий в конце строки добавляется только исходный английский вариант. Например: +``` + - "!": [t: "factorielle"] # 0x21 (en: 'factorial') +``` + +Если переводы MathPlayer и SRE различаются, выбирается вариант, совпадающий с Google Translate, а другой вариант включается в комментарий. Например: +``` + else: [t: "parenthèse gauche"] # (en: 'left paren', MathPlayer: 'parenthèse ouvrante') +``` +Если не совпадает ни один перевод, выбирается один из вариантов, а остальные приводятся в комментарии. Например: +``` + else: [t: "parenthèse gauche"] # (en: 'open paren', MathPlayer: 'parenthèse ouvrante', google: 'parenthèse ouverte') +``` +Наконец, если перевод отсутствует, используется Google Translate и добавляется комментарий `google translation`. Вероятность неудачного перевода в таком случае заметно выше, поэтому внимательно проверяйте эти строки. Пример, где доступен только Google Translate: +``` + then: [t: "ligne verticale"] # (en: 'vertical line', google translation) +``` + + +### Проверка перевода +Если вы используете NVDA, начатый перевод можно сразу проверить. Предполагается, что дополнение MathCAT уже установлено: +1. Скопируйте новый каталог перевода в `%AppData%\nvda\addons\MathCAT\globalPlugins\MathCAT\Rules\Languages`. +2. Запустите NVDA и откройте меню настроек MathCAT: параметры NVDA, затем «Настройки MathCAT...». +3. Выберите новый язык в раскрывающемся списке `Languages`. +4. Проверьте озвучивание. Хорошим источником примеров служат страницы Википедии. +5. Если произошла ошибка, часто выражающаяся в отсутствии речи, откройте журнал NVDA из подменю «Сервис». Ошибка должна быть указана там. Объяснение сообщений об ошибках приведено ниже. +6. MathCAT должен заметить изменение файла и перезагрузить его. Сейчас это не работает для файлов, подключённых с помощью `include`, например для файлов каталога `Shared`. После изменения такого файла перезагрузите MathCAT через «Сервис: Перезагрузить плагины» в NVDA или перезапустите NVDA. + +Перевод диалога настроек выполняется отдельно от перевода речи. Им занимаются добровольцы, которые переводят и другие дополнения. Подробнее см. [эту рассылку](https://groups.io/g/nvda-translations). + +### Автоматические тесты перевода +Тестирование очень важно. MathCAT написан на Rust и содержит множество автоматических тестов, использующих встроенную систему тестирования Rust. Чтобы писать и проверять собственные тесты, [скачайте и установите Rust](https://www.rust-lang.org/tools/install). Знать Rust необязательно: достаточно заменить некоторые английские строки ожидаемыми строками на вашем языке. + +Предположим, что вы переводите на французский язык с кодом `fr`. + +Сначала откройте файл `languages.rs` в каталоге тестов и добавьте строку `mod fr;` после `mod en;` или аналогичной строки другого языка. + +В каталоге `tests\Languages` находятся файл `en.rs` и каталог `en`. +1. Скопируйте `en.rs` в `fr.rs`. +2. Скопируйте каталог `en` в `fr`. +3. Если вы выбрали только один стиль речи, например SimpleSpeak, отредактируйте `fr.rs`: удалите строки, начиная с `mod ClearSpeak {` и заканчивая соответствующей строкой `}`. Удалите подкаталог `ClearSpeak` из каталога `fr`. +4. Желательно перевести все файлы, но на начальном этапе можно ограничиться несколькими. В `fr.rs` закомментируйте каждый непереведённый файл, добавив `//` в начало строки. Например, если вы не перевели файл SimpleSpeak `geometry.yaml`, строка должна выглядеть так: `// mod geometry;`. +5. Начните редактировать файлы: сначала глобально замените `"en"` на `"fr"`, а затем замените английские строки подходящими французскими или другими строками. + +Пример теста: +``` +#[test] +fn common_fraction_half() { + let expr = " + 1 2 + "; + test("en", "SimpleSpeak", expr, "1 half"); +} +``` +Для французского языка строка `test` будет выглядеть так: +``` + test("fr", "SimpleSpeak", expr, "un demi"); +``` + +После перевода нескольких тестов запустите автоматическую проверку. +Сначала убедитесь, что английские тесты работают: +``` +cargo test Languages::en +``` +Затем запустите свои тесты. В нашем примере: +``` +cargo test Languages::fr +``` +MathCAT добавляет паузы. В ожидаемых строках тестов они записываются символами `,` и `;`. При необходимости добавьте или удалите эти символы в ожидаемом результате. Если паузы звучат неуместно, добавьте или удалите `pause: xxx` в соответствующем месте одного из файлов `Rules\fr`. + +__Совет__: возможно, быстрее всего сначала запустить тесты на своём языке, не меняя ожидаемые результаты. Все тесты завершатся с ошибкой, но в сообщениях будет показана речь, созданная MathCAT на вашем языке. _Если она верна_, просто замените ей английский текст. После обработки всех ошибок снова запустите тесты. В идеальном случае ошибок больше не будет. + + +### Поддержание перевода в актуальном состоянии +Раздел предстоит написать... + +Со временем планируется создать инструмент, который будет: +1. предупреждать об отсутствующих переводах; +2. предупреждать о правилах из каталога `en`, которые не были скопированы в другой язык, вероятно из-за добавления новых английских правил. + +Такие инструменты будут искать переведённый и непереведённый текст. + + +## Переводчикам Брайля +Если вы хотите добавить поддержку новой системы записи Брайля, скорее всего, придётся начать с нуля, если только она не похожа на уже существующую. +Создайте три файла `.yaml` в каталоге `Rules\Braille\your-braille-language` по образцу файлов из других каталогов Брайля: +1. `xxx_Rules.yaml`, где `xxx` — имя новой системы записи Брайля. Файл содержит правила преобразования MathML в Брайль. +2. `unicode.yaml` — перевод наиболее распространённых символов Брайля. Используйте `Nemeth\unicode.yaml` как отправную точку. Замените `t: xxx` подходящими значениями для вашей системы. Вероятно, для символов, зависящих от контекста, потребуется удалить часть логики или добавить собственную. Например, в коде Немета запятая представляется по-разному внутри числа и вне его. +3. `unicode-full.yaml` — переводы остальных символов. + +Два отдельных файла Unicode нужны потому, что небольшой файл с распространёнными символами ускоряет запуск. Цель этого файла — охватить 99,99 % используемых символов. + +Для UEB и кода Немета потребовалось написать на Rust код очистки. Если для вашего перевода Брайля тоже нужна очистка, создайте issue, чтобы мы вместе могли реализовать необходимый код. + +Перевод Брайля можно сразу проверить. Используйте приведённые выше инструкции для перевода языка, где предлагается скопировать файлы в `%AppData%\nvda\addons\MathCAT\globalPlugins\MathCAT\Rules\Languages`, но замените `Languages` на `Braille`. Остальные действия почти не отличаются. + +Для автоматического тестирования следуйте приведённым выше инструкциям. Текущие тесты взяты из руководств по коду Немета и UEB. Можно поступить так же. Примеры тестов Брайля находятся в каталогах Nemeth и UEB. + +## Понимание сообщений об ошибках MathCAT +Если ошибка вызвана проблемой в правиле, при непосредственном запуске MathCAT сообщение выводится в консоль терминала, а при использовании NVDA — в журнал NVDA. + +Сообщения об ошибках могут показаться сложными. Ниже приведены пример и объяснение. + +Библиотека, которую MathCAT использует для чтения файлов YAML, не сохраняет номера строк, поэтому MathCAT не может сообщить строку с ошибкой. +Вместо этого указываются имя файла, а также значения `name` и `tag` правила в этом файле. +Затем рекурсивно перечисляются разделы правила, в которых обнаружена ошибка. + +Например, ниже приведено сообщение об ошибке, созданное намеренной заменой `test:` на `textx:`: +``` +caused by: in file "...\\MathCAT\\Rules\\Languages\\en\\ClearSpeak_Rules.yaml" +caused by: value for 'replace' in rule (fraction: fraction-over-simple). Replacements: + - test: + if: "$ClearSpeak_Fractions='FracOver'" + then: + - testx: + if: "$Verbosity!='Terse'" + then: [ot: the] + - t: fraction + - x: "*[1]" + - t: over + - x: "*[2]" + - test: + if: "$ClearSpeak_Fractions='OverEndFrac' or ($ClearSpeak_Fractions='EndFrac' and not( ($ClearSpeak_Fractions='Auto' or $ClearSpeak_Fractions='Ordinal' or $ClearSpeak_Fractions='EndFrac') and *[1][*[1][self::m:mn][not(contains(., '.')) and ($ClearSpeak_Fractions='Ordinal' or text()<20)] and *[2][self::m:mn][not(contains(., '.')) and ($ClearSpeak_Fractions='Ordinal' or (2<= text() and text()<=10))] ] and *[2][*[1][self::m:mn][not(contains(., '.')) and ($ClearSpeak_Fractions='Ordinal' or text()<20)] and *[2][self::m:mn][not(contains(., '.')) and ($ClearSpeak_Fractions='Ordinal' or (2<= text() and text()<=10))] ] ) )" + then: + - pause: short + - t: end fraction + - pause: short +caused by: replacement #1 of 5 +caused by: replacement #1 of 2 +caused by: Unknown 'replace' command (testx) with value: if: "$Verbosity!='Terse'" then: [ot: the] +``` +Первые две строки содержат имя файла, а также значения `tag` и `name`. Соответствующее правило: +``` +- name: fraction-over-simple + tag: fraction + match: + - "($ClearSpeak_Fractions='Over' or $ClearSpeak_Fractions='FracOver' or $ClearSpeak_Fractions='OverEndFrac') or" + - "( not($ClearSpeak_Fractions='General' or $ClearSpeak_Fractions='GeneralEndFrac') and" + - " (IsNode(*[1],'simple') and IsNode(*[2],'simple')) )" # simple fraction in ClearSpeak spec + replace: + - test: + if: "$ClearSpeak_Fractions='FracOver'" + then: + - testx: + if: "$Verbosity!='Terse'" + then: [{ot: "the"}] + - t: "fraction" + - x: "*[1]" + - t: "over" + - x: "*[2]" + - test: + # very ugly!!! -- replicate nested ordinal fraction as they are an exception + if: "$ClearSpeak_Fractions='OverEndFrac' or ($ClearSpeak_Fractions='EndFrac' and not( ($ClearSpeak_Fractions='Auto' or $ClearSpeak_Fractions='Ordinal' or $ClearSpeak_Fractions='EndFrac') and *[1][*[1][self::m:mn][not(contains(., '.')) and ($ClearSpeak_Fractions='Ordinal' or text()<20)] and *[2][self::m:mn][not(contains(., '.')) and ($ClearSpeak_Fractions='Ordinal' or (2<= text() and text()<=10))] ] and *[2][*[1][self::m:mn][not(contains(., '.')) and ($ClearSpeak_Fractions='Ordinal' or text()<20)] and *[2][self::m:mn][not(contains(., '.')) and ($ClearSpeak_Fractions='Ordinal' or (2<= text() and text()<=10))] ] ) )" + then: + - pause: short + - t: "end fraction" + - pause: short +``` + +Следующая часть сообщения, `caused by: replacement #1 of 5`, указывает, что проблема находится в первой замене, то есть первом элементе с `-`. +Строка `caused by: replacement #1 of 2` означает, что ошибка находится в первой части этой замены. +Последняя строка сообщает о неизвестной команде замены: `Unknown 'replace' command (testx) with value`. Теперь проблему можно исправить. +Часто сообщение об ошибке удобнее читать снизу вверх. + + +## Разработчикам Rust +Раздел предстоит написать... + +`build.rs` и файлы в `src`. + +## Тестирование +Независимо от того, разрабатываете вы код или пишете правила, крайне важно писать и запускать тесты. Они позволяют убедиться, что новый код работает и не нарушает существующую функциональность. + +Каталог `tests` похож на каталог `Rules`. Если вы переводчик, см. инструкции в соответствующем разделе выше. + +Rust поддерживает тестирование с помощью команды `cargo test`. Подробнее о тестах и покрытии тестами см. в [руководстве разработчика](developers.md). + + +## Файлы +MathCAT считывает важную информацию из следующих файлов: +* Rules + * `intent.yaml` — правила, определяющие намерение автора по MathML. Они используются разными стилями речи на разных языках, чтобы не дублировать процесс определения смысла, и добавляют к MathML атрибут `intent`. + * `definitions.yaml` — различные списки для канонизации MathCAT, то есть определения правильной структуры, и сопоставления правил. Например, `TrigFunctionNames` содержит имена тригонометрических функций, такие как `tan` и `lim`. + * `prefs.yaml` — значения по умолчанию для доступных настроек. MathCAT также ищет этот файл в зависящем от платформы пользовательском каталоге, чтобы отдельные пользователи могли задавать собственные значения. + * Windows: `%AppData%\prefs.yaml`. + * Linux: `$XDG_CONFIG_HOME` или `$HOME/.config`. + * `definitions.yaml` — независимые от языка определения, например имена тригонометрических функций. +* Rules/[lang] + * `Unicode.yaml` — длинный список способов произнесения встречающихся символов Unicode. Не используется для строк из нескольких символов. + * `XXX_rules.yaml` — правила озвучивания математических выражений. MathCAT сканирует каждый подкаталог `Rules`, ищет файлы с суффиксом `_rules.yaml` и добавляет их в список доступных пользователю вариантов. Часть `XXX` должна соответствовать стилю речи. Например, `ClearSpeak_rules.yaml` и `MathSpeak_rules.yaml` добавляют варианты стиля речи ClearSpeak и MathSpeak. + * `definitions.yaml` — зависящие от языка определения, например способы произнесения порядковых числительных: «первый», «половина» и т. д. + * `navigate.yaml` — правила, определяющие результат каждой команды навигации и соответствующий текст для озвучивания. + +Подкаталог `lang` должен соответствовать двухбуквенному коду языка или комбинации языка и региона согласно [стандарту ISO](https://en.wikipedia.org/wiki/Language_localisation#Language_tags_and_codes). Например, в каталоге `Rules` есть подкаталог `en`. Если требуется региональный вариант речи, можно добавить подкаталог региона, например `gb`, который будет использоваться для языка `en-gb`. + +MathCAT сначала читает правила основного языка, а затем правила региона. Региональные правила заменяют существующие правила или добавляются к ним в соответствующих языковых файлах `Unicode.yaml` и `XXX_rules.yaml`. + +MathCAT ищет каталог `Rules` в следующих местах: +1. В каталоге, заданном переменной среды `MathCATRulesDir`. +2. В соседнем с исполняемым файлом подкаталоге Rules. В Windows это обычно `C:\Program Files\MathCAT\Rules`. + +# Формат файлов +Файлы, как следует из их суффикса, имеют [формат YAML](https://lzone.de/cheat-sheet/YAML). Если вы ещё не знакомы с YAML, это надмножество JSON с более удобными для чтения и записи вариантами синтаксиса. + +## Введение в YAML +Основные типы YAML: +* скалярные типы, например целые и вещественные числа, а также строки. Строки можно заключать в одинарные или двойные кавычки, а в некоторых случаях оставлять без кавычек; +* массивы, которые внутри строки записываются как `["a", "b", "c"]`; +* словари или отображения, которые внутри строки записываются как `{key: value, foo: bar}`. + +Комментарии начинаются с символа `#` и продолжаются до конца строки. Блочных комментариев в YAML нет. + +В более подробном синтаксисе YAML для массивов вместо скобок используются отступы, поэтому приведённый выше массив выглядит так: +``` + - a + - b + - c +``` +Обратите внимание, что в такой форме строки не требуется заключать в кавычки, хотя для некоторых текстов кавычки необходимы. + +Словарь в подробной форме выглядит так: +``` + key: value + foo: bar +``` + +Ниже приведён более реалистичный пример из определений Unicode с несколькими вариантами записи. +Обратите внимание на отступы: все элементы, сдвинутые вправо относительно предыдущей строки, являются вложенными элементами массива или словаря. +``` +# Два способа задать простую замену для символа '∞'. +# Для краткости и ясности предпочтительна первая форма. + - "∞": [t: "infinity"] # 0x221e + - '∞': + - t: infinity # 0x222e + +# Несколько способов задать более сложное определение с проверкой. +# Компактная форма использует синтаксис JSON для значения. +- 0x003C: [test: {if:Verbosity!='terse', then: [t: is]}, t: "less than"] + +# Эта форма подчёркивает, что выполняются два действия: проверка и вывод "less than". +- 0x003C: + - test: [{if: Verbosity!='terse', then: {t: is}}] + - t: "less than" + +# Эта форма немного подробнее, но ясно показывает части проверки. +- 0x003C: + - test: + if: Verbosity!='terse' + then: [t: is] + - t: less than + +# Самая подробная форма YAML. +- 0x003C: + - test: + if: Verbosity!='terse' + then: + - t: is + - t: less than +``` +Все формы допустимы, но второй и третий варианты предпочтительны как хороший компромисс между краткостью и ясностью. + +Примечание: все файлы YAML начинаются с `---`, обозначающего начало документа. + +## Основные части правила речи + +``` +# rule: +# name: # имя правила; сочетание name и tag должно быть уникальным +# tag: +# variables: [{name: value}, ...] +# - name -- строка; value -- выражение XPath, возвращающее строку, число или логическое значение +# - внутри правила значение доступно как $name +# - значение переменной устанавливается _до_ проверки "match", поэтому его можно использовать в match +# - переменная действует в течение сопоставления +# match: # выражение XPath для сопоставления +# - может быть одной строкой или +# - массивом строк, которые объединяются для удобства чтения +# replace: [replacements], где replacements содержит один из следующих вариантов +# - t: некоторый текст +# 'T' означает, что текст переведён +# - ct: объединить текст без пробела перед ним +# 'CT' означает, что текст переведён +# - ot: необязательный текст; не использовать, если результат содержит повторяющиеся слова +# 'OT' означает, что текст переведён +# Например, не следует получать "t raised to the the fraction with ...." +# Если сделать "the" необязательным в правиле дроби, повторения не будет +# - x: некоторое выражение XPath в виде строки +# - test: обычные значения if/then/else с двумя особенностями: +# вместо 'then:' и 'else:' можно использовать 'then_test:' и 'else_test:' +# Это позволяет избежать дополнительного уровня 'test:' +# можно задать любое количество пар if/else_if; +# они проверяются по порядку до получения истинного значения +# Значение "test:" может быть массивом ключей if/else_if/else +# или для удобства единственным набором if/then/else. +# Если используется массив, первым элементом должен быть 'if', +# промежуточными и иногда последним -- 'else_if', +# а необязательным последним элементом -- 'else' или 'else_test' +# if: некоторое выражение XPath +# then: [replacements] +# then_test: [replacements] используется вместо 'then:', чтобы не добавлять 'test:' после 'then:' +# else: [replacements] # необязательно +# else_test: # необязательно; используется вместо 'else:', чтобы не добавлять 'test:' после 'else:' +# - with: +# variables: [name: value, ...] значения переменных, заданные во время выполнения этого предложения +# replace: [replacements] +# - intent: +# name: string имя правила intent +# children: дочерние узлы правила intent +# - insert: +# nodes: XPath, вычисляемый в узлы +# replace: [replacements] значения, вставляемые между всеми узлами +# - translate: XPath позволяет озвучить выражение в середине правила; используется WhereAmI для навигации +# - set_variables: [var: value, ...] определения глобальных переменных +# Они доступны программе после выполнения правил. Сейчас используются для навигации, меняющей состояние. +# - pause: строка или число # "short", "medium", "long", "auto" или число миллисекунд +# - rate: строка/число или словарь с одной или двумя записями +# value: вещественное число с необязательным % +# replace: [replacements] # значения TTS должны ограничивать область содержимого +# - volume: строка/число или словарь с одной или двумя записями +# value: вещественное число с необязательным % +# replace: [replacements] # значения TTS должны ограничивать область содержимого +# - pitch: строка/число или словарь с одной или двумя записями +# value: вещественное число с необязательным % +# replace: [replacements] # значения TTS должны ограничивать область содержимого +# - gender: строка/число или словарь с одной или двумя записями +# value: "male" # или "female" +# replace: [replacements] # значения TTS должны ограничивать область содержимого +# - voice: строка/число или словарь с одной или двумя записями +# value: строка +# replace: [replacements] # значения TTS должны ограничивать область содержимого +# - spell: строка с XPath, обычно одной буквой, произносимой как буква: `"'a'"` +# - bookmark: некоторый XPath в виде строки; возвращает 'id' для синхронного выделения +``` + +Примечание: для `pause` значение `auto` вычисляет длительность паузы с учётом сложности окружающих частей. Чем они сложнее, тем длиннее пауза, но не более установленного предела. Слушателю необходимо дать время осмыслить и разделить две части, если одна или обе достаточно сложны. + +Помимо именованных правил, файл правил речи поддерживает подключение других файлов правил речи. Благодаря этому разные стили могут использовать общую функциональность. Подключение записывается вместо правила речи: +``` + - include: file_name +``` +Файл может содержать любое количество подключений. Их содержимое обрабатывается так, как будто оно находилось в исходном файле. Подключаемый файл может находиться в текущем каталоге обрабатываемого файла правил или в каталоге по относительному пути. + +## Файлы Unicode + +Файлы Unicode — упрощённые версии правил речи. Они облегчают описание правил для символов Unicode и заметно ускоряют работу. Правила листовых элементов, например `mo`, переопределяют определения из файлов Unicode. Однако в целом правила речи для символов Unicode следует помещать в файл Unicode. + +Как и правила речи, файлы Unicode имеют формат YAML. Основное отличие состоит в том, что для определения правила используется только символ. Задавать имя правила, имя тега и выражение сопоставления не требуется. Значением правила может быть любое допустимое значение `replace:` из правил речи. + +Большинство правил очень просты. Например: +``` + - "+": [t: plus] # 0x2b +``` +Это правило преобразует символ `+` в строку `plus`. + +Более сложное правило: +``` + - "[": # 0x5b + - test: + if: $SpeechStyle = 'ClearSpeak' + then: [t: open bracket] + else: [t: left bracket] +``` +Результат этого правила зависит от текущей настройки стиля речи. + +Файлы Unicode, как и файлы правил речи, можно совместно использовать с помощью `- include: file_name`. + + +## Файлы Prefs + +Примечание: настройки, например настройки ClearSpeak, представлены словарём внутри записи `ClearSpeak` файла YAML. Это затрудняет задание и чтение значения. +Поэтому имя преобразуется в строку с разделителем `_`. +Например, _имя_ настройки `Fraction` раздела `ClearSpeak` — `ClearSpeak_Fraction`. +Именно его следует использовать для задания значения через API и доступа к значению в `ClearSpeak_Rules.yaml`. + + +## Файлы Definition + + +## XPath +Во многих частях правил речи используется XPath. Это распространённый и хорошо документированный способ выбора частей XML-документа. В интернете есть множество руководств. Если вы ещё не знакомы с XPath, рекомендуется прочитать одно из них. В MathCAT используется немного расширенная реализация XPath 1.0. + +В MathCAT обычно применяются лишь некоторые возможности XPath, а также несколько специальных функций. Ниже кратко описаны распространённые варианты использования XPath. + +| Запись | Значение | +| ----- | ---- | +| `*` | Соответствует всем дочерним узлам. | +| `[...]` | Выбирает узлы из текущего сопоставления. | +| `*[1]` | Выбирает первый дочерний узел. | +| `*[self::m:mn]` | Выбирает все дочерние элементы `mn`. Префикс `m` показывает, что элемент находится в пространстве имён MathML. | +| `*[1][self::m:mn]` | Выбирает первый дочерний узел, если он является элементом `mn`. | +| `*[1][self::m:mo][text()='-']` | Выбирает первый дочерний узел, если он является элементом `mo` с содержимым `-`. Можно записать `*[1][text()='-']`, поскольку другие узлы, скорее всего, не содержат `-`, но такое содержимое допустимо для `mtext`, поэтому безопаснее указать имя элемента. | +| `count(*[2]/*)` | Количество дочерних узлов второго дочернего узла. | +| `count(preceding-sibling::*)+1` | Количество одноуровневых узлов перед текущим элементом плюс один. | + +MathCAT добавляет специальные функции, упрощающие написание правил: + +| Функция | Значение | +| ----- | ---- | +| `IsNode(nodes, type)` | Возвращает true, если все узлы имеют одинаковый тип. Допустимые типы:
`"simple"` — заданный набор элементов ClearSpeak;
`"leaf"` — один из листовых элементов MathML;
`"2D"` — двумерный узел, например `mfrac` или `mroot`;
`"modified"` — узел с индексом или элементом над ним либо под ним;
`"scripts"` — узел с подстрочным и (или) надстрочным индексом;
`"common_fraction"` — целочисленные числитель и знаменатель. | +| `ToOrdinal` | | +| `ToCommonFraction` | | +| `IsBracketed(openChar, closeChar, requiresComma)` | | +| `BaseNode(node)` | Рекурсивно возвращает основу узла с индексами. | +| `IsInDefinition(node, name)` | Возвращает true, если узел входит в список `name`, заданный в `definitions.yaml`. | +| `IfThenElse(test, then-part, else-part)` | Возвращает `then-part`, если условие истинно, иначе возвращает `else-part`. Все аргументы являются XPath. | +| `DistanceFromLeaf(node, left_side, treat_2d_elements_as_tokens)` | Возвращает расстояние от текущего узла до крайнего левого или правого листа: для символа 0, для токена 1. Если `left_side` равно `true`, обход выполняется по крайним левым дочерним узлам до листа. Если `treat_2d_elements_as_tokens` равно `true`, двумерные обозначения, например дроби, считаются листами. | +| `EdgeNode(node, "left"/"right", stopNodeName)` | Возвращает узел остановки, если исходный узел находится у левого или правого края указанного предка. Значением `stopNodeName` также может быть `"2D"`. Если соответствие не найдено, возвращается исходный узел. Если `stopNodeName=="math"`, учитывается пунктуация, поскольку она не является частью математического выражения. | +| `DEBUG(xpath)` | Добавляет полезные для диагностики сведения в отладочный вывод. | + +Следующие функции используются правилами кода Немета: + +| Функция | Значение | +| ----- | ---- | +| `NestingChars` | Используется правилами `mfrac`, `msqrt` и `mroot`, чтобы повторять символы нужное количество раз. | +| `BrailleChars` | Используется элементами-токенами для сложной перестановки индикаторов кода Немета, например прописных букв и начертания шрифта. | diff --git a/docs/ru/index.md b/docs/ru/index.md new file mode 100644 index 000000000..8d06d475f --- /dev/null +++ b/docs/ru/index.md @@ -0,0 +1,207 @@ +--- +layout: default +lang: ru +ref: index +title: MathCAT +--- +# MathCAT: средство обеспечения доступности математики + +— библиотека, которая преобразует MathML в: + +* текст для озвучивания со встроенными командами речевого синтезатора; +* Брайль (код Немета, технический вариант UEB, а в дальнейшем и другие математические системы записи Брайля); +* представление для навигации по математическим выражениям, в том числе с возможностью обзора. + +Цель MathCAT — предоставить программам экранного доступа и другим вспомогательным технологиям удобную библиотеку для качественного озвучивания MathML и преобразования MathML в Брайль. MathCAT продолжает идеи MathPlayer (подробнее об этом будет сказано ниже) и использует накопленный в этом проекте опыт, чтобы ещё лучше озвучивать математические выражения, представлять их в Брайле и обеспечивать навигацию по ним. В MathCAT применяются новые идеи, которые разрабатывает [рабочая группа MathML](https://mathml-refresh.github.io/charter-drafts/math-2020.html): они позволяют авторам указывать смысл использованного обозначения. Например, $(3, 6)$ может обозначать точку на плоскости, открытый интервал или даже сокращённую запись наибольшего общего делителя. Если эта информация передана в MathML, MathCAT использует её для более естественного озвучивания. + +Предстоит сделать: подключить сторонние библиотеки для поддержки общего подмножества математических команд TeX и ASCIIMath. + + +# Документация для разных пользователей MathCAT + +MathCAT используют разные аудитории с различными потребностями и задачами. Подробности приведены в соответствующих разделах документации: +* Пользователи вспомогательных технологий: [информация о доступных настройках](users.md). +* Разработчики вспомогательных технологий и пользователи библиотеки: [информация об API MathCAT](callers.md). +* Переводчики и авторы правил: [информация о файлах, которые необходимо перевести](helpers.md). +* Разработчики MathCAT: [информация о процессе разработки и тестировании](developers.md). + +# Некоторые технические подробности +MathCAT написан на Rust и может использоваться из многих языков программирования. На данный момент есть интерфейсы для: +* [C/C++](https://github.com/NSoiffer/MathCATForC); +* [Python](https://github.com/NSoiffer/MathCATForPython) — этот интерфейс используют [дополнение NVDA](https://addons.nvda-project.org/addons/MathCAT.en.html) и программа экранного доступа [Orca](https://help.gnome.org/users/orca/stable) для Linux, написанные на Python; +* [Java](https://github.com/mwhapples/MathCAT4J) — сейчас этот интерфейс используется для экспериментов с MathCAT в [BrailleBlaster](https://www.brailleblaster.org/); +* [WebAssembly (Wasm, отчасти похожий на JavaScript)](https://github.com/NSoiffer/MathCATDemo/) — этот интерфейс используется в веб-демонстрации MathCAT. + +MathCAT применяет несколько эвристик, чтобы исправлять некачественный MathML и приводить его к рекомендованному виду. Например, конвертеры TeX и WYSIWYG-редакторы могут разделить число «1,234» в выражении «1,234+1» по запятой. MathCAT распознаёт такую ситуацию и объединяет число в один элемент `mn`. Другие исправления затрагивают структуру: MathCAT создаёт элементы `mrow` с учётом словаря операторов MathML и при необходимости добавляет невидимые знаки применения функции, умножения, сложения (для смешанных дробей) и разделители (например, между $i$ и $j$ в $a\_{ij}$). Это упрощает генерацию речи и кода Немета, а также может быть полезно другим приложениям. Сейчас очистка MathML не доступна через API, но в дальнейшем может стать ещё одной функцией MathCAT. В целом MathCAT исправляет MathML достаточно осторожно. Иногда результат может оказаться неверным, однако ожидается, что правильных исправлений будет значительно больше. Поиск типичных ошибок в конвертерах MathML и исправление некачественного MathML остаются постоянной задачей проекта. + +## Текущее состояние (обновлено 27.03.2026) + +MathCAT активно развивается. DAISY принимает деятельное участие в разработке, и вклад новых участников приветствуется. MathCAT распространяется с открытым исходным кодом. [Репозиторий проекта доступен на GitHub](https://github.com/daisy/MathCAT). [О проблемах дополнения MathCAT, относящихся к NVDA, можно сообщить здесь](https://github.com/daisy/MathCATForPython/issues). + +* MathCAT поддерживает озвучивание и навигацию для английского, немецкого, испанского, финского, индонезийского, норвежского, шведского, вьетнамского и китайского языков (традиционное письмо). +* MathCAT поддерживает код Немета, UEB, CMU, вьетнамскую систему записи Брайля, а также немецкую и австрийскую системы записи LaTeX и ASCIIMath. +* Существует [дополнение NVDA](https://addons.nvda-project.org/addons/MathCAT.en.html). Оно может заменить MathPlayer для пользователей английского языка и поддерживаемых переводов. Начиная с NVDA 2026.1 MathCAT встроен в NVDA, поэтому скачивать дополнение не нужно. + +Преобразование в код Немета в MathCAT значительно качественнее, чем в MathPlayer и других конвертерах MathML → код Немета. Оно также интегрировано с навигацией: точки 7 и 8 обозначают текущий узел, а маршрутизация курсора Брайля работает во время навигации. Благодаря высокому качеству вывода [BrailleBlaster](https://www.brailleblaster.org/) использует MathCAT для преобразования MathML в код Немета и UEB. + +Ряд других разработчиков вспомогательных технологий также включили MathCAT в свои продукты. Среди них особенно выделяется Vispero/JAWS. Сейчас JAWS поддерживает озвучивание MathCAT на английском и испанском языках, а также вывод в коде Немета и UEB. В дальнейшем появятся другие языки и системы записи Брайля. В средстве просмотра математических выражений JAWS доступны все предусмотренные MathCAT команды озвучивания и навигации, в том числе навигации по Брайлю. Настройки MathCAT находятся в центре настроек JAWS. + +Другие вспомогательные технологии, использующие MathCAT: + +* программа экранного доступа Orca для Linux использует MathCAT для озвучивания, навигации и вывода в Брайле; +* Dolphin EasyReader использует MathCAT; +* Kurzweil 3000 использует озвучивание MathCAT и одновременно выделяет соответствующее подвыражение двумя цветами, обеспечивая наглядное визуальное сопровождение для зрячих пользователей; +* Microsoft объявила, что Экранный диктор будет использовать MathCAT в одной из будущих версий. + +[_Другим компаниям_: если вы включили MathCAT в свой продукт и хотите, чтобы он был упомянут здесь, напишите мне по электронной почте или создайте issue с предложением обновить документацию.] + +Была разработана [демонстрационная версия](https://nsoiffer.github.io/MathCATDemo/), которая показывает некоторые возможности MathCAT и помогает при отладке. Сообщайте, пожалуйста, обо всех найденных ошибках. Эта демонстрация _не_ отражает типичный способ взаимодействия пользователей вспомогательных технологий с MathCAT, но показывает функции, которые такие технологии потенциально могут предоставить конечным пользователям: выделение озвучиваемого фрагмента, навигацию и вывод в Брайле. + +Планы дальнейшей работы: + +* Добавить другие языки. Если вы хотите перевести MathCAT на язык, который пока не поддерживается, создайте issue в [репозитории MathCAT на GitHub](https://github.com/daisy/MathCAT/issues). +* Поддержать дополнительные системы записи Брайля. Для добавления такой поддержки нужны три составляющие: + * спецификация системы; + * возможность задавать вопросы специалисту по соответствующей системе записи Брайля; + * человек, готовый перенести не менее 200 примеров из спецификации в используемый для тестов формат: MathML и строку символов Брайля в Unicode. Это может занять 30 часов или больше. + + Если вы можете собрать всё необходимое, создайте issue в [репозитории MathCAT на GitHub](https://github.com/daisy/MathCAT/issues). +* Реализовать преобразование _из_ Брайля _в_ MathML. +* Разработать двумерные варианты систем записи Брайля для многострочных обновляемых дисплеев Брайля, например Monarch и Canute 360. + + + +## Почему MathCAT? + +MathCAT — продолжение MathPlayer. Я начал разрабатывать функции доступности MathPlayer в компании Design Science в 2004 году вскоре после прихода в компанию. В то время MathPlayer был главным образом подключаемым модулем C++ для Internet Explorer (IE), отображавшим MathML на веб-страницах. Долгое время это была наиболее полная из доступных реализаций MathML. Первоначальную работу над отображением математических выражений выполнили основатель Design Science Пол Топпинг и технический директор компании, ныне покойный Роберт Майнер. Позже по ряду причин IE отказался от интерфейса, который MathPlayer использовал для отображения, и не предложил ему замену: веб переходил к использованию JavaScript в браузере и отказывался от рисков безопасности, связанных с внешним кодом. После этого MathPlayer стал библиотекой, предназначенной исключительно для обеспечения доступности и вызываемой другими программами, главным образом NVDA. MathPlayer был проприетарным, но распространялся бесплатно. + +В начале 2017 года я покинул Design Science. Позже в том же году WIRIS приобрела компанию. Я предложил бесплатно исправлять ошибки MathPlayer, и сначала эта инициатива получила поддержку. Но когда пришло время выпускать новую версию, многие сотрудники, работавшие в компании во время приобретения, уже ушли, а оставшаяся команда не была заинтересована в поддержке MathPlayer. Окончательное решение было принято только в конце 2020 года. В 2021 году я начал работать над заменой MathPlayer. В качестве дополнительной задачи я решил изучить Rust и реализовал проект на нём. Rust — низкоуровневый язык со строгой типизацией и безопасной работой с памятью, но без автоматической сборки мусора или подсчёта ссылок. Его часто называют более безопасной заменой C/C++. + +Rust достаточно эффективен. На компьютере с Core i7-770K (мощным по меркам примерно 2017 года процессором) для выражения среднего размера + + + + e + + + + 1 + 2 + + + + + ( + + + + x + + μ + + σ + + + ) + + + 2 + + + + + +требуется около 4 мс, чтобы создать строку ClearSpeak на английском языке +"_e raised to the exponent, negative 1 half times; open paren; the fraction with numerator; x minus mu; and denominator sigma; close paren squared, end exponent_" +и строку в коде Немета "⠑⠘⠤⠹⠂⠌⠆⠼⠈⠡⠷⠹⠭⠤⠨⠍⠌⠨⠎⠼⠾⠘⠘⠆". +Примерно 2 мс занимают очистка MathML, 1 мс — генерация речи, ещё 1 мс — генерация Брайля. Сюда входит проверка актуальности всех файлов правил, которая оказывается достаточно затратной. Эту проверку можно отключить с помощью настройки: она нужна главным образом при отладке. Если проверка отключена, время уменьшается до 2,3 мс. +На более мощном процессоре Intel Core Ultra 9 285 2025 года генерация речи и Брайля в одном потоке занимает около 1 мс. + +
+Показать MathML этого выражения +
+<math>
+  <mrow>
+    <msup>
+      <mi>e</mi>
+      <mrow>
+        <mo>−</mo>
+        <mfrac>
+          <mn>1</mn>
+          <mn>2</mn>
+        </mfrac>
+        <msup>
+          <mrow>
+            <mrow>
+              <mo>(</mo>
+              <mrow>
+                <mfrac>
+                  <mrow>
+                    <mi>x</mi>
+                    <mo>−</mo>
+                    <mi>μ</mi>
+                  </mrow>
+                  <mi>σ</mi>
+                </mfrac>
+              </mrow>
+              <mo>)</mo>
+            </mrow>
+          </mrow>
+          <mn>2</mn>
+        </msup>
+      </mrow>
+    </msup>
+  </mrow>
+</math>
+
+
+ +Для генерации речи и Брайля MathCAT использует внешние правила. +Их загрузка занимает около 40 мс. Она выполняется только при первом использовании правил или после изменения стиля речи, языка либо другой внешней настройки. Ещё 50 мс требуется для загрузки полных файлов Unicode для речи и Брайля, однако исследования показали, что в подавляющем большинстве англоязычных математических материалов для школьного обучения используются лишь немногие символы. +Судя по книгам по математике с открытым исходным кодом, первоначально загружаемого набора должно быть достаточно как минимум для 99,99 % символов, встречающихся в выражениях англоязычных школьных учебников математики. + +Размер библиотеки составляет около 3 МБ. + +Если вы разрабатываете решение для работы непосредственно в браузере, то есть используете JavaScript или другой браузерный язык, MathCAT, вероятно, не будет оптимальным выбором. При этом я, скорее всего, выделю из [MathCATDemo](https://github.com/NSoiffer/MathCATDemo/) интерфейс JavaScript, на котором построена демонстрационная версия. Пока обратите внимание на [Speech Rule Engine](https://github.com/zorkow/speech-rule-engine) (SRE) Фолькера Зорге. Он написан на TypeScript и, вероятно, подойдёт для браузерного решения, если вам не требуется Брайль. MathCAT поддерживает несколько систем записи Брайля и как минимум для кода Немета обеспечивает более качественный результат. + +# Благодарности + +Проекту помогали многие люди, и я очень признателен им за это! + +* Дэвид Карлайл — оказал неоценимую помощь с некоторыми выражениями сопоставления XPath. +* Сьюзан Джолли — терпеливо консультировала по генерации кода Немета и UEB, а также давала обратную связь о правильных и ошибочных результатах. Кроме того, она помогла мне при разработке эвристик для химических формул. +* Элейн А. Мур — помогла определить, что следует и не следует произносить для химических формул, а также какие записи имеют смысл с точки зрения химии. +* Ричард Орм — полностью разработал диалог настроек MathCAT для NVDA. +* Сэм Дули, Мюррей Сарджент и Фолькер Зорге — предоставили таблицы перевода символов в код Немета и тесты этого кода. +* Мориц Гросс ([Math4VIP](https://www.math4vip.de/)) — работал над различными частями кодовой базы Rust и создал инструмент Python для отслеживания прогресса локализации. + +Переводчики: + +* Китайский язык (традиционное письмо) — Хон-Джанг Янг. +* Финский язык — Сами Мяяття, Accessibility Library Celia и независимый специалист Эсси Вииппола. +* Немецкий язык — Назли Анджич, Роберт Граф и Пол Либбрехт (IU International University of Applied Sciences). +* Индонезийский язык — доктор Пинта Дениянти Сампоэрно, магистр наук; доктор Мейлиасари, бакалавр педагогических наук, магистр наук; Ари Хендарно, бакалавр педагогических наук, магистр компьютерных наук. +* Норвежский язык — Марте Гьельстад, Национальная библиотека Норвегии, Kvile. +* Русский язык — Данил Костенков. +* Испанский язык — Ноэлия Руис Мартинес, которая также помогала в разработке дополнения NVDA, и Мария Алло Рольдан. +* Шведский язык — Тим Арбореалис Лётберг, Шведское агентство доступных медиа (MTM), и Андерс Эклунд, SPSM. +* Вьетнамский язык — Данг Хоай Фук и Транг Фам. +* Другие языки??? — присоединяйтесь, чтобы я мог указать вас здесь... + +Первоначальный перевод многих символов Брайля для систем записи, разработанных в 2024 году и позднее, был существенно упрощён благодаря таблице, которую мне предоставил Георгиос Курупретроглу и над которой работала большая команда. Для получения подробной информации обратитесь к: + +* [MathBrailleCodes Repository](https://access.uoa.gr/mathbraille/index.php/en/), Speech and Accessibility Lab, National and Kapodistrian University of Athens, Greece: P. Riga, T. Antonakopoulou, D. Kouvaras, S. Lentas and G. Kouroupetroglou (2021) “[The BrailleMathCodes Repository](https://access.uoa.gr/mathbraille/index.php/en/)”, Proceedings of the 4th International Workshop on “[Digitization and e-Inclusion in Mathematics and Science 2021](https://workshop.sciaccess.net/deims2021/DEIMS2021_Proceedings.zip)” DEIMS2021, February 18-19, 2021, Tokyo, pp. 105-114. + +Спасибо всем участникам! + +# Обо мне + +Я работаю над обеспечением доступности математических выражений с 2002 года. В то время я занимался WYSIWYG-редактором формул Mathematica и другими элементами пользовательского интерфейса. Профессор Джон Гарднер, потерявший зрение пятнадцатью годами ранее, спросил, могу ли я сделать интерфейс Mathematica доступным. Мне удалось выполнить, вероятно, около 80 % работы, но компания не захотела продолжать проект. В конечном итоге я ушёл, а компания удалила код. Так начался мой путь в сфере доступности: шаг вперёд, шаг назад и снова вперёд, потому что возможность дать _каждому_ шанс открыть для себя радость математики и науки наполнила мою жизнь смыслом. + +Затем я перешёл в компанию Design Science, Inc (DSI), заинтересованную в обеспечении доступности математики. Незадолго до этого DSI разработала MathPlayer — подключаемый модуль для IE6, отображавший MathML. Я добавлял в него новые функции и при поддержке компании подал заявку на грант NSF для обеспечения доступности MathPlayer. Заявка была одобрена, а работа оказалась весьма успешной. В последующие годы я продолжал развивать MathPlayer. Однако из соображений безопасности Internet Explorer удалил интерфейс, от которого зависел MathPlayer. Возникает соблазн сказать, что именно это и погубило IE... После этого MathPlayer стал дополнением NVDA, предназначенным исключительно для обеспечения доступности. Дальнейшая работа по гранту IES совместно с ETS помогла усовершенствовать возможности MathPlayer. Финансируемые грантом пользовательские исследования дали ценную информацию. + +Подробнее об истории MathPlayer и появлении MathCAT вы можете узнать в разделе [«Почему MathCAT?»](#why-mathcat). + +На протяжении всего этого времени я стремился обеспечить поддержку математических выражений в интернете и сделать их доступными. Работая в Wolfram Research, я помог запустить проект W3C MathML и с тех пор участвую в деятельности рабочей группы. Сейчас я являюсь сопредседателем рабочей группы W3C Math. За эти годы я также участвовал в работе нескольких других комитетов и активно добивался включения доступности математических выражений в их стандарты. Среди таких групп — NIMAS, EPUB и PDF/UA. + +Для меня большая честь, что в 2023 году Национальная федерация слепых присудила мне премию Джейкоба Болотина в размере $25 000. Из этой суммы я пожертвовал $15 000 проекту _Open Collective_ для улучшения поддержки MathML в браузерах. [По этой ссылке можно узнать, как помочь улучшить поддержку MathML в браузерах](https://opencollective.com/mathml-core-support). diff --git a/docs/ru/nav-commands.md b/docs/ru/nav-commands.md new file mode 100644 index 000000000..e26f526e7 --- /dev/null +++ b/docs/ru/nav-commands.md @@ -0,0 +1,54 @@ +--- +layout: default +lang: ru +ref: nav-commands +title: Команды навигации MathCAT +--- +# Команды навигации MathCAT и соответствующие сочетания клавиш + +При навигации используются два независимых режима: +* Режим озвучивания после перемещения: выражение читается или описывается, то есть после каждого перемещения озвучивается его краткое описание или обзор. +* Режим навигации: перемещение по подвыражениям, небольшим фрагментам или символам. Сочетания Shift+стрелка вниз и Shift+стрелка вверх циклически переключают более подробные и более общие режимы. + +Объяснение этих режимов приведено после таблицы в разделе [«Режимы навигации»](#navigation-modes). + +Примечание: при навигации по выражению сочетание Ctrl+C в NVDA копирует математическое содержимое текущего узла в формате MathML, LaTeX, ASCIIMath или текста для озвучивания. + + +## Таблица команд навигации + +| Клавиша | Без модификатора | + Ctrl | + Shift | + Ctrl + Shift | +| --- | --- | --- | --- | --- | +| **Стрелка влево** | Перейти к предыдущему элементу | В таблице: перейти к предыдущей ячейке.
В столбиковой записи: перейти к предыдущей цифре.
Примечание: также можно использовать Ctrl+Alt+стрелка влево. | Прочитать предыдущий элемент | Описать предыдущий элемент | +| **Стрелка вправо** | Перейти к следующему элементу | В таблице: перейти к следующей ячейке.
В столбиковой записи: перейти к следующей цифре.
Примечание: также можно использовать Ctrl+Alt+стрелка вправо. | Прочитать следующий элемент | Описать следующий элемент | +| **Стрелка вверх** | Уменьшить детализацию | В таблице: перейти к ячейке выше.
В столбиковой записи: перейти к цифре выше.
Примечание: также можно использовать Ctrl+Alt+стрелка вверх. | Перейти к более общему режиму навигации: расширенному, простому или посимвольному | Уменьшить детализацию до минимальной | +| **Стрелка вниз** | Увеличить детализацию | В таблице: перейти к ячейке ниже.
В столбиковой записи: перейти к цифре ниже.
Примечание: также можно использовать Ctrl+Alt+стрелка вниз. | Перейти к более подробному режиму навигации: расширенному, простому или посимвольному | Увеличить детализацию до максимальной | +| **Enter** | Сообщить текущую позицию | Сообщить полную текущую позицию |   |   | +| **Цифры**
**1–10 (0 означает 10)** | Перейти к метке позиции | Установить метку позиции | Прочитать содержимое метки позиции | Описать содержимое метки позиции | +| **Пробел** | Прочитать текущий элемент | Прочитать текущую ячейку | Переключить режим речи между чтением и описанием | Описать текущий элемент | +| **Home** | Перейти к началу выражения | Перейти к началу строки | Перейти к началу столбца.
Перейти к верхней цифре. | _Пока не реализовано:_ прочитать от начала выражения | +| **End** | Перейти к концу выражения | Перейти к концу строки | Перейти к концу столбца.
Перейти к нижней цифре. | _Пока не реализовано:_ прочитать до конца выражения | +| **Backspace** | Вернуться к предыдущей позиции |   |   |   | + + +## Режимы навигации + +MathCAT поддерживает три режима навигации: расширенный, простой и посимвольный. Первые два режима следуют смыслу озвученного выражения, если только не выбран режим LiteralSpeech. Например, в выражении $\vert x+y \rvert > 0$ вертикальные линии, обозначающие абсолютную величину, не читаются буквально. Вместо них произносится «модуль». При увеличении детализации MathCAT сразу перейдёт к чтению «x плюс y». В посимвольном режиме при перемещении по выражению будут последовательно произнесены «вертикальная линия», «x», «плюс», «y», «вертикальная линия», «больше», «ноль». + +* _Расширенный режим_: перемещение между математически значимыми фрагментами: операторами, ограничителями и операндами. +* _Простой режим_: перемещение по словам. Когда встречается двумерное обозначение, например дробь или корень, оно читается целиком. Увеличение детализации позволяет изучить двумерное обозначение в том же режиме. После уменьшения детализации или выхода из двумерного обозначения выполняется возврат на внешний, более общий уровень навигации. +* _Посимвольный режим_: в действительности включает два полезных режима — режим слов и режим символов. Чтобы перейти к настоящему посимвольному режиму, увеличьте детализацию. Перемещение выполняется по словам или символам. Разница заметна в многозначных числах и именах функций из нескольких символов, например `sin`. В остальных случаях навигация по словам и символам работает одинаково. В обоих вариантах детализация дробей и других подобных элементов увеличивается автоматически. + +## Типичный сценарий использования + +Обычно навигация начинается с первого члена выражения, после чего пользователь при необходимости перемещается вправо. Для перехода между уровнями используются стрелки вверх и вниз. Для перемещения по элементам таблицы используется Alt+Ctrl+стрелка. + +Нажатие Backspace возвращает к предыдущей позиции, что не всегда совпадает с перемещением влево. Например, если стрелка вправо вывела вас из дроби, Backspace вернёт в ту же позицию знаменателя, а стрелка влево перейдёт ко всей дроби. + +Вероятно, большую часть времени вам будет удобнее один определённый режим навигации. Его можно задать в настройках MathCAT. +Однако в любой момент навигации режим можно переключить с помощью Shift+стрелка вверх или Shift+стрелка вниз. +Это полезно, поскольку у каждого режима навигации есть свои преимущества и ограничения. + +## Благодарности +Одна из версий этого документа была подготовлена в рамках проекта ClearSpeak. +ClearSpeak поддерживался Институтом педагогических наук Министерства образования США в рамках гранта R324A110355, предоставленного Educational Testing Service. diff --git a/docs/ru/new_translators_guide_MathCAT_revised.md b/docs/ru/new_translators_guide_MathCAT_revised.md new file mode 100644 index 000000000..a44bb6408 --- /dev/null +++ b/docs/ru/new_translators_guide_MathCAT_revised.md @@ -0,0 +1,568 @@ +--- +layout: default +lang: ru +ref: new-translators-guide +title: Руководство переводчика MathCAT +--- +# Руководство переводчика MathCAT + +В этом руководстве объясняется, как создать или обновить перевод MathCAT. В качестве примера целевого языка используется португальский. + +Большая часть руководства посвящена **переводу речи (TTS)**. Если вы работаете над Брайлем, всё равно сначала прочитайте описание основного процесса: настройка среды и тестирование во многом похожи. Затем перейдите к разделу [«Перевод Брайля»](#braille-translation), чтобы узнать об отличиях. + +Для качественного перевода **необязательно** быть опытным программистом. Некоторые успешные переводчики начинали с базовыми знаниями программирования без опыта работы с YAML и Rust. Важнее всего хорошо знать целевой язык, понимать, как на нём обычно произносят или записывают математические выражения, и тщательно проверять результат. + +Знание математики будет очень полезно. Необязательно быть профессиональным математиком, но необходимо замечать неестественные, неясные или математически неверные формулировки. Хорошей отправной точкой будет владение языком на уровне носителя и уверенное знание математики примерно на уровне университета. + +## Оценка трудозатрат + +Полный перевод требует времени, особенно если вы стремитесь сделать его надёжным и удобным. + +Судя по реальным проектам, тщательный перевод TTS может занять около **300–450 часов**, а перевод Брайля — около **160–240 часов**. Сроки сильно зависят от качества исходной заготовки, наличия похожего языка и вашего опыта программирования. + +## Нужна помощь в начале работы? + +Это руководство написали Марте Гьельстад, Тим Арбореалис Лётберг и Андерс Эклунд, создавшие норвежский и шведский переводы MathCAT. Если вам нужна помощь, напишите нам: + +**[mathcat-wg@daisylists.org](mailto:mathcat-wg@daisylists.org)** + +--- + +## Рекомендуемый процесс + +Работу над переводом MathCAT удобно организовать так: + +1. Настроить рабочую копию. +2. Обновить и проверить перевод с помощью аудита. +3. Как можно раньше начать слушать речь. +4. Добавить и запустить тесты. +5. Изучить режимы речи и уровни подробности. +6. Работать с файлами правил. +7. Попросить математика проверить перевод. +8. Провести пользовательское тестирование. +9. Отправить перевод. +10. Поддерживать перевод в актуальном состоянии. + +--- + +## Шаг 1. Настройте рабочую копию + +### Получите репозиторий + +MathCAT размещён на GitHub. + +Если вы раньше не использовали GitHub, не позволяйте этому вас остановить. Для перевода нужна лишь небольшая часть его возможностей: получить копию проекта, редактировать файлы и сохранять изменения. + +Часто проще всего начать с **GitHub Desktop**: +[https://desktop.github.com/](https://desktop.github.com/) + +Основной процесс: + +1. Создайте форк репозитория MathCAT. +2. Клонируйте его на свой компьютер. +3. Работайте в собственной ветке, например `pt`. + +Этого достаточно для начала. + +### Установите необходимые инструменты + +Потребуется несколько инструментов. Поначалу список может показаться длинным, но у каждого инструмента есть ясное назначение. + +* Git: [https://git-scm.com/](https://git-scm.com/) +* GitHub Desktop: [https://desktop.github.com/](https://desktop.github.com/) +* Редактор кода, например VS Code: [https://code.visualstudio.com/](https://code.visualstudio.com/) +* Rust: [https://rust-lang.org/tools/install/](https://rust-lang.org/tools/install/) +* NVDA: [https://www.nvaccess.org/download/](https://www.nvaccess.org/download/) +* MathCAT, установленный из магазина дополнений NVDA. + +Rust требуется только для запуска автоматических тестов. Чтобы переводить MathCAT, изучать программирование на Rust не нужно. + +--- + +## Шаг 2. Обновите перевод и выполните аудит + +Перед началом перевода убедитесь, что файлы вашего языка соответствуют текущей структуре английских файлов. Если пропустить этот шаг, можно потратить время на исправление проблем, вызванных устаревшими файлами. + +### Используйте инструмент аудита + +Документация: +[https://github.com/daisy/MathCAT/blob/main/PythonScripts/audit_translations/README.md](https://github.com/daisy/MathCAT/blob/main/PythonScripts/audit_translations/README.md) + +Используйте инструмент аудита, чтобы найти отсутствующие правила в файлах `pt`, и добавьте их. Подробные инструкции приведены в документации инструмента, поэтому начните с неё. + +### По возможности начните с похожего языка + +Если уже существует грамматически очень похожий язык, часто лучше начать с него, а не с английского или автоматически созданного перевода. + +Например, норвежский перевод был основан на шведском. Это может значительно сэкономить время, особенно если грамматика и порядок слов систематически отличаются от английских. Но перед переводом на свой язык всё равно используйте инструмент аудита для поиска отсутствующих правил. + +--- + +## Шаг 3. Как можно раньше начните слушать речь + +Слушайте результат по мере работы, чтобы быстро замечать ошибки. Перевод может выглядеть безупречно в файле YAML, но при озвучивании оказаться неестественным, повторяющимся или неясным. После перевода каждого файла полезно прослушать несколько выражений с помощью программы экранного доступа. + +### Настройте NVDA для локальных правил + +1. Скачайте NVDA: [https://www.nvaccess.org/download/](https://www.nvaccess.org/download/). +2. В NVDA откройте **Сервис → Магазин дополнений** и установите MathCAT. +3. Создайте копию каталога своего языка, например `pt`, и добавьте её в: + +```text +%AppData%\nvda\addons\MathCAT\globalPlugins\MathCAT\Rules\Languages +``` + +Теперь локальный перевод можно слушать в NVDA. + +### Проверяйте реальные выражения + +Для практической проверки речи возьмите выражения MathML из файлов тестов Rust и поместите их в простой HTML-документ. Затем откройте файл и прослушайте его в NVDA. + +Вносите по одному изменению, снова слушайте результат и делайте заметки. Так удобно изучать реальное поведение правил. + +--- + +## Шаг 4. Добавьте и запустите тесты + +Автоматические тесты — важнейшая часть процесса. Они помогают обнаруживать синтаксические ошибки, документируют ожидаемую речь и позволяют убедиться, что будущие изменения MathCAT незаметно не нарушат перевод. Полезно переводить тесты по мере работы, а не откладывать их напоследок. + +Откройте файл `languages.rs` в `MathCAT/tests` и добавьте строку, в случае португальского языка: + +```rust +mod pt; +``` + +Затем в каталоге `MathCAT/tests/Languages`: + +1. Скопируйте `en.rs` в `pt.rs`. +2. Скопируйте каталог `en` в `pt`. + +Если сначала вы выбрали только один стиль речи, другой можно удалить или закомментировать. + +В каждом тесте замените `"en"` на `"pt"`. + +После этого запустите: + +```bash +cargo test Languages::pt +``` + +Вероятно, при первом запуске многие тесты завершатся с ошибкой. Это нормально. + +### Понимание ошибок тестов + +Типичное сообщение об ошибке выглядит примерно так: + +```rust +left: "1 half" +right: "1 halv" +``` + +Главное здесь — сравнение: + +* `left` — ожидаемый текст, записанный в тесте; +* `right` — текст, фактически созданный MathCAT. + +Если созданный текст верен, скопируйте его в тест. Если неверен, исправьте правило. + +Иногда быстрее запустить тесты до перевода всех ожидаемых результатов. Сообщения об ошибках покажут, что MathCAT уже создаёт на вашем языке, и правильные строки можно будет перенести в тесты. + +### Пример простого теста + +```rust +#[test] +fn common_fraction_half() -> Result<()> { + let expr = " + 1 2 + "; + test("pt", "ClearSpeak", expr, "um meio")?; + return Ok(()); +} +``` + +Здесь: + +* `common_fraction_half` — имя теста; +* `expr` содержит выражение MathML; +* `"pt"` — код языка; +* `"ClearSpeak"` — режим речи; +* `"um meio"` — ожидаемый текст для озвучивания. + +### Пример с настройками + +Некоторые тесты используют `test_prefs`, чтобы проверять правило с заданными настройками. + +```rust +#[test] +fn common_fraction_tenths() -> Result<()> { + let expr = " + 17 10 + "; + test_prefs("en", "ClearSpeak", vec![("Verbosity", "Medium"), ("ClearSpeak_Fractions", "Auto")], expr, "17 tenths")?; + test_prefs("en", "ClearSpeak", vec![("Verbosity", "Medium"), ("ClearSpeak_Fractions", "Ordinal")], expr, "17 tenths")?; + return Ok(()); +} +``` + +В таких тестах: + +* `test_prefs` запускает обработку выражения с заданными настройками; +* `vec![]` содержит эти настройки; +* `("Verbosity", "Medium")` задаёт уровень подробности; +* `("ClearSpeak_Fractions", "Auto")` задаёт настройку ClearSpeak. + +В некоторых местах MathCAT также добавляет паузы. В строках тестов они обозначаются знаками препинания, например запятыми и точками с запятой. Если результаты почти совпадают, всегда проверяйте, не связана ли разница с паузами, а не с формулировкой. + +--- + +## Шаг 5. Изучите режимы речи и уровни подробности + +Есть два основных режима речи: ClearSpeak и SimpleSpeak. В статье Нила [A Comparison of Different Styles of Speech for Mathematics](https://scholarworks.calstate.edu/downloads/5t34sv64c) хорошо объясняется различие между ними. Кратко: + +* ClearSpeak должен быть похож на то, как учитель произносит математическое выражение на уроке. +* SimpleSpeak компактно озвучивает простые выражения. Например, $\frac{x}{y} + 1$ читается как «x over y plus one». Слова, обозначающие начало и конец дроби, не используются, потому что числитель и знаменатель просты. Выражение $\frac{x}{y+1}$ читается как «fraction, x over y plus one, end fraction». Здесь такие слова используются, поскольку знаменатель не является простым. + +Вы можете перевести оба стиля речи или сосредоточиться на одном. В зависимости от принятого способа произнесения математических выражений в целевом языке переведённые стили могут различаться сильнее или слабее английских. + +MathCAT также поддерживает уровни подробности **Terse**, **Medium** и **Verbose**. Переводчик сам определяет, насколько они должны различаться в его языке. Иногда естественный перевод объединяет два английских варианта в один, например если в целевом языке нет слова `the`. В других случаях для одного английского варианта может существовать несколько способов произнести выражение. Это нормально. Ясность важнее искусственно созданных различий. + +В ClearSpeak доступно много настроек, используемых в правилах и тестах. Подробнее см. [настройки ClearSpeak](https://github.com/daisy/MathCAT/blob/main/docs/ClearSpeakRulesAndPreferences.docx). Сейчас эти настройки не видны пользователям в настройках MathCAT в NVDA. + +--- + +## Шаг 6. Работайте с файлами правил + +### Какие файлы требуется перевести? + +Основная работа выполняется в файлах правил YAML. При переводе на португальский язык файлы находятся в каталоге `MathCAT\Rules\Languages\pt`: + +* `ClearSpeak_Rules.yaml` +* `definitions.yaml` +* `navigate.yaml` +* `overview.yaml` (необязательно) +* `SimpleSpeak_Rules.yaml` +* `unicode.yaml` +* `unicode-full.yaml` (частично необязательно) + +В подкаталоге `MathCAT\Rules\Languages\pt\SharedRules` находятся небольшие правила, из которых состоят правила в `ClearSpeak_Rules.yaml` и `SimpleSpeak_Rules.yaml`. Все эти файлы также требуется перевести. + +### С чего начать + +Для знакомства с принципами работы правил удобно начать, например, с `ClearSpeak_Rules.yaml` и нескольких файлов из `SharedRules`. + +Как указано выше, многие необходимые изменения будут обнаружены с помощью тестов. Однако файлы `unicode.yaml`, `unicode-full.yaml`, `navigate.yaml` и `definitions.yaml` недостаточно хорошо покрыты тестами и требуют особого внимания при ручной проверке. + +Не тратьте много времени на `unicode-full.yaml` в начале работы. Он содержит несколько тысяч строк, а большинство символов крайне редко встречаются на практике. Некоторые из них даже не отображаются в VS Code. Оставьте этот файл напоследок: достаточно просмотреть его и проверить наиболее узнаваемые символы. + +Файл `overview.yaml` также менее важен, чем остальные, поскольку не влияет непосредственно на чтение математических выражений. Однако он предоставляет программам экранного доступа краткие структурные обзоры выражений, поэтому со временем его стоит проверить. + +### Что делать + +Основная задача — найти все ключи YAML `t:`, соответствующие произносимым текстовым строкам. Строчная буква в ключе `t:` означает, что текст ещё не проверен переводчиком. Проверьте автоматически созданный перевод или измените его. После проверки замените ключ на `T:`, чтобы отметить обработанный фрагмент. + +Тот же принцип применяется ко всем ключам `ct:`, `ot:`, `spell:`, `pronounce:` и `IfThenElse:`, однако они встречаются гораздо реже, чем `t:`. + +### Адаптация и создание собственных правил + +Тесты покрывают большинство правил. Однако в португальском или другом языке могут существовать грамматические особенности, которых нет в английском и которые не охвачены тестами. Поэтому полезно просмотреть все правила и убедиться в их корректности. + +При грамматических различиях простой замены текстовых строк недостаточно. Необходимо изменить правила или добавить новые. Для совершенно новых правил полезно написать тесты, подтверждающие правильность результата. Не бойтесь пробовать разные варианты. + +Ниже приведён краткий обзор структуры правила MathCAT. Каждое правило описывает условия применения и произносимый текст. Основные части: + +* `name` — имя правила; +* `tag` — элемент MathML, к которому применяется правило; +* `match` — условие применения правила; +* `replace` — результат и действия MathCAT. + +Внутри `replace` часто используются следующие команды: + +* `t:` — произносимый текст; +* `ct:` — присоединить текст без пробела перед ним; +* `ot:` — необязательный текст для предотвращения повторений; +* `x:` — выражение XPath; +* `test:` — условная логика; +* `pause:` — пауза в речи; +* `bookmark:` — синхронное выделение. + +#### Пример 1 + +```yaml +- name: squared + tag: power + match: "*[2][self::m:mn][.='2'] and $ClearSpeak_Exponents = 'Auto'" + replace: + - x: "*[1]" + - bookmark: "*[2]/@id" + - t: "squared" # phrase(7 'squared' equals 49) +``` + +Это правило означает: если степень имеет показатель 2 и настройка `ClearSpeak_Exponents` равна `Auto`, следует вывести «основание в квадрате». Например, $x^2$ озвучивается как «x squared». + +Обратите внимание: + +* `match` определяет выражения, к которым применяется правило. +* Выражение после `x` — это XPath. Оно получает первый дочерний узел элемента степени, то есть основание. +* Строка после `t` должна быть переведена. После проверки перевода измените `t` на прописную `T`. + +#### Пример 2 + +```yaml +- name: fraction-over-text + tag: fraction + match: + - "not($ClearSpeak_Fractions='General' or $ClearSpeak_Fractions='GeneralEndFrac') and" + - "( " + - " ((*[1][self::m:mi or self::m:mtext][string-length(.)>1]) or " + - " (*[1][self::m:mrow][count(*)=3][ " + - " *[1][self::m:mn] and " + - " *[2][self::m:mo][.='⁢'] and " + - " *[3][self::m:mi or self::m:mtext][string-length(.)>1] ]) ) and" + - " ((*[2][self::m:mi or self::m:mtext][string-length(.)>1]) or " + - " (*[2][self::m:mrow][count(*)=3][ " + - " *[1][self::m:mn] and " + - " *[2][self::m:mo][.='⁢'] and " + - " *[3][self::m:mi or self::m:mtext][string-length(.)>1] ]) )" + - ")" + replace: + - x: "*[1]" + - t: "over" # phrase(the fraction 3 'over' 4) + - x: "*[2]" + - test: + if: "$ClearSpeak_Fractions='EndFrac' or $ClearSpeak_Fractions='OverEndFrac'" + then: + - pause: short + - t: "end fraction" # phrase(7 over 8 'end fraction') + - pause: short +``` + +Для применения правила должны выполняться все условия из `match`. + +В обычной формулировке: + +* настройка `ClearSpeak_Fractions` не должна иметь значение `General` или `GeneralEndFrac`, **и** +* числитель должен быть текстовой строкой длиной более одного символа или числом, неявно умноженным на такую строку, **и** +* знаменатель должен удовлетворять тем же условиям, что и числитель. + +Если все условия выполнены, дробь озвучивается как «числитель над знаменателем». Если `ClearSpeak_Fractions` имеет значение `EndFrac` или `OverEndFrac`, также добавляется короткая пауза перед фразой «конец дроби». + +Например, выражение $\frac{\text{meter}}{\text{second}}$ озвучивается как «meter over second». + +--- + +## Шаг 7. Попросите математика проверить перевод + +Когда перевод станет достаточно полным, попросите математика, учителя математики или другого специалиста-предметника, владеющего языком как родным, проверить представительный набор результатов. + +Полезно подготовить несколько примеров выражений вместе с расшифровками текста MathCAT. Попросите проверяющего отметить всё, что звучит математически странно, неоднозначно или непривычно. + +Такая проверка важна: перевод может быть грамматически верным, но не похожим на настоящую математическую речь. Специалист часто замечает проблемы в терминологии, именах символов и способах озвучивания крупных структур. + +При этом не всегда возможно идеально удовлетворить каждое пожелание. Особенности программ экранного доступа и структуры MathCAT могут означать, что наиболее элегантная с математической точки зрения формулировка не является самой удобной. + +--- + +## Шаг 8. Проведите пользовательское тестирование + +Если вы сами не являетесь опытным пользователем программы экранного доступа, пользовательское тестирование обязательно. + +Лучше всего привлекать незрячих пользователей, уверенно работающих с математикой, особенно специалистов в области STEM. Предоставьте им локальную версию MathCAT с переводом и HTML-файл с представительными выражениями MathML. + +Сначала полезно дать пользователям возможность самостоятельно попробовать перевод. Затем встретьтесь с ними и разберите дополнительные примеры вместе. Спрашивайте не только о правильности формулировок, но и о ясности структуры, полезности пауз и соответствии привычному способу восприятия. + +Устная математика оценивается не только по корректности: она должна быть удобна в реальной работе с программой экранного доступа. На практике обратная связь пользователей часто важнее теоретических предпочтений в грамматике, особенно если спор касается ясности или навигации, а не математического смысла. + +--- + +## Шаг 9. Отправьте перевод + +Перед созданием pull request или передачей перевода полезно выполнить итоговую проверку. Это уменьшит количество лишних исправлений и упростит рецензирование. + +### Итоговый список проверок + +Проверьте следующие пункты. + +#### Файлы и структура + +* Файлы вашего языка соответствуют текущей структуре английских файлов. +* Правила не пропущены: инструмент аудита запущен. +* В переводе нет очевидных остатков английского текста. + +#### Состояние перевода + +* Во всём проверенном тексте `t:` заменено на `T:`, `ct:` на `CT:`, `ot:` на `OT:` и т. д. +* Вы осознанно проверили формулировки, а не только использовали автоматический перевод. + +#### Речь + +* Вы прослушали представительный набор выражений в NVDA. +* На вашем языке результат звучит естественно и однозначно. +* Паузы и структура хорошо воспринимаются на слух. + +#### Тесты + +* Для вашего языка добавлены тесты. +* Все тесты проходят: + + ```bash + cargo test Languages::pt + ``` + +* Ожидаемые результаты тестов соответствуют задуманной речи, включая паузы. + +#### Экспертная проверка + +* Математик или другой специалист проверил представительный набор результатов. +* Основные проблемы терминологии и структуры устранены. + +#### Пользовательское тестирование + +* Проведено хотя бы некоторое тестирование с пользователями программ экранного доступа. +* Учтена обратная связь о ясности и удобстве использования. + +### Отправка перевода + +После выполнения списка проверок: + +1. **Создайте коммит с изменениями** в своей ветке, например `pt`. +2. **Отправьте ветку** на GitHub. +3. **Создайте pull request** в основной репозиторий MathCAT. + +В pull request полезно указать: + +* краткое описание перевода; +* завершённые части, например ClearSpeak, SimpleSpeak и тесты; +* известные ограничения и области, требующие дальнейшей работы. + +После этого сопровождающие проекта проверят работу и могут предложить изменения перед слиянием. + +### Когда перевод станет доступен пользователям? + +После слияния перевода с MathCAT: + +* он войдёт в будущие выпуски MathCAT; +* пользователи смогут выбрать язык в поддерживаемых программах экранного доступа, например NVDA. + +Чтобы использовать перевод сразу, не дожидаясь официального выпуска: + +* продолжайте использовать каталог локальных правил в NVDA или +* установите разрабатываемую версию MathCAT, содержащую ваши изменения. + +--- + +## Шаг 10. Поддерживайте перевод в актуальном состоянии + +Строго говоря, перевод никогда не бывает «завершён». Даже после слияния со временем появятся небольшие улучшения, особенно когда пользователи начнут работать с реальными материалами. + +Кроме того, английская версия активно развивается, и новые функции необходимо добавлять в перевод. Для синхронизации используйте [инструмент аудита перевода](https://github.com/daisy/MathCAT/blob/main/PythonScripts/audit_translations/README.md). + + + +--- + + +## Перевод Брайля + +Общий процесс перевода Брайля похож на перевод речи, но есть несколько важных отличий. + +Перед работой над правилами Брайля определите, какой стандарт математической записи используется в вашей стране. В некоторых странах существуют установленные системы записи Брайля, а в других применяются обозначения другого типа, например LaTeX или ASCIIMath. Решите заранее, какому стандарту должен следовать MathCAT. + +Если уже существует похожая система Брайля, её можно использовать как отправную точку. В противном случае, скорее всего, придётся начинать почти с нуля. + +### Файлы Брайля + +Для новой системы записи Брайля создайте три файла `.yaml` в `Rules\Braille\your-braille-language`: + +1. `xxx_Rules.yaml`, где `xxx` — имя системы записи Брайля. Файл содержит правила преобразования MathML в Брайль. +2. `unicode.yaml` — переводы наиболее распространённых символов. +3. `unicode-full.yaml` — переводы остальных символов. + +Два файла Unicode используются по практической причине: небольшой файл с распространёнными символами ускоряет запуск. + +### Пример шведского языка + +Шведский перевод Брайля следует официальной шведской системе математической записи Брайля. Это шеститочечный стандарт, предназначенный для печатных материалов. Устоявшегося восьмиточечного математического стандарта не было, поэтому перевод основывался на единственном признанном стандарте. + +Работа над шведским переводом также показала, что исходные стандарты не всегда достаточно строги для программного обеспечения. Некоторые печатные стандарты допускают расстановку пробелов в соответствии с печатным оригиналом. Для MathCAT этого недостаточно. Правила должны быть явными, поэтому иногда потребуется устранить несогласованности и принять однозначные решения о пробелах. + +### Тестирование Брайля + +Основной принцип похож на тестирование TTS, но вместо строк речи проверяются символы Брайля в Unicode. + +Удобно писать тесты Rust с ожидаемым выводом Брайля из национального стандарта или согласованных примеров. Запускайте тесты, изучайте ошибки и дорабатывайте правила до совпадения результата. + +В шведском переводе использовались примеры непосредственно из шведского стандарта Брайля, особенно для арифметики, дробей, подстрочных и надстрочных индексов, а также основных функций. Это помогло прочно связать правила с реальным стандартом. + +### Особые сложности Брайля + +При работе с Брайлем часто возникают проблемы, не встречающиеся в переводе речи. Например: + +* стандарт может быть неполным или несогласованным; +* стандарт может предполагать печатный контекст, а не автоматически создаваемый вывод; +* у некоторых символов может не быть однозначного устоявшегося представления; +* правила пробелов могут потребовать большей строгости, чем предполагает исходный стандарт. + +### Пользовательское тестирование Брайля + +Пользовательское тестирование Брайля так же важно, как тестирование речи. + +При работе над шведским переводом тестировщики использовали NVDA вместе со средством просмотра Брайля, чтобы контролировать результат во время удалённых сеансов. Это позволяло наблюдать вывод, даже если сами рецензенты читали его на физическом дисплее Брайля. + +Как и для речи, важна не только техническая правильность. Результат должен быть читаемым, согласованным и полезным в реальной работе. + +--- + +## Приложение + +### Практические советы для эффективной работы + +Инструменты искусственного интеллекта могут быть полезны при подготовке чернового перевода, поиске альтернативных формулировок и разборе сложного правила. Они также могут помочь при написании и редактировании правил. + +Однако относитесь к ним как к помощникам, а не как к авторитетным источникам. Автоматический перевод всегда требует проверки человеком, а изменения правил — тестирования на реальном выводе. + +В целом полезно вносить по одному изменению, часто запускать тесты и при возможности обсуждать сложные случаи с другими переводчиками или специалистами. + +### Устранение проблем + +Если что-то работает не так, как ожидалось, сначала проверьте отступы. Ошибки YAML распространены, и их бывает неожиданно трудно заметить. При использовании VS Code полезно установить расширение YAML, помогающее находить такие ошибки. + +Также внимательно изучайте результаты тестов и снова слушайте речь. Многие проблемы легче услышать, чем увидеть. + +### XPath + +Во многих частях правил речи используется XPath. Это распространённый и хорошо документированный способ выбора частей XML-документа. В интернете есть множество руководств. Если вы ещё не знакомы с XPath, рекомендуется прочитать одно из них. В MathCAT используется немного расширенная реализация XPath 1.0. + +В MathCAT обычно применяются лишь некоторые возможности XPath, а также несколько специальных функций. Ниже кратко описаны распространённые варианты использования XPath. + +| Запись | Значение | +| --- | --- | +| `*` | Соответствует всем дочерним узлам. | +| `[...]` | Выбирает узлы из текущего сопоставления. | +| `*[1]` | Выбирает первый дочерний узел. | +| `*[self::m:mn]` | Выбирает все дочерние элементы `mn`. Префикс `m` показывает, что элемент находится в пространстве имён MathML. | +| `*[1][self::m:mn]` | Выбирает первый дочерний узел, если он является элементом `mn`. | +| `*[1][self::m:mo][text()='-']` | Выбирает первый дочерний узел, если он является элементом `mo` с содержимым `-`. Можно записать `*[1][text()='-']`, поскольку другие узлы, скорее всего, не содержат `-`, но такое содержимое допустимо для `mtext`, поэтому безопаснее указать имя элемента. | +| `count(*[2]/*)` | Количество дочерних узлов второго дочернего узла. | +| `count(preceding-sibling::*)+1` | Количество одноуровневых узлов перед текущим элементом плюс один. | + +MathCAT добавляет специальные функции, упрощающие написание правил: + +| Функция | Значение | +| --- | --- | +| `IsNode(nodes, type)` | Возвращает true, если все узлы имеют одинаковый тип. Допустимые типы:
`"simple"` — заданный набор элементов ClearSpeak;
`"leaf"` — один из листовых элементов MathML;
`"2D"` — двумерный узел, например `mfrac` или `mroot`;
`"modified"` — узел с индексом или элементом над ним либо под ним;
`"scripts"` — узел с подстрочным и (или) надстрочным индексом;
`"common_fraction"` — целочисленные числитель и знаменатель. | +| `ToOrdinal` | | +| `ToCommonFraction` | | +| `IsBracketed(openChar, closeChar, requiresComma)` | | +| `BaseNode(node)` | Рекурсивно возвращает основу узла с индексами. | +| `IsInDefinition(node, name)` | Возвращает true, если узел входит в список `name`, заданный в `definitions.yaml`. | +| `IfThenElse(test, then-part, else-part)` | Возвращает `then-part`, если условие истинно, иначе возвращает `else-part`. Все аргументы являются выражениями XPath. | +| `DistanceFromLeaf(node, left_side, treat_2d_elements_as_tokens)` | Возвращает расстояние от текущего узла до крайнего левого или правого листа: для символа 0, для токена 1. Если `left_side` равно `true`, обход выполняется по крайним левым дочерним узлам до листа, иначе по правым. Если `treat_2d_elements_as_tokens` равно `true`, двумерные обозначения, например дроби, считаются одиночными токенами, подобными листам. | +| `EdgeNode(node, "left"/"right", stopNodeName)` | Возвращает узел остановки, если исходный узел находится у левого или правого края указанного предка. Значением `stopNodeName` также может быть `"2D"`. Если соответствие не найдено, возвращается исходный узел. Если `stopNodeName` равно `"math"`, учитывается пунктуация, поскольку она не является частью математического выражения. | +| `DEBUG(xpath)` | Добавляет полезные для диагностики сведения в отладочный вывод. | + +Следующие функции используются правилами кода Немета: + +| Функция | Значение | +| --- | --- | +| `NestingChars` | Используется правилами `mfrac`, `msqrt` и `mroot`, чтобы повторять символы нужное количество раз. | +| `BrailleChars` | Используется элементами-токенами для сложной перестановки индикаторов кода Немета, например прописных букв и начертания шрифта. | diff --git a/docs/ru/users.md b/docs/ru/users.md new file mode 100644 index 000000000..29d4fa9f1 --- /dev/null +++ b/docs/ru/users.md @@ -0,0 +1,203 @@ +--- +layout: default +lang: ru +ref: users +title: Руководство пользователя MathCAT +--- +# Логотип MathCAT Руководство пользователя + +## Рекомендации по выбору голоса + +В NVDA можно использовать разные синтезаторы речи. Для выбора откройте `Параметры:Настройки...` в NVDA, а затем выберите категорию `Речь`. Обычно доступны как минимум три варианта: eSpeak NG, Microsoft Speech API и голоса Windows OneCore. Все синтезаторы работают, но голоса Windows OneCore неправильно произносят «a», поэтому рекомендуется выбрать другой вариант. В частности, хорошей заменой голосам OneCore служит Microsoft Speech API. + +## Информация для пользователей MathCAT + +MathCAT поддерживает ряд настроек озвучивания, Брайля и навигации. Они описаны ниже. +Пока поддерживаются не все настройки. Для каждой настройки указано текущее состояние поддержки. Символ ✓ перед настройкой означает, что она поддерживается хотя бы частично. + +Примечание: в NVDA настройки задаются в диалоге параметров MathCAT. Чтобы открыть его, перейдите в параметры NVDA, выберите «Параметры», а затем «Настройки MathCAT...». Настройки разделены на три категории: «Речь», «Навигация» и «Брайль». Такое же разделение используется ниже. + +MathCAT поддерживает несколько режимов навигации. Способ входа в режим навигации и выхода из него зависит от используемой вспомогательной технологии (подробнее в списке ниже). MathCAT принимает те же команды и сочетания клавиш, что и MathPlayer. Они [перечислены в этом документе](nav-commands.md). +Документация описывает множество полезных способов навигации по математическим выражениям. Чтобы быстро начать работу: + +* Используйте клавиши-стрелки для перемещения влево, вправо, вверх и вниз по структуре математического выражения, например для входа в дробь и выхода из неё. +* Внутри таблицы нажимайте Ctrl+стрелка для перемещения по ячейкам. +* Нажимайте Home и End для перехода к началу и концу выражения. +* Нажимайте пробел для озвучивания текущей позиции. +* Нажимайте Shift+стрелка вверх или Shift+стрелка вниз для изменения режима навигации (см. [документацию по навигации](nav-commands.md)). + +Чтобы начать навигацию: + +* NVDA: нажмите NVDA+Alt+M или пробел для входа в режим навигации по математическому выражению, а для выхода нажмите Escape. + +Навигация MathCAT работает одинаково в Word и в браузере. + +При навигации по выражению сочетание Ctrl+C в NVDA копирует математическое содержимое текущего узла. Поддерживаются следующие форматы: + +* MathML (по умолчанию); +* LaTeX; +* ASCIIMath; +* текст для озвучивания. + +## Список настроек + +Ниже перечислены настройки. Большинство из них принимают только ограниченный набор значений, который указан в описании. +Значение по умолчанию приведено в \[квадратных скобках\]. + +### Настройки речи + +* ✓Impairment: [Blindness] + * Значения: Blindness, LowVision, LearningDisability. + * Описание: определяет, следует ли устранять неоднозначность некоторых обозначений при озвучивании. + * Состояние: основное внимание уделялось значению Blindness, но другие значения также частично поддерживаются. Эту поддержку необходимо улучшить. + +* ✓Language: [en] + * Значения: любой известный код языка и подкод, например `en-uk`. + [Список вариантов приведён на этом сайте](https://www.venea.net/web/culture_code). + * Описание: определяет используемый язык. + Если региональный вариант не найден среди правил озвучивания, используется основной язык. Если не найдены и правила для основного языка, используется английский язык (`en`). + * Состояние: сейчас поддерживаются только английский, испанский, финский, индонезийский, шведский, вьетнамский и китайский языки. + Другие языки будут добавляться с помощью добровольцев. + +* ✓SpeechStyle: [ClearSpeak] + * Значения: любой реализованный стиль речи. Сейчас доступны только ClearSpeak и SimpleSpeak. + * Описание: стиль речи, то есть согласованный подход к озвучиванию выражения. + * ClearSpeak был разработан ETS для важных экзаменов, например SAT. [Подробности спецификации ClearSpeak приведены в этом документе Word](../ClearSpeakRulesAndPreferences.docx). + * SimpleSpeak стремится сократить озвучивание: простые выражения, например $\frac{a}{b}$, читаются быстро, без обрамляющих слов («a over b»). Они отличаются от более сложных выражений, например $\frac{a}{b+1}$, в которых обрамляющие слова используются всегда («fraction a over b plus 1 end fraction»). + * Состояние: сейчас реализованы только ClearSpeak и SimpleSpeak, но в дальнейшем, вероятно, будет реализован MathSpeak. + +* ✓Verbosity: [Medium] + * Значения: Terse, Medium, Verbose. + * Описание: определяет количество дополнительных слов при озвучивании. Например, в подробном режиме квадратный корень читается как «the square root of x», а в кратком — как «square root x». + * Состояние: настройка поддерживается, но со временем, вероятно, будет дорабатываться. + +* ✓MathRate: [100] + * Значения: число от 1 до 100. + * Описание: изменяет относительную скорость речи в процентах от стандартной скорости речевого синтезатора. Значение `100` означает, что математические выражения читаются с той же скоростью, что и обычный текст. + Настройка работает только в реализациях, которые указывают MathCAT создавать разметку речевого синтезатора, например SSML. + * Состояние: настройка должна работать в NVDA. + +* ✓PauseFactor: [50] + * Значения: число от 0 до 100. + * Описание: изменяет относительную длительность добавляемых MathCAT пауз. Значение 0 отключает все паузы, а значение 100 увеличивает обычную длительность пауз в десять раз. + Настройка работает только в реализациях, которые указывают MathCAT создавать разметку речевого синтезатора, например SSML. + * Состояние: настройка должна работать в NVDA. + +* ✓SpeechSound: [None] + * Значения: None, Beep. + * Описание: перед чтением выражения и после него воспроизводится звуковой сигнал. + * Состояние: настройка должна работать в NVDA. + +* SubjectArea: [General] + * Состояние: настройка использовалась в MathPlayer, но пока не реализована. Я жду дальнейшего обсуждения в рабочей группе MathML: возможно, эта настройка будет использоваться для задания разных значений `intent` по умолчанию. + +* Chemistry: [SpellOut] + * Значения: SpellOut, AsCompound, Off. + * Описание: определяет способ чтения химических формул. Примеры для $\mathrm{H}_2\mathrm{O}$: + * ✓SpellOut: «H 2 O» (настройка подробности определяет, произносятся ли слова `sub` и `super`); + * AsCompound: «Water»; + * ✓Off: «H sub 2 O». + * Состояние: реализовано множество эвристик для определения того, является ли запись химической формулой. Распознавание химических обозначений не всегда очевидно, поэтому MathCAT иногда может не распознать формулу или по ошибке принять другую запись за химическую. Работа группы MathML может существенно упростить авторам явное указание химических формул. + +SpeechOverrides: + +* ✓CapitalLetters: "cap" # слово-префикс для прописных букв, если оно не задано в unicode.yaml; пустая строка передаёт обработку программе экранного доступа +* LeftParen: "" # переопределение слова +* RightParen: "" # переопределение слова + +ClearSpeak содержит ряд настроек. Они предназначены для авторов, но могут задаваться и пользователями, хотя обычно не слишком им полезны. + +* ✓CapitalLetters: Auto, SayCaps или изменение высоты тона +* ✓AbsoluteValue: Auto, AbsEnd, Cardinality, Determinant +* ✓Fraction: Auto, Ordinal, Over, FracOver, General, EndFrac, GeneralEndFrac, OverEndFrac, Per +* ✓Exponent: Auto, Ordinal, OrdinalPower, AfterPower +* ✓Roots: Auto, PosNegSqRoot, RootEnd, PosNegSqRootEnd +* ✓Functions: Auto, None +* ✓Trig: Auto, TrigInverse, ArcTrig +* ✓Log: Auto, LnAsNaturalLog +* ✓ImpliedTimes: Auto, MoreImpliedTimes, None +* ✓Paren: Auto, Speak, SpeakNestingLevel, Silent, CoordPoint, Interval +* ✓Matrix: Auto, SpeakColNum, SilentColNum, EndMatrix, Vector, EndVector, Combinatorics +* ✓MultiLineLabel: Auto, Case, Constraint, Equation, Line, None, Row, Step +* ✓MultiLineOverview: Auto, None +* ✓MultiLinePausesBetweenColumns: Short, Long +* ✓Sets: Auto, woAll, SilentBracket +* ✓MultSymbolX: Auto, By, Cross +* ✓MultSymbolDot: Auto, Dot +* ✓TriangleSymbol: Auto, Delta +* ✓Ellipses: Auto, AndSoOn +* ✓VerticalLine: Auto, SuchThat, Divides, Given +* ✓SetMemberSymbol: Auto, Belongs, Element, Member +* ✓Prime: Auto, Angle, Length +* ✓CombinationPermutation: Auto, ChoosePermute +* ✓Bar: Auto, Bar, Conjugate, Mean + +### Настройки навигации (см. [документацию по навигации](nav-commands.md)) + +* ✓NavMode: Enhanced — Enhanced, Simple, Character. +* ResetNavMode: false — запоминать и использовать предыдущее значение. +* Overview: false — озвучивать выражение или давать его описание и обзор. +* ResetOverView: true — запоминать и использовать предыдущее значение. +* ✓NavVerbosity: Medium — Terse, Medium, Full (слова для произнесения команды навигации). +* ✓AutoZoomOut: true — автоматически уменьшать детализацию двумерных выражений. Если настройка отключена, для принудительного уменьшения детализации используйте Shift+стрелка. + * `true`: если вы находитесь у края двумерного выражения, например дроби или надстрочного индекса, и пытаетесь выйти из него влево или вправо, перемещение разрешается, а уровень детализации устанавливается в соответствии с предыдущим или следующим элементом. + * `false`: перемещение влево или вправо за край двумерного выражения запрещено. Чтобы переместиться, необходимо уменьшить детализацию, возможно несколько раз, пока вы не перестанете находиться у края. +* CopyMathAS: определяет формат копирования математического содержимого текущего узла навигации: MathML, LaTeX, ASCIIMath или текст для озвучивания. + + +### Настройки Брайля + +* ✓BrailleCode: [Nemeth] + * Значения: любая реализованная система записи Брайля. + * Описание: используемая система математической записи Брайля. + * Состояние: сейчас поддерживаются ASCIIMath, ASCIIMath-Finnish, CMU, LaTeX, код Немета, шведская система, UEB и вьетнамская система. Поддержка других систем записи Брайля зависит от помощи новых участников. +* ✓BrailleNavHighlight: [EndPoints] + * Значения: Off, FirstChar, EndPoints, All. + * Описание: выделяет выбранный узел навигации точками 7 и 8. +* UEB: + * ✓START_MODE: [Grade2] + * Значения: Grade1, Grade2. + * Описание: предполагаемый начальный режим UEB. Значение Grade1 означает, что используется режим фрагмента Grade 1. + * ✓UseSpacesAroundAllOperators: [false] + * Значения: true/false. + * Описание: рекомендации UEB предполагают, что в младших классах может быть полезно добавлять пробелы вокруг таких операторов, как `+` и `-`. Обычно пробелы добавляются только вокруг операторов отношений, например `=` и `<`. + +Во многих системах записи Брайля можно определять пользовательские символы. MathCAT предоставляет для этого несколько настроек. + +В коде Немета определены начертания Bold, Italic, SansSerif и Script, но не определено начертание DoubleStruck (Blackboard Bold). +Здесь можно задать определяемое транскрибатором изменение начертания. По умолчанию DoubleStruck сопоставляется с Italic. + +* Nemeth: + * ✓SansSerif: "⠠⠨" + * ✓Bold: "⠸" + * ✓DoubleStruck: "⠨" + * ✓Script: "⠈" + * ✓Italic: "⠨" + +В [руководстве UEB по техническим материалам](https://iceb.org/Guidelines_for_Technical_Material_2008-10.pdf) рекомендуется обычно обрабатывать Fraktur и DoubleStruck как Script. +Вместо этого здесь можно задать пользовательский префиксный индикатор начертания. +Примечание: префиксы с первого по пятый: "⠈⠼", "⠘⠼", "⠸⠼", "⠐⠼", "⠨⠼". + +* UEB: + * ✓DoubleStruck: "⠈" [script] + * ✓Fraktur: "⠈" [script] + * ✓SansSerif: "⠈⠼" [первый определяемый транскрибатором префиксный индикатор начертания] + * ✓GreekVariant: "⠨" [по умолчанию Greek] + +Набор определяемых символов для вьетнамской системы ещё обсуждается. Вероятно, некоторые значения изменятся. + +* Vietnam: + * ✓UseDropNumbers: [false] + * Значения: true, false. + * Описание: опускает цифры на строку ниже в простых числовых дробях. + * ✓DoubleStruck: "⠈" [script] + * ✓Fraktur: "⠈" [script] + * ✓SansSerif: "⠈⠼" [первый определяемый транскрибатором префиксный индикатор начертания] + * ✓GreekVariant: "⠨" [по умолчанию Greek] + +### Другие настройки + +MathCAT исправляет некачественный MathML. В MathML числа часто размечаются неправильно. Чтобы исправить их корректно, MathCAT должен знать региональные настройки: символы, которые могут разделять группы цифр, и символы десятичного разделителя. Обычно ассистивная технология задаёт эти параметры на основе кода страны в документе. Но код страны может отсутствовать, и тогда ассистивной технологии приходится определять формат по коду языка. + +* DecimalSeparators: "." # [по умолчанию] +* BlockSeparators: ", \u00A0\u202F" # [по умолчанию; включает два варианта неразрывных пробелов] From 474006525695f30cba84e6a135aee50218b92e59 Mon Sep 17 00:00:00 2001 From: nsoiffer Date: Mon, 1 Jun 2026 22:30:56 -0700 Subject: [PATCH 06/37] Fix #585 -- SSML was being escaped. I earlier added code to prevent a SSML injection attack by escaping HTML. It was done at the wrong point and escape legal SSML. I have moved the check and conversion down to the leaves. I was very surprised there were no useful tests with SSML. All the main tests set `TTS=None`. I added a test that catches bad escaping. I also added some tests to check for leaf-based SSML attacks and also attribute-based ones. --- src/interface.rs | 109 +++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 5 +-- src/speech.rs | 40 ++++++++++++----- src/tts.rs | 49 +++++++++++++++++++-- 4 files changed, 186 insertions(+), 17 deletions(-) diff --git a/src/interface.rs b/src/interface.rs index 11b965028..55fe74698 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -1338,4 +1338,113 @@ mod tests { assert!(set_mathml(bad_mathml).is_err()); assert!(get_spoken_text().unwrap() == ""); } + + + + fn setup_speech_ssml() { + set_rules_dir(super::super::abs_rules_dir_path()).unwrap(); + set_preference("Language", "en").unwrap(); + set_preference("TTS", "SSML").unwrap(); + set_preference("MathRate", "80").unwrap(); + set_preference("SpeechStyle", "SimpleSpeak").unwrap(); + set_preference("Verbosity", "Medium").unwrap(); + } + + #[test] + fn test_no_escaping() -> Result<()> { + setup_speech_ssml(); + let expr = " + + x+y + x-y + + "; + set_mathml(&expr)?; + let speech = get_spoken_text()?; + // Rule-generated SSML must pass through verbatim (not XML-entity-encoded). + assert!(!speech.contains("<")); + assert!(!speech.contains(">")); + assert!(!speech.contains("&lt;")); + return Ok(()); + } + + /// The attack payload must not pass through verbatim (rule-generated SSML may contain `"#; + /// Same bytes as `PAYLOAD`, entity-encoded so attribute values are well-formed XML. + const PAYLOAD_ATTR_XML: &str = "<break time="50000ms"/>"; + /// Entity-encoded payload plus trailing literal text (well-formed in leaf element text). + const PAYLOAD_LEAF_XML: &str = "<break time="50000ms"/>note"; + + #[test] + /// User-supplied leaf text must not inject SSML when TTS is SSML. + fn leaf_text_ssml_attack_neutralized_in_speech() -> Result<()> { + setup_speech_ssml(); + // Entity-encoded payload: valid XML through set_mathml (no CDATA), decodes to PAYLOAD + "note". + let mathml = format!( + r#"{PAYLOAD_LEAF_XML}+ + {PAYLOAD_LEAF_XML}+ + {PAYLOAD_LEAF_XML}+ + {PAYLOAD_LEAF_XML}"# + ); + set_mathml(&mathml)?; + let speech = get_spoken_text()?; + assert_ssml_attack_neutralized(&speech, PAYLOAD); + assert!(speech.contains("note") || speech.contains("<")); + let mathml = format!( + "{PAYLOAD_LEAF_XML}+1" + ); + set_mathml(&mathml)?; + let speech = get_spoken_text()?; + assert_ssml_attack_neutralized(&speech, PAYLOAD); + assert!(speech.contains("note") || speech.contains("<")); + return Ok(()); + } + + #[test] + /// Attribute values read via xpath must not inject SSML when TTS is SSML. + fn attribute_ssml_attack_neutralized_in_speech() -> Result<()> { + use crate::speech::{SpeechRulesWithContext, SPEECH_RULES}; + + setup_speech_ssml(); + let mathml = format!( + r#"x"# + ); + set_mathml(&mathml)?; + let speech = get_spoken_text()?; + assert_ssml_attack_neutralized(&speech, PAYLOAD); + + // XPath Attribute nodes use replace_chars (same path as replace_nodes_string). + SPEECH_RULES.with(|rules| { + rules.borrow_mut().read_files()?; + let rules_ref = rules.borrow(); + let package = parser::parse(&mathml)?; + let math = get_element(&package); + let attr = math + .attribute("data-ssml-attack") + .expect("data-ssml-attack attribute"); + let work_package = Package::new(); + let mut ctx = + SpeechRulesWithContext::new(&rules_ref, work_package.as_document(), "", 0); + let from_attr = ctx.replace_chars(attr.value(), math)?; + assert_ssml_attack_neutralized(&from_attr, PAYLOAD); + assert!( + from_attr.contains("<"), + "attribute value should be XML-escaped for SSML: {from_attr}" + ); + Ok::<(), Error>(()) + })?; + return Ok(()); + } } diff --git a/src/main.rs b/src/main.rs index c196a114d..c02b0b653 100644 --- a/src/main.rs +++ b/src/main.rs @@ -193,8 +193,7 @@ fn main() { // "; let expr = r#" -vi-i=v - "#; + i=1 10 Si "#; // let instant = Instant::now(); // let rules_dir = "".to_string(); // Use MathCATRulesDir, potentially pointing to a zipped version @@ -210,7 +209,7 @@ fn main() { set_preference("DecimalSeparator", "Auto").unwrap(); set_preference("BrailleCode", "UEB").unwrap(); set_preference("BrailleNavHighlight", "On").unwrap(); - set_preference("TTS", "None").unwrap(); + set_preference("TTS", "SSML").unwrap(); set_preference("Verbosity", "Verbose").unwrap(); set_preference("NavVerbosity", "Verbose").unwrap(); set_preference("NavMode", "Enhanced").unwrap(); diff --git a/src/speech.rs b/src/speech.rs index 1ca431506..42eb4ffe4 100644 --- a/src/speech.rs +++ b/src/speech.rs @@ -146,6 +146,7 @@ fn speak_rules(rules: &'static std::thread::LocalKey>, math fn nestable_speak_rules<'c, 's:'c, 'm:'c>(rules_with_context: &mut SpeechRulesWithContext<'c, 's, 'm>, mathml: Element<'c>) -> Result { let mut speech_string = rules_with_context.match_pattern::(mathml) .context("Pattern match/replacement failure!")?; + debug!("Speech string: {}", speech_string); // Note: [[...]] is added around a matching child, but if the "id" is on 'mathml', the whole string is used if !rules_with_context.nav_node_id.is_empty() { // See https://github.com/NSoiffer/MathCAT/issues/174 for why we can just start the speech at the nav node @@ -328,6 +329,10 @@ pub trait TreeOrString<'c, 'm:'c, T> { fn replace_nodes<'s:'c, 'r>(rules: &'r mut SpeechRulesWithContext<'c, 's,'m>, nodes: Vec>, mathml: Element<'c>) -> Result; fn highlight_braille(braille: T, highlight_style: String) -> T; fn mark_nav_speech(speech: T) -> T; + /// Sanitize xpath-derived literal text before it becomes speech (not used for intent/braille trees). + fn sanitize_xpath_string(s: String, _rules_with_context: &SpeechRulesWithContext<'c, '_, 'm>) -> String { + return s; + } } impl<'c, 'm:'c> TreeOrString<'c, 'm, String> for String { @@ -358,6 +363,8 @@ impl<'c, 'm:'c> TreeOrString<'c, 'm, String> for String { fn mark_nav_speech(speech: String) -> String { return SpeechRulesWithContext::mark_nav_speech(speech); } + + // SSML/SAPI escaping is applied in replace_chars; xpath literals go through that path. } impl<'c, 'm:'c> TreeOrString<'c, 'm, Element<'m>> for Element<'m> { @@ -2444,6 +2451,14 @@ impl<'c, 's:'c, 'r, 'm:'c> SpeechRulesWithContext<'c, 's,'m> { return self.speech_rules; } + pub fn escape_string_for_safety(&self, s: String) -> String { + return crate::tts::escape_string_for_safety( + s, + self.speech_rules.name, + &self.speech_rules.pref_manager.borrow().get_tts(), + ); + } + pub fn get_context(&mut self) -> &mut sxd_xpath::Context<'c> { return &mut self.context_stack.base; } @@ -2734,7 +2749,7 @@ impl<'c, 's:'c, 'r, 'm:'c> SpeechRulesWithContext<'c, 's,'m> { }; let matched = match node { Node::Element(n) => self.match_pattern::(n)?, - Node::Text(t) => self.replace_chars(t.text(), mathml)?, + Node::Text(t) => self.replace_chars(t.text(), mathml)?, Node::Attribute(attr) => self.replace_chars(attr.value(), mathml)?, _ => bail!("replace_nodes: found unexpected node type!!!"), }; @@ -2746,6 +2761,13 @@ impl<'c, 's:'c, 'r, 'm:'c> SpeechRulesWithContext<'c, 's,'m> { /// Lookup unicode "pronunciation" of char. /// Note: TTS is not supported here (not needed and a little less efficient) pub fn replace_chars(&'r mut self, str: &str, mathml: Element<'c>) -> Result { + if is_quoted_string(str) { // quoted string -- already translated (set in get_braille_chars) + return Ok(unquote_string(str).to_string()); + } + self.replace_chars_escaping_xml_chars(str, mathml) + } + + fn replace_chars_escaping_xml_chars(&'r mut self, str: &str, mathml: Element<'c>) -> Result { let chars = str.chars().collect::>(); let rules = self.speech_rules; // handled in match_pattern -- temporarily leaving as comments in case something is missed and needed here @@ -2762,24 +2784,22 @@ impl<'c, 's:'c, 'r, 'm:'c> SpeechRulesWithContext<'c, 's,'m> { // return Ok( ch.to_string() ); // } // } - if is_quoted_string(str) { // quoted string -- already translated (set in get_braille_chars) - return Ok(unquote_string(str).to_string()); - } // in a string, avoid "a" -> "eigh", "." -> "point", etc if rules.translate_single_chars_only { if chars.len() == 1 { - return self.replace_single_char(chars[0], mathml) + return self.replace_single_char(chars[0], mathml); } else { - // more than one char -- fix up non-breaking space - return Ok(str.replace('\u{00A0}', " ").replace(['\u{2061}', '\u{2062}', '\u{2063}', '\u{2064}'], "")) + // more than one char -- user literal (e.g. mtext); fix up non-breaking space + let s = str.replace('\u{00A0}', " ").replace(['\u{2061}', '\u{2062}', '\u{2063}', '\u{2064}'], ""); + return Ok(self.escape_string_for_safety(s)); } - }; + } let result = chars.iter() .map(|&ch| self.replace_single_char(ch, mathml)) .collect::>>()? .join(""); - return Ok( result ); + return Ok(result); } fn replace_single_char(&'r mut self, ch: char, mathml: Element<'c>) -> Result { @@ -2805,7 +2825,7 @@ impl<'c, 's:'c, 'r, 'm:'c> SpeechRulesWithContext<'c, 's,'m> { self.translate_count = 0; // not in loop // debug!("*** Did not find unicode {} for char '{}'/{:#06x}", rules.name, ch, ch_as_u32); if rules.translate_single_chars_only || ch.is_ascii() { // speech or if braille, avoid loop (ASCII remains ASCII if not found) - return Ok(String::from(ch)); // no replacement, so just return the char and hope for the best + return Ok(self.escape_string_for_safety(String::from(ch))); } else { // braille -- must turn into braille dots // Emulate what NVDA does: generate (including single quotes) '\xhhhh' or '\yhhhhhh' let ch_as_int = ch as u32; diff --git a/src/tts.rs b/src/tts.rs index e1b28d0fc..521715381 100644 --- a/src/tts.rs +++ b/src/tts.rs @@ -72,7 +72,7 @@ use sxd_document::dom::Element; use yaml_rust::Yaml; use std::fmt; -use crate::speech::{SpeechRulesWithContext, MyXPath, TreeOrString}; +use crate::speech::{RulesFor, SpeechRulesWithContext, MyXPath, TreeOrString}; use std::string::ToString; use std::str::FromStr; use strum_macros::{Display, EnumString}; @@ -267,6 +267,24 @@ pub enum TTS { // Mac, } +/// Escape literal text so user MathML (e.g. `mtext`) cannot inject SSML/SAPI markup. +/// Applies only when generating speech (not braille) with SSML or SAPI5 output. +/// Returns `s` unchanged when no escaping is needed (no allocation). +pub fn escape_string_for_safety(s: String, rules_for: RulesFor, tts: &TTS) -> String { + if rules_for == RulesFor::Braille || (*tts != TTS::SSML && *tts != TTS::SAPI5) { + return s; + } + if !needs_xml_text_escape(&s) { + return s; + } + log::debug!("Escaping string for safety: {}", s); + return encode_safe(&s).into_owned(); +} + +fn needs_xml_text_escape(s: &str) -> bool { + return s.as_bytes().iter().any(|&b| b == b'&' || b == b'<' || b == b'>' || b == b'"'); +} + impl TTS { /// Given the tts command ("pause", "rate", etc) and its value, build the TTS data structure for it. /// @@ -455,16 +473,15 @@ impl TTS { if result.is_empty() { result += " "; } - // need to sanitize string so that SSML is not injected into it via mtext, etc. let speech = command.replacements.replace::(rules_with_context, mathml)?; - result += &encode_safe(&speech); + result += &speech; } let end_tag = match self { TTS::None => self.get_string_none(&command, prefs, false), TTS::SSML => self.get_string_ssml(&command, prefs, false), TTS::SAPI5 => self.get_string_sapi5(&command, prefs, false), - }; + }; if end_tag.is_empty() { return Ok( result ); // avoids adding in " " @@ -759,4 +776,28 @@ mod tests { assert!(!output.contains("100ms")); assert!(output.contains("300ms")); } + + /// Returns the same String allocation when escaping is not needed. + #[test] + fn escape_string_for_safety_no_alloc_when_clean() { + let input = "23".to_string(); + let ptr = input.as_ptr(); + let output = escape_string_for_safety(input, RulesFor::Speech, &TTS::SSML); + assert_eq!(output, "23"); + assert_eq!(output.as_ptr(), ptr); + let output = escape_string_for_safety(output, RulesFor::Braille, &TTS::SSML); + assert_eq!(output, "23"); + assert_eq!(output.as_ptr(), ptr); + } + + #[test] + fn escape_string_for_safety_escapes_markup() { + let output = escape_string_for_safety( + "".to_string(), + RulesFor::Speech, + &TTS::SSML, + ); + assert_eq!(output, "<break/>"); + } + } From 63a688bf068042ab1f6f6112b5e3832b1924483e Mon Sep 17 00:00:00 2001 From: nsoiffer Date: Tue, 2 Jun 2026 20:57:32 -0700 Subject: [PATCH 07/37] bump beta number --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 89c6e9f46..37c55108a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mathcat" -version = "0.7.6-beta.4" +version = "0.7.6-beta.5" authors = ["Neil Soiffer "] license = "MIT" description = "MathCAT: Math Capable Assistive Technology ('Speech and braille from MathML')" From f595f8d2766eee5fc74481ae92445ac81e79c2c5 Mon Sep 17 00:00:00 2001 From: nsoiffer Date: Tue, 2 Jun 2026 20:57:43 -0700 Subject: [PATCH 08/37] The range for MATHEMATICAL SANS-SERIF DIGITs had MATHEMATICAL MONOSPACE DIGITs as the match part of translate, so if one of the sans-serif digits were used, you ended up in a loop (no match, to it returned itself). --- Rules/Braille/ASCIIMath/unicode-full.yaml | 4 ++-- Rules/Braille/UEB/unicode-full.yaml | 2 +- Rules/Languages/en/unicode-full.yaml | 3 ++- Rules/Languages/fr/unicode-full.yaml | 2 +- Rules/Languages/ru/unicode-full.yaml | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Rules/Braille/ASCIIMath/unicode-full.yaml b/Rules/Braille/ASCIIMath/unicode-full.yaml index ded24da79..1a3688307 100644 --- a/Rules/Braille/ASCIIMath/unicode-full.yaml +++ b/Rules/Braille/ASCIIMath/unicode-full.yaml @@ -188,7 +188,7 @@ - "𝟎-𝟗": # 0x1d7ce - 0x1d7d7 - t: "bb𝐖" - - spell: "translate('.', '𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗', '0123456789')" + - spell: "translate('.', '𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫', '0123456789')" - "𝟬-𝟵": # 0x1D7EC - 0x1D7F5 - t: "sf𝐖" @@ -196,7 +196,7 @@ - "𝟢-𝟫": # 0x1d7e2 - 0x1d7eb - t: "sf𝐖" - - spell: "translate('.', '𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿', '0123456789')" + - spell: "translate('.', '𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫', '0123456789')" - "𝟶-𝟿": # 0x1d7f6 - 0x1d7ff - t: "tt𝐖" diff --git a/Rules/Braille/UEB/unicode-full.yaml b/Rules/Braille/UEB/unicode-full.yaml index f78feac35..373900be3 100644 --- a/Rules/Braille/UEB/unicode-full.yaml +++ b/Rules/Braille/UEB/unicode-full.yaml @@ -294,7 +294,7 @@ - "𝟢-𝟫": # 0x1d7e2 - 0x1d7eb - tc: "S" - - spell: "translate('.', '𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿', '0123456789')" + - spell: "translate('.', '𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫', '0123456789')" - "𝟶-𝟿": # 0x1d7f6 - 0x1d7ff - spell: "translate('.', '𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿', '0123456789')" diff --git a/Rules/Languages/en/unicode-full.yaml b/Rules/Languages/en/unicode-full.yaml index beb3e8009..737625b21 100644 --- a/Rules/Languages/en/unicode-full.yaml +++ b/Rules/Languages/en/unicode-full.yaml @@ -3682,7 +3682,8 @@ - spell: "translate('.', '', '0123456789')" - "𝟢-𝟫": # 0x1d7e2 - 0x1d7eb - - spell: "translate('.', '𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿', '0123456789')" + - spell: "translate('.', '𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫', '0123456789')" + - "𝟶-𝟿": # 0x1d7f6 - 0x1d7ff - spell: "translate('.', '𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿', '0123456789')" diff --git a/Rules/Languages/fr/unicode-full.yaml b/Rules/Languages/fr/unicode-full.yaml index 569463afa..dc8ab9db3 100644 --- a/Rules/Languages/fr/unicode-full.yaml +++ b/Rules/Languages/fr/unicode-full.yaml @@ -3686,7 +3686,7 @@ - spell: "translate('.', '', '0123456789')" - "𝟢-𝟫": # 0x1d7e2 - 0x1d7eb - - spell: "translate('.', '𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿', '0123456789')" + - spell: "translate('.', '𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫', '0123456789')" - "𝟶-𝟿": # 0x1d7f6 - 0x1d7ff - spell: "translate('.', '𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿', '0123456789')" diff --git a/Rules/Languages/ru/unicode-full.yaml b/Rules/Languages/ru/unicode-full.yaml index 37967e6c7..ae0f67f09 100644 --- a/Rules/Languages/ru/unicode-full.yaml +++ b/Rules/Languages/ru/unicode-full.yaml @@ -3703,7 +3703,7 @@ - spell: "translate('.', '', '0123456789')" - "𝟢-𝟫": # 0x1d7e2 - 0x1d7eb - - spell: "translate('.', '𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿', '0123456789')" + - spell: "translate('.', '𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫', '0123456789')" - "𝟶-𝟿": # 0x1d7f6 - 0x1d7ff - spell: "translate('.', '𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿', '0123456789')" From cf0fb1462e73b829fab652bf5971d7d1eb853bba Mon Sep 17 00:00:00 2001 From: nsoiffer Date: Tue, 2 Jun 2026 21:33:06 -0700 Subject: [PATCH 09/37] comment out some debug statements --- src/main.rs | 20 ++++++++++++++------ src/speech.rs | 12 ++++++------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/main.rs b/src/main.rs index c02b0b653..b1d240529 100644 --- a/src/main.rs +++ b/src/main.rs @@ -193,7 +193,10 @@ fn main() { // "; let expr = r#" - i=1 10 Si "#; + + 𝟢 + + "#; // let instant = Instant::now(); // let rules_dir = "".to_string(); // Use MathCATRulesDir, potentially pointing to a zipped version @@ -209,7 +212,7 @@ fn main() { set_preference("DecimalSeparator", "Auto").unwrap(); set_preference("BrailleCode", "UEB").unwrap(); set_preference("BrailleNavHighlight", "On").unwrap(); - set_preference("TTS", "SSML").unwrap(); + set_preference("TTS", "None").unwrap(); set_preference("Verbosity", "Verbose").unwrap(); set_preference("NavVerbosity", "Verbose").unwrap(); set_preference("NavMode", "Enhanced").unwrap(); @@ -233,10 +236,15 @@ fn main() { eprintln!("Error: exiting -- {}", errors_to_string(&e)); exit(1); }; - // match do_navigate_command("ZoomIn".to_string()) { - // Err(e) => eprintln!("Error: exiting -- {}", errors_to_string(&e)); exit(1);, - // Ok(speech) => info!("\nZoomIn speech: '{speech}'"), - // } + match do_navigate_command("ZoomIn".to_string()) { + Err(e) => {eprintln!("Error: exiting -- {}", errors_to_string(&e)); exit(1);}, + Ok(speech) => info!("\nZoomIn speech: '{speech}'"), + } + match do_navigate_command("ZoomIn".to_string()) { + Err(e) => {eprintln!("Error: exiting -- {}", errors_to_string(&e)); exit(1);}, + Ok(speech) => info!("\nZoomIn speech: '{speech}'"), + } + info!("\n"); // match do_navigate_command("ToggleZoomLockUp".to_string()) { // Err(e) => eprintln!("Error: exiting -- {}", errors_to_string(&e)); exit(1);, // Ok(speech) => info!("ToggleZoomLockUp speech: '{speech}'"), diff --git a/src/speech.rs b/src/speech.rs index 42eb4ffe4..2545d308a 100644 --- a/src/speech.rs +++ b/src/speech.rs @@ -146,7 +146,7 @@ fn speak_rules(rules: &'static std::thread::LocalKey>, math fn nestable_speak_rules<'c, 's:'c, 'm:'c>(rules_with_context: &mut SpeechRulesWithContext<'c, 's, 'm>, mathml: Element<'c>) -> Result { let mut speech_string = rules_with_context.match_pattern::(mathml) .context("Pattern match/replacement failure!")?; - debug!("Speech string: {}", speech_string); + // debug!("Speech string: {}", speech_string); // Note: [[...]] is added around a matching child, but if the "id" is on 'mathml', the whole string is used if !rules_with_context.nav_node_id.is_empty() { // See https://github.com/NSoiffer/MathCAT/issues/174 for why we can just start the speech at the nav node @@ -2578,15 +2578,15 @@ impl<'c, 's:'c, 'r, 'm:'c> SpeechRulesWithContext<'c, 's,'m> { if let Some(id) = mathml.attribute_value("id") && self.nav_node_id == id { let offset = mathml.attribute_value(crate::navigate::ID_OFFSET).unwrap_or("0"); - debug!("nav_node_adjust: id/name='{}/{}' offset?='{}'", id, name(mathml), - self.nav_node_offset.to_string().as_str() == offset - ); + // debug!("nav_node_adjust: id/name='{}/{}' offset?='{}'", id, name(mathml), + // self.nav_node_offset.to_string().as_str() == offset + // ); if is_leaf(mathml) || self.nav_node_offset.to_string().as_str() == offset { if self.speech_rules.name == RulesFor::Braille { let highlight_style = self.speech_rules.pref_manager.borrow().pref_to_string("BrailleNavHighlight"); return T::highlight_braille(speech, highlight_style); } else { - debug!("nav_node_adjust: id='{}' offset='{}/{}'", id, self.nav_node_offset, offset); + // debug!("nav_node_adjust: id='{}' offset='{}/{}'", id, self.nav_node_offset, offset); return T::mark_nav_speech(speech) } } @@ -2658,7 +2658,7 @@ impl<'c, 's:'c, 'r, 'm:'c> SpeechRulesWithContext<'c, 's,'m> { fn mark_nav_speech(speech: String) -> String { // add unique markers (since speech is mostly ascii letters and digits, most any symbol will do) // it's a bug (but happened during intent generation), we might have identical id's, choose innermost one - debug!("mark_nav_speech: adding [[ {} ]] ", &speech); + // debug!("mark_nav_speech: adding [[ {} ]] ", &speech); if !speech.contains("[[") { return "[[".to_string() + &speech + "]]"; } else { From 0c2c6680f6281f593817e32910e7fb82cb50fe40 Mon Sep 17 00:00:00 2001 From: nsoiffer Date: Tue, 2 Jun 2026 23:13:19 -0700 Subject: [PATCH 10/37] =?UTF-8?q?Add=2002DA=20(=CB=9A)=20to=20list=20of=20?= =?UTF-8?q?circle-like=20chars=20--=20I=20thought=20I=20had=20done=20this?= =?UTF-8?q?=20already,=20so=20it=20might=20in=20some=20other=20branch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/canonicalize.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/canonicalize.rs b/src/canonicalize.rs index 4ee84ae98..6117a5791 100644 --- a/src/canonicalize.rs +++ b/src/canonicalize.rs @@ -3422,7 +3422,7 @@ impl CanonicalizeContext { // FIX: MathType generates the wrong version of union and intersection ops (binary instead of unary) } else if !is_base && (parent_name == "msup" || parent_name == "msubsup") { mo_text = match mo_text { - "\u{00BA}"| "\u{2092}"| "\u{20D8}"| "\u{2218}" | "\u{25E6}" => "\u{00B0}", // circle-like objects -> degree + "\u{00BA}"| "\u{2092}"| "\u{20D8}"| "\u{2218}" | "\u{25E6}" | "\u{02DA}" => "\u{00B0}", // circle-like objects -> degree _ => mo_text, }; } else { From dec778fe22b47a175fb3005ae3b6fa4ce15c201a Mon Sep 17 00:00:00 2001 From: mgros Date: Fri, 5 Jun 2026 23:06:37 +0200 Subject: [PATCH 11/37] Add source language option to translation audit tool Allow audit-translations to compare a target language against any source language via --source, defaulting to English for existing workflows. Update CLI output, docs, and tests for dynamic source/target labels, including Swedish/Norwegian comparison coverage. Normalize Rich ANSI output in tests so golden and string assertions remain stable when terminal color is forced. --- PythonScripts/audit_translations/README.md | 19 ++-- PythonScripts/audit_translations/__init__.py | 2 +- PythonScripts/audit_translations/auditor.py | 46 ++++++---- PythonScripts/audit_translations/cli.py | 5 +- PythonScripts/audit_translations/differ.py | 2 +- PythonScripts/audit_translations/models.py | 10 +- PythonScripts/audit_translations/renderer.py | 23 +++-- .../audit_translations/tests/conftest.py | 34 +++++++ .../golden/rich/cli_calculus_verbose.golden | 6 +- .../rich/structure_diff_nonverbose.golden | 2 +- .../golden/rich/structure_diff_verbose.golden | 2 +- .../audit_translations/tests/test_auditor.py | 92 ++++++++++++++++--- .../tests/test_cli_end_to_end.py | 37 ++++++-- 13 files changed, 213 insertions(+), 67 deletions(-) diff --git a/PythonScripts/audit_translations/README.md b/PythonScripts/audit_translations/README.md index ee4af211d..b4d611d94 100644 --- a/PythonScripts/audit_translations/README.md +++ b/PythonScripts/audit_translations/README.md @@ -1,15 +1,15 @@ # MathCAT Translation Audit Tool -This tool compares English YAML rule files with translated versions to identify translation gaps and formatting issues. It assists translators in ensuring their translations are complete, consistent, and properly formatted. +This tool compares YAML rule files from a source language with translated versions to identify translation gaps and formatting issues. It assists translators in ensuring their translations are complete, consistent, and properly formatted. ### 🔍 Detection Capabilities The tool analyzes rule files to detect the following issues: -* **Missing Rules:** Rules present in the master English file but missing in the target translation. -* **Extra Rules:** Rules present in the translation but absent in English (flagged as potentially intentional language-specific additions). +* **Missing Rules:** Rules present in the source file but missing in the target translation. +* **Extra Rules:** Rules present in the target translation but absent in the source (flagged as potentially intentional language-specific additions). * **Untranslated Text:** Detects text keys that still use **lowercase** formatting, indicating they haven't been verified or translated yet. -* **Rule Differences:** Structural changes (match expressions, conditions, variables, or test/replace layout) between English and the translation. +* **Rule Differences:** Structural changes (match expressions, conditions, variables, or test/replace layout) between the source and target translation. Add `# audit-ignore` to a rule block to suppress auditing that rule. @@ -57,20 +57,23 @@ The tool automatically adjusts its matching logic based on the file type: **Syntax:** ```bash uv run audit-translations [--file ] +uv run audit-translations --source uv run audit-translations --list # If running from the repo root, point uv at the project: uv run --project PythonScripts audit-translations +uv run --project PythonScripts audit-translations --source uv run --project PythonScripts audit-translations --list ``` **Convenience Features:** * `--list`: Displays all available languages. * Region variants are shown as `lang-region` (e.g., `zz-aa`) based on subdirectories under `Rules/Languages/`. +* `--source`: Sets the source/reference language. Defaults to `en`. * `--file`: Audits a single specific file instead of the whole directory. * `--rules-dir`: Override the Rules/Languages directory path. * `--only`: Filter issue types (comma-separated): `missing`, `untranslated`, `extra`, `diffs`, `all`. -* `--verbose`: Show detailed output including English/translated snippets for rule differences. +* `--verbose`: Show detailed output including source/target snippets for rule differences. * **Summary Stats:** Provides a statistical summary after every run. **Examples:** @@ -88,19 +91,23 @@ uv run audit-translations es # Audit German translations uv run audit-translations de +# Compare Norwegian Bokmal against Swedish instead of English +uv run audit-translations nb --source sv + # Audit only a specific file uv run audit-translations es --file SharedRules/default.yaml # Audit a regional variant (merges Rules/Languages/de and Rules/Languages/de/CH) uv run audit-translations de-CH -# Show detailed output with English/translated snippets for rule differences +# Show detailed output with source/target snippets for rule differences uv run audit-translations es --verbose ``` **Running from the repo root (without `cd PythonScripts`):** ```bash uv run --project PythonScripts audit-translations es +uv run --project PythonScripts audit-translations nb --source sv uv run --project PythonScripts audit-translations --list ``` diff --git a/PythonScripts/audit_translations/__init__.py b/PythonScripts/audit_translations/__init__.py index 1d0764943..5773302a7 100644 --- a/PythonScripts/audit_translations/__init__.py +++ b/PythonScripts/audit_translations/__init__.py @@ -1,7 +1,7 @@ """ MathCAT Translation Audit Tool -Compares English YAML rule files with translated versions to identify translation +Compares source YAML rule files with translated versions to identify translation gaps and issues. This tool helps translators ensure their translations are complete and properly formatted. diff --git a/PythonScripts/audit_translations/auditor.py b/PythonScripts/audit_translations/auditor.py index 3c333628b..efc14790e 100644 --- a/PythonScripts/audit_translations/auditor.py +++ b/PythonScripts/audit_translations/auditor.py @@ -1,7 +1,7 @@ """ Auditing and comparison logic. -Contains functions for comparing English and translated files, +Contains functions for comparing source and translated files, and for performing full language audits. """ @@ -60,7 +60,7 @@ def compare_files( translated_region_path: Path | None = None, english_region_path: Path | None = None, ) -> ComparisonResult: - """Compare English and translated YAML files""" + """Compare source and translated YAML files""" def load_rules(path: Path | None) -> list[RuleInfo]: if path and path.exists(): @@ -95,14 +95,14 @@ def merge_rules(base_rules: list[RuleInfo], region_rules: list[RuleInfo]) -> lis include_extra = include_all or "extra" in issue_filter include_diffs = include_all or "diffs" in issue_filter - # Find missing rules (in English but not in translation) + # Find missing rules (in source but not in translation) missing_rules = [] if include_missing: for key, rule in english_by_key.items(): if key not in translated_by_key: missing_rules.append(rule) - # Find extra rules (in translation but not in English) + # Find extra rules (in translation but not in source) extra_rules = [] if include_extra: for key, rule in translated_by_key.items(): @@ -142,29 +142,35 @@ def audit_language( rules_dir: str | None = None, issue_filter: set[str] | None = None, verbose: bool = False, + source_language: str = "en", ) -> int: """Audit translations for a specific language. Returns total issue count.""" rules_dir_path = get_rules_dir(rules_dir) - english_dir = rules_dir_path / "en" - base_language, region = split_language_into_base_and_region(language) - translated_dir = rules_dir_path / base_language - translated_region_dir = translated_dir / region if region else None - english_region_dir = english_dir / region if region else None + source_base_language, source_region = split_language_into_base_and_region(source_language) + source_dir = rules_dir_path / source_base_language + source_region_dir = source_dir / source_region if source_region else None - if not english_dir.exists(): - raise AuditError(f"English rules directory not found: {english_dir}") + target_base_language, target_region = split_language_into_base_and_region(language) + translated_dir = rules_dir_path / target_base_language + translated_region_dir = translated_dir / target_region if target_region else None + + if not source_dir.exists(): + raise AuditError(f"Source rules directory not found: {source_dir}") + + if source_region and not (source_region_dir and source_region_dir.exists()): + raise AuditError(f"Source region directory not found: {source_region_dir}") if not translated_dir.exists(): - raise AuditError(f"Translation directory not found: {translated_dir}") + raise AuditError(f"Target rules directory not found: {translated_dir}") - if region and not (translated_region_dir and translated_region_dir.exists()): - raise AuditError(f"Region directory not found: {translated_region_dir}") + if target_region and not (translated_region_dir and translated_region_dir.exists()): + raise AuditError(f"Target region directory not found: {translated_region_dir}") # Get list of files to audit - files = [specific_file] if specific_file else get_yaml_files(english_dir, english_region_dir) + files = [specific_file] if specific_file else get_yaml_files(source_dir, source_region_dir) - print_audit_header(language, len(files)) + print_audit_header(language, len(files), source_language) total_issues = 0 total_missing = 0 @@ -175,13 +181,13 @@ def audit_language( files_ok = 0 for file_name in files: - english_path = english_dir / file_name + english_path = source_dir / file_name translated_path = translated_dir / file_name translated_region_path = translated_region_dir / file_name if translated_region_dir else None - english_region_path = english_region_dir / file_name if english_region_dir else None + english_region_path = source_region_dir / file_name if source_region_dir else None if not english_path.exists(): - console.print(f"\n[yellow]⚠ Warning:[/] English file not found: {english_path}") + console.print(f"\n[yellow]⚠ Warning:[/] Source file not found: {english_path}") continue result = compare_files( @@ -193,7 +199,7 @@ def audit_language( ) if result.has_issues: - issues = print_warnings(result, file_name, verbose, language) + issues = print_warnings(result, file_name, verbose, language, source_language) if issues > 0: files_with_issues += 1 total_issues += issues diff --git a/PythonScripts/audit_translations/cli.py b/PythonScripts/audit_translations/cli.py index aaba14449..ee1b48739 100644 --- a/PythonScripts/audit_translations/cli.py +++ b/PythonScripts/audit_translations/cli.py @@ -17,17 +17,19 @@ def main() -> None: sys.stdout.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser( - description="Audit MathCAT translation files against English originals", + description="Audit MathCAT translation files against a source language", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: uv run audit-translations es + uv run audit-translations nb --source sv uv run audit-translations de --file SharedRules/default.yaml uv run audit-translations --list """, ) parser.add_argument("language", nargs="?", help="Language code to audit (e.g., 'es', 'de', 'fi')") + parser.add_argument("--source", default="en", help="Source/reference language code (default: 'en')") parser.add_argument("--file", dest="specific_file", help="Audit only a specific file (e.g., 'SharedRules/default.yaml')") parser.add_argument("--list", action="store_true", help="List available languages") parser.add_argument("--rules-dir", help="Override Rules/Languages directory path") @@ -68,6 +70,7 @@ def main() -> None: args.rules_dir, issue_filter, args.verbose, + args.source, ) except AuditError as exc: console.print(f"\n[red]✗ Error:[/] {exc}") diff --git a/PythonScripts/audit_translations/differ.py b/PythonScripts/audit_translations/differ.py index 574c43993..5ae4483ce 100644 --- a/PythonScripts/audit_translations/differ.py +++ b/PythonScripts/audit_translations/differ.py @@ -1,7 +1,7 @@ """ Rule diffing logic. -Compares English and translated rules to find fine-grained structural differences. +Compares source and translated rules to find fine-grained structural differences. """ from .extractors import ( diff --git a/PythonScripts/audit_translations/models.py b/PythonScripts/audit_translations/models.py index a54dadb72..350ffed33 100644 --- a/PythonScripts/audit_translations/models.py +++ b/PythonScripts/audit_translations/models.py @@ -25,7 +25,7 @@ class IssueType(StrEnum): class DiffType(StrEnum): """Rule-difference subcategories used for fine-grained diagnostics.""" - MATCH = "match" # `match` XPath differs between English and translation. + MATCH = "match" # `match` XPath differs between source and translation. CONDITION = "condition" # `if` / `test` condition expressions differ. VARIABLES = "variables" # Variable names defined in `variables` differ. STRUCTURE = "structure" # Control-flow block shape/order differs (if/then/else/with/replace). @@ -91,7 +91,7 @@ def untranslated_keys(self) -> list[str]: @dataclass class RuleDifference: - """Fine-grained difference between English and translated rule""" + """Fine-grained difference between source and translated rule""" english_rule: RuleInfo translated_rule: RuleInfo @@ -107,10 +107,10 @@ def __post_init__(self) -> None: @dataclass class ComparisonResult: - """Results from comparing English and translated files""" + """Results from comparing source and translated files""" - missing_rules: list[RuleInfo] # Rules in English but not in translation - extra_rules: list[RuleInfo] # Rules in translation but not in English + missing_rules: list[RuleInfo] # Rules in source but not in translation + extra_rules: list[RuleInfo] # Rules in translation but not in source untranslated_text: list[tuple[RuleInfo, list[UntranslatedEntry]]] english_rule_count: int translated_rule_count: int diff --git a/PythonScripts/audit_translations/renderer.py b/PythonScripts/audit_translations/renderer.py index 2d0f975d3..a49d9d5c5 100644 --- a/PythonScripts/audit_translations/renderer.py +++ b/PythonScripts/audit_translations/renderer.py @@ -44,16 +44,23 @@ def rule_label(rule: RuleInfo) -> str: return f"[cyan]{escape(rule.name)}[/] [dim]({escape(tag)})[/]" +def language_label(language: str) -> str: + """Normalize a language code for display.""" + return language.lower().replace("_", "-") + + def print_warnings( result: ComparisonResult, file_name: str, verbose: bool = False, target_language: str = "tr", + source_language: str = "en", ) -> int: """Print warnings to console. Returns count of issues found.""" issues = 0 display_name = Path(file_name).as_posix() - target_label = target_language.lower().replace("_", "-") + source_label = language_label(source_language) + target_label = language_label(target_language) if not result.has_issues: return issues @@ -68,7 +75,7 @@ def print_warnings( console.print() console.rule(style="cyan") console.print(f"[{style}]{icon}[/] [bold]{escape(display_name)}[/]") - console.print(f" [dim]English: {result.english_rule_count} rules → Translated: {result.translated_rule_count} rules[/]") + console.print(f" [dim]{source_label}: {result.english_rule_count} rules → {target_label}: {result.translated_rule_count} rules[/]") console.rule(style="cyan") grouped_issues: dict[str, dict[str, Any]] = {} @@ -120,7 +127,7 @@ def add_issue(rule: RuleInfo, group_key: IssueGroupKey, payload: dict[str, Any]) console.print(f" [dim]{ISSUE_GROUP_LABELS[group_key]} [{len(entries)}][/]") for entry in entries: if issue_type is IssueType.MISSING_RULE: - console.print(f" [dim]•[/] [dim](line {entry['line_en']} in English)[/]") + console.print(f" [dim]•[/] [dim](line {entry['line_en']} in {source_label})[/]") elif issue_type is IssueType.EXTRA_RULE: console.print(f" [dim]•[/] [dim](line {entry['line_tr']} in {target_label})[/]") elif issue_type is IssueType.UNTRANSLATED_TEXT: @@ -131,11 +138,11 @@ def add_issue(rule: RuleInfo, group_key: IssueGroupKey, payload: dict[str, Any]) else: diff: RuleDifference = entry["diff"] console.print( - f" [dim]•[/] [dim](line {entry['line_en']} en, {entry['line_tr']} {target_label})[/]" + f" [dim]•[/] [dim](line {entry['line_en']} {source_label}, {entry['line_tr']} {target_label})[/]" ) console.print(f" [dim]{diff.description}[/]") if verbose: - console.print(f" [green]en:[/] {escape(diff.english_snippet)}") + console.print(f" [green]{source_label}:[/] {escape(diff.english_snippet)}") console.print(f" [red]{target_label}:[/] {escape(diff.translated_snippet)}") issues += len(entries) @@ -155,10 +162,10 @@ def file_count_color(file_count: int) -> str: return "red" -def print_audit_header(language: str, file_count: int) -> None: +def print_audit_header(language: str, file_count: int, source_language: str = "en") -> None: """Print the audit header panel.""" console.print(Panel(f"MathCAT Translation Audit: {language.upper()}", style="bold cyan")) - console.print("\n [dim]Comparing against English (en) reference files[/]") + console.print(f"\n [dim]Comparing against {language_label(source_language)} reference files[/]") console.print(f" [dim]Files to check: {file_count}[/]") @@ -197,4 +204,4 @@ def print_language_list(languages: list[tuple[str, int]]) -> None: table.add_row(code, f"[{color}]{count}[/] files") console.print(table) - console.print("\n [dim]Reference: en (English) - base translation[/]\n") + console.print("\n [dim]Default reference: en; use --source to compare against another language[/]\n") diff --git a/PythonScripts/audit_translations/tests/conftest.py b/PythonScripts/audit_translations/tests/conftest.py index c58afaada..b5c844b41 100644 --- a/PythonScripts/audit_translations/tests/conftest.py +++ b/PythonScripts/audit_translations/tests/conftest.py @@ -1,4 +1,38 @@ +""" +Shared pytest configuration for audit translation tests. + +Rich can emit ANSI styling codes into captured test output when a terminal or +environment variable forces color output. That made string and golden-output +assertions fail on some machines even though the visible CLI output was correct. +These helpers normalize captured renderer/CLI output so tests compare the text +users see, not terminal control bytes. +""" + import sys +import re + +import pytest + +from audit_translations.renderer import console # needed for running tests on Windows sys.stdout.reconfigure(encoding="utf-8") + +ANSI_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]") + + +def strip_ansi(text: str) -> str: + """Remove ANSI escape sequences from Rich output captured in tests.""" + return ANSI_RE.sub("", text) + + +@pytest.fixture(autouse=True) +def deterministic_rich_output(): + """Keep Rich output assertions stable when the shell forces ANSI colors.""" + old_no_color = console.no_color + old_force_terminal = console._force_terminal + console.no_color = True + console._force_terminal = False + yield + console.no_color = old_no_color + console._force_terminal = old_force_terminal diff --git a/PythonScripts/audit_translations/tests/golden/rich/cli_calculus_verbose.golden b/PythonScripts/audit_translations/tests/golden/rich/cli_calculus_verbose.golden index 776f9f3b2..a75c81d3d 100644 --- a/PythonScripts/audit_translations/tests/golden/rich/cli_calculus_verbose.golden +++ b/PythonScripts/audit_translations/tests/golden/rich/cli_calculus_verbose.golden @@ -2,18 +2,18 @@ │ MathCAT Translation Audit: ES │ ╰──────────────────────────────────────────────────────────────────────────────╯ - Comparing against English (en) reference files + Comparing against en reference files Files to check: 1 ──────────────────────────────────────────────────────────────────────────────── ⚠ SharedRules/calculus.yaml - English: 4 rules → Translated: 3 rules + en: 4 rules → es: 3 rules ──────────────────────────────────────────────────────────────────────────────── ≠ Rule Issues [13] (grouped by rule and issue type) • laplacian (laplacian) Missing in Translation [1] - • (line 4 in English) + • (line 4 in en) • divergence (divergence) Untranslated Text [3] • (line 10 es) "divergence" diff --git a/PythonScripts/audit_translations/tests/golden/rich/structure_diff_nonverbose.golden b/PythonScripts/audit_translations/tests/golden/rich/structure_diff_nonverbose.golden index 868bf0071..ad5320aa8 100644 --- a/PythonScripts/audit_translations/tests/golden/rich/structure_diff_nonverbose.golden +++ b/PythonScripts/audit_translations/tests/golden/rich/structure_diff_nonverbose.golden @@ -1,7 +1,7 @@ ──────────────────────────────────────────────────────────────────────────────── ✓ structure_diff.yaml - English: 1 rules → Translated: 1 rules + en: 1 rules → tr: 1 rules ──────────────────────────────────────────────────────────────────────────────── ≠ Rule Issues [1] (grouped by rule and issue type) diff --git a/PythonScripts/audit_translations/tests/golden/rich/structure_diff_verbose.golden b/PythonScripts/audit_translations/tests/golden/rich/structure_diff_verbose.golden index bde66a075..efd4de940 100644 --- a/PythonScripts/audit_translations/tests/golden/rich/structure_diff_verbose.golden +++ b/PythonScripts/audit_translations/tests/golden/rich/structure_diff_verbose.golden @@ -1,7 +1,7 @@ ──────────────────────────────────────────────────────────────────────────────── ✓ structure_diff.yaml - English: 1 rules → Translated: 1 rules + en: 1 rules → tr: 1 rules ──────────────────────────────────────────────────────────────────────────────── ≠ Rule Issues [1] (grouped by rule and issue type) diff --git a/PythonScripts/audit_translations/tests/test_auditor.py b/PythonScripts/audit_translations/tests/test_auditor.py index e1cd94b73..fafc59a3b 100644 --- a/PythonScripts/audit_translations/tests/test_auditor.py +++ b/PythonScripts/audit_translations/tests/test_auditor.py @@ -6,10 +6,11 @@ import pytest -from ..auditor import compare_files, get_yaml_files, list_languages +from ..auditor import audit_language, compare_files, get_yaml_files, list_languages from ..line_resolver import resolve_diff_lines from ..models import ComparisonResult, DiffType, RuleDifference, RuleInfo, UntranslatedEntry from ..renderer import console, print_warnings +from .conftest import strip_ansi @pytest.fixture() @@ -234,6 +235,73 @@ def test_compare_files_skips_untranslated_and_diffs_when_audit_ignored(tmp_path) assert result.rule_differences == [] +def test_audit_language_uses_configurable_source_language(tmp_path, fixed_console_width) -> None: + """ + Ensure non-English source comparisons remain directional. + + Source rules define missing items and source-side snippets. Target rules + define extra items and target-only untranslated text. + """ + rules_dir = tmp_path / "Rules" / "Languages" + source_dir = rules_dir / "sv" + target_dir = rules_dir / "nb" + source_dir.mkdir(parents=True) + target_dir.mkdir(parents=True) + + (source_dir / "sample.yaml").write_text( + """- name: common-rule + tag: mo + match: "self::m:mo" + replace: + - T: "source" +- name: source-only + tag: mi + match: "." + replace: + - T: "source only" +""", + encoding="utf-8", + ) + (target_dir / "sample.yaml").write_text( + """- name: common-rule + tag: mo + match: "self::m:mi" + replace: + - t: "target" +- name: target-only + tag: mi + match: "." + replace: + - T: "target only" +""", + encoding="utf-8", + ) + + with console.capture() as capture: + total_issues = audit_language( + "nb", + specific_file="sample.yaml", + rules_dir=str(rules_dir), + verbose=True, + source_language="sv", + ) + output = strip_ansi(capture.get()) + + assert total_issues == 4 + assert "Comparing against sv reference files" in output + assert "sv: 2 rules → nb: 2 rules" in output + assert "Missing in Translation [1]" in output + assert "(line 6 in sv)" in output + assert "Extra in Translation [1]" in output + assert "(line 6 in nb)" in output + assert 'Untranslated Text [1]' in output + assert '(line 5 nb) "target"' in output + assert "Match Pattern Differences [1]" in output + assert "(line 3 sv, 3 nb)" in output + assert "sv: self::m:mo" in output + assert "nb: self::m:mi" in output + + def test_get_yaml_files_includes_region(tmp_path) -> None: """ Ensures get_yaml_files merges base and region file lists. @@ -273,7 +341,7 @@ def test_list_languages_includes_region_codes(tmp_path) -> None: with console.capture() as capture: list_languages(str(rules_dir)) - output = capture.get() + output = strip_ansi(capture.get()) assert "zz" in output assert "zz-aa" in output @@ -298,7 +366,7 @@ def test_list_languages_ignores_sharedrules_as_region(tmp_path) -> None: with console.capture() as capture: list_languages(str(rules_dir)) - output = capture.get() + output = strip_ansi(capture.get()) assert "zz-aa" in output assert "zz-SharedRules" not in output @@ -320,7 +388,7 @@ def test_print_warnings_omits_snippets_when_not_verbose(fixed_console_width) -> with console.capture() as capture: print_warnings(result, "structure_diff.yaml", verbose=False) - output = capture.get() + output = strip_ansi(capture.get()) assert output == golden_path.read_text(encoding="utf-8") @@ -341,7 +409,7 @@ def test_print_warnings_includes_snippets_when_verbose(fixed_console_width) -> N with console.capture() as capture: print_warnings(result, "structure_diff.yaml", verbose=True) - output = capture.get() + output = strip_ansi(capture.get()) assert output == golden_path.read_text(encoding="utf-8") @@ -511,7 +579,7 @@ def test_print_warnings_shows_misaligned_structures() -> None: with console.capture() as capture: issues_count = print_warnings(result, "structure_misaligned.yaml", verbose=False) - output = capture.get() + output = strip_ansi(capture.get()) # Misaligned structure differences should be rendered. assert "Rule structure differs" in output, "Expected misaligned structure differences to be shown in display" @@ -538,7 +606,7 @@ def test_print_warnings_still_shows_missing_else() -> None: with console.capture() as capture: issues_count = print_warnings(result, "structure_missing_else.yaml", verbose=False) - output = capture.get() + output = strip_ansi(capture.get()) # CRITICAL: This legitimate difference should appear in output assert "Rule structure differs" in output, "Expected missing else block to be shown in output" @@ -597,7 +665,7 @@ def test_print_warnings_groups_multiple_subgroups_for_single_rule(fixed_console_ with console.capture() as capture: issues_count = print_warnings(result, "grouped.yaml", verbose=False) - output = capture.get() + output = strip_ansi(capture.get()) assert output.count("• grouped-rule (mi)") == 1 assert "Untranslated Text [2]" in output @@ -648,7 +716,7 @@ def test_print_warnings_groups_missing_and_extra_by_rule(fixed_console_width) -> with console.capture() as capture: issues_count = print_warnings(result, "mixed.yaml", verbose=False) - output = capture.get() + output = strip_ansi(capture.get()) assert output.count("• missing-rule (mn)") == 1 assert output.count("• extra-rule (mo)") == 1 @@ -692,13 +760,13 @@ def test_print_warnings_verbose_shows_snippets_only_for_differences(fixed_consol with console.capture() as capture: issues_count = print_warnings(result, "verbose.yaml", verbose=True) - output = capture.get() + output = strip_ansi(capture.get()) assert "Missing in Translation [1]" in output assert "Untranslated Text [1]" in output assert "Match Pattern Differences [1]" in output - assert output.count("en:") == 1 - assert output.count("tr:") == 1 + assert output.count(" en:") == 1 + assert output.count(" tr:") == 1 assert "en-snippet" in output assert "tr-snippet" in output assert issues_count == 3 diff --git a/PythonScripts/audit_translations/tests/test_cli_end_to_end.py b/PythonScripts/audit_translations/tests/test_cli_end_to_end.py index 2547b9eca..c4c8364b7 100644 --- a/PythonScripts/audit_translations/tests/test_cli_end_to_end.py +++ b/PythonScripts/audit_translations/tests/test_cli_end_to_end.py @@ -13,6 +13,7 @@ from .. import cli as audit_cli from ..renderer import console +from .conftest import strip_ansi def fixture_rules_dir() -> Path: @@ -33,7 +34,7 @@ def test_cli_main_rich_only_filters_issue_groups(capsys, monkeypatch) -> None: try: monkeypatch.setattr(sys, "argv", ["audit_translations", *args]) audit_cli.main() - output = capsys.readouterr().out + output = strip_ansi(capsys.readouterr().out) finally: console.width = old_width @@ -46,6 +47,26 @@ def test_cli_main_rich_only_filters_issue_groups(capsys, monkeypatch) -> None: assert "Structure Differences" not in output +def test_cli_main_accepts_source_language(capsys, monkeypatch) -> None: + """ + Ensure --source changes the reference language without changing target semantics. + """ + args = ["en", "--source", "es", "--rules-dir", str(fixture_rules_dir()), "--file", "overview.yaml"] + + old_width = console.width + console.width = 80 + try: + monkeypatch.setattr(sys, "argv", ["audit_translations", *args]) + audit_cli.main() + output = strip_ansi(capsys.readouterr().out) + finally: + console.width = old_width + + assert "Comparing against es reference files" in output + assert "es:" in output + assert "en:" in output + + def test_cli_main_rich_output_groups_by_rule_and_type(capsys, monkeypatch) -> None: """ Ensure rich CLI output is grouped by rule and subgrouped by issue type. @@ -61,7 +82,7 @@ def test_cli_main_rich_output_groups_by_rule_and_type(capsys, monkeypatch) -> No try: monkeypatch.setattr(sys, "argv", ["audit_translations", *args]) audit_cli.main() - output = capsys.readouterr().out + output = strip_ansi(capsys.readouterr().out) finally: console.width = old_width @@ -93,7 +114,7 @@ def test_cli_main_rich_output_matches_grouped_golden(capsys, monkeypatch) -> Non try: monkeypatch.setattr(sys, "argv", ["audit_translations", *args]) audit_cli.main() - output = capsys.readouterr().out + output = strip_ansi(capsys.readouterr().out) finally: console.width = old_width @@ -110,7 +131,7 @@ def test_cli_main_requires_language_or_list(capsys, monkeypatch) -> None: with pytest.raises(SystemExit) as exc: audit_cli.main() - output = capsys.readouterr().out + output = strip_ansi(capsys.readouterr().out) assert exc.value.code == 1 assert "Please specify a language code or use --list" in output @@ -127,7 +148,7 @@ def test_cli_main_rejects_unknown_only_token(capsys, monkeypatch) -> None: with pytest.raises(SystemExit) as exc: audit_cli.main() - output = capsys.readouterr().out + output = strip_ansi(capsys.readouterr().out) assert exc.value.code == 1 assert "Unknown issue types: bogus" in output @@ -144,10 +165,10 @@ def test_cli_main_reports_missing_region_directory(capsys, monkeypatch) -> None: with pytest.raises(SystemExit) as exc: audit_cli.main() - output = capsys.readouterr().out + output = strip_ansi(capsys.readouterr().out) assert exc.value.code == 1 - assert "Region directory not found" in output + assert "Target region directory not found" in output def test_cli_module_rich_output_groups_by_rule_and_type() -> None: @@ -173,7 +194,7 @@ def test_cli_module_rich_output_groups_by_rule_and_type() -> None: check=True, ) - output = result.stdout + output = strip_ansi(result.stdout) assert "≠ Rule Issues [13] (grouped by rule and issue type)" in output assert "• laplacian (laplacian)" in output assert "• divergence (divergence)" in output From 7ee775a9fcced17dacdd7fad201ba3055c49fb59 Mon Sep 17 00:00:00 2001 From: mgros Date: Fri, 5 Jun 2026 23:14:07 +0200 Subject: [PATCH 12/37] fix ruff --- PythonScripts/audit_translations/renderer.py | 8 ++++++-- PythonScripts/audit_translations/tests/conftest.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/PythonScripts/audit_translations/renderer.py b/PythonScripts/audit_translations/renderer.py index a49d9d5c5..38da74346 100644 --- a/PythonScripts/audit_translations/renderer.py +++ b/PythonScripts/audit_translations/renderer.py @@ -75,7 +75,10 @@ def print_warnings( console.print() console.rule(style="cyan") console.print(f"[{style}]{icon}[/] [bold]{escape(display_name)}[/]") - console.print(f" [dim]{source_label}: {result.english_rule_count} rules → {target_label}: {result.translated_rule_count} rules[/]") + console.print( + f" [dim]{source_label}: {result.english_rule_count} rules → " + f"{target_label}: {result.translated_rule_count} rules[/]" + ) console.rule(style="cyan") grouped_issues: dict[str, dict[str, Any]] = {} @@ -138,7 +141,8 @@ def add_issue(rule: RuleInfo, group_key: IssueGroupKey, payload: dict[str, Any]) else: diff: RuleDifference = entry["diff"] console.print( - f" [dim]•[/] [dim](line {entry['line_en']} {source_label}, {entry['line_tr']} {target_label})[/]" + f" [dim]•[/] [dim](line {entry['line_en']} {source_label}, " + f"{entry['line_tr']} {target_label})[/]" ) console.print(f" [dim]{diff.description}[/]") if verbose: diff --git a/PythonScripts/audit_translations/tests/conftest.py b/PythonScripts/audit_translations/tests/conftest.py index b5c844b41..a78b24283 100644 --- a/PythonScripts/audit_translations/tests/conftest.py +++ b/PythonScripts/audit_translations/tests/conftest.py @@ -8,8 +8,8 @@ users see, not terminal control bytes. """ -import sys import re +import sys import pytest From d86d7e5e6d254f76d5d1f047351196aeea92ee18 Mon Sep 17 00:00:00 2001 From: mgros Date: Fri, 5 Jun 2026 23:14:07 +0200 Subject: [PATCH 13/37] fix ruff --- PythonScripts/audit_translations/renderer.py | 7 +++++-- PythonScripts/audit_translations/tests/conftest.py | 2 +- PythonScripts/audit_translations/tests/test_auditor.py | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/PythonScripts/audit_translations/renderer.py b/PythonScripts/audit_translations/renderer.py index a49d9d5c5..c76da6b2c 100644 --- a/PythonScripts/audit_translations/renderer.py +++ b/PythonScripts/audit_translations/renderer.py @@ -75,7 +75,9 @@ def print_warnings( console.print() console.rule(style="cyan") console.print(f"[{style}]{icon}[/] [bold]{escape(display_name)}[/]") - console.print(f" [dim]{source_label}: {result.english_rule_count} rules → {target_label}: {result.translated_rule_count} rules[/]") + console.print( + f" [dim]{source_label}: {result.english_rule_count} rules → {target_label}: {result.translated_rule_count} rules[/]" + ) console.rule(style="cyan") grouped_issues: dict[str, dict[str, Any]] = {} @@ -138,7 +140,8 @@ def add_issue(rule: RuleInfo, group_key: IssueGroupKey, payload: dict[str, Any]) else: diff: RuleDifference = entry["diff"] console.print( - f" [dim]•[/] [dim](line {entry['line_en']} {source_label}, {entry['line_tr']} {target_label})[/]" + f" [dim]•[/] [dim](line {entry['line_en']} {source_label}, " + f"{entry['line_tr']} {target_label})[/]" ) console.print(f" [dim]{diff.description}[/]") if verbose: diff --git a/PythonScripts/audit_translations/tests/conftest.py b/PythonScripts/audit_translations/tests/conftest.py index b5c844b41..a78b24283 100644 --- a/PythonScripts/audit_translations/tests/conftest.py +++ b/PythonScripts/audit_translations/tests/conftest.py @@ -8,8 +8,8 @@ users see, not terminal control bytes. """ -import sys import re +import sys import pytest diff --git a/PythonScripts/audit_translations/tests/test_auditor.py b/PythonScripts/audit_translations/tests/test_auditor.py index fafc59a3b..cec10fe40 100644 --- a/PythonScripts/audit_translations/tests/test_auditor.py +++ b/PythonScripts/audit_translations/tests/test_auditor.py @@ -294,7 +294,7 @@ def test_audit_language_uses_configurable_source_language(tmp_path, fixed_consol assert "(line 6 in sv)" in output assert "Extra in Translation [1]" in output assert "(line 6 in nb)" in output - assert 'Untranslated Text [1]' in output + assert "Untranslated Text [1]" in output assert '(line 5 nb) "target"' in output assert "Match Pattern Differences [1]" in output assert "(line 3 sv, 3 nb)" in output From e0180a3e73ffe61556e3e294131a23c8ce582ca0 Mon Sep 17 00:00:00 2001 From: mgros Date: Fri, 5 Jun 2026 23:30:07 +0200 Subject: [PATCH 14/37] fix ruff --- PythonScripts/audit_translations/renderer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/PythonScripts/audit_translations/renderer.py b/PythonScripts/audit_translations/renderer.py index 38da74346..c76da6b2c 100644 --- a/PythonScripts/audit_translations/renderer.py +++ b/PythonScripts/audit_translations/renderer.py @@ -76,8 +76,7 @@ def print_warnings( console.rule(style="cyan") console.print(f"[{style}]{icon}[/] [bold]{escape(display_name)}[/]") console.print( - f" [dim]{source_label}: {result.english_rule_count} rules → " - f"{target_label}: {result.translated_rule_count} rules[/]" + f" [dim]{source_label}: {result.english_rule_count} rules → {target_label}: {result.translated_rule_count} rules[/]" ) console.rule(style="cyan") From cd48925b838f3547dca96c9c174a89b927d45595 Mon Sep 17 00:00:00 2001 From: Tuomas Pyorre <82783219+ttpyorre@users.noreply.github.com> Date: Sun, 7 Jun 2026 22:11:54 -0400 Subject: [PATCH 15/37] Core concept names additions and tests -- issue 381 (#592) * initial addition of core concept names * defnitions and tests, with a few failing * minus, plus, volume fixes. Other defined, not appearing in site --- Rules/Languages/en/definitions.yaml | 312 +++++++- tests/Languages/en/definitions.rs | 1026 +++++++++++++++++++++++++++ 2 files changed, 1311 insertions(+), 27 deletions(-) create mode 100644 tests/Languages/en/definitions.rs diff --git a/Rules/Languages/en/definitions.yaml b/Rules/Languages/en/definitions.yaml index 8bb43d7f3..dfd707341 100644 --- a/Rules/Languages/en/definitions.yaml +++ b/Rules/Languages/en/definitions.yaml @@ -1,7 +1,7 @@ --- - include: "../../definitions.yaml" -# If an "intent" is used, the 'terse:medium:verbose' speech for the intent name is given here for a prefix||infix||postfix||function fixity +# If an "intent" is used, the 'terse:medium:verbose' speech for the intent name is given here for a prefix||infix||postfix||function||nofix||silent fixity # If only one ":" is used, the first part is used for 'terse' and the second part is used for 'medium' and 'verbose' # If no ":"s are used, the same speech is used for all forms # If bracketing words make sense, they are separated with ";"s @@ -10,45 +10,303 @@ # for readability, spaces can be used around any of the delimiter characters # Note: if there are multiple fixities, the first one is used if the fixity is not given in the intent - IntentMappings: { - "indexed-by": "infix= ; sub; end sub: end sub: end subscript", - "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, "*") - - "absolute-value": "function= ; absolute value: the absolute value: the absolute value; end absolute value", "binomial": "infix=binomial; choose; end binomial", - "dimension-product": "infix=by", + + ### Functions and Inverses # "closed-interval": "other=closed-interval; from,to; end closed-interval", + # "closed-interval":"function=closed interval between; and", #NOTE: Check test, does not follow this pattern + #"closed-open-interval":"function=interval between; included and", + #"open-closed-interval":"function=interval between; and included", + #"open-interval":"function=open interval between; and", + "inverse":"function=inverse || postfix=inverse", + + "domain": "function= ; domain", + "codomain": "function= ; codomain", + + "image":"function=image", + #"fraction":"function=fraction; over; end fraction", # NOTE: Fails + "mixed-fraction":"infix=and", # NOTE: in website says function, but follow infix speech pattern. + "quotient":"function=integer part; divided by", # NOTE: Logic somewhere here failing, becomes "divided by of x comma, y" instead of "integer part of x divided by y" + "evaluated-at":"infix=evaluated at", + "remainder":"function=the remainder; divided by", + + "max":"function=max", + "min":"function=min", + + "power":"infix=to the power", + "root":"function=root", + "greatest-common-divisor": "function=gcd: the gcd: the greatest common divisor", + "least-common-multiple":"function=lcm: the lcm: the least common multiple", # In webpage typo "lest common" + + "absolute-value": "function= ; absolute value: the absolute value: the absolute value; end absolute value", + "complex-conjugate":"function=complex conjugate", + "complex-arg":"function=arg", + "real-part": "function=the real part", "imaginary-part": "function=imaginary part: the imaginary part: the imaginary part", - "least-common-multiple": "function=lcm: the lcm: the least common multiple", + + "polar-coordinate":"function=polar coordinate; comma", + "spherical-coordinate":"function=spherical coordinate; comma; comma", + "cartesian-coordinate":"function=cartesian coordinate; comma", + "coordinate":"function=coordinate; comma", + "floor":"function=floor", + "ceiling":"function=ceiling", + "round":"function=rounded-value", + "fractional-part":"function=fractional part", + + + ### 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", + "tends-to":"infix=tends to", + "tends-to-from-above":"infix=tends to from above", + "tends-to-from-below":"infix=tends to from below", + + + ### Sets + "set": "function= ; set: the set", + "set-difference":"function=set difference; and || infix=minus", # NOTE: not tested + "complement":"function=complement", + #"empty-set":"nofix=empty set", + "cardinality":"function=cardinality", # NOTE: does not have a defined speech template in website + "list":"function=list", + "tuple": "function= ; tuple: the tuple", + + + ### Sequence and Series + "sum":"function= ; sum over : sum ; ", #: sum over : sum from; to", + "product":"function=product || function=product over || function=product from; to", + + + ### Elementary classical functions + "sine":"function=sin: sine", + "cosine":"function=cos: cosine", + "tangent":"function=tan: tangent", + "secant":"function=seech: secant", + "cosecant":"function=co-seech: cosecant", + "cotangent":"function=co-tan: cotangent", + + "arcsine":"function=arcsine", + "arccosine":"function=arccosine", + "arctangent":"function=arctangent", + "arcsecant":"function=arcsecant", + "arccosecant":"function=arc-cosecant", + "arccotangent":"function=arc-cotangent", + + "hyperbolic-sine":"function=shine", + "hyperbolic-cosine":"function=cosh", + "hyperbolic-tangent":"function=tanch", + "hyperbolic-secant":"function=sech", + "hyperbolic-cosecant":"function=cosech", + "hyperbolic-cotangent":"function=coth", + + "arc-hyperbolic-sine":"function=arc shine", + "arc-hyperbolic-cosine":"function=arc cosh", + "arc-hyperbolic-tangent":"function=arc tanch", + "arc-hyperbolic-secant":"function=arc sech", + "arc-hyperbolic-cosecant":"function=arc cosech", + "arc-hyperbolic-cotangent":"function=arc coth", + + "exponential":"function=exponential", + "natural-logarithm": "function=l n: natural log: natural log", + "logarithm":"function=log", ##Check arity 2 + + + ### Statistics and Probability + "mean":"function=mean", + "standard-deviation":"function=standard deviation", + "variance":"function=variance", + "median":"function=median", + "mode":"function=mode", + + "conditional-probability":"function=probability; given", # NOTE: Check test + + + ### Linear Algebra + "vector": "function= ; vector || prefix=vector", # prefix not tested, also prefix not on webpage + "matrix":"function=matrix", # NOTE: Failing test, recheck + "determinant":"function=determinant", + "adjugate":"function=adjugate", + "magnitude":"function=magnitude", + "norm": "function=; norm: norm: norm; end norm", + "span":"function=span", + + "unit-vector":"prefix=unit vector", + + "identity-matrix":"nofix=identity matrix", # NOTE: no function specified + "transpose":"function=transpose || postfix=transpose", # postfix needs testing + "dimensional-product":"infix=by", # INFIX + + + ### Constants and Sets + "set-of-integers":"nofix=ℤ: set of all integers", + "set-of-reals":"nofix=ℝ: set of all real numbers", + "set-of-rationals":"nofix=ℚ: set of all rational numbers", + "set-of-natural-numbers":"nofix=ℕ: set of all natural numbers", + "set-of-complex-numbers":"nofix=ℂ: set of all complex numbers", + "set-of-primes":"nofix=ℙ: set of all prime numbers", + + + ### Geometry + "line-segment":"prefix=line segment", + "directed-line-segment":"prefix=directed line segment", + "line":"prefix=line", + "ray":"prefix=ray", + "arc":"prefix=arc", + + "length":"function=length", + "area":"function=area", + + "point":"prefix=point", ## NOTE: Has ??? for property in site. Should it be prefix? Or something else. + + ### Separators + "time-separator":"infix=", + + ### General Concepts + "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, test converts "and" to "comma," + #"indexed-by": "infix= ; sub; end sub: end sub: end subscript", + "indexed-by": "infix= ; indexed by; ", + + "highlight":"postfix=highlighted", + "least-common-denominator":"function=least common denominator", + "rate":"infix=per", + "translation":"function= translation by; comma", # NOTE: not tested, changes "translation" -> "comma" in test + "constraint":"infix= ; with constraint; ", + + "binomial-coefficient":"infix=choose", + "pochhammer":"function=permutation", + "permutation-cycle":"function=permutation cycle", + "embellished-name":"infix=with annotation", + + ### 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. + + + ### Other + ## Default fixity function + "curl": "function=curl", + "divergence": "function=divergence", + "gradient": "function=gradient", + "laplacian": "function=laplacian", + + ## Default fixity prefix + "angle": "prefix=angle", + "angle-measure": "prefix=angle measure", + "change": "prefix=change in", + "for-all": "prefix=for all", + "measured-angle": "prefix=measured angle", + "not": "prefix=not", + "number-of": "prefix=number of", + "partial-derivative": "prefix=partial", + "right-angle": "prefix=right angle", + "square-root-of": "prefix=square root of", + "there-does-not-exist": "prefix=there does not exist", + "there-exists": "prefix=there exists", + + + ## Default fixity infix + "and": "infix=and", + "applied-to": "infix=applied to", + "approximately": "infix=approximately", + "congruent": "infix=congruent to", + "cartesian-product": "infix=cartesian product", + "composed-with": "infix=composed with", + "cross-product": "infix=cross: cross product: cross product", + "defined-as": "infix=defined as", + "divided-by": "infix=divided by", + "divides": "infix=divides", + "does-not-belong-to": "infix=does not belong to", + "does-not-divide": "infix=does not divide", + "dot-product": "infix=dot product", + "downwards-diagonal-ellipsis": "infix=downwards diagonal ellipsis", + "direct-product": "infix=direct product", + "element-of": "infix=element of", + "ellipsis": "infix=ellipsis", + "equals": "infix=equals", + "equivalent-to": "infix=equivalent to", + "evaluates-to": "infix=evaluates to", + "given": "infix=given", + "greater-than": "infix=greater than", + "greater-than-or-equal-to": "infix=greater than or equal to", + "identically-equals": "infix=identically equals", + "if-and-only-if": "infix=if and only if", + "implies": "infix=implies", + "inner-product": "infix=inner product", + "intersection": "infix=intersection", + "less-than": "infix=less than", + "less-than-or-equal-to": "infix=less than or equal to", + "list-separator": "infix=comma", + "maps-to": "infix=maps to", + "member-of": "infix=member of", + "minus-or-plus": "infix=minus or plus", + "not-subset": "infix=not subset of", + "not-superset": "infix=not superset of", + "not-equal-to": "infix=not equal to", + "not-member-of": "infix=not member of", + "not-parallel-to": "infix=not parallel to", + "obtained-from": "infix=obtained from", + "or": "infix=or", + "outer-product": "infix=outer product", + "parallel-to": "infix=parallel to", + "perpendicular": "infix=perpendicular to", + "plus": "infix=plus || prefix=positive", # Prefix not tested + "minus": "infix=minus || prefix=negative", # Prefix not tested + "plus-or-minus": "infix=plus or minus", + "precedes": "infix=precedes", + "proportional": "infix=proportional to", + "range-separator": "infix=through", + "ratio": "infix=ratio", + "similar": "infix=similar to", + "subset": "infix=subset of", + "subset-or-equal": "infix=subset or equal to", + "succeeds": "infix=succeeds", + "such-that": "infix=such that", + "superset": "infix=superset of", + "superset-or-equal": "infix=superset or equal to", + "tilde": "infix=tilde", + "times": "infix=times", + "union": "infix=union", + "upwards-diagonal-ellipsis": "infix=upwards diagonal ellipsis", + "vertical-ellipsis": "infix=vertical ellipsis", + "xor": "infix=exclusive or", + + ## Default fixity postfix + "factorial": "postfix=factorial", + "percent": "postfix=percent", + + ## Default fixity nofix + "diameter":"nofix=d: diameter", + "distance":"nofix=d; D: distance", + "probability":"nofix=P: probability", + "radius":"nofix=r: radius", + "volume":"nofix=V: volume || function=volume", + "exponential-e":"nofix=e", + "imaginary-i":"nofix=i", + "differential-d":"nofix=d", + "golden-ratio":"nofix=golden ratio", + + + ## Other : Not tested, don't appear in https://w3c.github.io/mathml-docs/intent-core-concepts/ + "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, "*") # "large-op": "infix=over || other=from,to", - "limit": "prefix=limit as: the limit as: the limit as", "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", - "natural-logarithm": "function=l n: natural log: natural log", - "minus": "infix=minus || prefix=negative", - "plus": "infix=plus || prefix=positive", # "pochhammer": "infix=permutations of", # arguments are in reverse order, so can't work here - "real-part": "function=the real part", - "set-of-integers": "nofix=set of all integers", - - "transpose": "postfix=transpose || function=transpose", - "norm": "function=; norm: norm: norm; end norm", "trace": "function= ; trace : trace: the trace; end trace", "dimension": "function=; dimension : dimension: the dimension; end dimension", "homomorphism": "function= ; homomorphism : homomorphism: the homomorphism; end homomorphism", "kernel": "function= ; kernel : kernel: the kernel; end kernel", - "vector": "function= ; vector || prefix=vector", - "cross-product": "infix=cross: cross product: cross product", - "dot-product": "infix=dot: dot product: dot: dot product", - - "divergence": "function= ; dihv: divergence: divergence; end divergence", - "curl": "function= ; curl; end curl", - "gradient": "function= ; del: gradient: gradient; end gradient", - "laplacian": "function=lahplahsian", # speech engines don't do a good job with "laplacian" - + "chemistry-concentration": "function= ; concentration: concentration of: the concentration of; end concentration", } diff --git a/tests/Languages/en/definitions.rs b/tests/Languages/en/definitions.rs new file mode 100644 index 000000000..6680b80c5 --- /dev/null +++ b/tests/Languages/en/definitions.rs @@ -0,0 +1,1026 @@ +/// Tests for rules in definitions: +/// * modified var +use crate::common::*; +use anyhow::Result; + +#[test] +fn tuple_basic() -> Result<()> { + // function + let expr = r#" + + + x + y + + + "#; + + test("en", "ClearSpeak", expr, "the tuple of x comma, y")?; + test("en", "SimpleSpeak", expr, "the tuple of x comma, y")?; + + return Ok(()); +} + +#[test] +fn my_set_basic() -> Result<()> { + let expr = r#" + + + x + y + + + "#; + + test("en", "ClearSpeak", expr, "the set of x comma, y")?; + + Ok(()) +} + +#[test] +fn fixed_test() -> Result<()> { + let expr = r#" + + R + + + "#; + + test("en", "ClearSpeak", expr, "set of all real numbers")?; + Ok(()) +} + +#[test] +fn i_test() -> Result<()> { + let expr = r#" + + i + + + "#; + + test("en", "ClearSpeak", expr, "i")?; + Ok(()) +} + + +#[test] +fn floor_basic() -> Result<()> { + let expr = r#" + + + x + + + "#; + + test("en", "ClearSpeak", expr, "floor of x")?; + + Ok(()) +} + +#[test] +fn set_difference_basic() -> Result<()> { + let expr = r#" + + + A + + B + + + "#; + + test( "en", "ClearSpeak", expr, "set difference of cap eigh and cap b")?; + + Ok(()) +} + +#[test] +fn postfix_test() -> Result<()> { + let tests = [ + ( + "transpose", + r#" + + x + T + + "#, + "x transpose", + ), + ( + "highlight", + r#" + + x + + "#, + "x highlighted", + ), + ]; + + // Loop through all test cases, name _, body, and expected result + for (_, body, expected) in tests { + let expr = format!( + r#" + + {} + + "#, + body + ); + + test("en", "ClearSpeak", &expr, expected)?; + } + + Ok(()) +} + +#[test] +fn prefix_test() -> Result<()> { + // Prefix test limit, unit-vector, line-segment + // Directed-line-segment, line, ray, arc + let tests = [ + ( + "limit", + r#" + + x + + "#, + "limit as x", + ), + ( + "unit-vector", + r#" + + x + ^ + + "#, + "unit vector x", + ), + ( + "line-segment", + r#" + + x + ¯ + + "#, + "line segment x", + ), + ]; + + for (_, body, expected) in tests { + let expr = format!( + r#" + + {} + + "#, + body + ); + + test("en", "ClearSpeak", &expr, expected)?; + } + + Ok(()) +} + + +#[test] +fn functions_and_inverses_tests() -> Result<()> { + let tests = vec![ + //("closed-interval", "closed interval between x and y"), + //("closed-open-interval", "interval between x included and y"), + //("open-closed-interval", "interval between x and y included"), + //("open-interval", "open interval between x and y"), + + ("inverse", "inverse of x"), + ("domain", "domain of x"), + ("codomain", "codomain of x"), + ("image", "image of x"), + + //("fraction", "fraction x over y end fraction"), + ("mixed-fraction", "x and y"), + ("quotient", "integer part of x divided by y"), + ("evaluated-at", "x evaluated at y"), + ("remainder", "the remainder x divided by y"), + + ("max", "max x"), + ("min", "min x"), + + ("power", "x to the power y"), + + ("root", "root x"), + + ("greatest-common-divisor", "gcd: the gcd: the greatest common divisor x"), + ("least-common-multiple", "lcm: the lcm: the least common multiple x"), + + ("absolute-value", "absolute value: the absolute value: the absolute value x end absolute value"), + + ("complex-conjugate", "complex conjugate x"), + ("complex-arg", "arg x"), + ("real-part", "the real part x"), + ("imaginary-part", "imaginary part: the imaginary part: the imaginary part x"), + + ("polar-coordinate", "polar coordinate x comma y"), + ("spherical-coordinate", "spherical coordinate x comma y comma z"), + ("cartesian-coordinate", "cartesian coordinate x comma y"), + ("coordinate", "coordinate x comma y"), + + ("floor", "floor x"), + ("ceiling", "ceiling x"), + ("round", "rounded-value x"), + ("fractional-part", "fractional part x"), + + + ]; + + for (intent, expected) in tests { + let expr = match intent { + + "max" + | "min" + | "greatest-common-divisor" + | "least-common-multiple" + | "spherical-coordinate" + | "cartesian-coordinate" + | "coordinate" => { + format!( + " + + x + y + z + + ", + intent + ) + } + + "power" + | "mixed-fraction" + | "quotient" + | "evaluated-at" + | "remainder" + | "closed-interval" + | "closed-open-interval" + | "open-closed-interval" + | "open-interval" + | "polar-coordinate" => { + format!( + " + + x + y + + ", + intent + ) + } + + _ => { + format!( + " + + x + + ", + intent + ) + } + }; + + test("en", "ClearSpeak", &expr, expected)?; + } + + Ok(()) +} + + +#[test] +fn calculus_tests() -> Result<()> { + let tests = vec![ + //("derivative", "the derivative of x with respect to x"), + //"definite-integral", "integral over x from x to x"), + + // prefix + ("limit", "limit as x"), + + // infix + ("tends-to", "x tends to y"), + ("tends-to-from-above", "x tends to from above y"), + ("tends-to-from-below", "x tends to from below y"), + ]; + + for (intent, expected) in tests { + let expr = match intent { + "limit" => { + format!( + " + + x + + ", + intent + ) + } + + _ => { + // infix cases + format!( + " + + x + y + + ", + intent + ) + } + }; + + test("en", "ClearSpeak", &expr, expected)?; + } + + Ok(()) +} + + +#[test] +fn set_tests() -> Result<()> { + let tests = vec![ + ("set", "the set of x"), + // ("set-difference", "set difference of x and y"), + ("complement", "complement of x"), + //("empty-set", "empty set"), + ("cardinality", "cardinality of x"), + ("list", "list of x"), + ("tuple", "the tuple of x"), + ]; + + for (intent, expected) in tests { + let expr = format!( + " + + x + + ", + intent + ); + + test("en", "ClearSpeak", &expr, expected)?; + } + + Ok(()) +} + + +#[test] +fn sequence_and_series_intents() -> Result<()> { + let tests = [ + ( + "sum-1", + r#" + + + x + + "#, + "sum of x", + ), + ( + "sum-2", + r#" + + + i + x + + "#, + "sum over i of x", + ), + ( + "sum-3", + r#" + + + i + n + x + + "#, + "sum from i to n of x", + ), + ( + "product-1", + r#" + + + x + + "#, + "product of x", + ), + ( + "product-2", + r#" + + + i + x + + "#, + "product over i of x", + ), + ( + "product-3", + r#" + + + i + n + x + + "#, + "product from i to n of x", + ), + ]; + + for (_, body, expected) in tests { + let expr = format!( + r#" + + {} + + "#, + body + ); + + test("en", "ClearSpeak", &expr, expected)?; + } + + Ok(()) +} + +#[test] +fn elementary_classical_tests() -> Result<()> { + let tests = vec![ + // Trig + ("sine", "sine of x"), + ("cosine", "cosine of x"), + ("tangent", "tangent of x"), + ("secant", "secant of x"), + ("cosecant", "cosecant of x"), + ("cotangent", "cotangent of x"), + + // Inverse trig + ("arcsine", "arcsine of x"), + ("arccosine", "arccosine of x"), + ("arctangent", "arctangent of x"), + ("arcsecant", "arcsecant of x"), + ("arccosecant", "arc-cosecant of x"), + ("arccotangent", "arc-cotangent of x"), + + // Hyperbolic trig + ("hyperbolic-sine", "shine of x"), + ("hyperbolic-cosine", "cosh of x"), + ("hyperbolic-tangent", "tanch of x"), + ("hyperbolic-secant", "sech of x"), + ("hyperbolic-cosecant", "cosech of x"), + ("hyperbolic-cotangent", "coth of x"), + + // Inverse hyperbolic trig + ("arc-hyperbolic-sine", "arc shine of x"), + ("arc-hyperbolic-cosine", "arc cosh of x"), + ("arc-hyperbolic-tangent", "arc tanch of x"), + ("arc-hyperbolic-secant", "arc sech of x"), + ("arc-hyperbolic-cosecant", "arc cosech of x"), + ("arc-hyperbolic-cotangent", "arc coth of x"), + ]; + + for (intent, expected) in tests { + let expr = format!( + " + + x + + ", + intent + ); + + test("en", "ClearSpeak", &expr, expected)?; + } + + Ok(()) +} + +#[test] +fn statistics_and_probability_tests() -> Result<()> { + let tests = vec![ + ("mean", "mean of x"), + ("standard-deviation", "standard deviation of x"), + ("variance", "variance of x"), + ("median", "median of x"), + ("mode", "mode of x"), + + // conditional probability typically two arguments + // ("conditional-probability", "probability of x given y"), + ]; + + for (intent, expected) in tests { + let expr = match intent { + "conditional-probability" => format!( + " + + P + ( + + x + | + y + + ) + + ", + intent + ), + _ => format!( + " + + x + + ", + intent + ), + }; + + test("en", "ClearSpeak", &expr, expected)?; + } + + Ok(()) +} + +#[test] +fn linear_algebra_tests() -> Result<()> { + let tests = vec![ + ("vector", "vector of x"), + // ("matrix", "matrix of x"), + ("determinant", "determinant of x"), + ("adjugate", "adjugate of x"), + ("magnitude", "magnitude of x"), + ("norm", "norm of x"), + ("span", "span of x"), + + // transpose supports both postfix and function; we test function explicitly + ("transpose", "transpose of x"), + + // dimensional product is infix + ("dimensional-product", "x by y"), + + // unit-vector is prefix + ("unit-vector", "unit vector x") + ]; + + for (intent, expected) in tests { + let expr: String = match intent { + "dimensional-product" => { + " + + x + y + + " + .to_string() + } + _ => format!( + " + + x + + " + ), + }; + + test("en", "ClearSpeak", &expr, expected)?; + } + + Ok(()) +} + +#[test] +fn nofix_set_tests() -> Result<()> { + let tests = vec![ + ("set-of-integers", "ℤ", "set of all integers"), + ("set-of-reals", "ℝ", "set of all real numbers"), + ("set-of-rationals", "ℚ", "set of all rational numbers"), + ("set-of-natural-numbers", "ℕ", "set of all natural numbers"), + ("set-of-complex-numbers", "ℂ", "set of all complex numbers"), + ("set-of-primes", "ℙ", "set of all prime numbers"), + ]; + + for (intent, symbol, expected) in tests { + let expr = format!( + " + {} + ", + intent, + symbol + ); + + test("en", "ClearSpeak", &expr, expected)?; + } + + Ok(()) +} + +#[test] +fn geometry_prefix_multi_value_tests() -> Result<()> { + let tests = vec![ + ("line-segment", "line segment x y"), + ("directed-line-segment", "directed line segment x y"), + ("line", "line x y"), + ("ray", "ray x y"), + ("arc", "arc x y"), + ("point", "point x y z"), + ]; + + for (intent, expected) in tests { + let expr = match intent{ + "point" => { + format!( + " + + x + y + z + + " + ) + } + + _ => { + format!( + " + + x + y + + " + ) + } + }; + + test("en", "ClearSpeak", &expr, expected)?; + } + + Ok(()) +} + +#[test] +fn geometry_prefix_tests() -> Result<()> { + let tests = vec![ + ("length", "length of x"), + ("area", "area of x"), + ("volume", "volume of x"), + ]; + + for (intent, expected) in tests { + let expr = format!( + " + + x + + " + ); + + test("en", "ClearSpeak", &expr, expected)?; + } + + Ok(()) +} + +#[test] +fn separator_tests() -> Result<()> { + let expr = format!( + " + + x + y + + " + ); + + test("en", "ClearSpeak", &expr, "x y")?; + Ok(()) +} + +#[test] +fn general_concepts_tests() -> Result<()> { + let tests = vec![ + // Unary structural + ("fenced-group", "fenced-group of x"), + ("highlight", "x highlighted"), + ("least-common-denominator", "least common denominator of x comma, y comma, z"), // add x, y , z ... + ("pochhammer", "permutation of x"), + ("permutation-cycle", "permutation cycle of x"), + + // Binary structural / infix-style + // ("ordered-pair", "the pair of x and y"), + ("rate", "x per y"), + + ("binomial-coefficient", "x choose y"), + ("embellished-name", "x with annotation y"), + ("indexed-by", "x indexed by y"), + // ("translation", "translation by x comma, y"), // Changes translation to comma + ("constraint", "x with constraint y"), + ]; + + for (intent, expected) in tests { + let expr = match intent { + "ordered-pair" + | "rate" + | "constraint" + | "binomial-coefficient" + | "embellished-name" + | "translation" + | "indexed-by" => { + format!( + " + + x + y + + ", + intent + ) + } + "least-common-denominator" => { + format!( + " + + x + y + z + + ", + intent + ) + } + + // unary cases + _ => { + format!( + " + + x + + ", + intent + ) + } + }; + + test("en", "ClearSpeak", &expr, expected)?; + } + + Ok(()) +} + +#[test] +fn grouping_tests() -> Result<()> { + let tests = vec![ + ("annotation", "x which is y"), + // ("braced-group", "grouped x end-grouped"), + // ("repeating-decimal", "repeating decimal of x"), + ]; + + for (intent, expected) in tests { + let expr = match intent { + "annotation" => { + format!( + " + + x + y + + ", + intent + ) + } + _ => { + format!( + " + + x + + ", + intent + ) + } + }; + + test("en", "ClearSpeak", &expr, expected)?; + } + + Ok(()) +} + +#[test] +fn function_default_fixity_tests() -> Result<()> { + let tests = vec![ + ("curl", "curl of x"), + ("divergence", "divergence of x"), + ("gradient", "gradient of x"), + ("laplacian", "laplacian of x"), + ]; + + for (intent, expected) in tests { + let expr = format!( + " + + x + + ", + intent + ); + + test("en", "ClearSpeak", &expr, expected)?; + } + + Ok(()) +} + +#[test] +fn prefix_default_fixity_tests() -> Result<()> { + let tests = vec![ + ("angle", "angle x"), + ("angle-measure", "angle measure x"), + ("change", "change in x"), + ("for-all", "for all x"), + ("measured-angle", "measured angle x"), + ("not", "not x"), + ("number-of", "number of x"), + ("partial-derivative", "partial x"), + ("right-angle", "right angle x"), + ("square-root-of", "square root of x"), + ("there-does-not-exist", "there does not exist x"), + ("there-exists", "there exists x"), + ]; + + for (intent, expected) in tests { + let expr = format!( + " + + x + + ", + intent + ); + + test("en", "ClearSpeak", &expr, expected)?; + } + + Ok(()) +} + +#[test] +fn infix_default_fixity_tests() -> Result<()> { + let tests = vec![ + ("and", "x and y"), + ("applied-to", "x applied to y"), + ("approximately", "x approximately y"), + ("congruent", "x congruent to y"), + ("cartesian-product", "x cartesian product y"), + ("composed-with", "x composed with y"), + ("cross-product", "x cross product y"), + ("defined-as", "x defined as y"), + ("divided-by", "x divided by y"), + ("divides", "x divides y"), + ("does-not-belong-to", "x does not belong to y"), + ("does-not-divide", "x does not divide y"), + ("dot-product", "x dot product y"), + ("downwards-diagonal-ellipsis", "x downwards diagonal ellipsis y"), + ("direct-product", "x direct product y"), + ("element-of", "x element of y"), + ("ellipsis", "x ellipsis y"), + ("equals", "x equals y"), + ("equivalent-to", "x equivalent to y"), + ("evaluates-to", "x evaluates to y"), + ("given", "x given y"), + ("greater-than", "x greater than y"), + ("greater-than-or-equal-to", "x greater than or equal to y"), + ("identically-equals", "x identically equals y"), + ("if-and-only-if", "x if and only if y"), + ("implies", "x implies y"), + ("inner-product", "x inner product y"), + ("intersection", "x intersection y"), + ("less-than", "x less than y"), + ("less-than-or-equal-to", "x less than or equal to y"), + ("list-separator", "x comma y"), + ("maps-to", "x maps to y"), + ("member-of", "x member of y"), + ("minus", "x minus y"), + ("minus-or-plus", "x minus or plus y"), + ("not-subset", "x not subset of y"), + ("not-superset", "x not superset of y"), + ("not-equal-to", "x not equal to y"), + ("not-member-of", "x not member of y"), + ("not-parallel-to", "x not parallel to y"), + ("obtained-from", "x obtained from y"), + ("or", "x or y"), + ("outer-product", "x outer product y"), + ("parallel-to", "x parallel to y"), + ("perpendicular", "x perpendicular to y"), + ("plus", "x plus y"), + ("plus-or-minus", "x plus or minus y"), + ("precedes", "x precedes y"), + ("proportional", "x proportional to y"), + ("range-separator", "x through y"), + ("ratio", "x ratio y"), + ("similar", "x similar to y"), + ("subset", "x subset of y"), + ("subset-or-equal", "x subset or equal to y"), + ("succeeds", "x succeeds y"), + ("such-that", "x such that y"), + ("superset", "x superset of y"), + ("superset-or-equal", "x superset or equal to y"), + ("tilde", "x tilde y"), + ("times", "x times y"), + ("union", "x union y"), + ("upwards-diagonal-ellipsis", "x upwards diagonal ellipsis y"), + ("vertical-ellipsis", "x vertical ellipsis y"), + ("xor", "x exclusive or y"), + ]; + + for (intent, expected) in tests { + let expr = format!( + " + + x + y + + ", + intent + ); + + test("en", "ClearSpeak", &expr, expected)?; + } + + Ok(()) +} + +#[test] +fn postfix_default_fixity_tests() -> Result<()> { + let tests = vec![ + ("factorial", "x factorial"), + ("percent", "x percent"), + ]; + + for (intent, expected) in tests { + let expr = format!( + " + + x + + ", + intent + ); + + test("en", "ClearSpeak", &expr, expected)?; + } + + Ok(()) +} + +#[test] +fn nofix_default_fixity_tests() -> Result<()> { + let tests = vec![ + ("diameter", "d", "diameter"), + ("distance", "D", "distance"), + ("probability", "P", "probability"), + ("radius", "r", "radius"), + ("volume", "V", "volume"), + ("exponential-e", "e", "e"), + ("imaginary-i", "i", "i"), + ("differential-d", "d", "d"), + ("golden-ratio", "φ", "golden ratio"), + ]; + + for (intent, symbol, expected) in tests { + let expr = format!( + " + {} + ", + intent, + symbol + ); + + test("en", "ClearSpeak", &expr, expected)?; + } + + Ok(()) +} \ No newline at end of file From 80438d6929349f0a247c1de0c35b08fb84c265b5 Mon Sep 17 00:00:00 2001 From: "U-rainshowers\\neils" Date: Mon, 8 Jun 2026 05:28:46 -0700 Subject: [PATCH 16/37] fix clippy warnings --- src/main.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main.rs b/src/main.rs index b1d240529..e058fe83f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -236,44 +236,44 @@ fn main() { eprintln!("Error: exiting -- {}", errors_to_string(&e)); exit(1); }; - match do_navigate_command("ZoomIn".to_string()) { + match do_navigate_command("ZoomIn") { Err(e) => {eprintln!("Error: exiting -- {}", errors_to_string(&e)); exit(1);}, Ok(speech) => info!("\nZoomIn speech: '{speech}'"), } - match do_navigate_command("ZoomIn".to_string()) { + match do_navigate_command("ZoomIn") { Err(e) => {eprintln!("Error: exiting -- {}", errors_to_string(&e)); exit(1);}, Ok(speech) => info!("\nZoomIn speech: '{speech}'"), } info!("\n"); - // match do_navigate_command("ToggleZoomLockUp".to_string()) { + // match do_navigate_command("ToggleZoomLockUp") { // Err(e) => eprintln!("Error: exiting -- {}", errors_to_string(&e)); exit(1);, // Ok(speech) => info!("ToggleZoomLockUp speech: '{speech}'"), // } - // match do_navigate_command("MovePrevious".to_string()) { + // match do_navigate_command("MovePrevious") { // Err(e) => eprintln!("Error: exiting -- {}", errors_to_string(&e)); exit(1);, // Ok(speech) => info!("MovePrevious speech: '{speech}'"), // } - // match do_navigate_command("MovePrevious".to_string()) { + // match do_navigate_command("MovePrevious") { // Err(e) => eprintln!("Error: exiting -- {}", errors_to_string(&e)); exit(1);, // Ok(speech) => info!("MovePrevious speech: '{}'", speech), // } - // match do_navigate_command("MovePrevious".to_string()) { + // match do_navigate_command("MovePrevious") { // Err(e) => eprintln!("Error: exiting -- {}", errors_to_string(&e)); exit(1);, // Ok(speech) => info!("MovePrevious speech: '{}'", speech), // } - // match do_navigate_command("MoveNext".to_string()) { + // match do_navigate_command("MoveNext") { // Err(e) => eprintln!("Error: exiting -- {}", errors_to_string(&e)); exit(1);, // Ok(speech) => info!("MoveNext speech: '{}'", speech), // } - // match do_navigate_command("MoveNext".to_string()) { + // match do_navigate_command("MoveNext") { // Err(e) => eprintln!("Error: exiting -- {}", errors_to_string(&e)); exit(1);, // Ok(speech) => info!("MoveNext speech: '{}'", speech), // } - // match do_navigate_command("MoveNext".to_string()) { + // match do_navigate_command("MoveNext") { // Err(e) => eprintln!("Error: exiting -- {}", errors_to_string(&e)); exit(1);, // Ok(speech) => info!("MoveNext speech: '{}'", speech), // } - // match do_navigate_command("MoveNext".to_string()) { + // match do_navigate_command("MoveNext") { // Err(e) => eprintln!("Error: exiting -- {}", errors_to_string(&e)); exit(1);, // Ok(speech) => info!("MoveNext speech: '{}'", speech), // } From f3796a94ad179f145d67a35110154a21f384d257 Mon Sep 17 00:00:00 2001 From: mgros Date: Mon, 8 Jun 2026 20:19:11 +0200 Subject: [PATCH 17/37] use "ruamel.yaml>=0.19.1" in uv.lock and pyproject.toml --- PythonScripts/pyproject.toml | 2 +- PythonScripts/uv.lock | 17 ++++------------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/PythonScripts/pyproject.toml b/PythonScripts/pyproject.toml index 6f919ee0c..5a33bdeed 100644 --- a/PythonScripts/pyproject.toml +++ b/PythonScripts/pyproject.toml @@ -17,7 +17,7 @@ dependencies = [ "jsonpath-ng>=1.8.0", "pyyaml", "rich", - "ruamel.yaml", + "ruamel.yaml>=0.19.1", ] [project.scripts] diff --git a/PythonScripts/uv.lock b/PythonScripts/uv.lock index 993f3f44b..f29327c59 100644 --- a/PythonScripts/uv.lock +++ b/PythonScripts/uv.lock @@ -299,7 +299,7 @@ requires-dist = [ { name = "jsonpath-ng", specifier = ">=1.8.0" }, { name = "pyyaml" }, { name = "rich" }, - { name = "ruamel-yaml" }, + { name = "ruamel-yaml", specifier = ">=0.19.1" }, ] [package.metadata.requires-dev] @@ -364,22 +364,13 @@ wheels = [ [[package]] name = "ruamel-yaml" -version = "0.19.0" +version = "0.19.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ruamel-yaml-clibz", marker = "platform_python_implementation == 'CPython'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0c/5d/8a1de57b5a11245c61c906d422cd1e66b6778e134a1c68823a451be5759c/ruamel_yaml-0.19.0.tar.gz", hash = "sha256:ff19233e1eb3e9301e7a3d437847713e361a80faace167639327efbe8c0e5f95", size = 142095, upload-time = "2025-12-31T16:47:31.837Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/3e/835d495068a4bb03419ce8c5464734ff6f3343df948e033cb5e5f81f7f08/ruamel_yaml-0.19.0-py3-none-any.whl", hash = "sha256:96ea8bafd9f3fdb0181ce3cc05e6ec02ce0a8788cbafa9b5a6e47c76fe26dfc6", size = 117777, upload-time = "2025-12-31T16:47:29.07Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, ] -[[package]] -name = "ruamel-yaml-clibz" -version = "0.3.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8f/95/9bcc25e84703180c3941062796572e0fc73bd659086efdc4ef9b8af19e36/ruamel_yaml_clibz-0.3.4.tar.gz", hash = "sha256:e99077ac6aa4943af1000161a0cb793a379c5c8cd03ea8dd3803e0b58739b685", size = 231076, upload-time = "2025-12-31T17:11:09.341Z" } - [[package]] name = "ruff" version = "0.15.4" From fc0a327c94c4fde59430ba358f9bb84b490217a1 Mon Sep 17 00:00:00 2001 From: V3nky Date: Fri, 12 Jun 2026 00:36:27 +0530 Subject: [PATCH 18/37] handle canonicalize failure in containerized environments (#574) --- src/prefs.rs | 16 +++++++++++++++- src/shim_filesystem.rs | 21 +++++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/prefs.rs b/src/prefs.rs index 590709fad..00126d26c 100644 --- a/src/prefs.rs +++ b/src/prefs.rs @@ -259,9 +259,23 @@ impl PreferenceManager { /// /// If rules_dir is an empty PathBuf, the existing rules_dir is used (an error if it doesn't exist) pub fn initialize(&mut self, rules_dir: PathBuf) -> Result<()> { + // Resolve the rules directory to an absolute, canonical path. + // If canonicalize() fails (e.g., ACCESS_DENIED in containers), fall back to: + // - returning the path as-is if it is already absolute, + // - prepending the current working directory if it is relative. + // Note: if current_dir() also fails, unwrap_or_default yields an empty PathBuf, + // and the result may remain relative. #[cfg(not(feature = "include-zip"))] let rules_dir = match rules_dir.canonicalize() { - Err(e) => bail!("set_rules_dir: could not canonicalize path {}: {}", rules_dir.display(), e), + Err(_e) => { + if rules_dir.is_absolute() { + rules_dir + } else { + std::env::current_dir() + .unwrap_or_default() + .join(&rules_dir) + } + }, Ok(rules_dir) => rules_dir, }; diff --git a/src/shim_filesystem.rs b/src/shim_filesystem.rs index 085d90240..b758a4213 100644 --- a/src/shim_filesystem.rs +++ b/src/shim_filesystem.rs @@ -340,14 +340,31 @@ cfg_if! { } } + /// Resolves the path to an absolute, canonical form using the OS. + /// If `canonicalize()` fails (e.g., ACCESS_DENIED in containers), falls back to: + /// - returning the path as-is if it is already absolute, + /// - prepending the current working directory if it is relative. + /// Note: the fallback does not resolve symlinks or normalize `..`/`.` segments. pub fn canonicalize_shim(path: &Path) -> std::io::Result { - return path.canonicalize(); + match path.canonicalize() { + Ok(p) => Ok(p), + Err(_) => { + if path.is_absolute() { + Ok(path.to_path_buf()) + } else { + // Prepend cwd to make the relative path absolute. + // unwrap_or_default yields an empty PathBuf if cwd is unavailable, + // in which case the returned path will still be relative. + Ok(std::env::current_dir().unwrap_or_default().join(path)) + } + } + } } pub fn read_to_string_shim(path: &Path) -> Result { let path = match path.canonicalize() { Ok(path) => path, - Err(e) => bail!("Read error while trying to canonicalize in read_to_string_shim {}: {}", path.display(), e), + Err(_) => path.to_path_buf(), }; debug!("Reading file '{}'", &path.display()); match std::fs::read_to_string(&path) { From 6cfd4a115778ca10e30950a686c112dce71d3e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Gro=C3=9F?= Date: Fri, 12 Jun 2026 21:56:54 +0200 Subject: [PATCH 19/37] add 1 week dependency cooldown for uv (#599) --- PythonScripts/pyproject.toml | 3 +++ PythonScripts/uv.lock | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/PythonScripts/pyproject.toml b/PythonScripts/pyproject.toml index 5a33bdeed..7b9adf68d 100644 --- a/PythonScripts/pyproject.toml +++ b/PythonScripts/pyproject.toml @@ -33,6 +33,9 @@ dev = [ requires = ["uv_build>=0.9.25,<0.10.0"] build-backend = "uv_build" +[tool.uv] +exclude-newer = "1 week" + [tool.uv.build-backend] module-name = "audit_translations" module-root = "" diff --git a/PythonScripts/uv.lock b/PythonScripts/uv.lock index f29327c59..81e0404c4 100644 --- a/PythonScripts/uv.lock +++ b/PythonScripts/uv.lock @@ -2,6 +2,10 @@ version = 1 revision = 3 requires-python = ">=3.14" +[options] +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer-span = "P1W" + [[package]] name = "anyio" version = "4.12.1" From dc9e535b355e9521d960bd6fcdb8c34d5a6406c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Gro=C3=9F?= Date: Fri, 12 Jun 2026 22:00:39 +0200 Subject: [PATCH 20/37] fix rules for digits in german (#596) --- Rules/Languages/de/unicode-full.yaml | 40 ++++++++++++++-------------- tests/Languages/de/alphabets.rs | 10 +++++++ 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/Rules/Languages/de/unicode-full.yaml b/Rules/Languages/de/unicode-full.yaml index 6c9b5e320..636b825dc 100644 --- a/Rules/Languages/de/unicode-full.yaml +++ b/Rules/Languages/de/unicode-full.yaml @@ -3587,10 +3587,10 @@ - "": [T: "mutiger nabla"] # 0xf509 (en: 'bold nabla', google translation) - "𝞩": [T: "mutiger nabla"] # 0x1d7a9 (en: 'bold nabla', google translation) - - "": [T: "bold null"] # 0xf52e (en: 'bold zero', google translation) - - "𝟎": [T: "bold null"] # 0x1d7ce (en: 'bold zero', google translation) - - "": [T: "mutig"] # 0xf52f (en: 'bold one', google translation) - - "𝟏": [T: "mutig"] # 0x1d7cf (en: 'bold one', google translation) + - "": [T: "fett null"] # 0xf52e (en: 'bold zero', google translation) + - "𝟎": [T: "fett null"] # 0x1d7ce (en: 'bold zero', google translation) + - "": [T: "fett eins"] # 0xf52f (en: 'bold one', google translation) + - "𝟏": [T: "fett eins"] # 0x1d7cf (en: 'bold one', google translation) - "": [T: "fett zwei"] # 0xf530 (en: 'bold two', google translation) - "𝟐": [T: "fett zwei"] # 0x1d7d0 (en: 'bold two', google translation) - "": [T: "fett drei"] # 0xf531 (en: 'bold three', google translation) @@ -3599,12 +3599,12 @@ - "𝟒": [T: "fett vier"] # 0x1d7d2 (en: 'bold four', google translation) - "": [T: "fett fünf"] # 0xf533 (en: 'bold five', google translation) - "𝟓": [T: "fett fünf"] # 0x1d7d3 (en: 'bold five', google translation) - - "": [T: "mutige sechs"] # 0xf534 (en: 'bold six', google translation) - - "𝟔": [T: "mutige sechs"] # 0x1d7d4 (en: 'bold six', google translation) + - "": [T: "fett sechs"] # 0xf534 (en: 'bold six', google translation) + - "𝟔": [T: "fett sechs"] # 0x1d7d4 (en: 'bold six', google translation) - "": [T: "fett sieben"] # 0xf535 (en: 'bold seven', google translation) - "𝟕": [T: "fett sieben"] # 0x1d7d5 (en: 'bold seven', google translation) - - "": [T: "mutig bei"] # 0xf536 (en: 'bold eight', google translation) - - "𝟖": [T: "mutig bei"] # 0x1d7d6 (en: 'bold eight', google translation) + - "": [T: "fett acht"] # 0xf536 (en: 'bold eight', google translation) + - "𝟖": [T: "fett acht"] # 0x1d7d6 (en: 'bold eight', google translation) - "": [T: "fett neun"] # 0xf537 (en: 'bold nine', google translation) - "𝟗": [T: "fett neun"] # 0x1d7d7 (en: 'bold nine', google translation) - "": [T: "null"] # 0xf542 (en: 'zero', google translation) @@ -3623,14 +3623,14 @@ - "𝟨": [T: "sechs"] # 0x1d7e8 (en: 'six', google translation) - "": [T: "sieben"] # 0xf549 (en: 'seven', google translation) - "𝟩": [T: "sieben"] # 0x1d7e9 (en: 'seven', google translation) - - "": [T: "bei"] # 0xf54a (en: 'eight', google translation) - - "𝟪": [T: "bei"] # 0x1d7ea (en: 'eight', google translation) + - "": [T: "acht"] # 0xf54a (en: 'eight', google translation) + - "𝟪": [T: "acht"] # 0x1d7ea (en: 'eight', google translation) - "": [T: "neun"] # 0xf54b (en: 'nine', google translation) - "𝟫": [T: "neun"] # 0x1d7eb (en: 'nine', google translation) - - "": [T: "bold null"] # 0xf54c (en: 'bold zero', google translation) - - "𝟬": [T: "bold null"] # 0x1d7ec (en: 'bold zero', google translation) - - "": [T: "mutig"] # 0xf54d (en: 'bold one', google translation) - - "𝟭": [T: "mutig"] # 0x1d7ed (en: 'bold one', google translation) + - "": [T: "fett null"] # 0xf54c (en: 'bold zero', google translation) + - "𝟬": [T: "fett null"] # 0x1d7ec (en: 'bold zero', google translation) + - "": [T: "fett eins"] # 0xf54d (en: 'bold one', google translation) + - "𝟭": [T: "fett eins"] # 0x1d7ed (en: 'bold one', google translation) - "": [T: "fett zwei"] # 0xf54e (en: 'bold two', google translation) - "𝟮": [T: "fett zwei"] # 0x1d7ee (en: 'bold two', google translation) - "": [T: "fett drei"] # 0xf54f (en: 'bold three', google translation) @@ -3639,12 +3639,12 @@ - "𝟰": [T: "fett vier"] # 0x1d7f0 (en: 'bold four', google translation) - "": [T: "fett fünf"] # 0xf551 (en: 'bold five', google translation) - "𝟱": [T: "fett fünf"] # 0x1d7f1 (en: 'bold five', google translation) - - "": [T: "mutige sechs"] # 0xf552 (en: 'bold six', google translation) - - "𝟲": [T: "mutige sechs"] # 0x1d7f2 (en: 'bold six', google translation) + - "": [T: "fett sechs"] # 0xf552 (en: 'bold six', google translation) + - "𝟲": [T: "fett sechs"] # 0x1d7f2 (en: 'bold six', google translation) - "": [T: "fett sieben"] # 0xf553 (en: 'bold seven', google translation) - "𝟳": [T: "fett sieben"] # 0x1d7f3 (en: 'bold seven', google translation) - - "": [T: "mutig bei"] # 0xf554 (en: 'bold eight', google translation) - - "𝟴": [T: "mutig bei"] # 0x1d7f4 (en: 'bold eight', google translation) + - "": [T: "fett acht"] # 0xf554 (en: 'bold eight', google translation) + - "𝟴": [T: "fett acht"] # 0x1d7f4 (en: 'bold eight', google translation) - "": [T: "fett neun"] # 0xf555 (en: 'bold nine', google translation) - "𝟵": [T: "fett neun"] # 0x1d7f5 (en: 'bold nine', google translation) - "": [T: "null"] # 0xf556 (en: 'zero', google translation) @@ -3663,8 +3663,8 @@ - "𝟼": [T: "sechs"] # 0x1d7fc (en: 'six', google translation) - "": [T: "sieben"] # 0xf55d (en: 'seven', google translation) - "𝟽": [T: "sieben"] # 0x1d7fd (en: 'seven', google translation) - - "": [T: "bei"] # 0xf55e (en: 'eight', google translation) - - "𝟾": [T: "bei"] # 0x1d7fe (en: 'eight', google translation) + - "": [T: "acht"] # 0xf55e (en: 'eight', google translation) + - "𝟾": [T: "acht"] # 0x1d7fe (en: 'eight', google translation) - "": [T: "neun"] # 0xf55f (en: 'nine', google translation) - "𝟿": [T: "neun"] # 0x1d7ff (en: 'nine', google translation) - "": [T: "unbekannter charakter"] # 0xf700 (en: 'unknown character', google translation) diff --git a/tests/Languages/de/alphabets.rs b/tests/Languages/de/alphabets.rs index 309db4eba..b01206a7d 100644 --- a/tests/Languages/de/alphabets.rs +++ b/tests/Languages/de/alphabets.rs @@ -405,3 +405,13 @@ fn enclosed_numbers() -> Result<()> { return Ok(()); } + +#[test] +fn mathematical_digit_names() -> Result<()> { + let expr = " 𝟎,𝟏,𝟖"; + test("de", "SimpleSpeak", expr, "fett null komma fett eins komma fett acht")?; + let expr = " 𝟪,𝟾"; + test("de", "SimpleSpeak", expr, "acht komma acht")?; + Ok(()) + +} From fc4bf9362a90c6c87e2e2293367d7fac1f9e4538 Mon Sep 17 00:00:00 2001 From: Hon-Jang Yang Date: Sat, 13 Jun 2026 04:03:37 +0800 Subject: [PATCH 21/37] update navigation rules for traditional chinese (#588) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * copy english version of navigate.yaml from NVDA2026.1 and translate * 更新 definitions.yaml 與 navigate.yaml * update unicode-full.yaml * 區分完全/已完全放大/縮小 * 移至->移到, 表->表格, 數學式->數學, 退出->離開, 不能->無法, 當前->目前。 overview line 47, SimpleSpeak_Rules.yaml line 39, "根號"->""。mroot.rs 對應修改 * 移到上一格 -> 往上一格,移到下一格 -> 往下一格, 佔位符 -> 書籤,設定佔位符 -> 設定書籤,已是最上層 -> 不在別的式子裡面,上一層是 -> 在後面式子裡面,字元模式 開n 次方根 的讀法,基底 -> 基、底 * 向量 -> 射線 * 基 -> 基底,底 -> 基底 --- .../Languages/zh/tw/SharedRules/default.yaml | 10 +- .../Languages/zh/tw/SharedRules/geometry.yaml | 4 +- Rules/Languages/zh/tw/SimpleSpeak_Rules.yaml | 2 +- Rules/Languages/zh/tw/definitions.yaml | 40 +- Rules/Languages/zh/tw/navigate.yaml | 1031 ++++++++--------- Rules/Languages/zh/tw/overview.yaml | 4 +- Rules/Languages/zh/tw/unicode-full.yaml | 138 +-- tests/Languages/zh/tw/SimpleSpeak/geometry.rs | 2 +- tests/Languages/zh/tw/SimpleSpeak/mroot.rs | 10 +- 9 files changed, 575 insertions(+), 666 deletions(-) diff --git a/Rules/Languages/zh/tw/SharedRules/default.yaml b/Rules/Languages/zh/tw/SharedRules/default.yaml index 4b7cae70f..b127189d8 100644 --- a/Rules/Languages/zh/tw/SharedRules/default.yaml +++ b/Rules/Languages/zh/tw/SharedRules/default.yaml @@ -131,17 +131,17 @@ - test: if: "$Verbosity!='Terse'" then: [T: ""] # phrase("'the' root of x") + - T: "根指數" # phrase("the root of x 'with index' 5") + - x: "*[2]" - T: "根號" - - T: "開方次數" # phrase("the root of x 'with index' 5") - - x: "*[1]" - pause: short - test: if: "$Verbosity!='Terse'" then: [T: ""] # phrase("the root 'of' x") - - x: "*[2]" + - x: "*[1]" - pause: short - test: - if: "not(IsNode(*[2],'leaf'))" + if: "not(IsNode(*[1],'leaf'))" then: [T: "結束根號"] # phrase("root of x 'end root symbol'") @@ -406,7 +406,7 @@ - NumColumns: "count(*[1]/*) - IfThenElse(*/self::m:mlabeledtr, 1, 0)" match: "." replace: - - T: "表" # phrase(the 'table with' 3 rows) + - T: "表格有" # phrase(the 'table with' 3 rows) - x: count(*) - test: if: count(*)=1 diff --git a/Rules/Languages/zh/tw/SharedRules/geometry.yaml b/Rules/Languages/zh/tw/SharedRules/geometry.yaml index e4241c0f4..85a6c6ba4 100644 --- a/Rules/Languages/zh/tw/SharedRules/geometry.yaml +++ b/Rules/Languages/zh/tw/SharedRules/geometry.yaml @@ -23,12 +23,12 @@ - test: if: "$Verbosity='Verbose'" then: - - T: "向量" # phrase('the ray from' A to B) + - T: "射線" # phrase('the ray from' A to B) - x: "*[1]" - T: "到" # phrase(the ray from A 'to' B) - x: "*[2]" else: - - T: "向量" # phrase(the 'ray'A B) + - T: "射線" # phrase(the 'ray'A B) - x: "*[1]" - x: "*[2]" diff --git a/Rules/Languages/zh/tw/SimpleSpeak_Rules.yaml b/Rules/Languages/zh/tw/SimpleSpeak_Rules.yaml index 10a487480..7877cb2e3 100644 --- a/Rules/Languages/zh/tw/SimpleSpeak_Rules.yaml +++ b/Rules/Languages/zh/tw/SimpleSpeak_Rules.yaml @@ -36,7 +36,7 @@ tag: root match: "." replace: - - T: "根號" + - T: "" - x: "*[1]" - test: if: "$Verbosity!='Terse'" diff --git a/Rules/Languages/zh/tw/definitions.yaml b/Rules/Languages/zh/tw/definitions.yaml index 93460509a..280ec6039 100644 --- a/Rules/Languages/zh/tw/definitions.yaml +++ b/Rules/Languages/zh/tw/definitions.yaml @@ -48,28 +48,28 @@ } - NavigationParts: { - "large-op": "base; lower limit; upper limit", - "mfrac": "numerator;denominator", - "fraction": "numerator;denominator", - "msqrt": "root", - "square-root": "root", - "mroot": "root;root index", - "root": "root;root index", - "msub": "base;subscript", - "sub": "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", + "large-op": "基底; 下限; 上限", + "mfrac": "分子;分母", + "fraction": "分子;分母", + "msqrt": "根號", + "square-root": "根號", + "mroot": "根號;根指數", + "root": "根號;根指數", + "msub": "基底;下標", + "sub": "基底;下標", + "msup": "基底;上標", + "say-super": "基底;上標", + "skip-super": "基底;上標", + "power": "基底;指數", + "msubsup": "基底;下標;上標", + "munder": "基底;下限", + "mover": "基底;上限", + "munderover": "基底;下限;上限", # 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": "進入", + "out": "離開", } @@ -90,7 +90,7 @@ "g": "克", "m": "米", # British spelling works for US also "mol": "莫耳", - "s": "秒", "″": "秒", "\"": "秒", "sec": "秒", # "sec" not actually legal + "s": "秒", "sec": "秒", # "sec" not actually legal # derived units "Bq": "貝克", diff --git a/Rules/Languages/zh/tw/navigate.yaml b/Rules/Languages/zh/tw/navigate.yaml index a41067957..0e94a5350 100644 --- a/Rules/Languages/zh/tw/navigate.yaml +++ b/Rules/Languages/zh/tw/navigate.yaml @@ -3,9 +3,9 @@ # # The general form for many rules is: # 1. Say the command if this is first rule to fire (MatchCounter) and depending upon "NavVerbosity"s value +# This will increment MatchCounter so that the command won't be spoken again # 2. Say info about moving into/out of 2D structures # 3. Set some variables and possibly recurse. -# If recursing, "MatchCounter" should be incremented. # If stopping, "NavNode" should be set. # # The meaning of NavVerbosity: @@ -33,119 +33,86 @@ # ReadZoomLevel -- -1 for Enhanced, otherwise the distance from leaf the rules should maintain # PlaceMarkerIndex +# Note: the rules for saying a command and announcing what is said when moving in/out of a 2d exprs are hacks +# They depend upon special variables "SayCommand" and "Move2D" being set and if they are, the rules are activated. +# If/when functions can be defined in a rules file, it is likely these would be much better done via those functions +# as they would likely be much more efficient and also cleaner. -# Rules for speaking what happens when moving into or out of a notation -- name: into-or-out-of - tag: mfrac - match: "$Move2D != ''" +# Rules for announcing the command +- name: say-command + tag: "!*" + match: "$SayCommand != ''" # value should be '', 'true', or 'false' + variables: [Prefix: "''"] replace: - - x: "$Move2D" - test: - if: "count($Child2D/preceding-sibling::*)=0" - then: - test: - if: "$Child2D/..[@linethickness='0']" - then: [T: "全"] # phrase(the 'numerator' of a fraction) - else: [T: "分子"] - else: - test: - if: "$Child2D/..[@linethickness='0']" - then: [T: "選"] - else: [T: "分母"] # phrase(the 'denominator' of a fraction) - - pause: "medium" - -- name: into-or-out-of - tag: msqrt - match: "$Move2D != ''" - replace: - - x: "$Move2D" - - T: "平方根內" # phrase(the 'square root' of x) - - pause: "medium" - -- name: into-or-out-of - tag: mroot - match: "$Move2D != ''" - replace: - - x: "$Move2D" - - test: - if: "count($Child2D/preceding-sibling::*)=0" - then: [T: "根號內"] # phrase(the cube 'root' of x) - else: [T: "開方次數"] # phrase(the 'root index' of x is 3) - - pause: "medium" - -- name: into-or-out-of - tag: msub - match: "$Move2D != ''" - replace: - - x: "$Move2D" - - test: - if: "count($Child2D/preceding-sibling::*)=0" - then: [T: "基本"] # phrase(the 'base' of the power) - else: [T: "下標"] # phrase(x with 'subscript' 2) - - pause: "medium" - -- name: into-or-out-of - tag: msup - match: "$Move2D != ''" - replace: - - x: "$Move2D" - - test: - if: "count($Child2D/preceding-sibling::*)=0" - then: [T: "基本"] # phrase(the 'base' of the power) - else: [T: "上標"] # phrase(x with 'superscript' 2) # FIX: it would be better to use the word used when reading (power, exponent, ...) - - pause: "medium" - -- name: into-or-out-of - tag: msubsup - match: "$Move2D != ''" - replace: - - x: "$Move2D" - - test: - - if: "count($Child2D/preceding-sibling::*)=0" - then: [T: "基本"] # phrase(the 'base' of the power) - - else_if: "count($Child2D/preceding-sibling::*)=1" - then: [T: "下標"] # phrase(x with 'subscript' 2) - else: [T: "上標"] # phrase(x with 'superscript' 2) # FIX: it would be better to use the word used when reading (power, exponent, ...) - - pause: "medium" - -- name: into-or-out-of - tag: munder - match: "$Move2D != ''" - replace: - - x: "$Move2D" - - test: - if: "count($Child2D/preceding-sibling::*)=0" - then: [T: "基本"] # phrase(the 'base' of the power) - else: [T: "下層"] # phrase(the 'lower limit' of the function is zero) - - pause: "medium" - -- name: into-or-out-of - tag: mover - match: "$Move2D != ''" - replace: - - x: "$Move2D" - - test: - if: "count($Child2D/preceding-sibling::*)=0" - then: [T: "基本"] # phrase(the 'base' of the power) - else: [T: "上層"] # phrase(the 'upper limit' of the function is zero) - - pause: "medium" + - if: "$MatchCounter = 0 and $SayCommand = 'true'" + then_test: + - if: "self::m:math and starts-with($NavCommand, 'ZoomOut')" + then: [T: "已完全縮小", pause: "medium"] + - else_if: "IsNode(., 'leaf') and starts-with($NavCommand, 'ZoomIn')" + then: [T: "已完全放大", pause: "medium"] + else: + - test: + - if: "starts-with($NavCommand, 'Zoom')" + then: [set_variables: [Prefix: "' '"]] # phrase('zoom' in to see more details) #hjy -- 'zoom' is a bit weird in Chinese, so skip it. + - else_if: "starts-with($NavCommand, 'Move')" + then: [set_variables: [Prefix: "'移到'"]] # phrase('move' to next entry in table) + - else_if: "starts-with($NavCommand, 'Read')" + then: [set_variables: [Prefix: "'讀出'"]] # phrase('read' to next entry in table) + - else_if: "starts-with($NavCommand, 'Describe')" + then: [set_variables: [Prefix: "'描述'"]] # phrase('describe' to next entry in table) + - test: + if: "$Prefix != ''" + then: + - x: "$Prefix" + - test: + - if: "substring($NavCommand, 5) = 'In'" + then: [T: "放大"] # phrase(zoom 'in' to see more details) + - else_if: "substring($NavCommand, 5) = 'InAll'" + # HACK: '\uF8FE' is used internally for the concatenation char by 'ct' -- this gets "ed" concatenated to "zoom" + then: [T: "完全放大"] # phrase(zoom 'out all the way' to see more details) + - else_if: "substring($NavCommand, 5) = 'Out'" + then: [T: "縮小"] # phrase(zoom 'out' to see more details) + - else_if: "substring($NavCommand, 5) = 'OutAll'" + # HACK: '\uF8FE' is used internally for the concatenation char by 'ct' -- this gets "ed" concatenated to "zoom" + then: [T: "完全縮小"] # phrase(zoom 'out all the way' to see more details) + - else_if: "substring($NavCommand, 5) = 'Next'" # string-length($Prefix)+1 ###fix by hjy + then: [T: "下一項"] # phrase(move to the 'right') + - else_if: "substring($NavCommand, 9) = 'Next'" # string-length($Prefix)+1 ###fix by hjy for describe + then: [T: "下一項"] # phrase(move to the 'right') + - else_if: "substring($NavCommand, 5) = 'Previous'" #string-length($Prefix)+1 ###fix by hjy + then: [T: "上一項"] # phrase(move to the 'left') + - else_if: "substring($NavCommand, 9) = 'Previous'" #string-length($Prefix)+1 ###fix by hjy for describe + then: [T: "上一項"] # phrase(move to the 'left') + - else_if: "substring($NavCommand, 5) = 'Current'" + then: [T: "目前"] # phrase(who is the 'current' president) + - else_if: "substring($NavCommand, 9) = 'Current'" + then: [T: "目前"] # phrase(who is the 'current' president) + - else_if: "substring($NavCommand, 5) = 'LineStart'" + then: [T: "到行首"] # phrase(move 'to start of line') + - else_if: "substring($NavCommand, 5) = 'LineEnd'" + then: [T: "到行尾"] # phrase(move 'to end of line') + - pause: "medium" + - set_variables: [MatchCounter: "$MatchCounter + 1"] + +- name: into-or-out-of-silent + tag: "*" + # saying "out of row n" is not very useful, so skip it + match: "$Move2D != '' and (not(@data-from-mathml) or @data-from-mathml = name(.)) and + (name(.)='mrow' or name(.) = 'mtr' or name(.) = 'mlabeledtr' or @data-from-mathml = 'mtable')" + replace: [] -- name: into-or-out-of - tag: munderover - match: "$Move2D != ''" +- name: into-or-out-of-mtr + tag: [mtr, mlabeledtr] + match: "$Move2D = '進入'" replace: - - x: "$Move2D" - - test: - - if: "count($Child2D/preceding-sibling::*)=0" - then: [T: "基本"] # phrase(the 'base' of the power) - - else_if: "count($Child2D/preceding-sibling::*)=1" - then: [T: "下層"] # phrase(the 'lower limit' of the function is zero) - else: [T: "上層"] # phrase(the 'upper limit' of the function is zero) + - T: "行" + - x: "count($Child2D/preceding-sibling::*)+1" - pause: "medium" -- name: into-or-out-of - tag: mmultiscripts - match: "$Move2D != ''" +- name: into-or-out-of-mmultiscripts + tag: "*" + match: "$Move2D != '' and (@data-from-mathml='mmultiscripts' or self::m:mmultiscripts)" replace: - test: if: "name($Child2D)!='none'" @@ -157,36 +124,45 @@ - x: "$Move2D" - test: - if: "$NumPrecedingSiblings=0" - then: [T: "基本"] # phrase(the 'base' of the power) + then: [T: "基底"] # 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: "下標"] # phrase(x with 'subscript' 2) - else: [T: "上標"] # phrase(x with 'superscript' 2) + if: "$NumPrecedingSiblings mod 2 = 0" + then: [T: "前下標"] # phrase(x with 'subscript' 2) + else: [T: "前上標"] # phrase(x with 'superscript' 2) else: - test: if: "$NumPrecedingSiblings mod 2 = 0" - then: [T: "上標"] # phrase(x with 'superscript' 2) - else: [T: "下標"] # phrase(x with 'subscript' 2) + then: [T: "上標"] # phrase(x with 'pre-superscript' 2) + else: [T: "下標"] # phrase(x with 'pre-subscript' 2) - pause: "medium" -- name: into-or-out-of - tag: mtd - match: "$Move2D = '進入'" - replace: - - x: "$Move2D" - - T: "行" # phrase(the first 'column' in the table) - - x: "count($Child2D/preceding-sibling::*)+1" - - pause: "medium" - -- name: into-or-out-of - tag: [mtr, mlabeledtr] - match: "$Move2D = '進入'" +# Rules for speaking what happens when moving into or out of a notation +- name: into-or-out-of-default + 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'" + match: "$Move2D != '' and not(self::m:math or @data-from-mathml = 'mtable' or @data-from-mathml = 'mtd') " replace: - - x: "$Move2D" - - x: "count($Child2D/preceding-sibling::*)+1" - - pause: "medium" + - with: + variables: + - PartNumber: "count($Child2D/preceding-sibling::*)" + - PartName: "GetNavigationPartName(name(.), $PartNumber)" + replace: + - x: "$Move2D" + - test: + - if: "$PartName != ''" + then: [x: "$PartName"] + - else_if: "count(*) = 1" + then_test: + if: "$NavVerbosity = 'Verbose'" + then: [x: "translate(name(.), '-_', ' ')"] # e.g., "in absolute value" + else: + - T: "第" # phrase(the 'part' of the expression) + - x: "count($Child2D/preceding-sibling::*) + 1" + - T: "部分" # phrase(the 'part' of the expression) + - pause: "medium" - name: default-move # nothing to do (not 2D) -- need to catch $Move2D though so rules based on NavCommand don't trigger @@ -207,20 +183,20 @@ then: - test: - if: "$PreviousNavCommand = 'ZoomIn'" - then: [T: "撤消放大"] # phrase('undo zoom in') + then: [T: "復原放大"] # phrase('undo zoom in') - else_if: "$PreviousNavCommand = 'ZoomOut'" - then: [T: "撤消縮小"] # phrase('undo zoom out') + then: [T: "復原縮小"] # phrase('undo zoom out') - else_if: "$PreviousNavCommand = 'ZoomInAll'" - then: [T: "一路撤消放大"] # phrase('undo zooming in all the way') + then: [T: "復原完全放大"] # phrase('undo zooming in all the way') - else_if: "$PreviousNavCommand = 'ZoomOutAll'" - then: [T: "一路撤消縮小"] # phrase('undo zooming out all the way') + then: [T: "復原完全縮小"] # phrase('undo zooming out all the way') - else_if: "$PreviousNavCommand = 'MovePrevious' or $PreviousNavCommand = 'MovePreviousZoom'" - then: [T: "撤消向左移動"] # phrase('undo move left') + then: [T: "復原移到上一項"] # phrase('undo move left') - else_if: "$PreviousNavCommand = 'MoveNext' or $PreviousNavCommand = 'MoveNextZoom'" - then: [T: "撤消向右移動"] # phrase('undo move right') + then: [T: "復原移到下一項"] # phrase('undo move right') - else_if: "$PreviousNavCommand = 'None'" - then: [T: "沒有以前的命令"] # phrase('no previous command') - - pause: "long" + then: [T: "沒有前一指令"] # phrase('no previous command') + - pause: "medium" - set_variables: [NavNode: "@id"] # many times, for typographic reasons, people include punctuation at the end of a math expr @@ -242,40 +218,32 @@ replace: - test: if: "$MatchCounter = 0 and $NavVerbosity != 'Terse'" - then: [T: "一路放大", pause: "long"] # phrase('zoomed in all of the way') + then: [T: "已完全放大", pause: "long"] # phrase('zoomed in all of the way') - test: if: "$ReadZoomLevel!=-1" then: - set_variables: [ReadZoomLevel: "0"] - set_variables: [NavNode: "@id"] -# special case of zooming into a matrix or determinant -- move to the first row -- name: zoom-in-matrix - - tag: mrow - match: - - "$NavCommand = 'ZoomIn' and count(*)=3 and " - - "*[2][self::m:mtable and (IsBracketed(., '(', ')') or IsBracketed(., '[', ']') or IsBracketed(., '|', '|'))]" - replace: - - test: - if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'" - then: [T: "放大", pause: "long"] # phrase('zoom in') - - set_variables: [NavNode: "*[2]/*[1]/@id"] - -# special case of zooming into a table -- move to the first row +# special case of zooming into a table -- move to the first row (if only one row, first column) - name: zoom-in-table - tag: mtable - match: "$NavCommand = 'ZoomIn'" + tag: "*" + match: "$NavCommand = 'ZoomIn' and (name(.) = 'mtable' or (count(*)=1 and *[1][@data-from-mathml='mtable']))" replace: + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] - test: - if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'" - then: [T: "放大", pause: "long"] # phrase('zoom in') - - set_variables: [NavNode: "*[1]/@id"] + if: "count(*)=1" + then: + - set_variables: [NavNode: "*[1]/*[1]/@id"] + else: + - set_variables: [NavNode: "*[1]/@id"] - name: zoom-in-mrow-in-math - # Moving to first or last is meaningless the 'math' has only an 'mrow' inside -- dig inside and do it again + # zooming in only once is meaningless because 'math' has only a single child and it was spoken at the math level -- dig inside and do it again tag: math - match: "count(*)=1 and ($NavCommand = 'ZoomIn' or $NavCommand = 'MoveNextZoom' or $NavCommand = 'MovePreviousZoom')" + match: "$NavCommand = 'ZoomIn' or $NavCommand = 'MoveNextZoom' or $NavCommand = 'MovePreviousZoom'" replace: - test: if: "$NavCommand = 'MovePreviousZoom'" @@ -289,117 +257,119 @@ match: - "($NavCommand = 'ZoomIn' or " - " ($NavCommand = 'MoveNextZoom' or $NavCommand = 'MovePreviousZoom') and $NavMode='Enhanced') and " - - "count(*)=1 and (*[1][self::m:mrow] and not(self::m:msqrt or self::m:menclose))" + - "count(*)=1 and + (*[1][self::m:mrow or @data-from-mathml='mrow'] and + not(@data-from-mathml='msqrt' or self::m:msqrt or @data-from-mathml='menclose' or self::m:menclose))" replace: - - test: - if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'" - then: [T: "放大", pause: "long"] # phrase('zoom in') - with: - variables: [MatchCounter: "$MatchCounter + 1"] - replace: - - test: - if: "$NavCommand = 'MovePreviousZoom'" - then: [x: "*[last()]"] - else: [x: "*[1]"] + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] + - test: + if: "name(*[1]) != 'mrow'" + then: + - with: + variables: [Move2D: "'進入'", Child2D: "IfThenElse(count(*)=0, $Move2D, $Move2D)"] # phrase('in' the denominator) + replace: [x: "IfThenElse($NavCommand = 'MovePreviousZoom', 1, $Child2D)"] + - test: + if: "$NavCommand = 'MovePreviousZoom'" + then: [x: "*[last()]"] + else: [x: "*[1]"] - name: zoom-in-enhanced - tag: "*" match: "$NavCommand = 'ZoomIn' and $NavMode='Enhanced'" replace: - - test: - if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'" - then: [T: "放大", pause: "long"] # phrase('zoom in') + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] - test: - if: "self::m:mtr or self::m:mlabeledtr" then: - with: - variables: [Move2D: "'在'", Child2D: "*[1]/*[1]"] # phrase('in' the denominator) + variables: [Move2D: "'進入'", Child2D: "*[1]/*[1]"] # phrase('in' the denominator) replace: [x: "."] - set_variables: [NavNode: "*[1]/*[1]/@id"] # skip mtd - - else_if: "*[1][self::m:mrow and IsBracketed(., '(', ')', false) or IsBracketed(., '[', ']', false)]" # auto zoom + - else_if: "*[1][self::m:mrow and (IsBracketed(., '(', ')', false) or IsBracketed(., '[', ']', false))]" # auto zoom then: - with: - variables: [Move2D: "'在'", Child2D: "*[1]"] # phrase('in' the denominator) + variables: [Move2D: "'進入'", Child2D: "*[1]"] # phrase('in' the denominator) replace: [x: "."] - set_variables: [NavNode: "*[1]/*[2]/@id"] # skip parens/brackets else: - with: - variables: [Move2D: "'在'", Child2D: "*[1]"] # phrase('in' the denominator) + variables: [Move2D: "'進入'", Child2D: "*[1]"] # phrase('in' the denominator) replace: [x: "."] - set_variables: [NavNode: "*[1]/@id"] -- name: zoom-in-2D-not-enhanced + +- name: zoom-in-simple tag: "*" - match: "$NavCommand = 'ZoomIn' and $NavMode!='Enhanced' and IsNode(., '2D')" + match: "$NavCommand = 'ZoomIn' and $NavMode='Simple'" replace: - - test: - if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'" - then: [T: "放大", pause: "long"] # phrase('zoom in') - with: - variables: [Move2D: "'在'", Child2D: "*[1]"] # phrase('in' the denominator) + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] replace: [x: "."] - - with: - variables: [MatchCounter: "$MatchCounter + 1", NavCommand: "'MoveNextZoom'"] - replace: [x: "*[1]"] + - test: + if: "DEBUG($MatchCounter) > 1 and IsNode(., '2D') " + then: [set_variables: [NavNode: "@id"]] # time to stop, not going "in" to next thing, so before "Move2D" + else: + - with: + variables: [Move2D: "'進入'", Child2D: "*[1]"] # phrase('in' the denominator) + replace: [x: "."] + - x: "*[1]" -- name: zoom-in-default + # At this point, we are zooming in on a non-2D element, non-leaf in Character mode +- name: zoom-in-2D-character tag: "*" - match: "$NavCommand = 'ZoomIn'" + match: "$NavCommand = 'ZoomIn' and (IsNode(., '2D') or not(IsNode(., 'mathml')))" replace: - - test: - if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'" - then: [T: "放大", pause: "long"] # phrase('zoom in') - with: - variables: [Move2D: "'在'", Child2D: "*[1]"] # phrase('in' the denominator) + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] + - with: + variables: [Move2D: "'進入'", Child2D: "*[1]"] # phrase('in' the denominator) replace: [x: "."] - test: - if: "$NavMode='Character'" + if: "$NavMode = 'Simple'" then: + - set_variables: [NavNode: "*[1]/@id"] + else: - with: - variables: [MatchCounter: "$MatchCounter + 1"] + variables: [NavCommand: "'MoveNextZoom'"] replace: [x: "*[1]"] - else_test: - if: "self::m:mtd" - then: [x: "*[1]"] - else: - - test: - if: "$ReadZoomLevel!=-1" - then: - - set_variables: [ReadZoomLevel: "DistanceFromLeaf(*[1], true, $NavMode!='Character')"] - - set_variables: [NavNode: "*[1]/@id"] + + # At this point, we are zooming in on a non-2D element, non-leaf in Character mode +- name: zoom-in-default + tag: "*" + match: "$NavCommand = 'ZoomIn'" + replace: + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] + - x: "*[1]" + - name: zoom-in-all-default tag: "*" match: "$NavCommand = 'ZoomInAll'" replace: - - test: - if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'" - then: [T: "一路放大", pause: "medium"] # phrase('zoom in all the way') - with: - variables: [Move2D: "'在'", Child2D: "*[1]"] # phrase('in' the denominator) + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] replace: [x: "."] - with: - variables: [MatchCounter: "$MatchCounter + 1"] - replace: [x: "*[1]"] + variables: [Move2D: "'進入'", Child2D: "*[1]"] # phrase('in' the denominator) + replace: [x: "."] + - x: "*[1]" - name: zoom-out tag: math match: "$NavCommand = 'ZoomOut' or $NavCommand = 'ZoomOutAll'" replace: - - test: - if: "$MatchCounter = 0 and $NavVerbosity != 'Terse'" - then: [T: "一路縮小", pause: "long"] # phrase('zoomed out all the the way') + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] - set_variables: [NavNode: "*[1]/@id"] # no-op for $NavCommand = 'ZoomOut' -- name: zoom-out-top - tag: "*" - match: - - "($NavCommand = 'ZoomOut' or $NavCommand = 'ZoomOutAll') and" - - "parent::m:math " - replace: - - x: ".." # let math rule deal with it - - name: skip-punct-at-end-zoom-out tag: mrow match: @@ -409,19 +379,25 @@ replace: - x: ".." +- name: zoom-out-top + tag: "*" + match: + - "($NavCommand = 'ZoomOut' or $NavCommand = 'ZoomOutAll') and" + - "parent::m:math " + replace: + - x: ".." # let math rule deal with it + - name: zoom-out-all-default tag: "*" match: "$NavCommand = 'ZoomOutAll'" replace: - - test: - if: "$MatchCounter = 0 and $NavVerbosity != 'Terse'" - then: [T: "一路縮小", pause: "medium"] # phrase('zoomed out all the the way') - with: - variables: [Move2D: "'離開'", Child2D: "."] - replace: [x: ".."] + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] - with: - variables: [MatchCounter: "$MatchCounter + 1"] + variables: [Move2D: "'離開'", Child2D: "."] replace: [x: ".."] + - x: ".." # deal with internal zooming: MoveNextZoom and MovePreviousZoom @@ -432,12 +408,12 @@ - "($NavCommand = 'MoveNextZoom' or $NavCommand = 'MovePreviousZoom') and " - "$NavMode = 'Enhanced'" replace: - # don't bother with MatchCounter since we only get here if > 1 - with: - variables: [Move2D: "'在'", Child2D: "*[1]"] # phrase('in' the denominator) + variables: [Move2D: "'進入'", Child2D: "*[1]"] # phrase('in' the denominator) replace: [x: "."] - test: - - if: "count(*)> 1 or IsNode(., 'leaf') or self::m:msqrt or self::m:menclose" + - if: "count(*)> 1 or IsNode(., 'leaf') or + @data-from-mathml='msqrt' or self::m:msqrt or @data-from-mathml='menclose' or self::m:menclose" then: [set_variables: [NavNode: "@id"]] else: [x: "*[1]"] @@ -448,7 +424,7 @@ replace: #don't bother with MatchCounter since we only get here if > 1 - test: - if: "$ReadZoomLevel >= DistanceFromLeaf(., false, $NavMode!='Character')" + if: "IsNode(., 'leaf') or $ReadZoomLevel >= DistanceFromLeaf(., false, $NavMode!='Character')" then: # - with: # variables: [Move2D: "'in'", Child2D: "following-sibling::*[1]"] # phrase('in' the denominator) @@ -456,7 +432,7 @@ - set_variables: [NavNode: "@id"] else: - with: - variables: [Move2D: "'在'", Child2D: "*[1]"] # phrase('in' the denominator) + variables: [Move2D: "'進入'", Child2D: "*[1]"] # phrase('in' the denominator) replace: [x: "."] - x: "*[1]" @@ -475,7 +451,7 @@ - set_variables: [NavNode: "@id"] else: - with: - variables: [Move2D: "'在'", Child2D: "*[last()]"] # phrase('in' the denominator) + variables: [Move2D: "'進入'", Child2D: "*[last()]"] # phrase('in' the denominator) replace: [x: "."] - x: "*[last()]" @@ -486,14 +462,14 @@ tag: mtd match: "$Move2D = '' and ($NavCommand = 'ZoomOut')" replace: - - test: - if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'" - then: [T: "縮小", pause: "medium"] # phrase('zoom out' of expression) + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] # we need to speak it here - - T: "列" # phrase(the first 'row' of the matrix) - # if we let the speech rules speak the row, it is given just the MathML for the row, so the row # will always be '1' - - x: "count(../preceding-sibling::*)+1" - - pause: medium + # - t: "row" # phrase(the first 'row' of the matrix) + # # if we let the speech rules speak the row, it is given just the MathML for the row, so the row # will always be '1' + # - x: "count(../preceding-sibling::*)+1" + # - pause: medium - set_variables: [NavNode: "../@id"] - name: zoom-out @@ -502,26 +478,23 @@ match: "$NavCommand = 'ZoomOut'" replace: - with: - variables: [MatchCounter: "$MatchCounter + 1"] - replace: - - test: - if: "$MatchCounter = 1 and $NavVerbosity = 'Verbose'" - then: [T: "縮小", pause: "medium"] # phrase('zoom out' of the expression) + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] + - test: + if: "$NavMode='Enhanced' and parent::*[self::m:mrow and (IsBracketed(., '(', ')', false) or IsBracketed(., '[', ']', false))]" + then: [x: ".."] # auto-zoom: move out a level and retry + else: + - with: + variables: [Move2D: "'離開'", Child2D: "."] + replace: [x: ".."] - test: - if: "$NavMode='Enhanced' and parent::*[self::m:mrow and IsBracketed(., '(', ')', false) or IsBracketed(., '[', ']', false)]" - then: [x: ".."] # auto-zoom: move out a level and retry + if: "parent::m:mtd" + then: [x: ".."] else: - - with: - variables: [Move2D: "'離開'", Child2D: "."] - replace: [x: ".."] - test: - if: "parent::m:mtd" - then: [x: ".."] - else: - - test: - if: "$ReadZoomLevel!=-1" - then: [set_variables: [ReadZoomLevel: "DistanceFromLeaf(.., true, $NavMode!='Character')"]] - - set_variables: [NavNode: "../@id"] + if: "$ReadZoomLevel!=-1" + then: [set_variables: [ReadZoomLevel: "DistanceFromLeaf(.., true, $NavMode!='Character')"]] + - set_variables: [NavNode: "../@id"] # ********* MoveStart/End *************** - name: math-move-to-start-or-end @@ -536,42 +509,56 @@ then: - test: - if: "$NavCommand = 'MoveStart'" - then: [T: "移至數學開頭"] # phrase('move to start of math') + then: [T: "移到數學開頭"] # phrase('move to start of math') - else_if: "$NavCommand = 'MoveLineStart'" - then: [T: "移至列頭"] # phrase('move to start of line') + then: [T: "移到行首"] # phrase('move to start of line') - else_if: "$NavCommand = 'MoveEnd'" - then: [T: "移至數學結束"] # phrase('move to end of math') - else: [T: "移至列尾"] # "$NavCommand = 'MoveLineEnd'" # phrase('move to end of line') + then: [T: "移到數學結尾"] # phrase('move to end of math') + else: [T: "移到行尾"] # "$NavCommand = 'MoveLineEnd'" # phrase('move to end of line') - pause: "medium" - test: if: "$NavCommand = 'MoveStart' or $NavCommand = 'MoveLineStart'" then: # move inside of the mrow inside of 'math' or inside the fraction, etc (hence two levels down) + # Note: an apparent bug in the xpath code doesn't let me use IfThenElse for the 2 if: then: below - with: variables: [NavCommand: "'MoveNextZoom'"] - replace: [x: "*[1]/*[1]"] + replace: + - test: + if: "*[1]/*[1]" # could be a x, so no grandchild + then: [x: "*[1]/*[1]"] + else: [x: "*[1]"] else: - with: variables: [NavCommand: "'MovePreviousZoom'"] - replace: [x: "*[last()]/*[last()]"] + replace: + - test: + if: "*[last()]/*[last()]" # could be a x, so no grandchild + then: [x: "*[last()]/*[last()]"] + else: [x: "*[last()]"] # We stop when the parent is 2d (e.g., frac), but not if in leaf base of msub/msup/msubsup/mmultiscripts because that's really on the same line - name: move-to-start-or-end-2d tag: "*" match: - "($NavCommand = 'MoveLineStart' or $NavCommand = 'MoveLineEnd') and IsNode(.., '2D') and" - - "not( IsNode(., 'leaf') and (parent::m:msub or parent::m:msup or parent::m:msubsup or parent::m:mmultiscripts) )" + - "not( IsNode(., 'leaf') and" + - " parent::*[1][self::m:msub or self::m:msup or self::m:msubsup or self::m:mmultiscripts or" + - " @data-from-mathml and" + - " (@data-from-mathml='msub' or @data-from-mathml='msup' or" + - " @data-from-mathml='msubsup' or @data-from-mathml='mmultiscripts')" + - " ] )" replace: - test: if: "$NavVerbosity = 'Verbose'" then: - test: if: "$NavCommand = 'MoveLineStart'" - then: [T: "移至列頭"] # phrase('move to start of line') - else: [T: "移至列尾"] # "$NavCommand = 'MoveLineEnd'" # phrase('move to end of line') + then: [T: "移到行首"] # phrase('move to start of line') + else: [T: "移到行尾"] # "$NavCommand = 'MoveLineEnd'" # phrase('move to end of line') - pause: "medium" - test: - if: "self::m:mrow" + if: "self::m:mrow or @data-from-mathml = 'mrow'" then_test: if: "$NavCommand = 'MoveLineStart'" then: @@ -604,7 +591,7 @@ - "$NavCommand='MoveColumnStart' or $NavCommand='MoveColumnEnd' or" - "$NavCommand='ReadCellCurrent'" replace: - - T: "不在表中" # phrase('not in table') + - T: "不在表格內" # phrase('not in table') - pause: long - set_variables: [SpeakExpression: "'false'"] @@ -618,7 +605,7 @@ - test: if: "$NavVerbosity = 'Verbose'" then: - - T: "向左移" # phrase('move left') + - T: "移到上一項" # phrase('move left') - pause: short - test: if: "$NavVerbosity != 'Terse'" @@ -635,7 +622,7 @@ else: - set_variables: [NavNode: "preceding-sibling::*[1]/*[1]/@id"] else: - - T: "沒有前一行" # phrase('no previous column' in the table) + - T: "沒有左一行" # phrase('no previous column' in the table) - set_variables: [SpeakExpression: "'false'"] - name: move-cell-next @@ -648,7 +635,7 @@ - test: if: "$NavVerbosity = 'Verbose'" then: - - T: "向右移" # phrase('move right') + - T: "移到下一項" # phrase('move right') - pause: short - test: if: "$NavVerbosity != 'Terse'" @@ -665,7 +652,7 @@ else: - set_variables: [NavNode: "following-sibling::*[1]/*[1]/@id"] else: - - T: "沒有下一行" # phrase('no next column' in the table) + - T: "沒有右一行" # phrase('no next column' in the table) - set_variables: [SpeakExpression: "'false'"] - name: move-cell-up @@ -681,7 +668,7 @@ - test: if: "$NavVerbosity = 'Verbose'" then: - - T: "向上移動" # phrase('move up' to previous row in the table) + - T: "往上一格" # phrase('move up' to previous row in the table) - pause: short - test: if: "$NavVerbosity != 'Terse'" @@ -701,7 +688,7 @@ else: - set_variables: [NavNode: "../preceding-sibling::*[1]/*[$Column]/*[1]/@id"] else: - - T: "沒有前一列" # phrase('no previous row' in the table) + - T: "沒有上一列" # phrase('no previous row' in the table) - set_variables: [SpeakExpression: "'false'"] - name: move-cell-down @@ -717,7 +704,7 @@ - test: if: "$NavVerbosity = 'Verbose'" then: - - T: "向下移動" # phrase('move down to the next row in the table) + - T: "往下一格" # phrase('move down to the next row in the table) - pause: short - test: if: "$NavVerbosity != 'Terse'" @@ -744,20 +731,14 @@ tag: [mtr, mlabeledtr] match: "$NavCommand='MoveCellUp'" replace: + - test: + if: "$NavVerbosity = 'Verbose'" + then: + - T: "移到上一列" # phrase('move to previous row' to the previous row in the table) + - pause: medium - test: if: "preceding-sibling::*" then: - - test: - if: "$NavVerbosity = 'Verbose'" - then: - - T: "向上移動" # phrase('move up' to the previous row in the table) - - pause: medium - - test: - if: "$NavVerbosity != 'Terse'" - then: - - T: "列" # phrase(the previous 'row' in the table) - - x: "count(preceding-sibling::*)" - - pause: medium - test: if: "$NavMode='Character'" then: @@ -767,27 +748,21 @@ else: - set_variables: [NavNode: "preceding-sibling::*[1]/@id"] else: - - T: "沒有前一列" # phrase('no previous row' in the table) + - T: "沒有上一列" # phrase('no previous row' in the table) - set_variables: [SpeakExpression: "'false'"] - name: move-cell-down tag: [mtr, mlabeledtr] match: "$NavCommand='MoveCellDown'" replace: + - test: + if: "$NavVerbosity = 'Verbose'" + then: + - T: "移到下一列" # phrase('move to next row' to the next row in the table) + - pause: medium - test: if: "following-sibling::*" then: - - test: - if: "$NavVerbosity = 'Verbose'" - then: - - T: "向下移動" # phrase('move down' to the next row in the table) - - pause: medium - - test: - if: "$NavVerbosity != 'Terse'" - then: - - T: "列" # phrase(the previous 'row' in the table) - - x: "count(preceding-sibling::*)+2" - - pause: medium - test: if: "$NavMode='Character'" then: @@ -800,6 +775,32 @@ - T: "沒有下一列" # phrase('no next row' in the table) - set_variables: [SpeakExpression: "'false'"] +- name: move-cell-previous + # if a row is selected, there is no previous/next column, so this is trivial + tag: [mtr, mlabeledtr] + match: "$NavCommand='MoveCellPrevious'" + replace: + - test: + if: "$NavVerbosity = 'Verbose'" + then: + - T: "移到左一行" # phrase('move to previous column' to the previous row in the table) + - pause: medium + - T: "沒有左一行" # phrase('no previous column' in the table) + - set_variables: [SpeakExpression: "'false'"] + +- name: move-cell-next + # if a row is selected, there is no previous/next column, so this is trivial + tag: [mtr, mlabeledtr] + match: "$NavCommand='MoveCellNext'" + replace: + - test: + if: "$NavVerbosity = 'Verbose'" + then: + - T: "移到右一行" # phrase('move to next column' to the next row in the table) + - pause: medium + - T: "沒有右一行" # phrase('no next column' in the table) + - set_variables: [SpeakExpression: "'false'"] + - name: default-read-cell tag: "*" match: "$NavCommand='ReadCellCurrent'" @@ -813,19 +814,20 @@ - test: if: "$NavVerbosity = 'Verbose'" then: - - T: "閱讀當前項目" # phrase('read current entry' in the table) + - T: "讀出目前項目" # phrase('read current entry' in the table) - pause: medium - test: if: "$NavVerbosity != 'Terse'" then: - T: "列" # phrase(the previous 'row' in the table) - x: "count($MTD[1]/../preceding-sibling::*)+1" + - pause: short - T: "行" # phrase(the previous 'column' in the table) - x: "count($MTD[1]/preceding-sibling::*)+1" - pause: short - set_variables: [NavNode: "$MTD[1]/*[1]/@id"] else: - - T: "不在表中" # phrase('not in table' or matrix) + - T: "不在表格內" # phrase('not in table' or matrix) - pause: long - set_variables: [SpeakExpression: "'false'"] @@ -1065,7 +1067,7 @@ then: - x: "ancestor::m:mtd[1]" # try again on an mtd node else: - - T: "不在表中" # phrase('not in table' or matrix) + - T: "不在表格內" # phrase('not in table' or matrix) - pause: long - set_variables: [SpeakExpression: "'false'"] @@ -1087,7 +1089,7 @@ then: [x: "$Following"] else: - with: - variables: [Move2D: "'在'", Child2D: "$Following"] # phrase('in' the denominator) + variables: [Move2D: "'進入'", Child2D: "$Following"] # phrase('in' the denominator) replace: [x: ".."] - with: variables: [NavCommand: "'MoveNextZoom'"] @@ -1109,7 +1111,7 @@ then: [x: "$Preceding"] else: - with: - variables: [Move2D: "'在'", Child2D: "$Preceding"] # phrase('in' the denominator) + variables: [Move2D: "'進入'", Child2D: "$Preceding"] # phrase('in' the denominator) replace: [x: ".."] - with: variables: [NavCommand: "'MovePreviousZoom'"] @@ -1124,24 +1126,14 @@ - "($NavCommand = 'MoveNext' or $NavCommand = 'ReadNext' or $NavCommand = 'DescribeNext') and" - "following-sibling::*[1][name(.)='mo' and translate(., '\u2061\u2062\u2063\u2064', '')='']" replace: - - test: - if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'" - then: - - test: - - if: "$NavCommand = 'MoveNext'" - then: [T: "移動"] # phrase('move' to next entry in table) - - else_if: "$NavCommand = 'ReadNext'" - then: [T: "讀"] # phrase('read' next entry in table) - else: [T: "描述"] # phrase('describe' next entry in table) - - T: "往右" # phrase(move 'right') # phrase(move 'right') - - pause: short - with: - variables: [MatchCounter: "$MatchCounter + 1"] - replace: - - test: - if: "following-sibling::*[1][.='\u2062' or .='\u2064'] and $NavMode='Enhanced'" # invisible times and plus - then: [set_variables: [NavNode: "following-sibling::*[1]/@id"]] - else: [x: "following-sibling::*[1]"] + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] + - test: + if: "following-sibling::*[1][.='\u2062' or .='\u2064'] and + ($NavMode='Enhanced' or ($NavMode='Simple' and following-sibling::*[2][not(IsNode(., 'mathml'))]))" # invisible times and plus + then: [set_variables: [NavNode: "following-sibling::*[1]/@id"]] + else: [x: "following-sibling::*[1]"] - name: move-next-no-auto-zoom-at-edge # at edge of 2D and in a mode where moving right isn't an option @@ -1150,11 +1142,15 @@ match: "$NavCommand = 'MoveNext' and $NavMode!='Character' and not($AutoZoomOut) and $EdgeNode/@id!=@id" replace: - test: - if: "$MatchCounter = 0 and $NavVerbosity != 'Terse' and $NavCommand = 'MoveNext'" + if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'" then: - - T: "無法往右移動" # phrase('cannot move right') + - T: "無法移到下一項" # phrase('cannot move right') + - pause: medium - with: - variables: [Move2D: "'結束'", Child2D: "$EdgeNode/*[last()]"] + variables: + - Move2D: "'end of'" + - Child2D: "$EdgeNode/*[last()]" + - MatchCounter: $MatchCounter + 1 replace: [x: "$EdgeNode"] - pause: long - set_variables: [SpeakExpression: "'false'"] @@ -1167,17 +1163,19 @@ - "(self::m:math or name(EdgeNode(., 'right', 'math'))='math')" # at edge of math replace: - test: - if: "$MatchCounter = 0 and $NavVerbosity != 'Terse'" + if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'" then: - - T: "不能" # phrase('cannot' move right in expression) + - T: "無法" # phrase('cannot' move right in expression) - test: - if: "$NavCommand = 'MoveNext'" - then: [T: "移動"] # phrase('move' to next entry in table) + then: [T: "移到"] # phrase('move' to next entry in table) - else_if: "$NavCommand = 'ReadNext'" - then: [T: "讀"] # phrase('read' next entry in table) + then: [T: "讀出"] # phrase('read' next entry in table) else: [T: "描述"] # phrase('describe' next entry in table) - - T: "往右,數學結束" # phrase(move 'right, end of math') - - pause: long + - T: "下一項" # phrase(move 'right') + - pause: short + - T: "數學結尾" # phrase(move 'end of math') + - pause: long - set_variables: [SpeakExpression: "'false'"] - name: move-next-auto-zoom-up-one-level @@ -1192,48 +1190,34 @@ - " )" - ")" replace: + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] - test: - if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'" + if: "following-sibling::*" then: - - test: - - if: "$NavCommand = 'MoveNext'" - then: [T: "移動"] # phrase('move' to next entry in table) - - else_if: "$NavCommand = 'ReadNext'" - then: [T: "讀"] # phrase('read' next entry in table) - else: [T: "描述"] # phrase('describe' next entry in table) - - T: "往右" # phrase(move 'right') # phrase(move 'right') - - pause: short - - with: - variables: [MatchCounter: "$MatchCounter + 1", NavCommand: "'MoveNext'"] - replace: - - test: - if: "following-sibling::*" - then: - - with: - variables: [Move2D: "'在'", Child2D: "."] # phrase('in' the denominator) - replace: [x: ".."] - else: - - with: - variables: [Move2D: "'離開'", Child2D: "."] - replace: [x: ".."] - - test: - if: "following-sibling::*" - then: - - x: ".." # move out of parens - else: - - x: ".." + - with: + variables: [Move2D: "'進入'", Child2D: "."] # phrase('in' the denominator) + replace: [x: ".."] + else: + - with: + variables: [Move2D: "'離開'", Child2D: "."] + replace: [x: ".."] + - x: ".." # At this point, if XXXNext, then we know there is must be a right sibling - name: move-next-default tag: mtd match: "$Move2D = '' and ($NavCommand = 'MoveNext' or $NavCommand = 'ReadNext' or $NavCommand = 'DescribeNext')" replace: + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] - test: - # can't get here with MatchCounter=0, so no need to echo command if: "following-sibling::*" then: - test: - if: "$NavVerbosity != 'Terse'" + if: "$NavVerbosity = 'Verbose'" then: - T: "行" # phrase(the previous 'column' in the table) - x: "count(preceding-sibling::*)+2" @@ -1250,25 +1234,21 @@ - name: move-next-default tag: [mtr, mlabeledtr] - match: "$Move2D = '' and ($NavCommand = 'MoveNext' or $NavCommand = 'ReadNext' or $NavCommand = 'DescribeNext')" + match: "$Move2D = '' and + ($NavCommand = 'MoveNext' or $NavCommand = 'ReadNext' or $NavCommand = 'DescribeNext') and + following-sibling::*" replace: + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] - test: - # can't get here with MatchCounter=0, so no need to echo command - if: "following-sibling::*" + if: "$NavMode = 'Character'" then: - - T: "列" # phrase(the previous 'row' in the table) - - x: "count(preceding-sibling::*)+2" - - T: "第 1 行" # phrase('column 1' in the table) - - pause: medium - - test: - if: "$NavMode = 'Character'" - then: - - with: - variables: [NavCommand: "'MoveNextZoom'"] - replace: [x: "following-sibling::*[1]"] - else: - - set_variables: [NavNode: "following-sibling::*[1]/*[1]/*[1]/@id"] - else: [x: ".."] # try again for after + - with: + variables: [NavCommand: "'MoveNextZoom'"] + replace: [x: "following-sibling::*[1]"] + else: + - set_variables: [NavNode: "following-sibling::*[1]/@id"] - name: move-next-auto-zoom-parens # auto-zoom into next child if next child is parenthesized expr @@ -1278,20 +1258,12 @@ - "$NavMode='Enhanced' and" - "parent::m:mrow and following-sibling::* and" - "following-sibling::*[1][self::m:mrow and count(*)=3 and " #exclude empty parens - - " (IsBracketed(., '(', ')') or IsBracketed(., '[', ']'))" - - " ]" + - " (IsBracketed(., '(', ')') or IsBracketed(., '[', ']'))" + - " ]" replace: - - test: - if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'" - then: - - test: - - if: "$NavCommand = 'MoveNext'" - then: [T: "移動"] # phrase('move' to next entry in table) - - else_if: "$NavCommand = 'ReadNext'" - then: [T: "讀"] # phrase('read' next entry in table) - else: [T: "描述"] # phrase('describe' next entry in table) - - T: "往右" # phrase(move 'right') # phrase(move 'right') - - pause: short + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] - set_variables: [NavNode: "following-sibling::*[1]/*[2]/@id"] # normal cases for MoveNext @@ -1302,17 +1274,9 @@ - "($NavCommand = 'MoveNext' or $NavCommand = 'ReadNext' or $NavCommand = 'DescribeNext') and" - "$ReadZoomLevel>=0" replace: - - test: - if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'" - then: - - test: - - if: "$NavCommand = 'MoveNext'" - then: [T: "移動"] # phrase('move' to next entry in table) - - else_if: "$NavCommand = 'ReadNext'" - then: [T: "讀"] # phrase('read' next entry in table) - else: [T: "描述"] # phrase('describe' next entry in table) - - T: "往右" # phrase(move 'right') # phrase(move 'right') - - pause: short + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] - test: # if in base (nothing before), we must be moving to a script, so "in" will be said if: "preceding-sibling::* and following-sibling::*[1][name(.)='none']" @@ -1320,39 +1284,33 @@ - with: variables: [Move2D: "'離開'", Child2D: "."] replace: [x: ".."] - - with: - variables: [MatchCounter: "$MatchCounter + 1"] - replace: [x: "following-sibling::*[1]"] # skip over 'none' + - x: "following-sibling::*[1]" else: - with: - variables: [Move2D: "'在'", Child2D: "following-sibling::*[1]"] # phrase('in' the denominator) + variables: [Move2D: "'進入'", Child2D: "following-sibling::*[1]"] # phrase('in' the denominator) replace: [x: ".."] - with: - variables: [MatchCounter: "$MatchCounter + 1", NavCommand: "'MoveNextZoom'"] + variables: [NavCommand: "'MoveNextZoom'"] replace: [x: "following-sibling::*[1]"] - name: move-next-default tag: "*" match: "$NavCommand = 'MoveNext' or $NavCommand = 'ReadNext' or $NavCommand = 'DescribeNext'" replace: + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] - test: - if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'" - then: + if: "following-sibling::*[1][@data-from-mathml='none' or @data-from-mathml='mprescripts']" + then: [x: "following-sibling::*[1]"] + else: - test: - - if: "$NavCommand = 'MoveNext'" - then: [T: "移動"] # phrase('move' to next entry in table) - - else_if: "$NavCommand = 'ReadNext'" - then: [T: "讀"] # phrase('read' next entry in table) - else: [T: "描述"] # phrase('describe' next entry in table) - - T: "往右" # phrase(move 'right') # phrase(move 'right') - - pause: short - - test: - if: "IsNode(.., '2D')" - then: - - with: - variables: [Move2D: "'在'", Child2D: "following-sibling::*[1]"] # phrase('in' the denominator) - replace: [x: ".."] - - set_variables: [NavNode: "following-sibling::*[1]/@id"] + if: "IsNode(.., '2D') or not(IsNode(.., 'mathml'))" + then: + - with: + variables: [Move2D: "'進入'", Child2D: "following-sibling::*[1]"] # phrase('in' the denominator) + replace: [x: ".."] + - set_variables: [NavNode: "following-sibling::*[1]/@id"] # ======== Move/Read/Describe Previous rules ================= @@ -1363,38 +1321,31 @@ - "($NavCommand = 'MovePrevious' or $NavCommand = 'ReadPrevious' or $NavCommand = 'DescribePrevious') and" - "preceding-sibling::*[1][name(.)='mo' and translate(., '\u2061\u2062\u2063\u2064', '')='']" replace: - - test: - if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'" - then: - - test: - - if: "$NavCommand = 'MovePrevious'" - then: [T: "移動"] # phrase('move' to next entry in table) - - else_if: "$NavCommand = 'ReadPrevious'" - then: [T: "讀"] # phrase('read' next entry in table) - else: [T: "描述"] # phrase('describe' next entry in table) - - T: "往左" # phrase(move 'left') - - pause: short - with: - variables: [MatchCounter: "$MatchCounter + 1"] - replace: - - test: - if: "preceding-sibling::*[1][.='\u2062' or .='\u2064'] and $NavMode='Enhanced'" # invisible times and plus - then: [set_variables: [NavNode: "preceding-sibling::*[1]/@id"]] - else: [x: "preceding-sibling::*[1]"] + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] + - test: + if: "preceding-sibling::*[1][.='\u2062' or .='\u2064'] and $NavMode='Enhanced'" # invisible times and plus + then: [set_variables: [NavNode: "preceding-sibling::*[1]/@id"]] + else: [x: "preceding-sibling::*[1]"] -# two rules for when can't move right +# two rules for when can't move left - name: move-previous-no-auto-zoom-at-edge - # at edge of 2D and in a mode where moving right isn't an option + # at edge of 2D and in a mode where moving left isn't an option tag: "*" variables: [EdgeNode: "EdgeNode(., 'left', '2D')"] match: "$NavCommand = 'MovePrevious' and $NavMode!='Character' and not($AutoZoomOut) and $EdgeNode/@id!=@id" replace: - test: - if: "$MatchCounter = 0 and $NavVerbosity != 'Terse' and $NavCommand = 'MovePrevious'" + if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose' and $NavCommand = 'MovePrevious'" then: - - T: "無法向左移動" # phrase('cannot move left' in expression) + - T: "無法移到上一項" # phrase('cannot move left' in expression) + - pause: medium - with: - variables: [Move2D: "'離開'", Child2D: "$EdgeNode/*[1]"] + variables: + - Move2D: "'end of'" + - Child2D: "$EdgeNode/*[1]" + - MatchCounter: $MatchCounter + 1 replace: [x: "$EdgeNode"] - pause: long @@ -1405,7 +1356,7 @@ - "($NavCommand = 'MovePrevious' or $NavCommand = 'ReadPrevious' or $NavCommand = 'DescribePrevious') and" - "(self::m:math or name(EdgeNode(., 'left', 'math'))='math')" replace: - - T: "開始數學" # phrase('start of math') + - T: "數學開頭" # phrase('start of math') - pause: long - set_variables: [SpeakExpression: "'false'"] @@ -1416,10 +1367,14 @@ - "name(EdgeNode(., 'left', 'math'))='math'" # at edge of math replace: - test: - if: "$MatchCounter = 0 and $NavVerbosity != 'Terse'" + if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'" then: - - T: "無法向左移動,數學開始" # phrase('cannot move left, start of math') - - pause: long + - T: "無法移到上一項" # phrase('cannot move left') + - pause: short + - with: + variables: [Move2D: "'start of'", Child2D: "."] + replace: [x: "."] + - pause: long - set_variables: [SpeakExpression: "'false'"] - name: move-previous-auto-zoom-up-one-level @@ -1434,36 +1389,20 @@ - " )" - ")" replace: + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] - test: - if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'" + if: "preceding-sibling::*" then: - - test: - - if: "$NavCommand = 'MovePrevious'" - then: [T: "移動"] # phrase('move' to next entry in table) - - else_if: "$NavCommand = 'ReadPrevious'" - then: [T: "讀"] # phrase('read' next entry in table) - else: [T: "描述"] # phrase('describe' next entry in table) - - T: "往左" # phrase(move 'left') - - pause: short - - with: - variables: [MatchCounter: "$MatchCounter + 1", NavCommand: "'MovePrevious'"] - replace: - - test: - if: "preceding-sibling::*" - then: - - with: - variables: [Move2D: "'在'", Child2D: "."] # phrase('in' the denominator) - replace: [x: ".."] - else: - - with: - variables: [Move2D: "'離開'", Child2D: "."] - replace: [x: ".."] - - test: - if: "preceding-sibling::*" - then: - - x: .. # move out of parens - else: - - x: ".." + - with: + variables: [Move2D: "'進入'", Child2D: "."] # phrase('in' the denominator) + replace: [x: ".."] + else: + - with: + variables: [Move2D: "'離開'", Child2D: "."] + replace: [x: ".."] + - x: ".." - name: move-previous-auto-zoom-parens # auto-zoom into previous child if previous child is parenthesized expr @@ -1480,22 +1419,14 @@ - " (IsBracketed(., '(', ')') or IsBracketed(., '[', ']'))" - " ]" replace: - - test: - if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'" - then: - - test: - - if: "$NavCommand = 'MovePrevious'" - then: [T: "移動"] # phrase('move' to next entry in table) - - else_if: "$NavCommand = 'ReadPrevious'" - then: [T: "讀"] # phrase('read' next entry in table) - else: [T: "描述"] # phrase('describe' next entry in table) - - T: "往左" # phrase(move 'left') - - pause: short + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] - test: if: "not(parent::m:mrow)" then: - with: - variables: [Move2D: "'在'", Child2D: "preceding-sibling::*[1]"] # phrase('in' the denominator) + variables: [Move2D: "'進入'", Child2D: "preceding-sibling::*[1]"] # phrase('in' the denominator) replace: [x: ".."] - set_variables: [NavNode: "preceding-sibling::*[1]/*[2]/@id"] @@ -1503,44 +1434,37 @@ - name: move-previous-default tag: mtd - match: "$Move2D = '' and ($NavCommand = 'MovePrevious' or $NavCommand = 'ReadPrevious' or $NavCommand = 'DescribePrevious')" + match: "$Move2D = '' and + ($NavCommand = 'MovePrevious' or $NavCommand = 'ReadPrevious' or $NavCommand = 'DescribePrevious') and + preceding-sibling::*" replace: + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] - test: - # can't get here with MatchCounter=0, so no need to echo command - if: "preceding-sibling::*" + if: "$NavVerbosity = 'Verbose'" then: - - test: - if: "$NavVerbosity != 'Terse'" - then: - - T: "行" # phrase(the first 'column' in the table) - - x: "count(preceding-sibling::*)" - - pause: short - - test: - if: "$NavMode = 'Character'" - then: - - with: - variables: [NavCommand: "'MovePreviousZoom'"] - replace: [x: "preceding-sibling::*[1]"] - else: [set_variables: [NavNode: "preceding-sibling::*[1]/*[last()]/@id"]] - else: - - x: ".." # try again at the row level + - T: "行" # phrase(the first 'column' in the table) + - x: "count(preceding-sibling::*)" + - pause: short + - test: + if: "$NavMode = 'Character'" + then: + - with: + variables: [NavCommand: "'MovePreviousZoom'"] + replace: [x: "preceding-sibling::*[1]"] + else: [set_variables: [NavNode: "preceding-sibling::*[1]/*[last()]/@id"]] - name: move-previous-default tag: [mtr, mlabeledtr] match: "$Move2D = '' and ($NavCommand = 'MovePrevious' or $NavCommand = 'ReadPrevious' or $NavCommand = 'DescribePrevious')" replace: + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] - test: - # can't get here with MatchCounter=0, so no need to echo command if: "preceding-sibling::*" then: - - test: - if: "$NavVerbosity != 'Terse'" - then: - - T: "列" # phrase('row' five in table) - - x: "count(preceding-sibling::*)" - - T: "行" # phrase('column' five in table) - - x: "count(*)" - - pause: medium - test: if: "$NavMode = 'Character'" then: @@ -1548,7 +1472,7 @@ variables: [NavCommand: "'MovePreviousZoom'"] replace: [x: "preceding-sibling::*[1]"] else: - - set_variables: [NavNode: "preceding-sibling::*[1]/*[last()]/*[last()]/@id"] + - set_variables: [NavNode: "preceding-sibling::*[1]/@id"] else: [x: ".."] # try again for after - name: move-previous-locked-zoom-level @@ -1558,6 +1482,9 @@ - "($NavCommand = 'MovePrevious' or $NavCommand = 'ReadPrevious' or $NavCommand = 'DescribePrevious') and" - "$ReadZoomLevel>=0" replace: + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] - test: # if moving into base (nothing before), we must be moving to the base, so "in" will be said if: "count(preceding-sibling::*) > 2 and preceding-sibling::*[1][name(.)='none']" @@ -1565,50 +1492,33 @@ - with: variables: [Move2D: "'離開'", Child2D: "."] replace: [x: ".."] - - with: - variables: [MatchCounter: "$MatchCounter + 1"] - replace: [x: "preceding-sibling::*[1]"] # skip over 'none' + - x: "preceding-sibling::*[1]" # skip over 'none' else: - - test: - if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'" - then: - - test: - - if: "$NavCommand = 'MovePrevious'" - then: [T: "移動"] # phrase('move' to next entry in table) - - else_if: "$NavCommand = 'ReadPrevious'" - then: [T: "讀"] # phrase('read' next entry in table) - else: [T: "描述"] # phrase('describe' next entry in table) - - T: "往左" # phrase(move 'left') - - pause: short - with: - variables: [Move2D: "'在'", Child2D: "preceding-sibling::*[1]"] # phrase('in' the denominator) + variables: [Move2D: "'進入'", Child2D: "preceding-sibling::*[1]"] # phrase('in' the denominator) replace: [x: ".."] - with: - variables: [MatchCounter: "$MatchCounter + 1", NavCommand: "'MovePreviousZoom'"] + variables: [NavCommand: "'MovePreviousZoom'"] replace: [x: "preceding-sibling::*[1]"] - name: move-previous-default tag: "*" match: "$NavCommand = 'MovePrevious' or $NavCommand = 'ReadPrevious' or $NavCommand = 'DescribePrevious'" replace: + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] - test: - if: "$MatchCounter = 0 and $NavVerbosity = 'Verbose'" - then: + if: "preceding-sibling::*[1][@data-from-mathml='none' or @data-from-mathml='mprescripts']" + then: [x: "preceding-sibling::*[1]"] + else: - test: - - if: "$NavCommand = 'MovePrevious'" - then: [T: "移動"] # phrase('move' to next entry in table) - - else_if: "$NavCommand = 'ReadPrevious'" - then: [T: "讀"] # phrase('read' next entry in table) - else: [T: "描述"] # phrase('describe' next entry in table) - - T: "往左" # phrase(move 'left') - - pause: short - - test: - if: "IsNode(.., '2D')" - then: - - with: - variables: [Move2D: "'在'", Child2D: "preceding-sibling::*[1]"] # phrase('in' the denominator) - replace: [x: ".."] - - set_variables: [NavNode: "preceding-sibling::*[1]/@id"] + if: "IsNode(.., '2D') or not(IsNode(.., 'mathml'))" + then: + - with: + variables: [Move2D: "'進入'", Child2D: "preceding-sibling::*[1]"] # phrase('in' the denominator) + replace: [x: ".."] + - set_variables: [NavNode: "preceding-sibling::*[1]/@id"] # ********* ReadZoomLevel toggle *************** # These set ::NavMode @@ -1624,7 +1534,7 @@ - set_variables: [NavMode: "'Character'", ReadZoomLevel: "1"] - else_if: "$NavMode = 'Character'" then: - - T: "簡單" # phrase(a 'simple' way to do something) + - T: "簡易" # phrase(a 'simple' way to do something) - set_variables: [NavMode: "'Simple'", ReadZoomLevel: "1"] - else: - T: "增強" # phrase(an 'enhanced' way to do something) @@ -1645,7 +1555,7 @@ - test: - if: "$NavMode = 'Enhanced'" then: - - T: "簡單" # phrase(an 'simple' way to do something) + - T: "簡易" # phrase(a 'simple' way to do something) - set_variables: [NavMode: "'Simple'", ReadZoomLevel: "1"] - else_if: "$NavMode = 'Character'" then: @@ -1670,12 +1580,13 @@ - test: if: "$Overview = 'true'" then: - - T: "移動後唸出式子" # phrase('speak expression after move') + - T: "讀出式子" # phrase('speak expression after move') + - pause: long - set_variables: [Overview: "'false'"] else: - - T: "移動後概述式子" # phrase('overview of expression after move') + - T: "描述式子" # phrase('overview of expression after move') + - pause: long - set_variables: [Overview: "'true'"] - - pause: long - name: current tag: "*" @@ -1686,11 +1597,11 @@ then: - test: - if: "$NavCommand = 'ReadCurrent'" - then: [T: "讀"] # phrase('read' next entry in table) + then: [T: "讀出"] # phrase('read' next entry in table) else: [T: "描述"] # phrase('describe' next entry in table) - - T: "當前的" # phrase('current' entry in table) + - T: "目前" # phrase('current' entry in table) + - pause: long - set_variables: [NavNode: "@id"] - - pause: long # this needs to be near the end because we only test for 'Describe', "Read", etc., and we don't want to get 'DescribeNext', etc. - name: placemarker @@ -1706,13 +1617,13 @@ then: - test: - if: "starts-with($NavCommand, 'Read')" - then: [T: "讀"] # phrase('read' next entry in table) + then: [T: "讀出"] # phrase('read' next entry in table) - else_if: "starts-with($NavCommand, 'Describe')" then: [T: "描述"] # phrase('describe' next entry in table) - else_if: "starts-with($NavCommand, 'MoveTo')" then: [T: "移到"] # phrase('move to' the next entry in table) else: [T: "設定"] # phrase('set' the value of the next entry in table) - - T: "佔位符" # phrase('placeholder' for the value) + - T: "書籤" # phrase('placeholder' for the value) - x: "$PlaceMarkerIndex" - pause: long - set_variables: [NavNode: "$PlaceMarker"] @@ -1724,7 +1635,7 @@ - test: if: "$NavVerbosity != 'Terse'" then: - - T: "設定佔位符" # phrase('set placeholder' to the value) + - T: "設定書籤" # phrase('set placeholder' to the value) - x: "$PlaceMarkerIndex" - pause: long - set_variables: [NavNode: "@id"] @@ -1735,8 +1646,6 @@ # Alternatively, it could insert a special token that Rust code does a "replace" on with the speech (e.g. SPEECH_AT{id}) # or a new command "speak" which takes a node id - name: where-am-i-start - - tag: "*" match: "($NavCommand = 'WhereAmI' or $NavCommand = 'WhereAmIAll') and $MatchCounter = 0" replace: @@ -1758,11 +1667,11 @@ - test: if: "$NavCommand = 'WhereAmI'" then: - - T: "內部沒東西" # phrase('inside of nothing more') + - T: "不在別的式子裡面" # phrase('inside of nothing more') - pause: long - set_variables: [SpeakExpression: "'false'"] else: - - T: "裡面" # phrase('inside' a big expression) + - T: "在後面式子裡面" # phrase('inside' a big expression) - pause: medium - set_variables: [NavNode: "@id"] @@ -1770,10 +1679,10 @@ tag: "*" match: "$NavCommand = 'WhereAmI' or $NavCommand = 'WhereAmIAll'" replace: - - T: "裡面" # phrase('inside' a big expression) + - T: "在後面式子裡面" # phrase('inside' a big expression) - pause: medium - test: - - if: "$NavMode='Enhanced' and parent::*[self::m:mrow and IsBracketed(., '(', ')', false) or IsBracketed(., '[', ']', false)]" + - if: "$NavMode='Enhanced' and parent::*[self::m:mrow and (IsBracketed(., '(', ')', false) or IsBracketed(., '[', ']', false))]" then: [x: ".."] # auto-zoom up - else_if: "$NavCommand = 'WhereAmI'" then: [set_variables: [NavNode: "@id"]] diff --git a/Rules/Languages/zh/tw/overview.yaml b/Rules/Languages/zh/tw/overview.yaml index 78bdcd0eb..240fb45c6 100644 --- a/Rules/Languages/zh/tw/overview.yaml +++ b/Rules/Languages/zh/tw/overview.yaml @@ -44,7 +44,7 @@ tag: mroot match: "." replace: - - T: "根號" + - T: "" - x: "*[1]" - test: if: "IsNode(*[1], 'simple')" @@ -94,7 +94,7 @@ - x: count(*[2]/*) - T: "乘" - x: count(*[2]/*[self::m:mtr][1]/*) - - T: "表" + - T: "表格" - name: short-mrow tag: mrow diff --git a/Rules/Languages/zh/tw/unicode-full.yaml b/Rules/Languages/zh/tw/unicode-full.yaml index 7e930e016..9e4e85087 100644 --- a/Rules/Languages/zh/tw/unicode-full.yaml +++ b/Rules/Languages/zh/tw/unicode-full.yaml @@ -2446,116 +2446,116 @@ - "㊽": [T: "圈圈內48"] # 0x32bd (en: 'circled number forty eight', GPT-5.4 translation) - "㊾": [T: "圈圈內49"] # 0x32be (en: 'circled number forty nine', GPT-5.4 translation) - "㊿": [T: "圈圈內50"] # 0x32bf (en: 'circled number fifty', GPT-5.4 translation) - - "㋌": [T: "汞"] # 0x32cc (en: 'mercury', google translation) - - "㋍": [t: "ergs"] # 0x32cd (google translation) + - "㋌": [T: "汞柱"] # 0x32cc (en: 'mercury', google translation) + - "㋍": [T: "耳格"] # 0x32cd (google translation) - "㋎": [T: "電子伏特"] # 0x32ce (en: 'electron volts', google translation) - "㋏": [T: "有限責任標誌"] # 0x32cf (en: 'limited liability sign', google translation) - - "㍱": [t: "海毛"] # 0x3371 (en: 'hectopascals', google translation) - - "㍲": [t: "達爾頓"] # 0x3372 (en: 'daltons', google translation) + - "㍱": [T: "百帕"] # 0x3371 (en: 'hectopascals', google translation) + - "㍲": [T: "道爾頓"] # 0x3372 (en: 'daltons', google translation) - "㍳": [T: "天文單位"] # 0x3373 (en: 'astronomical units', google translation) - - "㍴": [t: "酒吧"] # 0x3374 (en: 'bars', google translation) + - "㍴": [T: "巴"] # 0x3374 (en: 'bars', google translation) - "㍵": [t: "o v"] # 0x3375 (google translation) - - "㍶": [t: "parsecs"] # 0x3376 (google translation) - - "㍷": [T: "公寸"] # 0x3377 (en: 'decimeters', google translation) - - "㍸": [T: "公寸平方"] # 0x3378 (en: 'decimeters squared', google translation) - - "㍹": [T: "公寸立方"] # 0x3379 (en: 'decimeters cubed', google translation) + - "㍶": [T: "秒差距"] # 0x3376 (google translation) + - "㍷": [T: "分米"] # 0x3377 (en: 'decimeters', google translation) + - "㍸": [T: "平方分米"] # 0x3378 (en: 'decimeters squared', google translation) + - "㍹": [T: "立方分米"] # 0x3379 (en: 'decimeters cubed', google translation) - "㍺": [t: "樂器單元"] # 0x337a (en: 'instrumental units', google translation) - - "㎀": [t: "picoamps"] # 0x3380 (google translation) - - "㎁": [t: "nanoamps"] # 0x3381 (en: 'nanoamps', google translation) - - "㎂": [t: "microamps"] # 0x3382 (en: 'microamps', google translation) - - "㎃": [t: "milliamps"] # 0x3383 (en: 'milliamps', google translation) - - "㎄": [T: "kiloamps"] # 0x3384 (en: 'kiloamps', google translation) - - "㎅": [T: "千位元組"] # 0x3385 (en: 'kilobytes', google translation) - - "㎆": [T: "百萬位元組"] # 0x3386 (en: 'megabytes', google translation) - - "㎇": [T: "十億位元組"] # 0x3387 (en: 'gigabytes', google translation) - - "㎈": [T: "卡路里"] # 0x3388 (en: 'calories', google translation) - - "㎉": [T: "千卡"] # 0x3389 (en: 'kilocalories', google translation) - - "㎊": [t: "picofarads"] # 0x338a (google translation) - - "㎋": [t: "nanofarads"] # 0x338b (en: 'nanofarads', google translation) - - "㎌": [t: "microfarads"] # 0x338c (en: 'microfarads', google translation) + - "㎀": [t: "皮安培"] # 0x3380 (google translation) + - "㎁": [t: "奈安培"] # 0x3381 (en: 'nanoamps', google translation) + - "㎂": [t: "微安培"] # 0x3382 (en: 'microamps', google translation) + - "㎃": [t: "毫安培"] # 0x3383 (en: 'milliamps', google translation) + - "㎄": [T: "千安培"] # 0x3384 (en: 'kiloamps', google translation) + - "㎅": [T: "KB"] # 0x3385 (en: 'kilobytes', google translation) + - "㎆": [T: "MB"] # 0x3386 (en: 'megabytes', google translation) + - "㎇": [T: "GB"] # 0x3387 (en: 'gigabytes', google translation) + - "㎈": [T: "卡"] # 0x3388 (en: 'calories', google translation) + - "㎉": [T: "大卡"] # 0x3389 (en: 'kilocalories', google translation) + - "㎊": [t: "皮法拉"] # 0x338a (google translation) + - "㎋": [t: "奈法拉"] # 0x338b (en: 'nanofarads', google translation) + - "㎌": [t: "微法拉"] # 0x338c (en: 'microfarads', google translation) - "㎍": [T: "微克"] # 0x338d (en: 'micrograms', google translation) - "㎎": [T: "毫克"] # 0x338e (en: 'milligrams', google translation) - "㎏": [T: "公斤"] # 0x338f (en: 'kilograms', google translation) - "㎐": [T: "赫茲"] # 0x3390 (en: 'hertz', google translation) - "㎑": [T: "千赫"] # 0x3391 (en: 'kilohertz', google translation) - "㎒": [T: "百萬赫"] # 0x3392 (en: 'megahertz', google translation) - - "㎓": [T: "十億赫"] # 0x3393 (en: 'gigahertz', google translation) - - "㎔": [t: "terahertz"] # 0x3394 (google translation) + - "㎓": [T: "吉赫"] # 0x3393 (en: 'gigahertz', google translation) + - "㎔": [t: "兆赫"] # 0x3394 (google translation) - "㎕": [T: "微升"] # 0x3395 (en: 'microliters', google translation) - "㎖": [T: "毫升"] # 0x3396 (en: 'milliliters', google translation) - - "㎗": [t: "deciliters"] # 0x3397 (en: 'deciliters', google translation) - - "㎘": [T: "千升"] # 0x3398 (en: 'kiloliters', google translation) - - "㎙": [t: "femtometers"] # 0x3399 (google translation) + - "㎗": [t: "分升"] # 0x3397 (en: 'deciliters', google translation) + - "㎘": [T: "公秉"] # 0x3398 (en: 'kiloliters', google translation) + - "㎙": [t: "飛米"] # 0x3399 (google translation) - "㎚": [T: "奈米"] # 0x339a (en: 'nanometers', google translation) - "㎛": [T: "微米"] # 0x339b (en: 'micrometers', google translation) - "㎜": [T: "毫米"] # 0x339c (en: 'millimeters', google translation) - - "㎝": [T: "厘米"] # 0x339d (en: 'centimeters', google translation) + - "㎝": [T: "公分"] # 0x339d (en: 'centimeters', google translation) - "㎞": [T: "公里"] # 0x339e (en: 'kilometers', google translation) - - "㎟": [T: "毫米平方"] # 0x339f (en: 'millimeters squared', google translation) - - "㎠": [T: "厘米平方"] # 0x33a0 (en: 'centimeters squared', google translation) - - "㎡": [T: "米平方"] # 0x33a1 (en: 'meters squared', google translation) - - "㎢": [T: "公里平方"] # 0x33a2 (en: 'kilometers squared', google translation) - - "㎣": [T: "毫米立方"] # 0x33a3 (en: 'millimeters cubed', google translation) - - "㎤": [T: "厘米立方"] # 0x33a4 (en: 'centimeters cubed', google translation) - - "㎥": [T: "米立方"] # 0x33a5 (en: 'meters cubed', google translation) - - "㎦": [T: "公里立方"] # 0x33a6 (en: 'kilometers cubed', google translation) - - "㎧": [T: "每秒米"] # 0x33a7 (en: 'meters per second', google translation) - - "㎨": [T: "每秒平方米"] # 0x33a8 (en: 'meters per second squared', google translation) - - "㎩": [t: "pascals"] # 0x33a9 (en: 'pascals', google translation) - - "㎪": [t: "kilopascals"] # 0x33aa (google translation) - - "㎫": [t: "megapascals"] # 0x33ab (en: 'megapascals', google translation) - - "㎬": [t: "gigapascals"] # 0x33ac (google translation) + - "㎟": [T: "平方毫米"] # 0x339f (en: 'millimeters squared', google translation) + - "㎠": [T: "平方公分"] # 0x33a0 (en: 'centimeters squared', google translation) + - "㎡": [T: "平方公尺"] # 0x33a1 (en: 'meters squared', google translation) + - "㎢": [T: "平方公里"] # 0x33a2 (en: 'kilometers squared', google translation) + - "㎣": [T: "立方毫米"] # 0x33a3 (en: 'millimeters cubed', google translation) + - "㎤": [T: "立方公分"] # 0x33a4 (en: 'centimeters cubed', google translation) + - "㎥": [T: "立方公尺"] # 0x33a5 (en: 'meters cubed', google translation) + - "㎦": [T: "立方公里"] # 0x33a6 (en: 'kilometers cubed', google translation) + - "㎧": [T: "公尺/秒"] # 0x33a7 (en: 'meters per second', google translation) + - "㎨": [T: "公尺/平方秒"] # 0x33a8 (en: 'meters per second squared', google translation) + - "㎩": [t: "帕"] # 0x33a9 (en: 'pascals', google translation) + - "㎪": [t: "千帕"] # 0x33aa (google translation) + - "㎫": [t: "百萬帕"] # 0x33ab (en: 'megapascals', google translation) + - "㎬": [t: "吉帕"] # 0x33ac (google translation) - "㎭": [T: "弳"] # 0x33ad (en: 'rads', google translation) - - "㎮": [T: "每秒弳"] # 0x33ae (en: 'rads per second', google translation) - - "㎯": [T: "每秒平方弳"] # 0x33af (en: 'rads per second squared', google translation) - - "㎰": [t: "picseconds"] # 0x33b0 (en: 'picoseconds', google translation) - - "㎱": [t: "nanoseconds"] # 0x33b1 (en: 'nanoseconds', google translation) + - "㎮": [T: "弳/秒"] # 0x33ae (en: 'rads per second', google translation) + - "㎯": [T: "弳/平方秒"] # 0x33af (en: 'rads per second squared', google translation) + - "㎰": [t: "皮秒"] # 0x33b0 (en: 'picoseconds', google translation) + - "㎱": [t: "奈秒"] # 0x33b1 (en: 'nanoseconds', google translation) - "㎲": [T: "微秒"] # 0x33b2 (en: 'microseconds', google translation) - "㎳": [T: "毫秒"] # 0x33b3 (en: 'milliseconds', google translation) - - "㎴": [t: "picovolts"] # 0x33b4 (google translation) - - "㎵": [t: "nanovolts"] # 0x33b5 (en: 'nanovolts', google translation) + - "㎴": [t: "皮伏特"] # 0x33b4 (google translation) + - "㎵": [t: "奈伏特"] # 0x33b5 (en: 'nanovolts', google translation) - "㎶": [T: "微伏特"] # 0x33b6 (en: 'microvolts', google translation) - "㎷": [T: "毫伏特"] # 0x33b7 (en: 'millivolts', google translation) - "㎸": [T: "千伏特"] # 0x33b8 (en: 'kilovolts', google translation) - - "㎹": [t: "megavolts"] # 0x33b9 (google translation) - - "㎺": [t: "picowatts"] # 0x33ba (google translation) - - "㎻": [t: "nanowatts"] # 0x33bb (en: 'nanowatts', google translation) - - "㎼": [t: "microwatts"] # 0x33bc (google translation) + - "㎹": [t: "百萬伏特"] # 0x33b9 (google translation) + - "㎺": [t: "皮瓦"] # 0x33ba (google translation) + - "㎻": [t: "奈瓦"] # 0x33bb (en: 'nanowatts', google translation) + - "㎼": [t: "微瓦"] # 0x33bc (google translation) - "㎽": [T: "毫瓦"] # 0x33bd (en: 'milliwatts', google translation) - "㎾": [T: "千瓦"] # 0x33be (en: 'kilowatts', google translation) - "㎿": [T: "百萬瓦"] # 0x33bf (en: 'megawatts', google translation) - "㏀": [T: "千歐姆"] # 0x33c0 (en: 'kilo-ohms', google translation) - "㏁": [T: "百萬歐姆"] # 0x33c1 (google translation) - "㏂": [t: "attometers"] # 0x33c2 (google translation) - - "㏃": [t: "becquerels"] # 0x33c3 (en: 'becquerels', google translation) + - "㏃": [t: "貝克"] # 0x33c3 (en: 'becquerels', google translation) - "㏄": [T: "cc"] # 0x33c4 (en: 'cubic centimeters', google translation) - - "㏅": [t: "candelas"] # 0x33c5 (en: 'candelas', google translation) - - "㏆": [t: "coulombs per kilogram"] # 0x33c6 (en: 'coulombs per kilogram', google translation) + - "㏅": [t: "燭光"] # 0x33c5 (en: 'candelas', google translation) + - "㏆": [t: "庫侖/公斤"] # 0x33c6 (en: 'coulombs per kilogram', google translation) - "㏇": [t: "cardiac output"] # 0x33c7 (en: 'cardiac output', google translation) - "㏈": [T: "分貝"] # 0x33c8 (en: 'decibels', google translation) - - "㏉": [t: "grays"] # 0x33c9 (en: 'grays', google translation) - - "㏊": [t: "hectares"] # 0x33ca (en: 'hectares', google translation) + - "㏉": [t: "戈雷"] # 0x33c9 (en: 'grays', google translation) + - "㏊": [t: "公頃"] # 0x33ca (en: 'hectares', google translation) - "㏋": [T: "馬力"] # 0x33cb (en: 'horsepower', google translation) - "㏌": [T: "英寸"] # 0x33cc (en: 'inches', google translation) - "㏍": [t: "kilokelvins"] # 0x33cd (google translation) - "㏎": [T: "公里"] # 0x33ce (en: 'kilometers', google translation) - - "㏏": [t: "結"] # 0x33cf (en: 'knots', google translation) + - "㏏": [t: "節"] # 0x33cf (en: 'knots', google translation) - "㏐": [T: "流明"] # 0x33d0 (en: 'lumens', google translation) - "㏑": [T: "自然對數"] # 0x33d1 (en: 'natural log', google translation) - "㏒": [T: "對數"] # 0x33d2 (en: 'logarithm', google translation) - "㏓": [t: "勒克斯"] # 0x33d3 (en: 'lux', google translation) - - "㏔": [t: "millibarns"] # 0x33d4 (google translation) - - "㏕": [t: "mills"] # 0x33d5 (en: 'mills', google translation) - - "㏖": [t: "moles"] # 0x33d6 (en: 'moles', google translation) - - "㏗": [T: "p h"] # 0x33d7 (google translation) + - "㏔": [t: "毫邦"] # 0x33d4 (google translation) + - "㏕": [t: "密耳"] # 0x33d5 (en: 'mills', google translation) + - "㏖": [t: "莫耳"] # 0x33d6 (en: 'moles', google translation) + - "㏗": [T: "pH"] # 0x33d7 (google translation) - "㏘": [t: "picometers"] # 0x33d8 (en: 'picometers', google translation) - - "㏙": [T: "p p m"] # 0x33d9 (en: 'parts per million', google translation) - - "㏚": [t: "petaroentgens"] # 0x33da (google translation) - - "㏛": [t: "steradians"] # 0x33db (en: 'steradians', google translation) - - "㏜": [t: "sieverts"] # 0x33dc (google translation) - - "㏝": [t: "webers"] # 0x33dd (en: 'webers', google translation) - - "㏞": [T: "每米伏"] # 0x33de (en: 'volts per meter', google translation) - - "㏟": [T: "每米安"] # 0x33df (en: 'amps per meter', google translation) + - "㏙": [T: "PPM"] # 0x33d9 (en: 'parts per million', google translation) + - "㏚": [t: "拍侖琴"] # 0x33da (google translation) + - "㏛": [t: "立弳"] # 0x33db (en: 'steradians', google translation) + - "㏜": [t: "西弗"] # 0x33dc (google translation) + - "㏝": [t: "韋伯"] # 0x33dd (en: 'webers', google translation) + - "㏞": [T: "伏特/公尺"] # 0x33de (en: 'volts per meter', google translation) + - "㏟": [T: "安培/公尺"] # 0x33df (en: 'amps per meter', google translation) - "㏿": [T: "加侖"] # 0x33ff (en: 'gallons', google translation) - "": [t: "等於下面的帽子"] # 0xe900 (en: 'equals with hat below', google translation) - "": [t: "等於上面"] # 0xe901 (en: 'equals with plus above', google translation) diff --git a/tests/Languages/zh/tw/SimpleSpeak/geometry.rs b/tests/Languages/zh/tw/SimpleSpeak/geometry.rs index 20235702b..4a9b4c72a 100644 --- a/tests/Languages/zh/tw/SimpleSpeak/geometry.rs +++ b/tests/Languages/zh/tw/SimpleSpeak/geometry.rs @@ -30,7 +30,7 @@ fn arc_mtext() -> Result<()> { #[test] fn ray_mtext() -> Result<()> { let expr = " XY "; - test("zh-tw", "SimpleSpeak", expr, "向量 大寫 x 大寫 y")?; + test("zh-tw", "SimpleSpeak", expr, "射線 大寫 x 大寫 y")?; return Ok(()); } diff --git a/tests/Languages/zh/tw/SimpleSpeak/mroot.rs b/tests/Languages/zh/tw/SimpleSpeak/mroot.rs index c28fd8e71..57cf39c59 100644 --- a/tests/Languages/zh/tw/SimpleSpeak/mroot.rs +++ b/tests/Languages/zh/tw/SimpleSpeak/mroot.rs @@ -48,7 +48,7 @@ fn cube_root() -> Result<()> { let expr = " x 3 "; - test("zh-tw", "SimpleSpeak", expr, "根號 x 的 立方根")?; + test("zh-tw", "SimpleSpeak", expr, "x 的 立方根")?; return Ok(()); } @@ -58,7 +58,7 @@ fn ordinal_root() -> Result<()> { let expr = " x 9 "; - test("zh-tw", "SimpleSpeak", expr, "根號 x 的 9 次方根")?; + test("zh-tw", "SimpleSpeak", expr, "x 的 9 次方根")?; return Ok(()); } @@ -67,7 +67,7 @@ fn ordinal_root_2() -> Result<()> { let expr = " x 9.1 "; - test("zh-tw", "SimpleSpeak", expr, "根號 x 的 9.1 次方根")?; + test("zh-tw", "SimpleSpeak", expr, "x 的 9.1 次方根")?; return Ok(()); } @@ -77,7 +77,7 @@ fn simple_mi_root() -> Result<()> { let expr = " x n "; - test("zh-tw", "SimpleSpeak", expr, "根號 x 的 n 次方根")?; + test("zh-tw", "SimpleSpeak", expr, "x 的 n 次方根")?; return Ok(()); } @@ -91,7 +91,7 @@ fn simple_fraction_power() -> Result<()> { 13 "; - test("zh-tw", "SimpleSpeak", expr, "根號 x 的 3 分之 1 次方根")?; + test("zh-tw", "SimpleSpeak", expr, "x 的 3 分之 1 次方根")?; return Ok(()); } From 939be4ac931e611e8b57cc23a7f4642efa9401ac Mon Sep 17 00:00:00 2001 From: nsoiffer Date: Fri, 12 Jun 2026 23:18:31 +0200 Subject: [PATCH 22/37] Internally in definitions.yaml, MathCAT uses "nofix", but that's not a MathML airity property. Remove it. --- src/infer_intent.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/infer_intent.rs b/src/infer_intent.rs index 01f1d4ef8..0e481b836 100644 --- a/src/infer_intent.rs +++ b/src/infer_intent.rs @@ -102,9 +102,11 @@ fn add_fixity(intent: Element) { // debug!(" add_fixity: intent_name: {}, ", intent_name); if let Some(definition) = definitions.get_hashmap("IntentMappings").unwrap().get(intent_name) && let Some((fixity, _)) = definition.split_once("=") { - let new_properties = (if properties.is_empty() {":"} else {properties}).to_string() + fixity + ":"; - intent.set_attribute_value(INTENT_PROPERTY, &new_properties); - // debug!("Added fixity: new value '{}'", intent.attribute_value(INTENT_PROPERTY).unwrap()); + if fixity != "nofix" { + let new_properties = (if properties.is_empty() {":"} else {properties}).to_string() + fixity + ":"; + intent.set_attribute_value(INTENT_PROPERTY, &new_properties); + // debug!("Added fixity: new value '{}'", intent.attribute_value(INTENT_PROPERTY).unwrap()); + } }; }); } @@ -140,6 +142,7 @@ pub fn add_fixity_children(intent: Element) -> Element { let properties = mathml.attribute_value(INTENT_PROPERTY).unwrap_or_default(); let fixity = properties.rsplit(':').find(|&property| FIXITIES.contains(property)).unwrap_or_default(); let intent_name = name(mathml); + // debug!("add_fixity_child: fixity '{}', intent_name '{}'", fixity, intent_name); let op_name_id = mathml.attribute_value("id").unwrap_or("new-id"); match fixity { From 6dd4f6c39022ae635685a12c6b4fe9eea7a65e67 Mon Sep 17 00:00:00 2001 From: nsoiffer Date: Fri, 12 Jun 2026 23:20:58 +0200 Subject: [PATCH 23/37] don't say "indexed by", say "sub" --- Rules/Languages/en/definitions.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Rules/Languages/en/definitions.yaml b/Rules/Languages/en/definitions.yaml index dfd707341..6800e3122 100644 --- a/Rules/Languages/en/definitions.yaml +++ b/Rules/Languages/en/definitions.yaml @@ -169,8 +169,8 @@ "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, test converts "and" to "comma," - #"indexed-by": "infix= ; sub; end sub: end sub: end subscript", - "indexed-by": "infix= ; indexed by; ", + "indexed-by": "infix= ; sub; end sub: end sub: end subscript", + # "indexed-by": "infix= ; indexed by; ", "highlight":"postfix=highlighted", "least-common-denominator":"function=least common denominator", From 82a1cb49a9d42ff8c3aae2159d8a53a3d0abe57d Mon Sep 17 00:00:00 2001 From: nsoiffer Date: Sat, 13 Jun 2026 10:02:41 +0200 Subject: [PATCH 24/37] add some terse and verbose forms of speech --- Rules/Languages/en/definitions.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Rules/Languages/en/definitions.yaml b/Rules/Languages/en/definitions.yaml index 6800e3122..0945f9395 100644 --- a/Rules/Languages/en/definitions.yaml +++ b/Rules/Languages/en/definitions.yaml @@ -60,7 +60,7 @@ # "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", + "limit": "prefix=limit as:the limit as", "tends-to":"infix=tends to", "tends-to-from-above":"infix=tends to from above", "tends-to-from-below":"infix=tends to from below", @@ -192,8 +192,8 @@ ### Other ## Default fixity function "curl": "function=curl", - "divergence": "function=divergence", - "gradient": "function=gradient", + "divergence": "function=div:divergence", + "gradient": "function=del:gradient", "laplacian": "function=laplacian", ## Default fixity prefix @@ -312,7 +312,7 @@ # Names of functions that in terse mode don't say "of" (or it's equivalent in other languages) - TerseFunctionNames: { - "divergence", "gradient", "curl" + "divergence", "del:gradient", "curl" } - NavigationParts: { From 33b88ec59a761c5e426687261b109d2df6bf7a98 Mon Sep 17 00:00:00 2001 From: nsoiffer Date: Sat, 13 Jun 2026 10:03:17 +0200 Subject: [PATCH 25/37] fix typo --- tests/Languages/en/shared.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Languages/en/shared.rs b/tests/Languages/en/shared.rs index c9f98914c..ee42fc3be 100644 --- a/tests/Languages/en/shared.rs +++ b/tests/Languages/en/shared.rs @@ -449,7 +449,7 @@ fn mn_with_block_and_decimal_separators() -> Result<()> { #[test] fn divergence() -> Result<()> { let expr = "·F"; // may want to change this for another language - test_prefs("en", "SimpleSpeak", vec![("Verbosity", "Terse")], expr, "dihv cap f")?; + test_prefs("en", "SimpleSpeak", vec![("Verbosity", "Terse")], expr, "div cap f")?; test_prefs("en", "SimpleSpeak", vec![("Verbosity", "Verbose")], expr, "divergence of cap f")?; return Ok(()); From d96d6cd7bd920f50141fb14c251ab5373442bebf Mon Sep 17 00:00:00 2001 From: "U-rainshowers\\neils" Date: Sun, 14 Jun 2026 13:37:33 -0700 Subject: [PATCH 26/37] Added an html file with all the test exprs so that translators have examples to test. --- docs/test-exprs.html | 3727 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3727 insertions(+) create mode 100644 docs/test-exprs.html diff --git a/docs/test-exprs.html b/docs/test-exprs.html new file mode 100644 index 000000000..a89a04901 --- /dev/null +++ b/docs/test-exprs.html @@ -0,0 +1,3727 @@ + + + + + + MathCAT English Test Expressions + + + +

MathCAT English Test Expressions

+

MathML expressions from tests/Languages/en (excluding ClearSpeak).

+ + + +
+

alphabets

+
+ , +
+
+ , +
+
+ , +
+
+ Α,Ω +
+
+ α,ω +
+
+ , +
+
+ α,ω +
+
+ А,Я +
+
+ , +
+
+ , +
+
+ 🅐,🅩 +
+
+ , +
+
+ 𝔄,𝔜 +
+
+ 𝔞,𝔷 +
+
+ , +
+
+ , +
+
+ 𝕬,𝖅 +
+
+ 𝖆,𝖟 +
+
+ , +
+
+ , +
+
+ 𝔸,𝕐 +
+
+ 𝕒,𝕫 +
+
+ 𝟘,𝟡 +
+
+ , +
+
+ , +
+
+ , +
+
+ 𝒜,𝒵 +
+
+ 𝒶,𝓏 +
+
+ , +
+
+ , +
+
+ 𝓐,𝓩 +
+
+ 𝓪,𝔃 +
+
+ , +
+
+ , +
+
+ 𝐀,𝐙 +
+
+ 𝐚,𝐳 +
+
+ , +
+
+ , +
+
+ 𝐴,𝑍 +
+
+ 𝑎,𝑧 +
+
+ , +
+
+ , +
+
+ 𝖠,𝖹 +
+
+ 𝖺,𝗓 +
+
+ , +
+
+ , +
+
+ 𝗔,𝗭 +
+
+ 𝗮,𝘇 +
+
+ , +
+
+ , +
+
+ 𝘈,𝘡 +
+
+ 𝘢,𝘻 +
+
+ , +
+
+ , +
+
+ 𝘼,𝙕 +
+
+ 𝙖,𝙯 +
+
+ , +
+
+ , +
+
+ 𝙰,𝚉 +
+
+ 𝚊,𝚣 +
+
+ , +
+
+ , +
+
+ 𝚨,𝛀 +
+
+ 𝛂,𝛚 +
+
+ , +
+
+ , +
+
+ 𝛛,𝛡 +
+
+ , +
+
+ 𝛢,𝛺 +
+
+ 𝛼,𝜔 +
+
+ , +
+
+ , +
+
+ 𝜕,𝜛 +
+
+ , +
+
+ 𝜜,𝜴 +
+
+ 𝜶,𝝎 +
+
+ , +
+
+ , +
+
+ 𝝏,𝝕 +
+
+ , +
+
+ 𝝖,𝝮 +
+
+ 𝝰,𝞈 +
+
+ , +
+
+ , +
+
+ 𝞉,𝞏 +
+
+ , +
+
+ 𝞐,𝞨 +
+
+ 𝞪,𝟂 +
+
+ , +
+
+ , +
+
+ 𝟃,𝟉 +
+
+ , +
+
+ , +
+
+ , +
+
+ , +
+
+ , +
+
+ , +
+
+ , +
+
+ , +
+
+
+

chemistry

+
+ NaCl +
+
+ H2O +
+
+ C +
+
+ + [SO4] + 2 + +
+
+ Al2 + (SO4)3 +
+
+ + + C + H 3 + + C + H 2 + + O + H + + +
+
+ + + [ClO2] + + + + + [ClO4] + - + + +
+
+ + H2C + = + CH2 + +
+
+ + Fe + Cl3 + (aq) + +
+
+ + H2C + :: + CH2 + +
+
+ + C + + 6 + 14 + + + + N + + 7 + 14 + + + + + e + + + + 1 + + 0 + +
+
+ + + + + + + + A + + + + + + + + + 6 + + + + + + + + + 14 + + + + + + + + + + + + A + + + + + + + + + 2 + + + + + + + + 6 + + + + + + + + + + + + 2 + + + + + + + + 14 + + + + + + C + + + + + + + + + + + + A + + + + + + + + + 7 + + + + + + + + + 14 + + + + + + + + + + + + A + + + + + + + + + 2 + + + + + + + + 7 + + + + + + + + + + + + 2 + + + + + + + + 14 + + + + + + N + + + + + + + + + + + A + + + + + + + + + + 1 + + + + + + + + + 0 + + + + + + + + + + + + A + + + + + + + + + 2 + + + + + + + + + 1 + + + + + + + + + + + + 2 + + + + + + + + 0 + + + + + + e + + + +
+
+ + 2HCl+2Na + + 2NaCl+ + H 2 + + +
+
+ + + + SO + + + + + + + A + + + + + + + + 4 + + + + + + + + + + A + + + + + + 2 + + + + + + +
+
+ + + 2 + + + + + HCl + + + ( + + aq + + ) + + + + + 2 + + + + + Na + + + ( + + s + + ) + + + + + + 2 + + + + + NaCl + + + ( + + aq + + ) + + + + + + H + + + + + + + A + + + + + + + + 2 + + + + + + ( + + g + + ) + + +
+
+ + + + + H + 2 + + + + + ( + g + ) + + + + + + + + I + 2 + + + + + ( + g + ) + + + + + + + - + + + - + + + + + 2 + + + H + + I + + + ( + g + ) + + + + + +
+
+ + + + Fe + + II + + + + Fe + + III + + + + O + 4 + + + + +
+
+ + + + 2 + + + SO + + 2 + + + + + + + + + O + + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + + + SO + + 3 + + + + + + + + +
+
+
+

definitions

+
+ + + x + y + + +
+
+ + + x + y + + +
+
+ + R + +
+
+ + i + +
+
+ + + x + + +
+
+ + + A + + B + + +
+
+
+

intent

+
+ 2 x +
+
+ x T +
+
+ x T +
+
+ + x + y + z + +
+
+ + x + +
+
+ + x + +
+
+ + x + y + z + +
+
+ + x + +
+
+ + x + +
+
+ 2 x +
+
+ + + H + 2 + +
+
+ + + H + 2 + +
+
+ + + x + P + +
+
+
+

mtable

+
+ + + ( + + 3 + + ) + +
+
+ + + | + + 3 + + | + +
+
+ + + ( + + + + 3 + + + 5 + + + + ) + +
+
+ + + ( + + + + -x + + + 5 + + + 12 + + + + ) + +
+
+ + + ( + + + + + x+1 + + + + + + + x-1 + + + + ) + +
+
+ + + ( + + + + + x + + + + + + + a + + + + + + + x + + x+1 + + + + + + ) + +
+
+ + + | + + + + 2 + + + 1 + + + + + 7 + + + 5 + + + + + | + +
+
+ + + [ + + + + 3 + + + 1 + + + 4 + + + + + 0 + + + 2 + + + 6 + + + + ] + +
+
+ + + [ + + + + 3 + + + 1 + + + 4 + + + + + 0 + + + 2 + + + 6 + + + + ] + +
+
+ + + [ + + + + (3.1) + + + 3 + + + 1 + + + 4 + + + + + 0 + + + 2 + + + 6 + + + + ] + +
+
+ + + [ + + + + 1 + + + + + 2 + + + + + 3 + + + ] + +
+
+ + + ( + + + + 3 + + + + + 6 + + + + + 1 + + + + + 2 + + + + ) + +
+
+ + + ( + + + + 3 + + + + + 6 + + + + + 1 + + + + + (3.1) + + + 2 + + + + ) + +
+
+ + + ( + + + + 3 + + + 6 + + + 1 + + + 2 + + + + ) + +
+
+ + + ( + + + + 0 + + + 3 + + + 4 + + + 3 + + + + + 2 + + + 1 + + + 0 + + + 9 + + + + + 3 + + + 0 + + + 2 + + + 1 + + + + + 6 + + + 2 + + + 9 + + + 0 + + + + ) + +
+
+ + + ( + + + + 1 + + + 3 + + + + + 4 + + + 2 + + + + + 2 + + + 1 + + + + + 0 + + + 5 + + + + + ) + +
+
+ + | x | + +
+
+ + | + x+1 + | + +
+
+ + | S | + +
+
+ + + ( + + + + 2 + 1 + + + 7 + 5 + + + ) + +
+
+ + + ( + + + + 1 + + + 2 + + + 3 + + + ) + +
+
+ + + [ + + + + 1 2 + + + ] + +
+
+ ( + + + b11 + b12 + + + b21 + b22 + + + ) +
+
+ + + ( + + + + 2 + 1 + + + 7 + 5 + + + ) + +
+
+ + + ( + + + + 1 + + + 2 + + + 3 + + + ) + +
+
+ + + [ + + + + 1 2 + + + ] + +
+
+ ( + + + b11 + b12 + + + b21 + b22 + + + ) +
+
+ + + ( + + + + 2 + 1 + + + 7 + 5 + + + ) + +
+
+ + + ( + + + + 1 + + + 2 + + + 3 + + + ) + +
+
+ + + [ + + + + 1 2 + + + ] + +
+
+ ( + + + b11 + b12 + + + b21 + b22 + + + ) +
+
+ + + [ + + + 1 + 2 + -1 + 3 + + + -3 + 3 + -1 + 2 + + + 2 + 3 + 2 + -1 + + + ] + + +
+
+ + + ( + + + + 2 + 1 + + + 7 + 5 + + + ) + +
+
+ + + ( + + + + 1 + + + 2 + + + 3 + + + ) + +
+
+ + + [ + + + + 1 2 + + + ] + +
+
+ ( + + + b11 + b12 + + + b21 + b22 + + + ) +
+
+ + + ( + + + + 2 + 1 + + + 7 + 5 + + + ) + +
+
+ + + ( + + + + 1 + + + 2 + + + 3 + + + ) + +
+
+ + + [ + + + + 1 2 + + + ] + +
+
+ ( + + + b11 + b12 + + + b21 + b22 + + + ) +
+
+ ( + 32 + ) +
+
+ 1234 + abcd +
+
+ + + + + a + + + = + + + + b + + + c + + d + + + + + + + + + + e + + f + + + + +
+
+ [ + + 00 + 00 + + ] +
+
+ ( + + 100 + 010 + 001 + + ) +
+
+ [ + + 10 + 0-1 + + ] +
+
+ [ + + 10 + 00 + + ] +
+
+ ( + + 200 + 010 + 00x2 + + ) +
+
+ + + + (2) + 𝑏 = 2 + + + +
+
+
+

shared

+
+ + a ` + b ~ + c ̆ + b ̌ + c ` + + r ˇ + + x . + y ˙ + z ¨ + u + v + + x ^ + + t + +
+
+ + lim + x 0 + + + + sin x + x + + +
+
+ + lim + x 0 + + + sin x + +
+
+ Cmn +
+
+ Cmn +
+
+ Cn,m +
+
+ Pkn +
+
+ Pkn +
+
+ Pkn +
+
+ + R i j k l + +
+
+ + R i j k l m + I J K L + +
+
+ x +
+
+ P(A|B) +
+
+ + + + x + + k + + + i + + + + +
+
+ ij2k +
+
+ + x + + k + + + i + + +
+
+ I + = + + b + r + + + + z + I + +
+
+ C + + + I + + + X +
+
+ + + P + ( + A + + +  and  + + + B + ) + = + P + ( + A + + B + ) + = + P + ( + A + ) + P + ( + B + ) + . + + +
+
+ {2}. +
+
+ + + + ϕ + ( + x + ) + = + c + + e + + + + h + + 2 + + + + x + + 2 + + + + + , + + + +
+
+ + + + P + ( + A + + B + ) + = + + + + P + ( + A + + B + ) + + + P + ( + B + ) + + + + . + + + + +
+
+ + + [ + + 0 + + , + + 2 + π + + ) + + +
+
+ x^2+y^ +
+
+ 1 234 567 +
+
+ x + = + 2 + + 𝒔𝒊𝒏 + + t + + - + 1 +
+
+ 1,234.56 +
+
+ ·F +
+
+ ×F +
+
+ F +
+
+ + + + A + + + + + B + + + + +
+
+ + + + x×y + · + z/2 + + + ab + + + x! + + + +
+
+ + + f + + + ( + + x + ! + + ) + + + +
+
+ + + f + + + ( + + x + ! + + ) + + + +
+
+ + + + A + + + + + B + + + + +
+
+ + + f + + + ( + + x + ! + + ) + + + +
+
+
+

functions

+
+ + sinx+ + cosy+ + tanz+ + secα+ + cscϕ+ + cotφ + +
+
+ + sinhx+ + coshy+ + tanhz+ + sechα+ + cschϕ+ + cothφ + +
+
+ sin-1x +
+
+ sin2x +
+
+ tan3x +
+
+ sec4x +
+
+ sinhn-1x +
+
+ logx +
+
+ log(x+y) +
+
+ logbx +
+
+ logb(x+y) +
+
+ ln(x+y) +
+
+ lnx +
+
+ Covx +
+
+ exp(x) +
+
+ t(x) +
+
+ t(x) +
+
+ tx +
+
+ tx +
+
+ x y +
+
+ + 2 + ( 3 ) + +
+
+ + ( 2 ) + 3 + +
+
+ + a + b + = + ab + +
+
+ + ( + 25 + ) + x + +
+
+ + b + ( + xy + ) + +
+
+ + 2+ + ( + 2 + ) + +
+
+ + ( + 2x + ) + +1 + +
+
+ + + + + ( + 2x + ) + 2 + + + +
+
+ + 2 + + + + ( + 12 + ) + +
+
+ + ( + (c,d) + ) + +
+
+ + [ + [(]c,d) + ) + +
+
+ + ( + (c,d] + ] + +
+
+ + [ + [(]c,d] + ] + +
+
+ + ( + - ,d) + ) + +
+
+ + ( + - ,d] + ] + +
+
+
+

geometry

+
+ BC +
+
+ XY¯ +
+
+ BC +
+
+ XY +
+
+
+

large_ops

+
+ + + n=1 + 10 + + n +
+
+ + + S + + i +
+
+ + + n=1 + 10 + + n +
+
+ + + S + + i +
+
+ + ai +
+
+ + + n=1 + 10 + + n +
+
+ + + S + + i +
+
+ + ai +
+
+ + + i=1 + 10 + + Si +
+
+ + + C + + Si +
+
+ + Si +
+
+ + + i=1 + 10 + + Si +
+
+ + + C + + Si +
+
+ + Si +
+
+ + + + 0 + 1 + + f(x ) + + dx +
+
+ + + + + f(x ) + dx +
+
+ + f(x ) + dx +
+
+
+

linear_algebra

+
+ MT +
+
+ TrM +
+
+ DimM +
+
+ Hom(M) +
+
+ ker(L) +
+
+ + + + f + + + +
+
+ + + + x + + + y + + + +
+
+ + + + + f + + + p + + +
+
+ ( + b + + + + + ) + a +
+
+
+

mfrac

+
+ + 1 2 + +
+
+ + 2 3 + +
+
+ + 17 10 + +
+
+ + 89 10 + +
+
+ + + + x+y + + x-y + + + +
+
+ + + + x+ 1y + + x-y + + + +
+
+ + + + x+ 1y + + x-y + + + +
+
+ + + + x+ 2+1y + + x-y + + + +
+
+ + + + x + + x-y + + + +
+
+ + + x-y + x + + +
+
+ + + 62 + + mi + hr + + + +
+
+ + + 1 + + gal + mi + + + +
+
+ + + + 3 + gal + + mi + + +
+
+ + + 3 m + s2 + + +
+
+ 3 + 1 2 +
+
+ 3 + + 1 8 +
+
+ 3 + 7 83 +
+
+ + rise run + +
+
+ + + + 2miles + + 3gallons + + +
+
+ + + + + + 1 + 2 + + + + + 2 + 3 + + + + + +
+
+ 2 + ( + 7 3 + ) +
+
+ 2 + ( + n+7 3 + ) +
+
+ 2 + ( + 7 k+3 + ) +
+
+ 2 + ( + n+7 k+3 + ) +
+
+
+

msup

+
+ + x 2 + +
+
+ + x 3 + +
+
+ + x 4 + +
+
+ + x n + +
+
+ + x 0 + +
+
+ + x 2.0 + +
+
+ + + + 3 + + y+2 + + + +
+
+ + + x + - 2 + + +
+
+ + + x + 13 + + +
+
+ + + + 3 + + 2 + + x + 2 + + + + + +
+
+ + + + 3 + + - + 2 + + x + 2 + + + + + +
+
+ + + y + + 45 + 3 + + + +
+
+ + + y + + - + + 45 + 3 + + + + +
+
+ + + + e + + + 1 + 2 + + + x + 2 + + + + + +
+
+ + + + e + + + 1 + 2 + + + x + 2 + + + + + +
+
+ + + + 3 + + + 3 + + 10 + + + + + +
+
+ + + + 3 + + + + ( + + x+1 + ) + 2 + + + + + +
+
+ + + t + + 45 + n + + + +
+
+ + + t + + 45 + n+1 + + + +
+
+ + + t + + 45 + -3 + + + +
+
+ + + + e + + + 1 + 2 + + + + ( + + + + xμ + σ + + + ) + 2 + + + + + +
+
+ + + t + + b+1 + 3 + + + +
+
+
+

multiline

+
+ + + f( + x + )={ + + + + + 1 if x<0 + + + + + + 0 if x=0 + + + + + + 1 if x>0 + + + + +
+
+ + + + + + + x+y + + + = + + + 7 + + + + + + 2x+3y + + + = + + + + 17 + + + + + +
+
+
+

sets

+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + + 2 + + +
+
+ + + + 2 + + +
+
+ + + + 2 + + +
+
+ + + + 3 + + +
+
+ + + + 4 + + +
+
+ + + + + + + +
+
+ + + + - + + +
+
+ + + + + + + +
+
+ + + + - + + +
+
+ { } +
+
+ { 12} +
+
+ { 5 , 10 , 15 } +
+
+ { x:x>2 } +
+
+ { x|x>2 } +
+
+ 3+2i +
+
+ + + i + + + 1 + i 2 + +
+
+ { + x + + + : + 2 + < + x + < + 7 + } +
+
+ { + x + | + x  is an even number + } +
+
+
+

subscripts

+
+ x 1 +
+
+ x 1.2 +
+
+ x 1.2 3 +
+
+ x i +
+
+ x 1 102 +
+
+ x 1 2 +
+
+ x 1 x,2 +
+
+
+

units

+
+ Qg, + Rg, + Yg, + Zg, + Eg, + Pg, + Tg, + Gg, + Mg, + kg, + hg, + dag, + dg, + cg, + mg, + µg, + ng, + pg, + fg, + ag, + zg, + yg, + rg, + qg +
+
+ 1A,2A, + 1cd,2cd, + 1K,2K, + 1,2, + 1g,2g, + 1m,2m, + 1mol,2mol, + 1s,2s, + 1,2, + 1",2", + 1sec,2sec +
+
+ 1QA,2RA, + 1Ycd,2Zcd, + 1EK,2PK, + 1TK,2GK, + 1Mg,2kg, + 1hm,2dam, + 1dmol,2cmol, + 1ms,2µs, + 1nsec,2psec +
+
+ 1Bq,2Bq, + 1C,2C, + 1°C,2°C, + 1,2, + 1F,2F, + 1Gy,2Gy, + 1H,2H, + 1Hz,2Hz, + 1J,2J, + 1kat,2kat, + 1lm,2lm, + 1lx,2lx +
+
+ 1QBq,2RBq, + 1YC,2ZC, + 1EF,2PF, + 1TGy,2GGy, + 1MH,2kH, + 1daHz,2dHz, + 1cJ,2mJ, + 1µkat,2nkat, + 1plm,2flm, + 1alx,2zlx, + 1m°C,2µ°C, + 1p℃,2n℃ +
+
+ 1N,2N, + 1Ω,2Ω, + 1,2, + 1Pa,2Pa, + 1S,2S, + 1Sv,2Sv, + 1T,2T, + 1V,2V, + 1W,2W, + 1Wb,2Wb +
+
+ 1qN,2rN, + 1,2, + 1aΩ,2fΩ, + 1pPa,2nPa, + 1µS,2mS, + 1cSv,2dSv, + 1daT,2hT, + 1kV,2MV, + 1GW,2TW, + 1PWb,2EWb +
+
+ 1l,2l, + 1L,2L, + 1,2, + 1t,2t, + 1Da,2Da, + 1Np,2Np, + 1u,2u, + 1eV,2eV, + 1rad,2rad, + 1sr,2sr, + 1a,2a, + 1as,2as, + 1b,2b, + 1B,2B, + 1Bd,2Bd +
+
+ 1Ql,2Rl, + 1YL,2ZL, + 1Eℓ,2Pℓ, + 1Tt,2Gt, + 1MDa,2kDa, + 1dNp,2cNp, + 1hu,2dau, + 1meV,2µeV, + 1nrad,2prad, + 1fsr,2asr, + 1Ga,2Ma, + 1zas,2yas, + 1kb,2Mb, + 1GB,2TB, + 1TBd,2EBd +
+
+ 1,2, + 1",2", + 1,2, + 1',2', + 1min,2min, + 1h,2h, + 1hr,2hr, + 1Hr,2Hr, + 1d,2d, + 1dy,2dy, + 1w,2w, + 1wk,2wk, + 1y,2y, + 1yr,2yr +
+
+ 1°,2°, + 1deg,2deg, + 1arcmin,2arcmin, + 1amin,2amin, + 1am,2am, + 1MOA,2MOA, + 1arcsec,2arcsec, + 1asec,2asec +
+
+ 1au,2au, + 1ltyr,2ltyr, + 1pc,2pc, + 1Å,2Å, + 1,2, + 1fm,2fm +
+
+ 1ha,2ha, + 1dB,2dB, + 1atm,2atm, + 1amu,2amu, + 1bar,2bar, + 1cal,2cal, + 1Ci,2Ci, + 1grad,2grad, + 1M,2M, + 1R,2R, + 1rpm,2rpm, + 1fl dr,2fl dr, + 1,2, + 1dyn,2dyn, + 1erg,2erg +
+
+ 1Kib,2Kib, + 1Mib,2Mib, + 1Gib,2Gib, + 1Tib,2Tib, + 1Pib,2Pib, + 1Eib,2Eib, + 1Zib,2Zib, + 1Yib,2Yib, + 1KiB,2KiB, + 1MiB,2MiB, + 1GiB,2GiB, + 1TiB,2TiB, + 1PiB,2PiB, + 1EiB,2EiB, + 1ZiB,2ZiB, + 1YiB,2YiB +
+
+ 1.0l, + 2.0 m, + x ms, + yµs, + dag, + 1235daN, + 2.5µsec, + 32.34mol +
+
+ [ + 1t, + 2PA, + 3Pa, + 4.5mT + ] +
+
+ 3m, + 1km, + 3m, + 310F, + mmin +
+
+ + + From 12bcca9c9fe98a9beb91283a6d42344911c1d7c2 Mon Sep 17 00:00:00 2001 From: "U-rainshowers\\neils" Date: Sun, 14 Jun 2026 14:10:23 -0700 Subject: [PATCH 27/37] Fix remaining erros after `definitions.yaml` was updated -- needed to add `function:` --- tests/Languages/en/intent.rs | 1 + tests/Languages/intent/tables.rs | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/Languages/en/intent.rs b/tests/Languages/en/intent.rs index 795854781..aa75fbeed 100644 --- a/tests/Languages/en/intent.rs +++ b/tests/Languages/en/intent.rs @@ -133,6 +133,7 @@ fn silent_intent_underscore() -> Result<()> { #[test] fn intent_prob_x() -> Result<()> { + init_logger(); let expr = " x diff --git a/tests/Languages/intent/tables.rs b/tests/Languages/intent/tables.rs index ea3c0b7b1..c7968dee5 100644 --- a/tests/Languages/intent/tables.rs +++ b/tests/Languages/intent/tables.rs @@ -33,7 +33,7 @@ fn matrix_mtable_intent() -> Result<()> { 456 "#; let intent = " - + 1 2 @@ -57,7 +57,7 @@ fn matrix_mrow_intent() -> Result<()> { 456 "#; let intent = " - + 1 2 @@ -105,7 +105,7 @@ fn determinant_not_matrix() -> Result<()> { 45 "#; let intent = " - + '> 1 2 From 27699054a326c2e3f79ca70ee4823f4d85acbe58 Mon Sep 17 00:00:00 2001 From: "U-rainshowers\\neils" Date: Sun, 14 Jun 2026 15:49:19 -0700 Subject: [PATCH 28/37] fix clippy warning --- src/infer_intent.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/infer_intent.rs b/src/infer_intent.rs index 0e481b836..1112d859e 100644 --- a/src/infer_intent.rs +++ b/src/infer_intent.rs @@ -101,13 +101,12 @@ fn add_fixity(intent: Element) { let definitions = definitions.borrow(); // debug!(" add_fixity: intent_name: {}, ", intent_name); if let Some(definition) = definitions.get_hashmap("IntentMappings").unwrap().get(intent_name) && - let Some((fixity, _)) = definition.split_once("=") { - if fixity != "nofix" { + let Some((fixity, _)) = definition.split_once("=") && + fixity != "nofix" { let new_properties = (if properties.is_empty() {":"} else {properties}).to_string() + fixity + ":"; intent.set_attribute_value(INTENT_PROPERTY, &new_properties); // debug!("Added fixity: new value '{}'", intent.attribute_value(INTENT_PROPERTY).unwrap()); } - }; }); } } From 7cd9d01b8fa047cbe40bc8e60ee0c37676b72369 Mon Sep 17 00:00:00 2001 From: "U-rainshowers\\neils" Date: Sun, 14 Jun 2026 16:41:20 -0700 Subject: [PATCH 29/37] minor cleanup: remove ASCIIMath-fi reference --- src/braille.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/braille.rs b/src/braille.rs index b944578f0..5b6c91cbd 100644 --- a/src/braille.rs +++ b/src/braille.rs @@ -52,7 +52,6 @@ pub fn braille_mathml(mathml: Element, nav_node_id: &str) -> Result<(String, usi "Swedish" => swedish_cleanup(pref_manager, braille_string), "LaTeX" => LaTeX_cleanup(pref_manager, braille_string), "ASCIIMath" => ASCIIMath_cleanup(pref_manager, braille_string), - "ASCIIMath-fi" => ASCIIMath_cleanup(pref_manager, braille_string), _ => braille_string.trim_matches('⠀').to_string(), // probably needs cleanup if someone has another code, but this will have to get added by hand }; From 0554c3b8b444f532478170a8ff190d8486ae8fe8 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:04:35 +0300 Subject: [PATCH 30/37] First version of Russian Braille This commit adds first druft version for Russian Braille. --- Rules/Braille/Russian/Russian_Rules.yaml | 317 +++++++++++++++++++++++ Rules/Braille/Russian/definitions.yaml | 2 + Rules/Braille/Russian/unicode-full.yaml | 301 +++++++++++++++++++++ Rules/Braille/Russian/unicode.yaml | 289 +++++++++++++++++++++ src/braille.rs | 34 +++ tests/braille.rs | 4 + tests/braille/Russian/russian.rs | 37 +++ 7 files changed, 984 insertions(+) create mode 100644 Rules/Braille/Russian/Russian_Rules.yaml create mode 100644 Rules/Braille/Russian/definitions.yaml create mode 100644 Rules/Braille/Russian/unicode-full.yaml create mode 100644 Rules/Braille/Russian/unicode.yaml create mode 100644 tests/braille/Russian/russian.rs diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml new file mode 100644 index 000000000..1effd8c44 --- /dev/null +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -0,0 +1,317 @@ +--- +# Russian mathematical braille rules. + +- + name: whitespace + tag: "!*" + match: "not(self::m:math) and not($MatchingWhitespace) and (@data-previous-space-width >= 0.25 or @data-following-space-width >= 0.25)" + replace: + - with: + variables: [MatchingWhitespace: "true()"] + replace: + - test: + - if: "@data-previous-space-width > 1.1" + then: [t: "⠀"] + - else_if: "@data-previous-space-width >= 0.25" + then: [t: "W"] + - x: "." + - test: + - if: "@data-following-space-width > 1.1" + then: [t: "⠀"] + - else_if: "@data-following-space-width >= 0.25" + then: [t: "W"] + +- + name: omission-intent + tag: "!*" + match: "contains(@intent, ':blank')" + replace: [t: "⠀"] + +- + name: unicode-override + tag: "*" + match: "@data-unicode" + replace: [x: "@data-unicode"] + +- + name: default + tag: math + match: "." + variables: + - RowStart: "''" + - RowEnd: "''" + - MatchingWhitespace: "false()" + replace: [x: "*"] + +- + name: default + tag: [mrow, mstyle, semantics] + match: "." + replace: [x: "*[1] | *[position()>1]"] + +- + name: no-content + tag: [math, mrow] + match: "not(*)" + replace: [t: "⠀"] + +- + name: default + tag: msqrt + match: "." + replace: + - t: "⠩⠱" + - x: "*[1]" + - t: "⠹" + +- + name: default + tag: mroot + match: "." + replace: + - t: "⠩" + - x: "*[2]" + - t: "⠱" + - x: "*[1]" + - t: "⠹" + +- + name: default + tag: mfrac + match: "." + replace: + - t: "⠆" + - x: "*[1]" + - t: "⠀⠳" + - x: "*[2]" + - t: "⠰" + +- + name: binomial-frac + tag: mrow + match: "IsBracketed(., '(', ')') and *[2][self::m:mfrac][@linethickness=0]" + replace: + - x: "*[1]" + - t: "⠨⠉⠡" + - x: "*[2]/*[2]" + - t: "⠌" + - x: "*[2]/*[1]" + - x: "*[3]" + +- + name: binomial-table + tag: mrow + match: "IsBracketed(., '(', ')') and *[2][self::m:mtable][count(*)=2 and count(*[1])=1] and contains(@intent, 'binomial(')" + replace: + - x: "*[1]" + - t: "⠨⠉⠡" + - x: "*[2]/*[2]/*[1]/*[1]" + - t: "⠌" + - x: "*[2]/*[1]/*[1]/*[1]" + - x: "*[3]" + +- + name: default-matrix + tag: mrow + variables: + - RowStart: "*[1]" + - RowEnd: "*[3]" + match: + - "*[2][self::m:mtable] and" + - (IsBracketed(., '(', ')') or IsBracketed(., '[', ']') or IsBracketed(., '|', '|')) + replace: [x: "*[2]"] + +- + name: default + tag: mtable + match: "." + replace: + - t: "⠞⠃⠇" + - x: "*" + +- + name: default + tag: [mtr, mlabeledtr] + match: "." + replace: + - test: + if: "preceding-sibling::*" + then: [t: "⠨⠳"] + - x: $RowStart + - test: + if: .[self::m:mlabeledtr] + then: [x: "*[position()>1]"] + else: [x: "*"] + - x: $RowEnd + +- + name: default + tag: mtd + match: "." + replace: + - test: + if: "*" + then: + - test: + if: "preceding-sibling::*" + then: [t: "W"] + - x: "*" + +- + name: single-char-exceptions + tag: msup + match: "*[2][translate(., \"'*`ª°²³´¹º′″‴‵‶‷⁗\",'')='']" + replace: [x: "*"] + +- + name: default + tag: [msub, munder] + match: "." + replace: + - x: "*[1]" + - test: + if: "self::m:munder" + then: [t: "⠨⠡"] + else: [t: "⠡"] + - x: "*[2]" + - t: "⠱" + +- + name: default + tag: [msup, mover] + match: "." + replace: + - x: "*[1]" + - test: + if: "self::m:mover" + then: [t: "⠨⠌"] + else: [t: "⠌"] + - x: "*[2]" + - t: "⠱" + +- + name: default + tag: [msubsup, munderover] + match: "count(*)=3" + replace: + - x: "*[1]" + - test: + if: "self::m:munderover" + then: [t: "⠨⠡"] + else: [t: "⠡"] + - x: "*[2]" + - test: + if: "self::m:munderover" + then: [t: "⠨⠌"] + else: [t: "⠌"] + - x: "*[3]" + - t: "⠱" + +- + name: default + tag: mmultiscripts + match: "." + replace: [x: "*"] + +- + name: default + tag: menclose + match: "." + replace: + - t: "⠣" + - x: "*" + - t: "⠜" + +- + name: default + tag: mo + match: "." + replace: + - x: "text()" + +- + name: default + tag: mn + match: "." + replace: + - x: "BrailleChars(., 'Russian')" + +- + name: functions + tag: mi + match: "IsInDefinition(., 'Speech', 'FunctionNames')" + replace: + - test: + - if: ".='arcsin'" + then: [t: "⠫⠁⠎"] + - else_if: ".='arccos'" + then: [t: "⠫⠁⠉"] + - else_if: ".='arctg' or .='arctan'" + then: [t: "⠫⠁⠞"] + - else_if: ".='arcctg' or .='arccotan' or .='arccot'" + then: [t: "⠫⠁⠉⠞"] + - else_if: ".='sin'" + then: [t: "⠫⠎"] + - else_if: ".='cos'" + then: [t: "⠫⠉"] + - else_if: ".='tg' or .='tan'" + then: [t: "⠫⠞"] + - else_if: ".='ctg' or .='cotan' or .='cot'" + then: [t: "⠫⠉⠞"] + - else_if: ".='log'" + then: [t: "⠫⠇"] + - else_if: ".='ln'" + then: [t: "⠫⠇⠝"] + - else_if: ".='lg'" + then: [t: "⠫⠇⠛"] + - else_if: ".='lim'" + then: [t: "⠫⠇⠍"] + - else_if: ".='min'" + then: [t: "⠫⠍⠝"] + - else_if: ".='max'" + then: [t: "⠫⠍⠭"] + - else_if: ".='exp'" + then: [t: "⠫⠑"] + else: [x: "BrailleChars(., 'Russian')"] + +- + name: default + tag: [mi, mtext] + match: "." + replace: + - x: "BrailleChars(., 'Russian')" + +- + name: default + tag: ms + match: "." + replace: + - test: + if: "string(@lquote)!=''" + then: [x: "@lquote"] + else: [t: "⠦"] + - x: "BrailleChars(., 'Russian')" + - test: + if: "string(@rquote)!=''" + then: [x: "@rquote"] + else: [t: "⠴"] + +- + name: default-children + tag: "*" + match: "*" + replace: + - x: "*" + +- + name: default-no-children + tag: "*" + match: "text()" + replace: + - x: "text()" + +- + name: default-no-text + tag: "*" + match: "." + replace: [t: ""] diff --git a/Rules/Braille/Russian/definitions.yaml b/Rules/Braille/Russian/definitions.yaml new file mode 100644 index 000000000..4d0c77efb --- /dev/null +++ b/Rules/Braille/Russian/definitions.yaml @@ -0,0 +1,2 @@ +--- +- include: "../definitions.yaml" diff --git a/Rules/Braille/Russian/unicode-full.yaml b/Rules/Braille/Russian/unicode-full.yaml new file mode 100644 index 000000000..f78feac35 --- /dev/null +++ b/Rules/Braille/Russian/unicode-full.yaml @@ -0,0 +1,301 @@ +--- + - "⋇": [tc: "1⠌⠯⠦"] # 0x22C7 (Division times) + - "⊩": [tc: "⠸⠳⠿⠸⠒"] # 0x22A9 (Forces) + - "ℏ": [tc: "⠈⠒⠓"] # t: "B"F (reduced Plank's constant) + - "ⅆ": [tc: "⠙"] # 0x2146 + - "ⅇ": [tc: "⠑"] # 0x2147 + - "ⅈ": [tc: "⠊"] # 0x2148 + - "⨯": [tc: "⠐⠦"] # U+2A2F(VECTOR OR CROSS PRODUCT) -- make the same as 0x00D7 (Multiplication sign) + + + - "¼": [tc: "#N⠁N⠌N⠙"] # 0x00BC (Vulgar Fraction One Quarter) + - "½": [tc: "#N⠁N⠌N⠃"] # 0x00BD (Vulgar Fraction One Half) + - "¾": [tc: "#N⠉N⠌N⠙"] # 0x00BE (Vulgar Fraction Three Quarters) + - "⅐": [tc: "#N⠁N⠌N⠛"] # 0x2150 (Vulgar Fraction One Seventh) + - "⅑": [tc: "#N⠁N⠌N⠊"] # 0x2151 (Vulgar Fraction One Ninth) + - "⅒": [tc: "#N⠁N⠌N⠁N⠚"] # 0x2152 (Vulgar Fraction One Tenth) + - "⅓": [tc: "#N⠁N⠌N⠉"] # 0x2153 (Vulgar Fraction One Third) + - "⅔": [tc: "#N⠃N⠌N⠉"] # 0x2154 (Vulgar Fraction Two Thirds) + - "⅕": [tc: "#N⠁N⠌N⠑"] # 0x2155 (Vulgar Fraction One Fifth) + - "⅖": [tc: "#N⠃N⠌N⠑"] # 0x2156 (Vulgar Fraction Two Fifths) + - "⅗": [tc: "#N⠉N⠌N⠑"] # 0x2157 (Vulgar Fraction Three Fifths) + - "⅘": [tc: "#N⠙N⠌N⠑"] # 0x2158 (Vulgar Fraction Four Fifths) + - "⅙": [tc: "#N⠁N⠌N⠋"] # 0x2159 (Vulgar Fraction One Sixth) + - "⅚": [tc: "#N⠑N⠌N⠋"] # 0x215A (Vulgar Fraction Five Sixths) + - "⅛": [tc: "#N⠁N⠌N⠓"] # 0x215B (Vulgar Fraction One Eighth) + - "⅜": [tc: "#N⠉N⠌N⠓"] # 0x215C (Vulgar Fraction Three Eighths) + - "⅝": [tc: "#N⠑N⠌N⠓"] # 0x215D (Vulgar Fraction Five Eighths) + - "⅞": [tc: "#N⠛N⠌N⠓"] # 0x215E (Vulgar Fraction Seven Eighths) + - "↉": [tc: "#N⠚N⠌N⠑"] # 0x2189 (Vulgar Fraction Zero Thirds) + + + + - "ℊ": [tc: "TL⠛⠁"] # 0x210a (Script Small G) + - "ℋ": [tc: "TCL⠓⠁"] # 0x210b (Script Capital H) + - "ℒ": [tc: "TCL⠇"] # 0x2113 (Script Capital L) + - "ℓ": [tc: "TL⠇"] # 0x2113 (Script Small L) + - "℘": [tc: "TCL⠏"] # 0x2118 (Script Capital P) + - "ℛ": [tc: "TCL⠗"] # 0x211B (Script Capital R) + - "ℯ": [tc: "TL⠑"] # 0x212F (Script Small E) + - "ℰ": [tc: "TCL⠑"] # 0x2130 (Script Capital E) + - "ℱ": [tc: "TCL⠋"] # 0x2131 (Script Capital F) + - "ℳ": [tc: "TCL⠍"] # 0x2133 (Script Capital M) + - "ℴ": [tc: "TL⠕"] # 0x21334 (Script Small O) + - "ℌ": [tc: "DCL⠓"] # 0x210C (Fraktur Capital H) + - "ℑ": [tc: "DCL⠊"] # 0x2111 (Fraktur Capital I) + - "ℜ": [tc: "DCL⠗"] # 0x211C (Fraktur Capital R) + - "ℨ": [tc: "DCL⠵"] # 0x2128 (Fraktur Capital Z) + - "ℭ": [tc: "DCL⠉"] # 0x22DC (Fraktur Capital C) + + - "ℂ": [tc: "𝔹CL⠉"] # 0x2102 + - "ℍ": [tc: "𝔹CL⠓"] # 0x210d + - "ℕ": [tc: "𝔹CL⠝"] # 0x2115 + - "ℙ": [tc: "𝔹CL⠏"] # 0x2119 + - "ℚ": [tc: "𝔹CL⠟"] # 0x211a + - "ℝ": [tc: "𝔹CL⠗"] # 0x211d + - "ℤ": [tc: "𝔹CL⠵"] # 0x2124 + + + - "𝚨-𝛀": # 0x1d6a8 - 0x1d6c0 + - tc: "B" + - spell: "translate('.', '𝛂𝛃𝛄𝛅𝛆𝛇𝛈𝛉𝛊𝛋𝛌𝛍𝛎𝛏𝛐𝛑𝛒𝛓𝛔𝛕𝛖𝛗𝛘𝛙𝛚', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + + - "𝛂-𝛚": # 0x1d6c2 - 0x1d6da + - tc: "BGL" + - spell: "translate('.', '𝛂𝛃𝛄𝛅𝛆𝛇𝛈𝛉𝛊𝛋𝛌𝛍𝛎𝛏𝛐𝛑𝛒𝛓𝛔𝛕𝛖𝛗𝛘𝛙𝛚', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + + - "𝔄-𝔜": # 0x1d504 - 0x1d51d ('z' version is reserved) + - tc: "DC" + - spell: "translate('.', '𝔄𝔅𝔆𝔇𝔈𝔉𝔊𝔋𝔌𝔍𝔎𝔏𝔐𝔑𝔒𝔓𝔔𝔕𝔖𝔗𝔘𝔙𝔚𝔛𝔜', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝔞-𝔷": # 0x1d51e - 0x1d537 + - tc: "D" + - spell: "translate('.', '𝔞𝔟𝔠𝔡𝔢𝔣𝔤𝔥𝔦𝔧𝔨𝔩𝔪𝔫𝔬𝔭𝔮𝔯𝔰𝔱𝔲𝔳𝔴𝔵𝔶𝔷', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝕬-𝖅": # 0x1D56C - 0x1D585 + - tc: "BDC" + - spell: "translate('.', '𝕬𝕭𝕮𝕯𝕰𝕱𝕲𝕳𝕴𝕵𝕶𝕷𝕸𝕹𝕺𝕻𝕼𝕽𝕾𝕿𝖀𝖁𝖂𝖃𝖄𝖅', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝖆-𝖟": # 0x1d586 - 0x1d59f + - tc: "D" + - spell: "translate('.', '𝖆𝖇𝖈𝖉𝖊𝖋𝖌𝖍𝖎𝖏𝖐𝖑𝖒𝖓𝖔𝖕𝖖𝖗𝖘𝖙𝖚𝖛𝖜𝖝𝖞𝖟', 'abcdefghijklmnopqrstuvwxyz')" + + # double struck (blackboard bold) chars in math alphabetic block and also MathType private use area + # Some of these are reserved because they were used in Plane 0 -- that shouldn't be an issue other than causing the other chars to not display + - "𝔸-𝕐": # 0x1d504 - 0x1d51d ('z' version is reserved) + - tc: "DC" + - spell: "translate('.', '𝔸𝔹𝔺𝔻𝔼𝔽𝔾𝔿𝕀𝕁𝕂𝕃𝕄𝕅𝕆𝕇𝕈𝕉𝕊𝕋𝕌𝕍𝕎𝕏𝕐', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝕒-𝕫": # 0x1d552 - 0x1d56b + - tc: "D" + - spell: "translate('.', '𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝟘-𝟡": # 0x1d7d8 - 0x1d7e1 + - tc: "D" + - spell: "translate('.', '𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡', '0123456789')" + + # script chars in math alphabetic block and also MathType private use area + - "𝒜-𝒵": # 0x1d49c - 0x1d4b5 + - tc: "TsC" + - spell: "translate('.', '𝒜𝒝𝒞𝒟𝒠𝒡𝒢𝒣𝒤𝒥𝒦𝒧𝒨𝒩𝒪𝒫𝒬𝒭𝒮𝒯𝒰𝒱𝒲𝒳𝒴𝒵', 'abcdefghijklmnopqrstuvwxyza')" + + - "𝒶-𝓏": # 0x1d4b6 - 0x1d4cf + - tc: "Ts" + - spell: "translate('.', '𝒶𝒷𝒸𝒹𝒺𝒻𝒼𝒽𝒾𝒿𝓀𝓁𝓂𝓃𝓄𝓅𝓆𝓇𝓈𝓉𝓊𝓋𝓌𝓍𝓎𝓏', 'abcdefghijklmnopqrstuvwxyz')" + + # bold script chars in math alphabetic block + - "𝓐-𝓩": # 0x1d4d0 - 0x1d4e9 + - tc: "BTs" + - spell: "translate('.', '𝓐𝓑𝓒𝓓𝓔𝓕𝓖𝓗𝓘𝓙𝓚𝓛𝓜𝓝𝓞𝓟𝓠𝓡𝓢𝓣𝓤𝓥𝓦𝓧𝓨𝓩', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝓪-𝔃": # 0x1d4ea - 0x1d503 + - tc: "BTs" + - spell: "translate('.', '𝓪𝓫𝓬𝓭𝓮𝓯𝓰𝓱𝓲𝓳𝓴𝓵𝓶𝓷𝓸𝓹𝓺𝓻𝓼𝓽𝓾𝓿𝔀𝔁𝔂𝔃', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝐀-𝐙": # 0x1d400 - 0x1d419 + - tc: "B" + - spell: "translate('.', '𝐀𝐁𝐂𝐃𝐄𝐅𝐆𝐇𝐈𝐉𝐊𝐋𝐌𝐍𝐎𝐏𝐐𝐑𝐒𝐓𝐔𝐕𝐖𝐗𝐘𝐙', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝐚-𝐳": # 0x1d41a - 0x1d433 + - tc: "B" + - spell: "translate('.', '𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝐴-𝑍": # 0x1d434 - 0x1d44d + # don't include italics + - tc: "C" + - spell: "translate('.', '𝐴𝐵𝐶𝐷𝐸𝐹𝐺𝐻𝐼𝐽𝐾𝐿𝑀𝑁𝑂𝑃𝑄𝑅𝑆𝑇𝑈𝑉𝑊𝑋𝑌𝑍', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝑎-𝑧": # 0x1d44e - 0x1d467 + # don't include italics + - spell: "translate('.', '𝑎𝑏𝑐𝑑𝑒𝑓𝑔𝑕𝑖𝑗𝑘𝑙𝑚𝑛𝑜𝑝𝑞𝑟𝑠𝑡𝑢𝑣𝑤𝑥𝑦𝑧', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝑨-𝒁": # 0x1d468 - 0x1d481 + - tc: "BIC" + - spell: "translate('.', '𝑨𝑩𝑪𝑫𝑬𝑭𝑮𝑯𝑰𝑱𝑲𝑳𝑴𝑵𝑶𝑷𝑸𝑹𝑺𝑻𝑼𝑽𝑾𝑿𝒀𝒁', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝒂-𝒛": # 0x1d482 - 0x1d49b + - spell: "translate('.', '𝒂𝒃𝒄𝒅𝒆𝒇𝒈𝒉𝒊𝒋𝒌𝒍𝒎𝒏𝒐𝒑𝒒𝒓𝒔𝒕𝒖𝒗𝒘𝒙𝒚𝒛', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝖠-𝖹": # 0x1d5a0 - 0x1d5b9 + - spell: "translate('.', '𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" + + - "𝖺-𝗓": # 0x1d5ba - 0x1d5d3 + - spell: "translate('.', '𝖺𝖻𝖼𝖽𝖾𝖿𝗀𝗁𝗂𝗃𝗄𝗅𝗆𝗇𝗈𝗉𝗊𝗋𝗌𝗍𝗎𝗏𝗐𝗑𝗒𝗓', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝗔-𝗭": # 0x1d5d4 - 0x1d5ed + - test: + if: "not($IgnoreBold)" + then: [tc: "B"] + - spell: "translate('.', '𝗔𝗕𝗖𝗗𝗘𝗙𝗚𝗛𝗜𝗝𝗞𝗟𝗠𝗡𝗢𝗣𝗤𝗥𝗦𝗧𝗨𝗩𝗪𝗫𝗬𝗭', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" + + - "𝗮-𝘇": # 0x1d5ee - 0x1d607 + - test: + if: "not($IgnoreBold)" + then: [tc: "B"] + - spell: "translate('.', '𝗮𝗯𝗰𝗱𝗲𝗳𝗴𝗵𝗶𝗷𝗸𝗹𝗺𝗻𝗼𝗽𝗾𝗿𝘀𝘁𝘂𝘃𝘄𝘅𝘆𝘇', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝘈-𝘡": # 0x1d608 - 0x1d621 + # - tc: "italic" + - spell: "translate('.', '𝘈𝘉𝘊𝘋𝘌𝘍𝘎𝘏𝘐𝘑𝘒𝘓𝘔𝘕𝘖𝘗𝘘𝘙𝘚𝘛𝘜𝘝𝘞𝘟𝘠𝘡', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" + + - "𝘢-𝘻": # 0x1d622 - 0x1d63b + # - tc: "italic" + - spell: "translate('.', '𝘢𝘣𝘤𝘥𝘦𝘧𝘨𝘩𝘪𝘫𝘬𝘭𝘮𝘯𝘰𝘱𝘲𝘳𝘴𝘵𝘶𝘷𝘸𝘹𝘺𝘻', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝘼-𝙕": # 0x1d63c - 0x1d655 + # - tc: "bold italic" + - test: + if: "$IgnoreBold" + then: [tc: I] + else: [tc: "BI"] + - spell: "translate('.', '𝘼𝘽𝘾𝘿𝙀𝙁𝙂𝙃𝙄𝙅𝙆𝙇𝙈𝙉𝙊𝙋𝙌𝙍𝙎𝙏𝙐𝙑𝙒𝙓𝙔𝙕', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" + + - "𝙖-𝙯": # 0x1d656 - 0x1d66f + # - tc: "bold italic" + - test: + if: "$IgnoreBold" + then: [tc: I] + else: [tc: "BI"] + - spell: "translate('.', '𝙖𝙗𝙘𝙙𝙚𝙛𝙜𝙝𝙞𝙟𝙠𝙡𝙢𝙣𝙤𝙥𝙦𝙧𝙨𝙩𝙪𝙫𝙬𝙭𝙮𝙯', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝙰-𝚉": # 0x1d670 - 0x1d689 + - tc: "C" + - spell: "translate('.', '𝙰𝙱𝙲𝙳𝙴𝙵𝙶𝙷𝙸𝙹𝙺𝙻𝙼𝙽𝙾𝙿𝚀𝚁𝚂𝚃𝚄𝚅𝚆𝚇𝚈𝚉', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝚊-𝚣": # 0x1d68a - 0x1d6a3 + - spell: "translate('.', '𝚊𝚋𝚌𝚍𝚎𝚏𝚐𝚑𝚒𝚓𝚔𝚕𝚖𝚗𝚘𝚙𝚚𝚛𝚜𝚝𝚞𝚟𝚠𝚡𝚢𝚣', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝚤𝚥": # 0x1d6a4, 0x1d6a5 + - spell: "translate('.', '𝚤𝚥', 'ij')" # not sure what else these should be + + - "𝚨-𝛀": # 0x1d6a8 - 0x1d6c0 + - test: + if: "not($IgnoreBold)" + then: [tc: "B"] + - spell: "translate('.', '𝚨𝚩𝚪𝚫𝚬𝚭𝚮𝚯𝚰𝚱𝚲𝚳𝚴𝚵𝚶𝚷𝚸𝚹𝚺𝚻𝚼𝚽𝚾𝚿𝛀', 'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡ΢ΣΤΥΦΧΨΩ')" + + - "𝛂-𝛚": # 0x1d6c2 - 0x1d6da + - test: + if: "not($IgnoreBold)" + then: [tc: "B"] + - spell: "translate('.', '𝛂𝛃𝛄𝛅𝛆𝛇𝛈𝛉𝛊𝛋𝛌𝛍𝛎𝛏𝛐𝛑𝛒𝛓𝛔𝛕𝛖𝛗𝛘𝛙𝛚', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + - "𝛁": # 0x1d6c1 + - test: + if: "not($IgnoreBold)" + then: [tc: "B"] + - spell: "translate('.', '𝜵', '∇')" + + - "𝛛𝛜𝛝𝛞𝛟𝛠𝛡": # 0x1D6DB - 0x1D6E1 + - test: + if: "not($IgnoreBold)" + then: [tc: "B"] + - spell: "translate('.', '𝛛𝛜𝛝𝛞𝛟𝛠𝛡', '∂εθκφρπ')" + + - "𝛢-𝛺": # 0x1d6e2 - 0x1d6fa + - tc: "IC" + - spell: "translate('.', '𝛢𝛣𝛤𝛥𝛦𝛧𝛨𝛩𝛪𝛫𝛬𝛭𝛮𝛯𝛰𝛱𝛲𝛳𝛴𝛵𝛶𝛷𝛸𝛹𝛺', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + - "𝛼-𝜔": # 0x1d6fc - 0x1d714 + - spell: "translate('.', '𝛼𝛽𝛾𝛿𝜀𝜁𝜂𝜃𝜄𝜅𝜆𝜇𝜈𝜉𝜊𝜋𝜌𝜍𝜎𝜏𝜐𝜑𝜒𝜓𝜔', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + + - "𝛻": # 0x1d6fb + - spell: "translate('.', '𝜵', '∇')" + + - "𝜕𝜖𝜗𝜘𝜙𝜚𝜛": # 0x1d715 - 0x1d71b + # - tc: "italic" + - spell: "translate('.', '𝜕𝜖𝜗𝜘𝜙𝜚𝜛', '∂εθκφρπ')" + + - "𝜜-𝜴": # 0x1d71c - 0x1d734 + # - tc: "bold italic" + - tc: 'BIC⠁' + - spell: "translate('.', '𝜜𝜝𝜞𝜟𝜠𝜡𝜢𝜣𝜤𝜥𝜦𝜧𝜨𝜩𝜪𝜫𝜬𝜭𝜮𝜯𝜰𝜱𝜲𝜳𝜴', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + - "𝜶-𝝎": # 0x1d736 - 0x1d74e + - tc: "BI" + - spell: "translate('.', '𝜶𝜷𝜸𝜹𝜺𝜻𝜼𝜽𝜾𝜿𝝀𝝁𝝂𝝃𝝄𝝅𝝆𝝇𝝈𝝉𝝊𝝋𝝌𝝍𝝎', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + - "𝝏𝝐𝝑𝝒𝝓𝝔𝝕": # 0x1d74f - 0x1d755 + - tc: "BI" + - spell: "translate('.', '𝝏𝝐𝝑𝝒𝝓𝝔𝝕', '∂εκθφρπ')" + + - "𝜵": # 0x1d735 + - tc: "BI" + - spell: "translate('.', '𝜵', '∇')" + + - "𝝖-𝝮": # 0x1d756 - 0x1d76e + - tc: "BSC" + - spell: "translate('.', '𝝖𝝗𝝘𝝙𝝚𝝛𝝜𝝝𝝞𝝟𝝠𝝡𝝢𝝣𝝤𝝥𝝦𝝧𝝨𝝩𝝪𝝫𝝬𝝭𝝮', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + - "𝝰-𝞈": # 0x1d770 - 0x1d788 + - tc: "BS" + - spell: "translate('.', '𝝰𝝱𝝲𝝳𝝴𝝵𝝶𝝷𝝸𝝹𝝺𝝻𝝼𝝽𝝾𝝿𝞀𝞁𝞂𝞃𝞄𝞅𝞆𝞇𝞈', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + - "𝞉𝞊𝞋𝞌𝞍𝞎𝞏": # 0x1d789 - 0x1d78f + - tc: "B" + - spell: "translate('.', '𝞉𝞊𝞋𝞌𝞍𝞎𝞏', '∂εθκφρπ')" + + - "𝝯": # 0x1d76f + - tc: "B" + - spell: "translate('.', '𝜵', '∇')" + + - "𝞐-𝞨": # 0x1d790 - 0x1d7a8 + # - tc: "bold italic" + - test: + if: "$IgnoreBold" + then: [tc: I] + else: [tc: "BI"] + - spell: "translate('.', '𝞐𝞑𝞒𝞓𝞔𝞕𝞖𝞗𝞘𝞙𝞚𝞛𝞜𝞝𝞞𝞟𝞠𝞡𝞢𝞣𝞤𝞥𝞦𝞧𝞨', 'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡ΢ΣΤΥΦΧΨΩ')" + + - "𝞪-𝟂": # 0x1d7aa - 0x1d7c2 + # - tc: "bold italic" + - test: + if: "$IgnoreBold" + then: [tc: I] + else: [tc: "BI"] + - spell: "translate('.', '𝞪𝞫𝞬𝞭𝞮𝞯𝞰𝞱𝞲𝞳𝞴𝞵𝞶𝞷𝞸𝞹𝞺𝞻𝞼𝞽𝞾𝞿𝟀𝟁𝟂', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + - "𝟃𝟄𝟅𝟆𝟇𝟈𝟉": # 0x1d7c3 - 0x1d7c9 + - tc: "B" + - spell: "translate('.', '𝟃𝟄𝟅𝟆𝟇𝟈𝟉', '∂εθκφρπ')" + + - "𝞩": [tc: "B"] # 0x1d7a9 + + - "𝟎-𝟗": # 0x1d7ce - 0x1d7d7 + - tc: "B" + - spell: "translate('.', '𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗', '0123456789')" + + - "𝟬-𝟵": # 0x1D7EC - 0x1D7F5 + - tc: "BS" + - spell: "translate('.', '𝟬𝟭𝟮𝟯𝟰𝟱𝟲𝟳𝟴𝟵', '0123456789')" + + - "𝟢-𝟫": # 0x1d7e2 - 0x1d7eb + - tc: "S" + - spell: "translate('.', '𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿', '0123456789')" + + - "𝟶-𝟿": # 0x1d7f6 - 0x1d7ff + - spell: "translate('.', '𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿', '0123456789')" + diff --git a/Rules/Braille/Russian/unicode.yaml b/Rules/Braille/Russian/unicode.yaml new file mode 100644 index 000000000..1f2c733c7 --- /dev/null +++ b/Rules/Braille/Russian/unicode.yaml @@ -0,0 +1,289 @@ +--- +# Russian mathematical braille characters +- ".": [t: "⠠⠲"] +- ",": [t: "⠠⠂"] +- "!": [t: "⠠⠖"] +- "?": [t: "⠠⠢"] +- ":": [t: "⠠⠒"] +- ";": [t: "⠠⠆"] +- "{": [t: "⠠⠪"] +- "}": [t: "⠠⠕"] +- "+": [t: "⠀⠖"] +- "-": [t: "⠀⠤"] +- "=": [t: "⠀⠶"] +- ">": [t: "⠀⠕⠀"] +- "<": [t: "⠀⠪⠀"] +- "%": [t: "⠼⠴"] +- "±": [t: "⠀⠖⠤"] +- ";": [t: "⠠⠢"] +- "Ϳ": [t: "⠰⠊"] +- "·": [t: "⠰"] +- "α": [t: "⠰⠁"] +- "β": [t: "⠰⠃"] +- "γ": [t: "⠰⠛"] +- "δ": [t: "⠰⠙"] +- "ε": [t: "⠰⠑"] +- "ζ": [t: "⠰⠵"] +- "η": [t: "⠰⠚"] +- "θ": [t: "⠰⠓"] +- "ι": [t: "⠰⠊"] +- "κ": [t: "⠰⠅"] +- "λ": [t: "⠰⠇"] +- "μ": [t: "⠰⠍"] +- "ν": [t: "⠰⠝"] +- "ξ": [t: "⠰⠭"] +- "ο": [t: "⠰⠕"] +- "π": [t: "⠰⠏"] +- "ρ": [t: "⠰⠗"] +- "σ": [t: "⠰⠎"] +- "τ": [t: "⠰⠞"] +- "υ": [t: "⠰⠥"] +- "φ": [t: "⠰⠋"] +- "χ": [t: "⠰⠉"] +- "ψ": [t: "⠰⠽"] +- "ω": [t: "⠰⠺"] +- "Α": [t: "⠸⠁"] +- "Β": [t: "⠸⠃"] +- "Γ": [t: "⠸⠛"] +- "Δ": [t: "⠸⠙"] +- "Ε": [t: "⠸⠑"] +- "Ζ": [t: "⠸⠵"] +- "Η": [t: "⠸⠚"] +- "Θ": [t: "⠸⠓"] +- "Ι": [t: "⠸⠊"] +- "Κ": [t: "⠸⠅"] +- "Λ": [t: "⠸⠇"] +- "Μ": [t: "⠸⠍"] +- "Ν": [t: "⠸⠝"] +- "Ξ": [t: "⠸⠭"] +- "Ο": [t: "⠸⠕"] +- "Π": [t: "⠸⠏"] +- "Ρ": [t: "⠸⠗"] +- "Σ": [t: "⠸⠎"] +- "Τ": [t: "⠸⠞"] +- "Υ": [t: "⠸⠥"] +- "Φ": [t: "⠸⠋"] +- "Χ": [t: "⠸⠉"] +- "Ψ": [t: "⠸⠽"] +- "Ω": [t: "⠸⠺"] +- "ϐ": [t: "⠰⠃"] +- "ϑ": [t: "⠰⠓"] +- "ϕ": [t: "⠰⠋"] +- "ϖ": [t: "⠰⠏"] +- "ϱ": [t: "⠰⠗"] +- "′": [t: "⠔"] +- "″": [t: "⠔⠔"] +- "‴": [t: "⠔⠔⠔"] +- "∀": [t: "⠫⠄"] +- "∂": [t: "⠹"] +- "∃": [t: "⠫⠢"] +- "∅": [t: "⠈⠴"] +- "∇": [t: "⠫⠴"] +- "∈": [t: "⠀⠐⠪⠀"] +- "∉": [t: "⠘⠪"] +- "∋": [t: "⠀⠕⠂⠀"] +- "∌": [t: "⠈⠕⠂⠄"] +- "∎": [t: "⠀⠸⠇⠀"] +- "∏": [t: "⠸⠏"] +- "∑": [t: "⠸⠎"] +- "−": [t: "⠀⠤"] +- "∓": [t: "⠀⠤⠖"] +- "∔": [t: "⠈⠖"] +- "∕": [t: "⠠⠌"] +- "∖": [t: "⠀⠰⠤"] +- "∗": [t: "⠔"] +- "∘": [t: "⠴"] +- "√": [t: "⠩⠱"] +- "∛": [t: "⠩⠒⠱"] +- "∜": [t: "⠩⠲⠱"] +- "∞": [t: "⠻"] +- "∠": [t: "⠸⠪"] +- "∣": [t: "⠸"] +- "∤": [t: "⠀⠼⠀"] +- "∥": [t: "⠸⠸"] +- "∧": [t: "⠀⠰⠢"] +- "∨": [t: "⠀⠰⠔"] +- "∩": [t: "⠀⠰⠲"] +- "∪": [t: "⠀⠰⠴"] +- "∫": [t: "⠮"] +- "∬": [t: "⠮⠮"] +- "∭": [t: "⠮⠮⠮"] +- "∮": [t: "⠮⠴"] +- "∯": [t: "⠮⠮⠴"] +- "∴": [t: "⠀⠠⠡⠀"] +- "∵": [t: "⠀⠈⠌⠀"] +- "∶": [t: "⠀⠳"] +- "∸": [t: "⠈⠤"] +- "∼": [t: "⠀⠢"] +- "≅": [t: "⠀⠢"] +- "≈": [t: "⠀⠢⠢"] +- "≐": [t: "⠀⠒⠕"] +- "≔": [t: "⠀⠶⠒"] +- "≕": [t: "⠀⠶⠒"] +- "≖": [t: "⠀⠶⠴"] +- "≗": [t: "⠀⠶⠴"] +- "≛": [t: "⠀⠶⠆"] +- "≟": [t: "⠶⠢"] +- "≠": [t: "⠀⠾"] +- "≡": [t: "⠀⠰⠶"] +- "≢": [t: "⠀⠰⠾"] +- "≤": [t: "⠀⠪⠶"] +- "≥": [t: "⠀⠕⠶"] +- "≪": [t: "⠀⠪⠪⠀"] +- "≫": [t: "⠀⠕⠕⠀"] +- "≺": [t: "⠀⠒⠪⠀"] +- "≻": [t: "⠀⠕⠒⠀"] +- "≼": [t: "⠀⠒⠪⠶"] +- "≽": [t: "⠀⠕⠒⠶"] +- "⊂": [t: "⠀⠯⠀"] +- "⊃": [t: "⠀⠹⠀"] +- "⊄": [t: "⠈⠯"] +- "⊅": [t: "⠈⠹"] +- "⊆": [t: "⠀⠯⠶"] +- "⊇": [t: "⠀⠹⠶"] +- "⊕": [t: "⠀⠰⠖"] +- "⊥": [t: "⠼⠄"] +- "⊨": [t: "⠼⠎"] +- "⊼": [t: "⠰⠌"] +- "⊽": [t: "⠘⠡"] +- "⋅": [t: "⠄"] +- "⋆": [t: "⠔"] +- "⋮": [t: "⠠⠲⠲⠲"] +- "⋯": [t: "⠠⠲⠲⠲"] +- "⋰": [t: "⠠⠲⠲⠲"] +- "⋱": [t: "⠠⠲⠲⠲"] +- "△": [t: "⠸⠙"] +- "◻": [t: "⠶"] +- "♮": [t: "⠻⠨⠝⠻"] + +# Latin letters and digits used for mathematical identifiers. +- "a": [t: "⠁"] +- "A": [t: "C⠁"] +- "b": [t: "⠃"] +- "B": [t: "C⠃"] +- "c": [t: "⠉"] +- "C": [t: "C⠉"] +- "d": [t: "⠙"] +- "D": [t: "C⠙"] +- "e": [t: "⠑"] +- "E": [t: "C⠑"] +- "f": [t: "⠋"] +- "F": [t: "C⠋"] +- "g": [t: "⠛"] +- "G": [t: "C⠛"] +- "h": [t: "⠓"] +- "H": [t: "C⠓"] +- "i": [t: "⠊"] +- "I": [t: "C⠊"] +- "j": [t: "⠚"] +- "J": [t: "C⠚"] +- "k": [t: "⠅"] +- "K": [t: "C⠅"] +- "l": [t: "⠇"] +- "L": [t: "C⠇"] +- "m": [t: "⠍"] +- "M": [t: "C⠍"] +- "n": [t: "⠝"] +- "N": [t: "C⠝"] +- "o": [t: "⠕"] +- "O": [t: "C⠕"] +- "p": [t: "⠏"] +- "P": [t: "C⠏"] +- "q": [t: "⠟"] +- "Q": [t: "C⠟"] +- "r": [t: "⠗"] +- "R": [t: "C⠗"] +- "s": [t: "⠎"] +- "S": [t: "C⠎"] +- "t": [t: "⠞"] +- "T": [t: "C⠞"] +- "u": [t: "⠥"] +- "U": [t: "C⠥"] +- "v": [t: "⠧"] +- "V": [t: "C⠧"] +- "w": [t: "⠺"] +- "W": [t: "C⠺"] +- "x": [t: "⠭"] +- "X": [t: "C⠭"] +- "y": [t: "⠽"] +- "Y": [t: "C⠽"] +- "z": [t: "⠵"] +- "Z": [t: "C⠵"] +- "0": [t: "N⠚"] +- "1": [t: "N⠁"] +- "2": [t: "N⠃"] +- "3": [t: "N⠉"] +- "4": [t: "N⠙"] +- "5": [t: "N⠑"] +- "6": [t: "N⠋"] +- "7": [t: "N⠛"] +- "8": [t: "N⠓"] +- "9": [t: "N⠊"] + +# Russian literary braille letters for mtext and Cyrillic identifiers. +- "а": [t: "⠁"] +- "А": [t: "C⠁"] +- "б": [t: "⠃"] +- "Б": [t: "C⠃"] +- "в": [t: "⠺"] +- "В": [t: "C⠺"] +- "г": [t: "⠛"] +- "Г": [t: "C⠛"] +- "д": [t: "⠙"] +- "Д": [t: "C⠙"] +- "е": [t: "⠑"] +- "Е": [t: "C⠑"] +- "ё": [t: "⠡"] +- "Ё": [t: "C⠡"] +- "ж": [t: "⠚"] +- "Ж": [t: "C⠚"] +- "з": [t: "⠵"] +- "З": [t: "C⠵"] +- "и": [t: "⠊"] +- "И": [t: "C⠊"] +- "й": [t: "⠯"] +- "Й": [t: "C⠯"] +- "к": [t: "⠅"] +- "К": [t: "C⠅"] +- "л": [t: "⠇"] +- "Л": [t: "C⠇"] +- "м": [t: "⠍"] +- "М": [t: "C⠍"] +- "н": [t: "⠝"] +- "Н": [t: "C⠝"] +- "о": [t: "⠕"] +- "О": [t: "C⠕"] +- "п": [t: "⠏"] +- "П": [t: "C⠏"] +- "р": [t: "⠗"] +- "Р": [t: "C⠗"] +- "с": [t: "⠎"] +- "С": [t: "C⠎"] +- "т": [t: "⠞"] +- "Т": [t: "C⠞"] +- "у": [t: "⠥"] +- "У": [t: "C⠥"] +- "ф": [t: "⠋"] +- "Ф": [t: "C⠋"] +- "х": [t: "⠓"] +- "Х": [t: "C⠓"] +- "ц": [t: "⠉"] +- "Ц": [t: "C⠉"] +- "ч": [t: "⠟"] +- "Ч": [t: "C⠟"] +- "ш": [t: "⠱"] +- "Ш": [t: "C⠱"] +- "щ": [t: "⠭"] +- "Щ": [t: "C⠭"] +- "ъ": [t: "⠷"] +- "Ъ": [t: "C⠷"] +- "ы": [t: "⠮"] +- "Ы": [t: "C⠮"] +- "ь": [t: "⠾"] +- "Ь": [t: "C⠾"] +- "э": [t: "⠪"] +- "Э": [t: "C⠪"] +- "ю": [t: "⠳"] +- "Ю": [t: "C⠳"] +- "я": [t: "⠫"] +- "Я": [t: "C⠫"] diff --git a/src/braille.rs b/src/braille.rs index 5b6c91cbd..1c6ff0d05 100644 --- a/src/braille.rs +++ b/src/braille.rs @@ -50,6 +50,7 @@ pub fn braille_mathml(mathml: Element, nav_node_id: &str) -> Result<(String, usi "CMU" => cmu_cleanup(pref_manager, braille_string), "Finnish" => finnish_cleanup(pref_manager, braille_string), "Swedish" => swedish_cleanup(pref_manager, braille_string), + "Russian" => russian_cleanup(pref_manager, braille_string), "LaTeX" => LaTeX_cleanup(pref_manager, braille_string), "ASCIIMath" => ASCIIMath_cleanup(pref_manager, braille_string), _ => braille_string.trim_matches('⠀').to_string(), // probably needs cleanup if someone has another code, but this will have to get added by hand @@ -2166,6 +2167,38 @@ fn swedish_cleanup(pref_manager: Ref, raw_braille: String) -> return result.to_string(); } +fn russian_cleanup(_pref_manager: Ref, raw_braille: String) -> String { + static REPLACE_INDICATORS: LazyLock = LazyLock::new(|| Regex::new(r"([BCILNW#])").unwrap()); + static COLLAPSE_SPACES: LazyLock = LazyLock::new(|| Regex::new(r"⠀+").unwrap()); + + let mut raw_braille_without_repeated_number_indicators = String::with_capacity(raw_braille.len()); + let mut previous_char_was_digit = false; + for ch in raw_braille.chars() { + if ch == 'N' && previous_char_was_digit { + previous_char_was_digit = false; + continue; + } + raw_braille_without_repeated_number_indicators.push(ch); + previous_char_was_digit = matches!(ch, '⠚' | '⠁' | '⠃' | '⠉' | '⠙' | '⠑' | '⠋' | '⠛' | '⠓' | '⠊'); + } + + let result = REPLACE_INDICATORS.replace_all(&raw_braille_without_repeated_number_indicators, |cap: &Captures| { + match &cap[0] { + "B" => "⠸", + "C" => "⠠", + "I" => "⠨", + "L" => "", + "N" => "⠼", + "W" => "⠀", + "#" => "", + _ => "", + } + }); + return COLLAPSE_SPACES.replace_all(&result, "⠀") + .trim_matches('⠀') + .to_string(); +} + #[allow(non_snake_case)] fn LaTeX_cleanup(_pref_manager: Ref, raw_braille: String) -> String { static REMOVE_SPACE: LazyLock = LazyLock::new(|| Regex::new(r" ([\^_,;)\]}])").unwrap()); // '^', '_', ',', ';', ')', ']', '}' @@ -2329,6 +2362,7 @@ impl BrailleChars { "Vietnam" => BrailleChars:: get_braille_vietnam_chars(node, text_range), "Swedish" => BrailleChars:: get_braille_ueb_chars(node, text_range), // FIX: need to figure out what to implement "Finnish" => BrailleChars:: get_braille_ueb_chars(node, text_range), // FIX: need to figure out what to implement + "Russian" => BrailleChars:: get_braille_ueb_chars(node, text_range), _ => return Err(sxd_xpath::function::Error::Other(format!("get_braille_chars: unknown braille code '{code}'"))) }; return match result { diff --git a/tests/braille.rs b/tests/braille.rs index 12b70cb87..2fc04fffc 100644 --- a/tests/braille.rs +++ b/tests/braille.rs @@ -24,6 +24,10 @@ mod braille { mod vi; } + mod Russian { + mod russian; + } + mod LaTeX { mod augenbit; mod other; diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs new file mode 100644 index 000000000..2897a3717 --- /dev/null +++ b/tests/braille/Russian/russian.rs @@ -0,0 +1,37 @@ +use crate::common::*; +use anyhow::Result; + +#[test] +fn numbers_and_operators() -> Result<()> { + let expr = r#"5+12=17"#; + test_braille("Russian", expr, "⠼⠑⠀⠖⠼⠁⠃⠀⠶⠼⠁⠛")?; + return Ok(()); +} + +#[test] +fn fraction() -> Result<()> { + let expr = r#"x2"#; + test_braille("Russian", expr, "⠆⠭⠀⠳⠼⠃⠰")?; + return Ok(()); +} + +#[test] +fn scripts_and_root() -> Result<()> { + let expr = r#"x2+y"#; + test_braille("Russian", expr, "⠭⠌⠼⠃⠱⠀⠖⠩⠱⠽⠹")?; + return Ok(()); +} + +#[test] +fn cyrillic_text() -> Result<()> { + let expr = r#"угол"#; + test_braille("Russian", expr, "⠥⠛⠕⠇")?; + return Ok(()); +} + +#[test] +fn nested_fraction_and_root() -> Result<()> { + let expr = r#"x+1yx-y"#; + test_braille("Russian", expr, "⠆⠭⠀⠖⠩⠱⠆⠼⠁⠀⠳⠽⠰⠹⠀⠳⠭⠀⠤⠽⠰")?; + return Ok(()); +} From efd6741dcb0892da0d502140ecf6d0c1ccedccd0 Mon Sep 17 00:00:00 2001 From: nsoiffer Date: Mon, 1 Jun 2026 12:03:21 -0700 Subject: [PATCH 31/37] typo in example --- tests/braille/Russian/russian.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index 2897a3717..869bf972f 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -35,3 +35,24 @@ fn nested_fraction_and_root() -> Result<()> { test_braille("Russian", expr, "⠆⠭⠀⠖⠩⠱⠆⠼⠁⠀⠳⠽⠰⠹⠀⠳⠭⠀⠤⠽⠰")?; return Ok(()); } + +#[test] +fn wikipedia_times_divide() -> Result<()> { + let expr = r#"6×7:14=3"#; + test_braille("Russian", expr, "⠼⠋⠀⠦⠼⠛⠀⠲⠼⠁⠙⠀⠶⠼⠉")?; + return Ok(()); +} + +#[test] +fn wikipedia_linear_parens() -> Result<()> { + let expr = r#"3·(97)=6"#; + test_braille("Russian", expr, "⠼⠉⠄⠣⠼⠊⠀⠤⠼⠛⠜⠀⠶⠼⠋")?; + return Ok(()); +} + +#[test] +fn wikipedia_sqrt() -> Result<()> { + let expr = r#"10000<101"#; + test_braille("Russian", expr, "⠩⠱⠼⠁⠚⠚⠚⠚⠀⠪⠀⠼⠁⠚⠁")?; + return Ok(()); +} From d7877f5610e7dd18ef74961487498cab0351ebe6 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:20:37 +0300 Subject: [PATCH 32/37] Russian Braille fixes This commit adds fixes to Russian Braille. Now the test with parentheses is passed. Also there are devision and multiplication signs have been modified according to the Russian braille specs. --- Rules/Braille/Russian/unicode.yaml | 8 +++++++- tests/braille/Russian/russian.rs | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Rules/Braille/Russian/unicode.yaml b/Rules/Braille/Russian/unicode.yaml index 1f2c733c7..a0b0dc3cd 100644 --- a/Rules/Braille/Russian/unicode.yaml +++ b/Rules/Braille/Russian/unicode.yaml @@ -4,13 +4,15 @@ - ",": [t: "⠠⠂"] - "!": [t: "⠠⠖"] - "?": [t: "⠠⠢"] -- ":": [t: "⠠⠒"] +- ":": [t: "⠀⠲"] - ";": [t: "⠠⠆"] - "{": [t: "⠠⠪"] - "}": [t: "⠠⠕"] - "+": [t: "⠀⠖"] - "-": [t: "⠀⠤"] - "=": [t: "⠀⠶"] +- "(": [t: "⠣"] +- ")": [t: "⠜"] - ">": [t: "⠀⠕⠀"] - "<": [t: "⠀⠪⠀"] - "%": [t: "⠼⠴"] @@ -92,6 +94,10 @@ - "∕": [t: "⠠⠌"] - "∖": [t: "⠀⠰⠤"] - "∗": [t: "⠔"] +- "·": [t: "⠄"] +- "×": [t: "⠀⠦"] +- "⋅": [t: "⠄"] +- "⁢": [t: "⠄"] - "∘": [t: "⠴"] - "√": [t: "⠩⠱"] - "∛": [t: "⠩⠒⠱"] diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index 869bf972f..7ccb33d48 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -29,6 +29,26 @@ fn cyrillic_text() -> Result<()> { return Ok(()); } +#[test] +fn wikipedia_linear_parens() -> Result<()> { + let expr = r#"3(9-7)=6"#; + test_braille("Russian", expr, "⠼⠉⠄⠣⠼⠊⠀⠤⠼⠛⠜⠀⠶⠼⠋")?; + return Ok(()); +} + +#[test] +fn source_arithmetic_examples() -> Result<()> { + let expr = r#"2481=1944"#; + test_braille("Russian", expr, "⠼⠃⠙⠄⠼⠓⠁⠀⠶⠼⠁⠊⠙⠙")?; + + let expr = r#"783:9=87"#; + test_braille("Russian", expr, "⠼⠛⠓⠉⠀⠲⠼⠊⠀⠶⠼⠓⠛")?; + + let expr = r#"12×35=420"#; + test_braille("Russian", expr, "⠼⠁⠃⠀⠦⠼⠉⠑⠀⠶⠼⠙⠃⠚")?; + return Ok(()); +} + #[test] fn nested_fraction_and_root() -> Result<()> { let expr = r#"x+1yx-y"#; From 48f4cc2f1cae07a4db57201a6af4c8be4ef2f06e Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:45:55 +0300 Subject: [PATCH 33/37] Braille - Latin and Greek letters fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds a layer of Latin and Greek alphabets for Russian mathematical Braille. * In unicode.yaml Latin and Greek letters now go through the internal markers of the alphabetic mode. • In braille.rs russian_cleanup turns these markers into real signs: ◦ Latin lowercase: ⠠ ◦ Latin uppercase: ⠨ ◦ Greek lowercase: ⠰ ◦ Greek uppercase: ⠸ Added tests to russian.rs to x+A+y+B, the Latin letter after the number. --- Rules/Braille/Russian/unicode.yaml | 212 ++++++++++++++--------------- src/braille.rs | 30 +++- tests/braille/Russian/russian.rs | 30 +++- 3 files changed, 160 insertions(+), 112 deletions(-) diff --git a/Rules/Braille/Russian/unicode.yaml b/Rules/Braille/Russian/unicode.yaml index a0b0dc3cd..8b72b2ba6 100644 --- a/Rules/Braille/Russian/unicode.yaml +++ b/Rules/Braille/Russian/unicode.yaml @@ -20,59 +20,59 @@ - ";": [t: "⠠⠢"] - "Ϳ": [t: "⠰⠊"] - "·": [t: "⠰"] -- "α": [t: "⠰⠁"] -- "β": [t: "⠰⠃"] -- "γ": [t: "⠰⠛"] -- "δ": [t: "⠰⠙"] -- "ε": [t: "⠰⠑"] -- "ζ": [t: "⠰⠵"] -- "η": [t: "⠰⠚"] -- "θ": [t: "⠰⠓"] -- "ι": [t: "⠰⠊"] -- "κ": [t: "⠰⠅"] -- "λ": [t: "⠰⠇"] -- "μ": [t: "⠰⠍"] -- "ν": [t: "⠰⠝"] -- "ξ": [t: "⠰⠭"] -- "ο": [t: "⠰⠕"] -- "π": [t: "⠰⠏"] -- "ρ": [t: "⠰⠗"] -- "σ": [t: "⠰⠎"] -- "τ": [t: "⠰⠞"] -- "υ": [t: "⠰⠥"] -- "φ": [t: "⠰⠋"] -- "χ": [t: "⠰⠉"] -- "ψ": [t: "⠰⠽"] -- "ω": [t: "⠰⠺"] -- "Α": [t: "⠸⠁"] -- "Β": [t: "⠸⠃"] -- "Γ": [t: "⠸⠛"] -- "Δ": [t: "⠸⠙"] -- "Ε": [t: "⠸⠑"] -- "Ζ": [t: "⠸⠵"] -- "Η": [t: "⠸⠚"] -- "Θ": [t: "⠸⠓"] -- "Ι": [t: "⠸⠊"] -- "Κ": [t: "⠸⠅"] -- "Λ": [t: "⠸⠇"] -- "Μ": [t: "⠸⠍"] -- "Ν": [t: "⠸⠝"] -- "Ξ": [t: "⠸⠭"] -- "Ο": [t: "⠸⠕"] -- "Π": [t: "⠸⠏"] -- "Ρ": [t: "⠸⠗"] -- "Σ": [t: "⠸⠎"] -- "Τ": [t: "⠸⠞"] -- "Υ": [t: "⠸⠥"] -- "Φ": [t: "⠸⠋"] -- "Χ": [t: "⠸⠉"] -- "Ψ": [t: "⠸⠽"] -- "Ω": [t: "⠸⠺"] -- "ϐ": [t: "⠰⠃"] -- "ϑ": [t: "⠰⠓"] -- "ϕ": [t: "⠰⠋"] -- "ϖ": [t: "⠰⠏"] -- "ϱ": [t: "⠰⠗"] +- "α": [t: "g⠁"] +- "β": [t: "g⠃"] +- "γ": [t: "g⠛"] +- "δ": [t: "g⠙"] +- "ε": [t: "g⠑"] +- "ζ": [t: "g⠵"] +- "η": [t: "g⠚"] +- "θ": [t: "g⠓"] +- "ι": [t: "g⠊"] +- "κ": [t: "g⠅"] +- "λ": [t: "g⠇"] +- "μ": [t: "g⠍"] +- "ν": [t: "g⠝"] +- "ξ": [t: "g⠭"] +- "ο": [t: "g⠕"] +- "π": [t: "g⠏"] +- "ρ": [t: "g⠗"] +- "σ": [t: "g⠎"] +- "τ": [t: "g⠞"] +- "υ": [t: "g⠥"] +- "φ": [t: "g⠋"] +- "χ": [t: "g⠉"] +- "ψ": [t: "g⠽"] +- "ω": [t: "g⠺"] +- "Α": [t: "v⠁"] +- "Β": [t: "v⠃"] +- "Γ": [t: "v⠛"] +- "Δ": [t: "v⠙"] +- "Ε": [t: "v⠑"] +- "Ζ": [t: "v⠵"] +- "Η": [t: "v⠚"] +- "Θ": [t: "v⠓"] +- "Ι": [t: "v⠊"] +- "Κ": [t: "v⠅"] +- "Λ": [t: "v⠇"] +- "Μ": [t: "v⠍"] +- "Ν": [t: "v⠝"] +- "Ξ": [t: "v⠭"] +- "Ο": [t: "v⠕"] +- "Π": [t: "v⠏"] +- "Ρ": [t: "v⠗"] +- "Σ": [t: "v⠎"] +- "Τ": [t: "v⠞"] +- "Υ": [t: "v⠥"] +- "Φ": [t: "v⠋"] +- "Χ": [t: "v⠉"] +- "Ψ": [t: "v⠽"] +- "Ω": [t: "v⠺"] +- "ϐ": [t: "g⠃"] +- "ϑ": [t: "g⠓"] +- "ϕ": [t: "g⠋"] +- "ϖ": [t: "g⠏"] +- "ϱ": [t: "g⠗"] - "′": [t: "⠔"] - "″": [t: "⠔⠔"] - "‴": [t: "⠔⠔⠔"] @@ -97,7 +97,7 @@ - "·": [t: "⠄"] - "×": [t: "⠀⠦"] - "⋅": [t: "⠄"] -- "⁢": [t: "⠄"] +- "⁢": [t: ""] - "∘": [t: "⠴"] - "√": [t: "⠩⠱"] - "∛": [t: "⠩⠒⠱"] @@ -163,58 +163,58 @@ - "♮": [t: "⠻⠨⠝⠻"] # Latin letters and digits used for mathematical identifiers. -- "a": [t: "⠁"] -- "A": [t: "C⠁"] -- "b": [t: "⠃"] -- "B": [t: "C⠃"] -- "c": [t: "⠉"] -- "C": [t: "C⠉"] -- "d": [t: "⠙"] -- "D": [t: "C⠙"] -- "e": [t: "⠑"] -- "E": [t: "C⠑"] -- "f": [t: "⠋"] -- "F": [t: "C⠋"] -- "g": [t: "⠛"] -- "G": [t: "C⠛"] -- "h": [t: "⠓"] -- "H": [t: "C⠓"] -- "i": [t: "⠊"] -- "I": [t: "C⠊"] -- "j": [t: "⠚"] -- "J": [t: "C⠚"] -- "k": [t: "⠅"] -- "K": [t: "C⠅"] -- "l": [t: "⠇"] -- "L": [t: "C⠇"] -- "m": [t: "⠍"] -- "M": [t: "C⠍"] -- "n": [t: "⠝"] -- "N": [t: "C⠝"] -- "o": [t: "⠕"] -- "O": [t: "C⠕"] -- "p": [t: "⠏"] -- "P": [t: "C⠏"] -- "q": [t: "⠟"] -- "Q": [t: "C⠟"] -- "r": [t: "⠗"] -- "R": [t: "C⠗"] -- "s": [t: "⠎"] -- "S": [t: "C⠎"] -- "t": [t: "⠞"] -- "T": [t: "C⠞"] -- "u": [t: "⠥"] -- "U": [t: "C⠥"] -- "v": [t: "⠧"] -- "V": [t: "C⠧"] -- "w": [t: "⠺"] -- "W": [t: "C⠺"] -- "x": [t: "⠭"] -- "X": [t: "C⠭"] -- "y": [t: "⠽"] -- "Y": [t: "C⠽"] -- "z": [t: "⠵"] -- "Z": [t: "C⠵"] +- "a": [t: "l⠁"] +- "A": [t: "u⠁"] +- "b": [t: "l⠃"] +- "B": [t: "u⠃"] +- "c": [t: "l⠉"] +- "C": [t: "u⠉"] +- "d": [t: "l⠙"] +- "D": [t: "u⠙"] +- "e": [t: "l⠑"] +- "E": [t: "u⠑"] +- "f": [t: "l⠋"] +- "F": [t: "u⠋"] +- "g": [t: "l⠛"] +- "G": [t: "u⠛"] +- "h": [t: "l⠓"] +- "H": [t: "u⠓"] +- "i": [t: "l⠊"] +- "I": [t: "u⠊"] +- "j": [t: "l⠚"] +- "J": [t: "u⠚"] +- "k": [t: "l⠅"] +- "K": [t: "u⠅"] +- "l": [t: "l⠇"] +- "L": [t: "u⠇"] +- "m": [t: "l⠍"] +- "M": [t: "u⠍"] +- "n": [t: "l⠝"] +- "N": [t: "u⠝"] +- "o": [t: "l⠕"] +- "O": [t: "u⠕"] +- "p": [t: "l⠏"] +- "P": [t: "u⠏"] +- "q": [t: "l⠟"] +- "Q": [t: "u⠟"] +- "r": [t: "l⠗"] +- "R": [t: "u⠗"] +- "s": [t: "l⠎"] +- "S": [t: "u⠎"] +- "t": [t: "l⠞"] +- "T": [t: "u⠞"] +- "u": [t: "l⠥"] +- "U": [t: "u⠥"] +- "v": [t: "l⠧"] +- "V": [t: "u⠧"] +- "w": [t: "l⠺"] +- "W": [t: "u⠺"] +- "x": [t: "l⠭"] +- "X": [t: "u⠭"] +- "y": [t: "l⠽"] +- "Y": [t: "u⠽"] +- "z": [t: "l⠵"] +- "Z": [t: "u⠵"] - "0": [t: "N⠚"] - "1": [t: "N⠁"] - "2": [t: "N⠃"] diff --git a/src/braille.rs b/src/braille.rs index 1c6ff0d05..9fd64b4cc 100644 --- a/src/braille.rs +++ b/src/braille.rs @@ -2182,7 +2182,8 @@ fn russian_cleanup(_pref_manager: Ref, raw_braille: String) - previous_char_was_digit = matches!(ch, '⠚' | '⠁' | '⠃' | '⠉' | '⠙' | '⠑' | '⠋' | '⠛' | '⠓' | '⠊'); } - let result = REPLACE_INDICATORS.replace_all(&raw_braille_without_repeated_number_indicators, |cap: &Captures| { + let result = add_russian_alphabet_indicators(&raw_braille_without_repeated_number_indicators); + let result = REPLACE_INDICATORS.replace_all(&result, |cap: &Captures| { match &cap[0] { "B" => "⠸", "C" => "⠠", @@ -2197,6 +2198,33 @@ fn russian_cleanup(_pref_manager: Ref, raw_braille: String) - return COLLAPSE_SPACES.replace_all(&result, "⠀") .trim_matches('⠀') .to_string(); + + fn add_russian_alphabet_indicators(raw_braille: &str) -> String { + let mut result = String::with_capacity(raw_braille.len()); + let mut alphabet_mode = None; + for ch in raw_braille.chars() { + match ch { + 'l' | 'u' | 'g' | 'v' => { + if alphabet_mode != Some(ch) { + result.push_str(match ch { + 'l' => "⠠", // Latin lowercase: dots 6 + 'u' => "⠨", // Latin uppercase: dots 4-6 + 'g' => "⠰", // Greek lowercase: dots 5-6 + 'v' => "⠸", // Greek uppercase: dots 4-5-6 + _ => unreachable!(), + }); + alphabet_mode = Some(ch); + } + }, + 'C' | 'N' | '#' => { + alphabet_mode = None; + result.push(ch); + }, + _ => result.push(ch), + } + } + return result; + } } #[allow(non_snake_case)] diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index 7ccb33d48..589ab9863 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -11,14 +11,14 @@ fn numbers_and_operators() -> Result<()> { #[test] fn fraction() -> Result<()> { let expr = r#"x2"#; - test_braille("Russian", expr, "⠆⠭⠀⠳⠼⠃⠰")?; + test_braille("Russian", expr, "⠆⠠⠭⠀⠳⠼⠃⠰")?; return Ok(()); } #[test] fn scripts_and_root() -> Result<()> { let expr = r#"x2+y"#; - test_braille("Russian", expr, "⠭⠌⠼⠃⠱⠀⠖⠩⠱⠽⠹")?; + test_braille("Russian", expr, "⠠⠭⠌⠼⠃⠱⠀⠖⠩⠱⠠⠽⠹")?; return Ok(()); } @@ -30,7 +30,7 @@ fn cyrillic_text() -> Result<()> { } #[test] -fn wikipedia_linear_parens() -> Result<()> { +fn wikipedia_linear_parens_flat() -> Result<()> { let expr = r#"3(9-7)=6"#; test_braille("Russian", expr, "⠼⠉⠄⠣⠼⠊⠀⠤⠼⠛⠜⠀⠶⠼⠋")?; return Ok(()); @@ -52,7 +52,27 @@ fn source_arithmetic_examples() -> Result<()> { #[test] fn nested_fraction_and_root() -> Result<()> { let expr = r#"x+1yx-y"#; - test_braille("Russian", expr, "⠆⠭⠀⠖⠩⠱⠆⠼⠁⠀⠳⠽⠰⠹⠀⠳⠭⠀⠤⠽⠰")?; + test_braille("Russian", expr, "⠆⠠⠭⠀⠖⠩⠱⠆⠼⠁⠀⠳⠠⠽⠰⠹⠀⠳⠭⠀⠤⠽⠰")?; + return Ok(()); +} + +#[test] +fn latin_alphabet_indicators() -> Result<()> { + let expr = r#"x+A+y+B=x+y+A+B"#; + test_braille("Russian", expr, "⠠⠭⠀⠖⠨⠁⠀⠖⠠⠽⠀⠖⠨⠃⠀⠶⠠⠭⠀⠖⠽⠀⠖⠨⠁⠀⠖⠃")?; + return Ok(()); +} + +#[test] +fn alphabet_indicators_after_numbers_and_greek() -> Result<()> { + let expr = r#"2x+15=23"#; + test_braille("Russian", expr, "⠼⠃⠄⠠⠭⠀⠖⠼⠁⠑⠀⠶⠼⠃⠉")?; + + let expr = r#"L=2πr"#; + test_braille("Russian", expr, "⠨⠇⠀⠶⠼⠃⠰⠏⠠⠗")?; + + let expr = r#"α+β=Α+Β"#; + test_braille("Russian", expr, "⠰⠁⠀⠖⠃⠀⠶⠸⠁⠀⠖⠃")?; return Ok(()); } @@ -73,6 +93,6 @@ fn wikipedia_linear_parens() -> Result<()> { #[test] fn wikipedia_sqrt() -> Result<()> { let expr = r#"10000<101"#; - test_braille("Russian", expr, "⠩⠱⠼⠁⠚⠚⠚⠚⠀⠪⠀⠼⠁⠚⠁")?; + test_braille("Russian", expr, "⠩⠱⠼⠁⠚⠚⠚⠚⠹⠀⠪⠀⠼⠁⠚⠁")?; return Ok(()); } From a086d2e5158b29cbe29f63803f92df55d4e76194 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:00:38 +0300 Subject: [PATCH 34/37] Braille - Roots, powers and scripts This commit adds Roots, powers and scripts according to the Russian math Braille specs. --- Rules/Braille/Russian/Russian_Rules.yaml | 63 +++++++++++++++++++++++- Rules/Braille/Russian/unicode.yaml | 12 +++++ tests/braille/Russian/russian.rs | 22 +++++++-- 3 files changed, 93 insertions(+), 4 deletions(-) diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index 1effd8c44..f717e6b68 100644 --- a/Rules/Braille/Russian/Russian_Rules.yaml +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -67,7 +67,7 @@ - name: default tag: mroot - match: "." + match: "not(*[2][self::m:mn and translate(., '0123456789', '')=''])" replace: - t: "⠩" - x: "*[2]" @@ -75,6 +75,43 @@ - x: "*[1]" - t: "⠹" +- + name: integer-index + tag: mroot + match: "*[2][self::m:mn and translate(., '0123456789', '')='']" + replace: + - t: "⠩" + - x: "BrailleChars(translate(*[2], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - t: "⠱" + - x: "*[1]" + - t: "⠹" + +- + name: simple-numeric + tag: mfrac + match: + - "*[1][self::m:mn and translate(., '0123456789', '')=''] and" + - "*[2][self::m:mn and translate(., '0123456789', '')='']" + replace: + - x: "*[1]" + - x: "BrailleChars(translate(*[2], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + +- + name: simple-algebraic + tag: mfrac + match: + - "(*[1][self::m:mi] or *[1][self::m:mn and translate(., '0123456789', '')='']) and" + - "(*[2][self::m:mi] or *[2][self::m:mn and translate(., '0123456789', '')=''])" + replace: + - x: "*[1]" + - t: "⠳" + - test: + if: "*[2][self::m:mn]" + then: + - x: "BrailleChars(translate(*[2], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + else: + - x: "*[2]" + - name: default tag: mfrac @@ -163,6 +200,18 @@ match: "*[2][translate(., \"'*`ª°²³´¹º′″‴‵‶‷⁗\",'')='']" replace: [x: "*"] +- + name: integer + tag: [msub, munder] + match: "*[2][self::m:mn and translate(., '0123456789', '')='']" + replace: + - x: "*[1]" + - test: + if: "self::m:munder" + then: [t: "⠨⠡"] + else: [t: "⠡"] + - x: "BrailleChars(translate(*[2], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - name: default tag: [msub, munder] @@ -176,6 +225,18 @@ - x: "*[2]" - t: "⠱" +- + name: integer + tag: [msup, mover] + match: "*[2][self::m:mn and translate(., '0123456789', '')='']" + replace: + - x: "*[1]" + - test: + if: "self::m:mover" + then: [t: "⠨⠌"] + else: [t: "⠌"] + - x: "BrailleChars(translate(*[2], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - name: default tag: [msup, mover] diff --git a/Rules/Braille/Russian/unicode.yaml b/Rules/Braille/Russian/unicode.yaml index 8b72b2ba6..4407eb8ec 100644 --- a/Rules/Braille/Russian/unicode.yaml +++ b/Rules/Braille/Russian/unicode.yaml @@ -226,6 +226,18 @@ - "8": [t: "N⠓"] - "9": [t: "N⠊"] +# Lowered digits used in denominators and integer scripts. +- "\ue000": [t: "⠴"] +- "\ue001": [t: "⠂"] +- "\ue002": [t: "⠆"] +- "\ue003": [t: "⠒"] +- "\ue004": [t: "⠲"] +- "\ue005": [t: "⠢"] +- "\ue006": [t: "⠖"] +- "\ue007": [t: "⠶"] +- "\ue008": [t: "⠦"] +- "\ue009": [t: "⠔"] + # Russian literary braille letters for mtext and Cyrillic identifiers. - "а": [t: "⠁"] - "А": [t: "C⠁"] diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index 589ab9863..4cdf97962 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -11,14 +11,14 @@ fn numbers_and_operators() -> Result<()> { #[test] fn fraction() -> Result<()> { let expr = r#"x2"#; - test_braille("Russian", expr, "⠆⠠⠭⠀⠳⠼⠃⠰")?; + test_braille("Russian", expr, "⠠⠭⠳⠆")?; return Ok(()); } #[test] fn scripts_and_root() -> Result<()> { let expr = r#"x2+y"#; - test_braille("Russian", expr, "⠠⠭⠌⠼⠃⠱⠀⠖⠩⠱⠠⠽⠹")?; + test_braille("Russian", expr, "⠠⠭⠌⠆⠀⠖⠩⠱⠽⠹")?; return Ok(()); } @@ -52,7 +52,23 @@ fn source_arithmetic_examples() -> Result<()> { #[test] fn nested_fraction_and_root() -> Result<()> { let expr = r#"x+1yx-y"#; - test_braille("Russian", expr, "⠆⠠⠭⠀⠖⠩⠱⠆⠼⠁⠀⠳⠠⠽⠰⠹⠀⠳⠭⠀⠤⠽⠰")?; + test_braille("Russian", expr, "⠆⠠⠭⠀⠖⠩⠱⠼⠁⠳⠠⠽⠹⠀⠳⠭⠀⠤⠽⠰")?; + return Ok(()); +} + +#[test] +fn source_simple_fractions_scripts_roots() -> Result<()> { + let expr = r#"12"#; + test_braille("Russian", expr, "⠼⠁⠆")?; + + let expr = r#"a3"#; + test_braille("Russian", expr, "⠠⠁⠳⠒")?; + + let expr = r#"b7"#; + test_braille("Russian", expr, "⠠⠃⠡⠶")?; + + let expr = r#"x3"#; + test_braille("Russian", expr, "⠩⠒⠱⠠⠭⠹")?; return Ok(()); } From 91f629dbfdd5be807b3c7e7c6db14add4c1a0413 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:14:46 +0300 Subject: [PATCH 35/37] Update Russian Braille rules and mappings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix YAML replace syntax and extend Russian braille rules: add chemical-formula-operator-bond and prefix-geometry-ops rules, broaden function-name matching (include tg/ctg variants) and add separator markers. Update unicode mappings (degree sign, invisible characters, conditional ∪ mapping, parallel/arrow glyphs) and adjust several function/operator braille outputs. Add/expand tests to cover functions, geometry, matrices and chemical formulas to validate the changes. --- Rules/Braille/Russian/Russian_Rules.yaml | 62 ++++++++++++++++-------- Rules/Braille/Russian/unicode.yaml | 16 +++++- tests/braille/Russian/russian.rs | 35 +++++++++++++ 3 files changed, 92 insertions(+), 21 deletions(-) diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index f717e6b68..6ac752bc6 100644 --- a/Rules/Braille/Russian/Russian_Rules.yaml +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -162,9 +162,7 @@ name: default tag: mtable match: "." - replace: - - t: "⠞⠃⠇" - - x: "*" + replace: [x: "*"] - name: default @@ -283,6 +281,30 @@ - x: "*" - t: "⠜" +- + name: chemical-formula-operator-bond + tag: mo + match: "@data-chemical-bond" + replace: + - test: + - if: ".='-' or .=':'" + then: [t: "⠤"] + - else_if: ".='=' or .='∷'" + then: [t: "⠦"] + - else_if: ".='≡'" + then: [t: "⠿"] + else: [x: "text()"] + +- + name: prefix-geometry-ops + tag: mo + match: + - "parent::*[count(*) = 2 and name()='mrow' and not(preceding-sibling::*)] and" + - "( .='∠' or .='△' or .='∪' )" + replace: + - x: "text()" + - t: "#" + - name: default tag: mo @@ -300,39 +322,41 @@ - name: functions tag: mi - match: "IsInDefinition(., 'Speech', 'FunctionNames')" + match: + - "IsInDefinition(., 'Speech', 'FunctionNames') or" + - ".='tg' or .='ctg' or .='arctg' or .='arcctg'" replace: - test: - if: ".='arcsin'" - then: [t: "⠫⠁⠎"] + then: [t: "⠫⠁⠎#"] - else_if: ".='arccos'" - then: [t: "⠫⠁⠉"] + then: [t: "⠫⠁⠉#"] - else_if: ".='arctg' or .='arctan'" - then: [t: "⠫⠁⠞"] + then: [t: "⠫⠁⠞#"] - else_if: ".='arcctg' or .='arccotan' or .='arccot'" - then: [t: "⠫⠁⠉⠞"] + then: [t: "⠫⠁⠉⠞#"] - else_if: ".='sin'" - then: [t: "⠫⠎"] + then: [t: "⠫⠎#"] - else_if: ".='cos'" - then: [t: "⠫⠉"] + then: [t: "⠫⠉#"] - else_if: ".='tg' or .='tan'" - then: [t: "⠫⠞"] + then: [t: "⠫⠞#"] - else_if: ".='ctg' or .='cotan' or .='cot'" - then: [t: "⠫⠉⠞"] + then: [t: "⠫⠉⠞#"] - else_if: ".='log'" - then: [t: "⠫⠇"] + then: [t: "⠫⠇#"] - else_if: ".='ln'" - then: [t: "⠫⠇⠝"] + then: [t: "⠫⠇⠝#"] - else_if: ".='lg'" - then: [t: "⠫⠇⠛"] + then: [t: "⠫⠇⠛#"] - else_if: ".='lim'" - then: [t: "⠫⠇⠍"] + then: [t: "⠫⠇⠍#"] - else_if: ".='min'" - then: [t: "⠫⠍⠝"] + then: [t: "⠫⠍⠝#"] - else_if: ".='max'" - then: [t: "⠫⠍⠭"] + then: [t: "⠫⠍⠭#"] - else_if: ".='exp'" - then: [t: "⠫⠑"] + then: [t: "⠫⠑#"] else: [x: "BrailleChars(., 'Russian')"] - diff --git a/Rules/Braille/Russian/unicode.yaml b/Rules/Braille/Russian/unicode.yaml index 4407eb8ec..885f51a6b 100644 --- a/Rules/Braille/Russian/unicode.yaml +++ b/Rules/Braille/Russian/unicode.yaml @@ -76,6 +76,7 @@ - "′": [t: "⠔"] - "″": [t: "⠔⠔"] - "‴": [t: "⠔⠔⠔"] +- "°": [t: "⠨⠴"] - "∀": [t: "⠫⠄"] - "∂": [t: "⠹"] - "∃": [t: "⠫⠢"] @@ -97,7 +98,10 @@ - "·": [t: "⠄"] - "×": [t: "⠀⠦"] - "⋅": [t: "⠄"] +- "⁡": [t: ""] - "⁢": [t: ""] +- "⁣": [t: ""] +- "⁤": [t: ""] - "∘": [t: "⠴"] - "√": [t: "⠩⠱"] - "∛": [t: "⠩⠒⠱"] @@ -106,11 +110,15 @@ - "∠": [t: "⠸⠪"] - "∣": [t: "⠸"] - "∤": [t: "⠀⠼⠀"] -- "∥": [t: "⠸⠸"] +- "∥": [t: "⠸⠸#"] - "∧": [t: "⠀⠰⠢"] - "∨": [t: "⠀⠰⠔"] - "∩": [t: "⠀⠰⠲"] -- "∪": [t: "⠀⠰⠴"] +- "∪": + - test: + if: "following-sibling::*[1][self::m:mi]" + then: [t: "⠸⠜"] + else: [t: "⠀⠰⠴"] - "∫": [t: "⠮"] - "∬": [t: "⠮⠮"] - "∭": [t: "⠮⠮⠮"] @@ -159,6 +167,10 @@ - "⋰": [t: "⠠⠲⠲⠲"] - "⋱": [t: "⠠⠲⠲⠲"] - "△": [t: "⠸⠙"] +- "→": [t: "⠀⠒⠕"] +- "⟶": [t: "⠀⠒⠕"] +- "←": [t: "⠀⠦⠶"] +- "⟵": [t: "⠀⠦⠶"] - "◻": [t: "⠶"] - "♮": [t: "⠻⠨⠝⠻"] diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index 4cdf97962..313c45326 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -112,3 +112,38 @@ fn wikipedia_sqrt() -> Result<()> { test_braille("Russian", expr, "⠩⠱⠼⠁⠚⠚⠚⠚⠹⠀⠪⠀⠼⠁⠚⠁")?; return Ok(()); } + +#[test] +fn source_functions_logs_derivatives() -> Result<()> { + let expr = r#"cosα"#; + test_braille("Russian", expr, "⠫⠉⠰⠁")?; + + let expr = r#"tgxctgx=1"#; + test_braille("Russian", expr, "⠫⠞⠠⠭⠄⠫⠉⠞⠠⠭⠀⠶⠼⠁")?; + + let expr = r#"logxylogyx=1"#; + test_braille("Russian", expr, "⠫⠇⠡⠠⠭⠱⠽⠄⠫⠇⠡⠠⠽⠱⠭⠀⠶⠼⠁")?; + + let expr = r#"y(x)=f(x)"#; + test_braille("Russian", expr, "⠠⠽⠔⠣⠭⠜⠀⠶⠋⠣⠭⠜")?; + return Ok(()); +} + +#[test] +fn source_geometry_matrix_chemistry() -> Result<()> { + let expr = r#"ABC=15°3012"#; + test_braille("Russian", expr, "⠸⠪⠨⠁⠃⠉⠀⠶⠼⠁⠑⠨⠴⠼⠉⠚⠔⠼⠁⠃⠔⠔")?; + + let expr = r#"ABCD"#; + test_braille("Russian", expr, "⠨⠁⠃⠸⠸⠨⠉⠙")?; + + let expr = r#"(abcd)"#; + test_braille("Russian", expr, "⠣⠠⠁⠀⠃⠨⠳⠉⠀⠙⠜")?; + + let expr = r#"H2O"#; + test_braille("Russian", expr, "⠨⠓⠡⠆⠕")?; + + let expr = r#"H2C=CH2"#; + test_braille("Russian", expr, "⠨⠓⠡⠆⠉⠦⠉⠓⠡⠆")?; + return Ok(()); +} From d07e35e941b91c7f7d941178bb181863cd135c8e Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:13:16 +0300 Subject: [PATCH 36/37] Add Russian language to docs Add Russian (ru) to the list of supported speech languages and update Russian documentation and translation guidance. - docs/User_guide_for_MathCAT_en.md, docs/index.md: add Russian (ru) to supported languages lists. - docs/ru/User_guide_for_MathCAT_ru.md: include Russian in language options, reorder entries and apply minor wording fixes. - docs/ru/helpers.md: clarify wording, update examples and test instructions to reference ru instead of fr, and improve translation guidance and examples (including test and cargo commands). - docs/ru/index.md: reorder supported languages and polish phrasing. These edits enable and document Russian support and improve clarity for translators and developers working on the Russian localization. --- docs/User_guide_for_MathCAT_en.md | 1 + docs/index.md | 2 +- docs/ru/User_guide_for_MathCAT_ru.md | 7 +++-- docs/ru/helpers.md | 46 ++++++++++++++-------------- docs/ru/index.md | 4 +-- 5 files changed, 31 insertions(+), 29 deletions(-) diff --git a/docs/User_guide_for_MathCAT_en.md b/docs/User_guide_for_MathCAT_en.md index c63431a81..80e19314d 100644 --- a/docs/User_guide_for_MathCAT_en.md +++ b/docs/User_guide_for_MathCAT_en.md @@ -39,6 +39,7 @@ You can choose between the following settings for speech. Below each setting you - \[English (en)\] - Spanish (es) - Indonesian (id) + - Russian (ru) - Swedish (sv) - Vietnamese (vi) - Chinese, traditional (zh-tw) diff --git a/docs/index.md b/docs/index.md index c4c713172..64b82251d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -33,7 +33,7 @@ MathCAT uses a number of heuristics that try to repair poor MathML and put it in MathCAT is under active development. DAISY is actively participating in the development effort, and contributions are welcome. MathCAT is open source, and the [GitHub repository is available here](https://github.com/daisy/MathCAT). [NVDA-specific problems with the MathCAT add-on can be reported here](https://github.com/daisy/MathCATForPython/issues). * MathCAT currently supports speech and navigation for -English, German, Spanish, Finnish, Indonesian, Norwegian, Swedish, Vietnamese, and Chinese (Traditional). +English, German, Spanish, Finnish, Indonesian, Norwegian, Russian, Swedish, Vietnamese, and Chinese (Traditional). * MathCAT's braille support includes Nemeth, UEB, CMU, Vietnamese, German/Austrian LaTeX and ASCIIMath. * An [NVDA add-on](https://addons.nvda-project.org/addons/MathCAT.en.html) exists. It should be usable as a MathPlayer replacement for those using the English version or one of the supported translations. As of NVDA 2026.1, MathCAT is built into NVDA and no download is needed. diff --git a/docs/ru/User_guide_for_MathCAT_ru.md b/docs/ru/User_guide_for_MathCAT_ru.md index 43e230816..fec001990 100644 --- a/docs/ru/User_guide_for_MathCAT_ru.md +++ b/docs/ru/User_guide_for_MathCAT_ru.md @@ -43,11 +43,12 @@ MathCAT можно настроить в соответствии с вашим - Трудности в обучении. Более краткое озвучивание. - Язык: (по умолчанию используется язык программы экранного доступа) - \[Английский (en)\] - - Испанский (es) - - Индонезийский (id) - - Шведский (sv) - Вьетнамский (vi) + - Индонезийский (id) + - Испанский (es) - Китайский, традиционное письмо (zh-tw) + - Русский (ru) + - Шведский (sv) - Стиль речи: - \[ClearSpeak.\] Выражения озвучиваются примерно так, как их произнёс бы учитель на уроке. - SimpleSpeak. Выражения озвучиваются более кратко. Иногда такое чтение может быть неоднозначным. diff --git a/docs/ru/helpers.md b/docs/ru/helpers.md index bd63b7c4e..be36a6116 100644 --- a/docs/ru/helpers.md +++ b/docs/ru/helpers.md @@ -20,19 +20,19 @@ title: Руководство переводчика и разработчика ## Переводчикам на другие языки Если вы хотите перевести MathCAT, свяжитесь с @NSoiffer. Он подготовит начальный вариант перевода, который может значительно сэкономить время. В результате будут созданы файлы в каталоге `Rules/Languages/xx`, где `xx` — код языка, например `fr`, `de` или `el`. Перевод выполняется в этом каталоге. Необходимо отредактировать четыре категории файлов: -1. `definitions.yaml`: содержит переводы числительных, в том числе количественных и порядковых. Проверьте начальный перевод и внесите необходимые исправления. Эти числительные используются, например, при озвучивании фразы «три пятых». На некотором этапе образование числительных в языках становится регулярным, поэтому некоторые списки в файле можно сократить, а некоторые следует дополнить. Подробнее см. английские комментарии в файле. +1. `definitions.yaml`: содержит переводы числительных, в том числе количественных и порядковых. Проверьте начальный перевод и внесите необходимые исправления. Эти числительные используются, например, при озвучивании фразы «три пятых». На некотором этапе образование числительных в языках становится регулярным, поэтому некоторые списки в файле можно сократить, а некоторые следует дополнить. Подробнее об этом можно узнать из английских комментариев в файле. 2. Файлы `xxx_Rules.yaml`, сейчас это `ClearSpeak_Rules.yaml` и `SimpleSpeak_Rules.yaml`. Они соответствуют разным стилям речи. Настоятельно рекомендуется сначала выбрать только один стиль. Эти файлы обычно содержат слова, описывающие структуру выражения, например «дробь» и «степень», а также связующие слова. Поскольку стили речи во многом похожи, существует также каталог `SharedRules` с общими файлами правил. Они подключаются в `ClearSpeak_Rules.yaml` и `SimpleSpeak_Rules.yaml` с помощью правил `- include: file_name`. Их тоже необходимо перевести.

-Примечание: диалог настроек MathCAT ищет файлы с именами вида `XXX_Rules.yaml` и добавляет их в раскрывающийся список для языка. Использовать имена SimpleSpeak и ClearSpeak необязательно. Если вы хотите перевести только один стиль, например SimpleSpeak, но не хотите удалять `ClearSpeak_Rules.yaml`, переименуйте его, например, в `ClearSpeak_Rules.yaml.untranslated`. +Примечание: диалог настроек MathCAT ищет файлы с именами вида `XXX_Rules.yaml` и добавляет их в комбинированный список для языка. Использовать имена SimpleSpeak и ClearSpeak необязательно. Если вы хотите перевести только один стиль, например SimpleSpeak, но не хотите удалять `ClearSpeak_Rules.yaml`, переименуйте его, например, в `ClearSpeak_Rules.yaml.untranslated`.

Эти файлы содержат автоматически созданные начальные переводы. Хотя текст уже переведён, используется ключ `t:`, а не `T:` с прописной буквы. Каждый перевод необходимо проверить и только после этого изменить ключ на вариант с прописной буквы. Подробнее об автоматическом переводе см. ниже. * В некоторых языках нет смысла произносить эквиваленты слов «the» и, возможно, «of» во фразе «the square root of x». В таком случае замените их пустыми строками. - * В некоторых языках меняется порядок слов. Переставляйте слова свободно, но внимательно следите за отступами: в YAML они значимы. + * В некоторых языках меняется порядок слов. Переставляйте слова свободно, но внимательно следите за отступами: в YAML они имеют значение. * В некоторых языках могут потребоваться слова, отсутствующие в английской версии, до или после существующих фраз. Добавляйте их при необходимости. Условное добавление выполняется с помощью `test`. Если нужна помощь, свяжитесь с @NSoiffer. - * Паузы между словами и фразами могут значительно улучшить понятность речи. Исходные паузы выбраны для английского языка. Настройте их в соответствии со звучанием синтезаторов вашего языка. Паузы легко добавлять, удалять и изменять. Их длительность масштабируется в соответствии с текущей скоростью речи. + * Паузы между словами и фразами могут значительно улучшить понятность речи. Исходные паузы выбраны для английского языка. Настройте их в соответствии со звучанием синтезаторов речи вашего языка. Паузы легко добавлять, удалять и изменять. Их длительность масштабируется в соответствии с текущей скоростью речи. 3. Файлы Unicode: `unicode.yaml` и `unicode-full.yaml`. Они содержат такие символы, как `<` и `∫`. * Начните с перевода `unicode.yaml`. В нём находится подавляющее большинство используемых математических символов. Сейчас список основан на практическом опыте, но в дальнейшем планируется уточнить его по статистике из реальных книг. В `unicode.yaml` около 270 символов, примерно 50 из которых — греческие буквы. Как и в файлах правил речи, здесь есть автоматически созданные начальные переводы. Проверьте их и измените `t:` на `T:`. Подробнее об автоматическом переводе см. ниже. * В `unicode-full.yaml` тысячи строк. Вернитесь к нему после завершения остальных переводов и работайте столько, сколько сможете: большинство этих символов встречаются только в сложной математике и даже там используются редко. Наиболее важны: @@ -57,15 +57,15 @@ __ПРИМЕЧАНИЕ__: Сейчас почти завершён перехо then: [t: "is"] - t: "greater than" ``` -При переводе на французский язык слова после `t:` заменяются примерно так: +При переводе на русский язык слова после `t:` заменяются примерно так: ``` - - "=": [T: "égale"] # 0x3d + - "=": [T: "равно"] # 0x3d - ">": # 0x3e - test: if: "$Verbosity!='Terse'" - then: [T: "est"] - - T: "supérieur à" + then: [T: ""] + - T: "больше" ``` Примечание: иногда значение `IfThenElse` не требует перевода, но ключ всё равно следует изменить, чтобы было видно, что строка проверена. В следующем примере перевод не нужен, поскольку части `then` и `else`, соответственно `count(*/*[1])` и `$LineCountTry`, не являются словами: @@ -84,15 +84,15 @@ __ПРИМЕЧАНИЕ__: Сейчас почти завершён перехо Если переводы MathPlayer и SRE различаются, выбирается вариант, совпадающий с Google Translate, а другой вариант включается в комментарий. Например: ``` - else: [t: "parenthèse gauche"] # (en: 'left paren', MathPlayer: 'parenthèse ouvrante') + else: [t: "левая круглая скобка"] # (en: 'left paren', MathPlayer: 'открывающая круглая скобка') ``` Если не совпадает ни один перевод, выбирается один из вариантов, а остальные приводятся в комментарии. Например: ``` - else: [t: "parenthèse gauche"] # (en: 'open paren', MathPlayer: 'parenthèse ouvrante', google: 'parenthèse ouverte') + else: [t: "левая круглая скобка"] # (en: 'open paren', MathPlayer: 'открывающая круглая скобка', google: 'открытая скобка') ``` Наконец, если перевод отсутствует, используется Google Translate и добавляется комментарий `google translation`. Вероятность неудачного перевода в таком случае заметно выше, поэтому внимательно проверяйте эти строки. Пример, где доступен только Google Translate: ``` - then: [t: "ligne verticale"] # (en: 'vertical line', google translation) + then: [t: "вертикальная линия"] # (en: 'vertical line', google translation) ``` @@ -100,7 +100,7 @@ __ПРИМЕЧАНИЕ__: Сейчас почти завершён перехо Если вы используете NVDA, начатый перевод можно сразу проверить. Предполагается, что дополнение MathCAT уже установлено: 1. Скопируйте новый каталог перевода в `%AppData%\nvda\addons\MathCAT\globalPlugins\MathCAT\Rules\Languages`. 2. Запустите NVDA и откройте меню настроек MathCAT: параметры NVDA, затем «Настройки MathCAT...». -3. Выберите новый язык в раскрывающемся списке `Languages`. +3. Выберите новый язык в комбинированном списке `Languages`. 4. Проверьте озвучивание. Хорошим источником примеров служат страницы Википедии. 5. Если произошла ошибка, часто выражающаяся в отсутствии речи, откройте журнал NVDA из подменю «Сервис». Ошибка должна быть указана там. Объяснение сообщений об ошибках приведено ниже. 6. MathCAT должен заметить изменение файла и перезагрузить его. Сейчас это не работает для файлов, подключённых с помощью `include`, например для файлов каталога `Shared`. После изменения такого файла перезагрузите MathCAT через «Сервис: Перезагрузить плагины» в NVDA или перезапустите NVDA. @@ -110,16 +110,16 @@ __ПРИМЕЧАНИЕ__: Сейчас почти завершён перехо ### Автоматические тесты перевода Тестирование очень важно. MathCAT написан на Rust и содержит множество автоматических тестов, использующих встроенную систему тестирования Rust. Чтобы писать и проверять собственные тесты, [скачайте и установите Rust](https://www.rust-lang.org/tools/install). Знать Rust необязательно: достаточно заменить некоторые английские строки ожидаемыми строками на вашем языке. -Предположим, что вы переводите на французский язык с кодом `fr`. +Предположим, что вы переводите на русский язык с кодом `ru`. -Сначала откройте файл `languages.rs` в каталоге тестов и добавьте строку `mod fr;` после `mod en;` или аналогичной строки другого языка. +Сначала откройте файл `languages.rs` в каталоге тестов и добавьте строку `mod ru;` после `mod en;` или аналогичной строки другого языка. В каталоге `tests\Languages` находятся файл `en.rs` и каталог `en`. -1. Скопируйте `en.rs` в `fr.rs`. -2. Скопируйте каталог `en` в `fr`. -3. Если вы выбрали только один стиль речи, например SimpleSpeak, отредактируйте `fr.rs`: удалите строки, начиная с `mod ClearSpeak {` и заканчивая соответствующей строкой `}`. Удалите подкаталог `ClearSpeak` из каталога `fr`. -4. Желательно перевести все файлы, но на начальном этапе можно ограничиться несколькими. В `fr.rs` закомментируйте каждый непереведённый файл, добавив `//` в начало строки. Например, если вы не перевели файл SimpleSpeak `geometry.yaml`, строка должна выглядеть так: `// mod geometry;`. -5. Начните редактировать файлы: сначала глобально замените `"en"` на `"fr"`, а затем замените английские строки подходящими французскими или другими строками. +1. Скопируйте `en.rs` в `ru.rs`. +2. Скопируйте каталог `en` в `ru`. +3. Если вы выбрали только один стиль речи, например SimpleSpeak, отредактируйте `ru.rs`: удалите строки, начиная с `mod ClearSpeak {` и заканчивая соответствующей строкой `}`. Удалите подкаталог `ClearSpeak` из каталога `ru`. +4. Желательно перевести все файлы, но на начальном этапе можно ограничиться несколькими. В `ru.rs` закомментируйте каждый непереведённый файл, добавив `//` в начало строки. Например, если вы не перевели файл SimpleSpeak `geometry.yaml`, строка должна выглядеть так: `// mod geometry;`. +5. Начните редактировать файлы: сначала глобально замените `"en"` на `"ru"`, а затем замените английские строки подходящими русскими или другими строками. Пример теста: ``` @@ -131,9 +131,9 @@ fn common_fraction_half() { test("en", "SimpleSpeak", expr, "1 half"); } ``` -Для французского языка строка `test` будет выглядеть так: +Для русского языка строка `test` будет выглядеть так: ``` - test("fr", "SimpleSpeak", expr, "un demi"); + test("fr", "SimpleSpeak", expr, "одна вторая"); ``` После перевода нескольких тестов запустите автоматическую проверку. @@ -143,9 +143,9 @@ cargo test Languages::en ``` Затем запустите свои тесты. В нашем примере: ``` -cargo test Languages::fr +cargo test Languages::ru ``` -MathCAT добавляет паузы. В ожидаемых строках тестов они записываются символами `,` и `;`. При необходимости добавьте или удалите эти символы в ожидаемом результате. Если паузы звучат неуместно, добавьте или удалите `pause: xxx` в соответствующем месте одного из файлов `Rules\fr`. +MathCAT добавляет паузы. В ожидаемых строках тестов они записываются символами `,` и `;`. При необходимости добавьте или удалите эти символы в ожидаемом результате. Если паузы звучат неуместно, добавьте или удалите `pause: xxx` в соответствующем месте одного из файлов `Rules\ru`. __Совет__: возможно, быстрее всего сначала запустить тесты на своём языке, не меняя ожидаемые результаты. Все тесты завершатся с ошибкой, но в сообщениях будет показана речь, созданная MathCAT на вашем языке. _Если она верна_, просто замените ей английский текст. После обработки всех ошибок снова запустите тесты. В идеальном случае ошибок больше не будет. diff --git a/docs/ru/index.md b/docs/ru/index.md index 8d06d475f..25c7d1b5a 100644 --- a/docs/ru/index.md +++ b/docs/ru/index.md @@ -8,7 +8,7 @@ title: MathCAT — библиотека, которая преобразует MathML в: -* текст для озвучивания со встроенными командами речевого синтезатора; +* текст для озвучивания со встроенными командами синтезатора речи; * Брайль (код Немета, технический вариант UEB, а в дальнейшем и другие математические системы записи Брайля); * представление для навигации по математическим выражениям, в том числе с возможностью обзора. @@ -38,7 +38,7 @@ MathCAT применяет несколько эвристик, чтобы ис MathCAT активно развивается. DAISY принимает деятельное участие в разработке, и вклад новых участников приветствуется. MathCAT распространяется с открытым исходным кодом. [Репозиторий проекта доступен на GitHub](https://github.com/daisy/MathCAT). [О проблемах дополнения MathCAT, относящихся к NVDA, можно сообщить здесь](https://github.com/daisy/MathCATForPython/issues). -* MathCAT поддерживает озвучивание и навигацию для английского, немецкого, испанского, финского, индонезийского, норвежского, шведского, вьетнамского и китайского языков (традиционное письмо). +* MathCAT поддерживает озвучивание и навигацию для английского, вьетнамского, индонезийского, испанского, китайского (традиционное письмо), немецкого, норвежского, русского, финского и шведского языков. * MathCAT поддерживает код Немета, UEB, CMU, вьетнамскую систему записи Брайля, а также немецкую и австрийскую системы записи LaTeX и ASCIIMath. * Существует [дополнение NVDA](https://addons.nvda-project.org/addons/MathCAT.en.html). Оно может заменить MathPlayer для пользователей английского языка и поддерживаемых переводов. Начиная с NVDA 2026.1 MathCAT встроен в NVDA, поэтому скачивать дополнение не нужно. From b0885ee6360087b494a5eaad7bbb28a19bd1bbf9 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Fri, 12 Jun 2026 07:54:03 +0300 Subject: [PATCH 37/37] Refactor Russian function braille mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace hardcoded function-name branches in Russian_Rules.yaml with a DefinitionValue lookup that pulls from a new RussianFunctionBraille mapping. Add RussianFunctionBraille entries to definitions.yaml (private-use codepoints U+E100–U+E10E) and map those codepoints to braille strings in unicode.yaml. Add a test covering tan/cot rendering. This centralizes function braille representations and simplifies the rule logic. --- Rules/Braille/Russian/Russian_Rules.yaml | 37 ++---------------------- Rules/Braille/Russian/definitions.yaml | 8 +++++ Rules/Braille/Russian/unicode.yaml | 15 ++++++++++ tests/braille/Russian/russian.rs | 3 ++ 4 files changed, 28 insertions(+), 35 deletions(-) diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index 6ac752bc6..2848c62f4 100644 --- a/Rules/Braille/Russian/Russian_Rules.yaml +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -322,42 +322,9 @@ - name: functions tag: mi - match: - - "IsInDefinition(., 'Speech', 'FunctionNames') or" - - ".='tg' or .='ctg' or .='arctg' or .='arcctg'" + match: "DefinitionValue(., 'Braille', 'RussianFunctionBraille') != ''" replace: - - test: - - if: ".='arcsin'" - then: [t: "⠫⠁⠎#"] - - else_if: ".='arccos'" - then: [t: "⠫⠁⠉#"] - - else_if: ".='arctg' or .='arctan'" - then: [t: "⠫⠁⠞#"] - - else_if: ".='arcctg' or .='arccotan' or .='arccot'" - then: [t: "⠫⠁⠉⠞#"] - - else_if: ".='sin'" - then: [t: "⠫⠎#"] - - else_if: ".='cos'" - then: [t: "⠫⠉#"] - - else_if: ".='tg' or .='tan'" - then: [t: "⠫⠞#"] - - else_if: ".='ctg' or .='cotan' or .='cot'" - then: [t: "⠫⠉⠞#"] - - else_if: ".='log'" - then: [t: "⠫⠇#"] - - else_if: ".='ln'" - then: [t: "⠫⠇⠝#"] - - else_if: ".='lg'" - then: [t: "⠫⠇⠛#"] - - else_if: ".='lim'" - then: [t: "⠫⠇⠍#"] - - else_if: ".='min'" - then: [t: "⠫⠍⠝#"] - - else_if: ".='max'" - then: [t: "⠫⠍⠭#"] - - else_if: ".='exp'" - then: [t: "⠫⠑#"] - else: [x: "BrailleChars(., 'Russian')"] + - x: "DefinitionValue(., 'Braille', 'RussianFunctionBraille')" - name: default diff --git a/Rules/Braille/Russian/definitions.yaml b/Rules/Braille/Russian/definitions.yaml index 4d0c77efb..c2fe639ed 100644 --- a/Rules/Braille/Russian/definitions.yaml +++ b/Rules/Braille/Russian/definitions.yaml @@ -1,2 +1,10 @@ --- - include: "../definitions.yaml" + +- RussianFunctionBraille: { + "arcsin": "\uE100", "arccos": "\uE101", "arctg": "\uE102", "arctan": "\uE102", + "arcctg": "\uE103", "arccotan": "\uE103", "arccot": "\uE103", + "sin": "\uE104", "cos": "\uE105", "tg": "\uE106", "tan": "\uE106", "ctg": "\uE107", "cotan": "\uE107", "cot": "\uE107", + "log": "\uE108", "ln": "\uE109", "lg": "\uE10A", + "lim": "\uE10B", "min": "\uE10C", "max": "\uE10D", "exp": "\uE10E" + } diff --git a/Rules/Braille/Russian/unicode.yaml b/Rules/Braille/Russian/unicode.yaml index 885f51a6b..37d64bd97 100644 --- a/Rules/Braille/Russian/unicode.yaml +++ b/Rules/Braille/Russian/unicode.yaml @@ -102,6 +102,21 @@ - "⁢": [t: ""] - "⁣": [t: ""] - "⁤": [t: ""] +- "\uE100": [t: "⠫⠁⠎#"] +- "\uE101": [t: "⠫⠁⠉#"] +- "\uE102": [t: "⠫⠁⠞#"] +- "\uE103": [t: "⠫⠁⠉⠞#"] +- "\uE104": [t: "⠫⠎#"] +- "\uE105": [t: "⠫⠉#"] +- "\uE106": [t: "⠫⠞#"] +- "\uE107": [t: "⠫⠉⠞#"] +- "\uE108": [t: "⠫⠇#"] +- "\uE109": [t: "⠫⠇⠝#"] +- "\uE10A": [t: "⠫⠇⠛#"] +- "\uE10B": [t: "⠫⠇⠍#"] +- "\uE10C": [t: "⠫⠍⠝#"] +- "\uE10D": [t: "⠫⠍⠭#"] +- "\uE10E": [t: "⠫⠑#"] - "∘": [t: "⠴"] - "√": [t: "⠩⠱"] - "∛": [t: "⠩⠒⠱"] diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index 313c45326..e19a963bc 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -121,6 +121,9 @@ fn source_functions_logs_derivatives() -> Result<()> { let expr = r#"tgxctgx=1"#; test_braille("Russian", expr, "⠫⠞⠠⠭⠄⠫⠉⠞⠠⠭⠀⠶⠼⠁")?; + let expr = r#"tanx+cotx"#; + test_braille("Russian", expr, "⠫⠞⠠⠭⠀⠖⠫⠉⠞⠠⠭")?; + let expr = r#"logxylogyx=1"#; test_braille("Russian", expr, "⠫⠇⠡⠠⠭⠱⠽⠄⠫⠇⠡⠠⠽⠱⠭⠀⠶⠼⠁")?;