diff --git a/.gitignore b/.gitignore index db15d2058..fef5d915c 100644 --- a/.gitignore +++ b/.gitignore @@ -37,7 +37,6 @@ PythonScripts/encrypt.py PythonScripts/intent-coverage.py PythonScripts/PythonScripts.zip PythonScripts/google-defs.yaml -BrailleDocs src/errors.rs src/pattern.pest @@ -57,4 +56,4 @@ Rules/*/*/*.zip # local caches when building Jekyll docs/_site docs/.jekyll-cache -docs/Gemfile.lock \ No newline at end of file +docs/Gemfile.lock diff --git a/BrailleDocs/B2. Signografia basica de las lenguas cooficiales espanolas V3.pdf b/BrailleDocs/B2. Signografia basica de las lenguas cooficiales espanolas V3.pdf new file mode 100644 index 000000000..3322b721a Binary files /dev/null and b/BrailleDocs/B2. Signografia basica de las lenguas cooficiales espanolas V3.pdf differ diff --git a/BrailleDocs/BrailleMathCodes Repository.xlsx b/BrailleDocs/BrailleMathCodes Repository.xlsx new file mode 100644 index 000000000..ef6358de8 Binary files /dev/null and b/BrailleDocs/BrailleMathCodes Repository.xlsx differ diff --git a/BrailleDocs/CMU-changes-en.docx b/BrailleDocs/CMU-changes-en.docx new file mode 100644 index 000000000..b1c515226 Binary files /dev/null and b/BrailleDocs/CMU-changes-en.docx differ diff --git a/BrailleDocs/CMU-changes.docx b/BrailleDocs/CMU-changes.docx new file mode 100644 index 000000000..6c002b503 Binary files /dev/null and b/BrailleDocs/CMU-changes.docx differ diff --git a/BrailleDocs/CMU-chemistry(en).pdf b/BrailleDocs/CMU-chemistry(en).pdf new file mode 100644 index 000000000..3ee264b58 Binary files /dev/null and b/BrailleDocs/CMU-chemistry(en).pdf differ diff --git a/BrailleDocs/CMU-chemistry(es).pdf b/BrailleDocs/CMU-chemistry(es).pdf new file mode 100644 index 000000000..ea7849076 Binary files /dev/null and b/BrailleDocs/CMU-chemistry(es).pdf differ diff --git a/BrailleDocs/Codigo matematico unificado para Iberoamerica (en).pdf b/BrailleDocs/Codigo matematico unificado para Iberoamerica (en).pdf new file mode 100644 index 000000000..b3c667dc6 Binary files /dev/null and b/BrailleDocs/Codigo matematico unificado para Iberoamerica (en).pdf differ diff --git a/BrailleDocs/Codigo matematico unificado para Iberoamerica V2 fuente guia enmarcada-en.pdf b/BrailleDocs/Codigo matematico unificado para Iberoamerica V2 fuente guia enmarcada-en.pdf new file mode 100644 index 000000000..f3dfdf9bb Binary files /dev/null and b/BrailleDocs/Codigo matematico unificado para Iberoamerica V2 fuente guia enmarcada-en.pdf differ diff --git a/BrailleDocs/Codigo matematico unificado para Iberoamerica V2 fuente guia enmarcada.pdf b/BrailleDocs/Codigo matematico unificado para Iberoamerica V2 fuente guia enmarcada.pdf new file mode 100644 index 000000000..0b4470fdb Binary files /dev/null and b/BrailleDocs/Codigo matematico unificado para Iberoamerica V2 fuente guia enmarcada.pdf differ diff --git a/BrailleDocs/Codigo matematico unificado para Iberoamerica.pdf b/BrailleDocs/Codigo matematico unificado para Iberoamerica.pdf new file mode 100644 index 000000000..0fdafb629 Binary files /dev/null and b/BrailleDocs/Codigo matematico unificado para Iberoamerica.pdf differ diff --git a/BrailleDocs/EN_Finnish braille for math, phy and chem_2.pdf b/BrailleDocs/EN_Finnish braille for math, phy and chem_2.pdf new file mode 100644 index 000000000..0ea99b69b Binary files /dev/null and b/BrailleDocs/EN_Finnish braille for math, phy and chem_2.pdf differ diff --git a/BrailleDocs/EuroBraille-abbrevs-Mathlib.tex b/BrailleDocs/EuroBraille-abbrevs-Mathlib.tex new file mode 100644 index 000000000..a8b5d27fe --- /dev/null +++ b/BrailleDocs/EuroBraille-abbrevs-Mathlib.tex @@ -0,0 +1,281 @@ +% +% LaTeX- Vereinfachungen / U.Nitsch 1994 +% ---------------------------------------- +% Liste aller Kommandos +% ----------------------- +% Kommando anstelle von ... Bedeutung +% +% <= \le kleiner gleich +% << \ll viel kleiner +% >= \ge groesser gleich +% >> \gg viel groesser +% ~a ... \alpha ... kleine griechische Buchstaben +% ~A ... \Alpha ... grosse griechische Buchstaben +% \8 \infty unendlich +% \apx \approx etwa +% \be \begin{equation} +% \bea \begin{eqnarray} +% \beas \begin{eqnarray*} +% \bs \backslash Backslash +% \cd \cdots mittige Dots +% \da \downarrow einfacher Pfeil nach unten +% \dd \ddots diagonale Dots +% \Do \o kleines daenisches 'o' +% \ee \end{equation} +% \eea \end{eqnarray} +% \eeas \end{eqnarray*} +% \eqv \equiv aequivalent +% \es \emptyset leere Menge +% \ex \exists es existiert +% \f \frac Bruch +% \fa \forall fuer alle +% \fmf Rahmen um einfache Formel +% \fsf Rahmen um mehrzeilige Formel +% \inn \int\nolimits Integral nolimits +% \inl \int\limits Integral limits +% \l< \langle Winkelklammer links +% \la \leftarrow einfacher Pfeil nach links +% \ld \ldots line Dots +% \lgla \longleftarrow langer Pfeil nach links +% \lglra \longleftrightarrow langer Pfeil nach links und rechts +% \lgmt \longmapsto langer folgt-aus-Pfeil +% \lgra \longrightarrow langer Pfeil nach rechts +% \lra \leftrightarrow einfacher Pfeil nach links und rechts +% \Lra \leftrightarrow doppelter Pfeil nach links und rechts +% \lt \leadsto fuehrt-zu-Pfeil +% \mt \mapsto folgt-aus-Pfeil +% \n \not Negation +% \o+ \oplus +% \o- \ominus +% \o. \opoint +% \o/ \oslash +% \oin \oint\nolimits Kreisintegral nolimits +% \oil \oint\limits Kreisintegral limits +% \ol \overline Ueberstrich +% \ox \otimes +% \pn \prod\nolimits Produkt nolimits +% \pl \prod\limits Produkt limits +% \Pl \l kleines polnisches 'l' +% \pll \parallel parallel +% \r> \rangle Winkelklammer rechts +% \ra \rightarrow einfacher Pfeil nach rechts +% \Ra \rightarrow doppelter Pfeil nach rechts +% \s \sqrt Wurzel +% \sbs \subset Untermenge +% \sbse \subseteq Untermenge und gleich +% \sps \supset Obermenge +% \spse \supseteq Obermenge und gleich +% \sun \sum\nolimits Summe nolimits +% \sul \sum\limits Summe limits +% \tri \triangle Dreieck +% \ua \uparrow einfacher Pfeil nach unten +% \uda \updownarrow einfacher Pfeil nach unten und oben +% \ul \underline Unterstrich +% \vd \vdots vertikale Dots +% \x \times +% +% Noetige Umdefinitionen +% ------------------------ +% Rettung der Symbole von \o und \l +% +\newsavebox{\boxA} +\sbox{\boxA}{\o} +\def\Do{\usebox{\boxA}} +\newsavebox{\boxB} +\sbox{\boxB}{\l} +\def\Pl{\usebox{\boxB}} +% +% Mathematische Umgebungen +% -------------------------- +% Math- und Mathindent-Umgebung nicht weiter beachtet +% +\def\be{\begin{equation}} +\def\ee{\end{equation}} +\def\bea{\begin{eqnarray}} +\def\eea{\end{eqnarray}} +\def\beas{\begin{eqnarray*}} +\def\eeas{\end{eqnarray*}} +% +% Exponenten und Indizes +% ------------------------ +% keine Vereinfachungen noetig +% +% Brueche +% --------- +\def\f{\frac} +% +% Wurzeln +% --------- +\def\s{\sqrt} +% +% Auslassungen +% -------------- +\def\ld{\ldots} +\def\cd{\cdots} +\def\vd{\vdots} +\def\dd{\ddots} +% +% Griechische Buchstaben +% ------------------------ +% Die verkuerzenden Schreibweisen der griech. Buchstaben sind aus der +% "Systematik der internat. Math.-schrift fuer Blinde" abgeleitet. +% +\catcode`\~=\active +\def~#1{\ifx#1a\alpha\fi + \ifx#1b\beta\fi + \ifx#1g\gamma\fi + \ifx#1d\delta\fi + \ifx#1e\epsilon\fi + \ifx#1z\zeta\fi + \ifx#1j\eta\fi + \ifx#1h\theta\fi + \ifx#1i\iota\fi + \ifx#1k\kappa\fi + \ifx#1l\lambda\fi + \ifx#1m\mu\fi + \ifx#1n\nu\fi + \ifx#1x\xi\fi + \ifx#1o o\fi + \ifx#1p\pi\fi + \ifx#1r\rho\fi + \ifx#1s\sigma\fi + \ifx#1t\tau\fi + \ifx#1u\upsilon\fi + \ifx#1f\phi\fi + \ifx#1c\chi\fi + \ifx#1y\psi\fi + \ifx#1w\omega\fi + \ifx#1A A\fi + \ifx#1B B\fi + \ifx#1G\Gamma\fi + \ifx#1D\Delta\fi + \ifx#1E E\fi + \ifx#1Z Z\fi + \ifx#1J H\fi + \ifx#1H\Theta\fi + \ifx#1I I\fi + \ifx#1K K\fi + \ifx#1L\Lambda\fi + \ifx#1M M\fi + \ifx#1N N\fi + \ifx#1X\Xi\fi + \ifx#1O O\fi + \ifx#1P\Pi\fi + \ifx#1R P\fi + \ifx#1S\Sigma\fi + \ifx#1T T\fi + \ifx#1U\Upsilon\fi + \ifx#1F\Phi\fi + \ifx#1C X\fi + \ifx#1Y\Psi\fi + \ifx#1W\Omega\fi} +% +% Zweistellige Operatoren +% ------------------------- +% es wird mit dem daenischen 'o' konkurriert (wurde darum umbenannt) +% +\def\o#1{\ifx#1+\oplus\fi + \ifx#1-\ominus\fi + \ifx#1/\oslash\fi + \ifx#1.\odot\fi} +\def\ox{\otimes} +% +% Relationen +% ------------ +\newsavebox{\boxC} +\sbox{\boxC}{$<$} +\catcode`\<=\active +\def<#1{\ifx#1=\le + \else\ifx#1<\ll + \else\usebox{\boxC}#1\fi\fi} +\newsavebox{\boxD} +\sbox{\boxD}{$>$} +\catcode`\>=\active +\def>#1{\ifx#1=\ge + \else\ifx#1>\gg + \else\usebox{\boxD}#1\fi\fi} +\def\n{\not} +\def\sbs{\subset} +\def\sps{\supset} +\def\sbse{\subseteq} +\def\spse{\supseteq} +\def\tri{\triangle} +\def\apx{\approx} +\def\eqv{\equiv} +\def\pll{\parallel} +% +% Pfeil- oder Zeigersymbole +% --------------------------- +% die wichtigsten Symbole wurden verkuerzt +% +\def\la{\leftarrow} +\def\ra{\rightarrow} +\def\Ra{\Rightarrow} +\def\ua{\uparrow} +\def\da{\downarrow} +\def\lra{\leftrightarrow} +\def\Lra{\leftrightarrow} +\def\uda{\updownarrow} +\def\lgla{\longleftarrow} +\def\lgra{\longrightarrow} +\def\lglra{\longleftrightarrow} +\def\mt{\mapsto} +\def\lgmt{\longmapsto} +\def\lt{\leadsto} +% +% Vermischte Symbole +% -------------------- +\def\8{\infty} +\def\es{\emptyset} +\def\fa{\forall} +\def\ex{\exists} +\def\x{\times} +% +% Symbole mit unterschiedlichen Groessen und Grenzangaben +% --------------------------------------------------------- +% viele moegliche Abkuerzungen sind schon in Gebrauch +% +\def\inl{\int\limits} +\def\inn{\int\nolimits} +\def\oil{\oint\limits} +\def\oin{\oint\nolimits} +\def\pl{\prod\limits} +\def\pn{\prod\nolimits} +\def\sul{\sum\limits} +\def\sun{\sum\nolimits} +% +% Funktionen +% ------------ +% keine Aenderungen empfehlenswert +% +% Begrenzer +% ----------- +% +\def\l<{\langle} +\def\r>{\rangle} +\def\bs{\backslash} +% +% Diakritische Zeichen +% ---------------------- +% Akzente, zu selten benoetigt +% +\def\ol{\overline} +\def\ul{\underline} +% +% Gestaltungsmittel +% ------------------- +% Rahmen um Formeln, Determinanten, Matrizen +% +\def\fsf#1{\fbox{$#1$}} +\def\fmf#1{\fbox{$\begin{array}{rcl}#1\end{array}$}} +% +\def\dtm#1#2{\left|\begin{array}{#1}#2\end{array}\right|} +\def\mat#1{\left(\begin{pmatrix}#1\end{pmatrix}\right)} +\def\kla#1#2{\left\{\begin{array}{#1}#2\end{array}\right.} +% +% Anmerkung +% ----------- +% Die in den Saveboxen abgelegten Symbole werden auch nach +% Schriftgroessenaenderungen in der Groesse dargestellt, die +% bei der Einspeicherung gegolten hat. +% diff --git a/BrailleDocs/Mathematik zum Ausdrucken SS (de).pdf b/BrailleDocs/Mathematik zum Ausdrucken SS (de).pdf new file mode 100644 index 000000000..e632d3768 Binary files /dev/null and b/BrailleDocs/Mathematik zum Ausdrucken SS (de).pdf differ diff --git a/BrailleDocs/Mathematik zum Ausdrucken SS (en).pdf b/BrailleDocs/Mathematik zum Ausdrucken SS (en).pdf new file mode 100644 index 000000000..9d62997d7 Binary files /dev/null and b/BrailleDocs/Mathematik zum Ausdrucken SS (en).pdf differ diff --git a/BrailleDocs/Nemeth UEB Rulebook May 2020 (Provisional).pdf b/BrailleDocs/Nemeth UEB Rulebook May 2020 (Provisional).pdf new file mode 100644 index 000000000..49b8c4339 Binary files /dev/null and b/BrailleDocs/Nemeth UEB Rulebook May 2020 (Provisional).pdf differ diff --git a/BrailleDocs/Notation-Math-Braille_jan2007_2024_FR_1-02.docx b/BrailleDocs/Notation-Math-Braille_jan2007_2024_FR_1-02.docx new file mode 100644 index 000000000..19971f7c0 Binary files /dev/null and b/BrailleDocs/Notation-Math-Braille_jan2007_2024_FR_1-02.docx differ diff --git a/BrailleDocs/Swedish-braille(en).pdf b/BrailleDocs/Swedish-braille(en).pdf new file mode 100644 index 000000000..f41bbcd3e Binary files /dev/null and b/BrailleDocs/Swedish-braille(en).pdf differ diff --git a/BrailleDocs/Swedish-braille(sv).pdf b/BrailleDocs/Swedish-braille(sv).pdf new file mode 100644 index 000000000..eed7df18a Binary files /dev/null and b/BrailleDocs/Swedish-braille(sv).pdf differ diff --git a/BrailleDocs/Vietnamese.Braille.Rules.for.Maths.and.Chemistry.docx b/BrailleDocs/Vietnamese.Braille.Rules.for.Maths.and.Chemistry.docx new file mode 100644 index 000000000..b534287ef Binary files /dev/null and b/BrailleDocs/Vietnamese.Braille.Rules.for.Maths.and.Chemistry.docx differ diff --git a/BrailleDocs/Vietnamese.Uncontracted.Braille.Update.in.MathCAT.docx b/BrailleDocs/Vietnamese.Uncontracted.Braille.Update.in.MathCAT.docx new file mode 100644 index 000000000..c6ee7b3e8 Binary files /dev/null and b/BrailleDocs/Vietnamese.Uncontracted.Braille.Update.in.MathCAT.docx differ diff --git a/BrailleDocs/brajlowska_notacja_en_matematyczna_fizyczna_chemiczna.pdf b/BrailleDocs/brajlowska_notacja_en_matematyczna_fizyczna_chemiczna.pdf new file mode 100644 index 000000000..03c8a946b Binary files /dev/null and b/BrailleDocs/brajlowska_notacja_en_matematyczna_fizyczna_chemiczna.pdf differ diff --git a/BrailleDocs/brajlowska_notacja_matematyczna_fizyczna_chemiczna en-US.pdf b/BrailleDocs/brajlowska_notacja_matematyczna_fizyczna_chemiczna en-US.pdf new file mode 100644 index 000000000..e2275f762 Binary files /dev/null and b/BrailleDocs/brajlowska_notacja_matematyczna_fizyczna_chemiczna en-US.pdf differ diff --git a/BrailleDocs/brajlowska_notacja_matematyczna_fizyczna_chemiczna.pdf b/BrailleDocs/brajlowska_notacja_matematyczna_fizyczna_chemiczna.pdf new file mode 100644 index 000000000..1def1d75a Binary files /dev/null and b/BrailleDocs/brajlowska_notacja_matematyczna_fizyczna_chemiczna.pdf differ diff --git a/BrailleDocs/desktop.ini b/BrailleDocs/desktop.ini new file mode 100644 index 000000000..f466cdee6 --- /dev/null +++ b/BrailleDocs/desktop.ini @@ -0,0 +1,5 @@ +[LocalizedFileNames] +B2. Signografia basica de las lenguas cooficiales espanolas V3.pdf=@B2. Signografia basica de las lenguas cooficiales espanolas V3.pdf,0 +Codigo matematico unificado para Iberoamerica (en).pdf=@Codigo matematico unificado para Iberoamerica (en).pdf,0 +BrailleMathCodes Repository.xlsx=@BrailleMathCodes Repository.xlsx,0 +Vietnamese.Uncontracted.Braille.Update.in.MathCAT.docx=@Vietnamese.Uncontracted.Braille.Update.in.MathCAT.docx,0 diff --git a/BrailleDocs/nemeth-code-mathematics-science-notation-1972-revision.pdf b/BrailleDocs/nemeth-code-mathematics-science-notation-1972-revision.pdf new file mode 100644 index 000000000..f3f09c983 Binary files /dev/null and b/BrailleDocs/nemeth-code-mathematics-science-notation-1972-revision.pdf differ diff --git a/BrailleDocs/nemeth1972-ocr.pdf b/BrailleDocs/nemeth1972-ocr.pdf new file mode 100644 index 000000000..92763d628 Binary files /dev/null and b/BrailleDocs/nemeth1972-ocr.pdf differ diff --git a/BrailleDocs/notation_braille_chimie2008.fr.en.docx b/BrailleDocs/notation_braille_chimie2008.fr.en.docx new file mode 100644 index 000000000..068b825d6 Binary files /dev/null and b/BrailleDocs/notation_braille_chimie2008.fr.en.docx differ diff --git a/BrailleDocs/notation_braille_chimie2008.pdf b/BrailleDocs/notation_braille_chimie2008.pdf new file mode 100644 index 000000000..889449b6b Binary files /dev/null and b/BrailleDocs/notation_braille_chimie2008.pdf differ diff --git a/BrailleDocs/notation_mathematique_braille2_0 en-US.pdf b/BrailleDocs/notation_mathematique_braille2_0 en-US.pdf new file mode 100644 index 000000000..1577733d7 Binary files /dev/null and b/BrailleDocs/notation_mathematique_braille2_0 en-US.pdf differ diff --git a/BrailleDocs/notation_mathematique_braille2_0.fr.en.docx b/BrailleDocs/notation_mathematique_braille2_0.fr.en.docx new file mode 100644 index 000000000..6635ef707 Binary files /dev/null and b/BrailleDocs/notation_mathematique_braille2_0.fr.en.docx differ diff --git a/BrailleDocs/notation_mathematique_braille2_0.pdf b/BrailleDocs/notation_mathematique_braille2_0.pdf new file mode 100644 index 000000000..8b5a83a17 Binary files /dev/null and b/BrailleDocs/notation_mathematique_braille2_0.pdf differ diff --git a/BrailleDocs/~$etnamese.Braille.Rules.for.Maths.and.Chemistry.docx b/BrailleDocs/~$etnamese.Braille.Rules.for.Maths.and.Chemistry.docx new file mode 100644 index 000000000..7482c6b73 Binary files /dev/null and b/BrailleDocs/~$etnamese.Braille.Rules.for.Maths.and.Chemistry.docx differ 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')" 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..c76da6b2c 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,9 @@ 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 +129,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 +140,12 @@ 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}, " + f"{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 +165,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 +207,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..a78b24283 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 re import sys +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..cec10fe40 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 diff --git a/PythonScripts/frequent-ops-set.txt b/PythonScripts/frequent-ops-set.txt index caffbe906..f3fdbccd5 100644 --- a/PythonScripts/frequent-ops-set.txt +++ b/PythonScripts/frequent-ops-set.txt @@ -3,75 +3,38 @@ ')', '(', '=', - 'x', '-', '+', ',', '\u2061', - 'n', - 'i', - 't', - 'y', - 'a', - "' '", + ' ', '.', - 'm', - 's', - 'e', - 'c', - 'd', '/', '<', - 'o', '>', - 'f', - 'r', - 'b', - 'A', - 'l', '|', - 'z', - 'g', - 'k', - 'u', '[', - 'p', ']', - 'h', 'θ', 'π', '⋅', '{', - 'B', - 'v', - 'C', '→', '′', - 'P', '∫', '}', - 'T', '≤', - 'S', '×', - 'q', '∂', '"', '^', - 'w', '∞', - 'F', '𝐯', '∈', - 'R', - 'E', ':', - 'j', '°', '∑', - 'V', 'α', - 'D', '?', '…', '𝐫', @@ -83,23 +46,14 @@ 'ı', '⋯', '!', - 'L', - 'N', 'ȷ', - 'X', 'λ', - 'G', - 'I', '≥', '_', '∗', '≈', '∇', '·', - 'M', - 'H', - 'Q', - '1', '𝐱', 'β', '≠', @@ -108,36 +62,26 @@ 'ℝ', '𝐅', '𝐮', - '0', - 'U', - 'W', '$', 'ℤ', - '2', 'φ', '∩', '𝐰', '∪', '″', '𝐤', - 'Y', '∧', '÷', 'σ', - 'K', 'ϕ', '‖', '≡', - 'O', '∬', '𝐞', - '5', 'ρ', - '3', 'ω', '∣', '𝐚', - '4', '𝐧', '∨', 'ℬ', @@ -151,20 +95,15 @@ '∘', '⟹', '⊆', - '6', 'ε', '𝐢', - '8', '\u2064', 'ℕ', '∀', '⟺', 'δ', - '9', - '7', '𝐩', '𝐲', - 'Z', '⏟', '∅', '⇒', @@ -174,7 +113,6 @@ 'Σ', 'ℂ', 'τ', - 'J', '⋮', 'ℚ', '𝒫', @@ -259,7 +197,6 @@ '𝐄', 'ζ', '𝒏', - '🍎', '𝒄', '𝒯', '”', @@ -277,10 +214,8 @@ '𝐂', '↑', '𝒊', - '🍐', '𝐐', '𝕊', - '🍌', 'µ', '𝐠', '𝒎', @@ -423,4 +358,26 @@ '⊻', '〈', '〉', -} + 'Χ', + '𝐙', + '𝙲', + '≺', + '𝚛', + '↻', + '︷', + '‘', + '│', + '𝒔', + '𝑭', + '𝑳', + '𝟓', + '𝐎', + 'Ψ', + '𝙾', + '◃', + '✠', + '𝟐', + '⋊', + '𝙸', + '𝚑', +¨} diff --git a/PythonScripts/pyproject.toml b/PythonScripts/pyproject.toml index 6f919ee0c..7b9adf68d 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] @@ -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 993f3f44b..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" @@ -299,7 +303,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 +368,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" 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/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index 1effd8c44..4ab7154a5 100644 --- a/Rules/Braille/Russian/Russian_Rules.yaml +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -40,9 +40,16 @@ variables: - RowStart: "''" - RowEnd: "''" + - InSystemLeftBrace: "false()" - MatchingWhitespace: "false()" replace: [x: "*"] +- + name: system-left-brace-opener + tag: mo + match: "not(preceding-sibling::*) and following-sibling::*[1][self::m:mtable] and count(../*)=2" + replace: [t: ""] + - name: default tag: [mrow, mstyle, semantics] @@ -67,7 +74,7 @@ - name: default tag: mroot - match: "." + match: "not(*[2][self::m:mn and translate(., '0123456789', '')=''])" replace: - t: "⠩" - x: "*[2]" @@ -75,6 +82,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 @@ -110,6 +154,16 @@ - x: "*[2]/*[1]/*[1]/*[1]" - x: "*[3]" +- + name: system-left-brace-table + tag: mtable + variables: + - InSystemLeftBrace: "true()" + - RowStart: "''" + - RowEnd: "''" + match: "not(following-sibling::*) and count(../*)=2" + replace: [x: "*"] + - name: default-matrix tag: mrow @@ -125,9 +179,7 @@ name: default tag: mtable match: "." - replace: - - t: "⠞⠃⠇" - - x: "*" + replace: [x: "*"] - name: default @@ -137,7 +189,15 @@ - test: if: "preceding-sibling::*" then: [t: "⠨⠳"] - - x: $RowStart + - test: + - if: "$InSystemLeftBrace and not(preceding-sibling::*)" + then: [t: "⠏⠀"] + - else_if: "$InSystemLeftBrace and not(following-sibling::*)" + then: [t: "⠧⠀"] + - else_if: "$InSystemLeftBrace" + then: [t: "⠇⠀"] + - else: + - x: $RowStart - test: if: .[self::m:mlabeledtr] then: [x: "*[position()>1]"] @@ -163,6 +223,62 @@ match: "*[2][translate(., \"'*`ª°²³´¹º′″‴‵‶‷⁗\",'')='']" replace: [x: "*"] +- + name: chemistry-mhchem-equilibrium-arrow + tag: mover + match: + - "*[1][substring(., 1, 1)='↽'] and" + - "*[2][substring(., string-length(), 1)='⇀']" + replace: + - t: "⠀⠒⠕⠀⠦⠶" + +- + name: vector-arrow + tag: mover + match: + - "*[2][translate(., '→⟶⇀⃗', '')=''] and" + - "string-length(translate(normalize-space(*[1]), ' ', '')) > 0" + replace: + - x: "*[1]" + - test: + if: "string-length(translate(normalize-space(*[1]), ' ', '')) > 1" + then: [t: "⠨⠒⠂"] + else: [t: "⠒⠂"] + +- + name: vector-bar + tag: mover + match: + - "*[2][translate(., '¯‾_ ̄', '')=''] and" + - "string-length(translate(normalize-space(*[1]), ' ', '')) > 0" + replace: + - x: "*[1]" + - test: + if: "string-length(translate(normalize-space(*[1]), ' ', '')) > 1" + then: [t: "⠨⠒"] + else: [t: "⠒"] + +- + name: geometry-arc + tag: mover + match: "*[2][translate(., '⌒⏜︵', '')='']" + replace: + - t: "⠸⠜" + - t: "#" + - x: "*[1]" + +- + 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 +292,71 @@ - x: "*[2]" - t: "⠱" +- + name: chemistry-repeated-charge + tag: msup + match: + - "(ancestor::*[@data-chem-equation or contains(@intent, ':chemical-equation')] or" + - " *[1][self::m:mi or self::m:mtext or self::m:mrow or self::m:msub or self::m:mmultiscripts]) and" + - "*[2][self::m:mrow and count(*) > 1 and not(*[not(self::m:mo[.='+' or .='-' or .='−'])])]" + replace: + - x: "*[1]" + - t: "⠌" + - x: "BrailleChars(translate(string(count(*[2]/*)), '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - test: + if: "*[2]/*[1][.='+']" + then: [t: "⠖"] + else: [t: "⠤"] + +- + name: chemistry-charge + tag: msup + match: + - "(ancestor::*[@data-chem-equation or contains(@intent, ':chemical-equation')] or" + - " *[1][self::m:mi or self::m:mtext or self::m:mrow or self::m:msub or self::m:mmultiscripts]) and" + - "*[2][translate(., '+-−0123456789 ', '')=''] and" + - "*[2][contains(., '+') or contains(., '-') or contains(., '−')]" + replace: + - x: "*[1]" + - t: "⠌" + - test: + - if: "*[2][translate(., '+ ', '')='']" + then: + - test: + if: "string-length(translate(normalize-space(*[2]), ' ', '')) > 1" + then: + - x: "BrailleChars(translate(string(string-length(translate(normalize-space(*[2]), ' ', ''))), '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - t: "⠖" + - else_if: "*[2][translate(., '-− ', '')='']" + then: + - test: + if: "string-length(translate(normalize-space(*[2]), ' ', '')) > 1" + then: + - x: "BrailleChars(translate(string(string-length(translate(normalize-space(*[2]), ' ', ''))), '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - t: "⠤" + - else_if: "*[2][translate(substring(., 1, string-length(.) - 1), '0123456789', '')='' and substring(., string-length(.), 1)='+']" + then: + - x: "BrailleChars(translate(substring(*[2], 1, string-length(*[2]) - 1), '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - t: "⠖" + - else_if: "*[2][translate(substring(., 1, string-length(.) - 1), '0123456789', '')='' and (substring(., string-length(.), 1)='-' or substring(., string-length(.), 1)='−')]" + then: + - x: "BrailleChars(translate(substring(*[2], 1, string-length(*[2]) - 1), '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - t: "⠤" + - else: + - x: "*[2]" + +- + 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] @@ -189,6 +370,83 @@ - x: "*[2]" - t: "⠱" +- + name: large-op-limits + tag: [msubsup, munderover] + match: + - "count(*)=3 and" + - "*[1][self::m:mo][translate(., '∫∬∭∮∑∏', '')='']" + replace: + - x: "*[1]" + - test: + if: "self::m:munderover" + then: [t: "⠨⠡"] + else: [t: "⠡"] + - test: + if: "*[2][self::m:mn and translate(., '0123456789', '')='']" + then: + - x: "BrailleChars(translate(*[2], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + else: + - x: "*[2]" + - t: "⠱" + - test: + if: "self::m:munderover" + then: [t: "⠨⠌"] + else: [t: "⠌"] + - test: + if: "*[3][self::m:mn and translate(., '0123456789', '')='']" + then: + - x: "BrailleChars(translate(*[3], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + else: + - x: "*[3]" + - t: "⠱" + +- + name: chemistry-charge-with-subscript + tag: msubsup + match: + - "(ancestor::*[@data-chem-equation or contains(@intent, ':chemical-equation')] or" + - " *[1][self::m:mi or self::m:mtext or self::m:mrow]) and" + - "*[3][translate(., '+-−0123456789', '')=''] and" + - "*[3][contains(., '+') or contains(., '-') or contains(., '−')]" + replace: + - x: "*[1]" + - test: + if: "*[2][self::m:mn and translate(., '0123456789', '')='']" + then: + - t: "⠡" + - x: "BrailleChars(translate(*[2], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + else: + - t: "⠡" + - x: "*[2]" + - t: "⠱" + - t: "⠌" + - test: + - if: "*[3][translate(., '+', '')='']" + then: + - test: + if: "string-length(*[3]) > 1" + then: + - x: "BrailleChars(translate(string(string-length(*[3])), '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - t: "⠖" + - else_if: "*[3][translate(., '-−', '')='']" + then: + - test: + if: "string-length(*[3]) > 1" + then: + - x: "BrailleChars(translate(string(string-length(*[3])), '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - t: "⠤" + - else_if: "*[3][translate(substring(., 1, string-length(.) - 1), '0123456789', '')='' and substring(., string-length(.), 1)='+']" + then: + - x: "BrailleChars(translate(substring(*[3], 1, string-length(*[3]) - 1), '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - t: "⠖" + - else_if: "*[3][translate(substring(., 1, string-length(.) - 1), '0123456789', '')='' and (substring(., string-length(.), 1)='-' or substring(., string-length(.), 1)='−')]" + then: + - x: "BrailleChars(translate(substring(*[3], 1, string-length(*[3]) - 1), '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - t: "⠤" + - else: + - x: "*[3]" + - name: default tag: [msubsup, munderover] @@ -222,6 +480,75 @@ - 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: chemical-reaction-operator + tag: mo + match: + - "(@data-chem-equation-op or ancestor::*[@data-chem-equation or contains(@intent, ':chemical-equation')]) and" + - "translate(., '+-=→⟶←⟵⇌⥂⥄↑↓', '')=''" + replace: + - test: + - if: ".='+'" + then: [t: "⠀⠖"] + - else_if: ".='-'" + then: [t: "⠀⠤"] + - else_if: ".='='" + then: [t: "⠀⠶"] + - else_if: ".='→' or .='⟶'" + then: [t: "⠀⠒⠕"] + - else_if: ".='←' or .='⟵'" + then: [t: "⠀⠦⠶"] + - else_if: ".='⇌'" + then: [t: "⠀⠒⠕⠀⠦⠶"] + - else_if: ".='⥂'" + then: [t: "⠀⠒⠕⠀⠦⠶"] + - else_if: ".='⥄'" + then: [t: "⠀⠦⠶⠀⠒⠕"] + - else_if: ".='↑'" + 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: operator-functions + tag: mo + match: "DefinitionValue(., 'Braille', 'RussianFunctionBraille') != ''" + replace: + - x: "DefinitionValue(., 'Braille', 'RussianFunctionBraille')" + +- + name: div-operator-function + tag: mo + match: "contains(., 'div')" + replace: + - t: "⠫⠙⠊⠧#" + - name: default tag: mo @@ -229,6 +556,15 @@ replace: - x: "text()" +- + name: decimal-comma + tag: mn + match: "contains(., ',') and translate(., '0123456789,', '')=''" + replace: + - x: "BrailleChars(substring-before(., ','), 'Russian')" + - t: "⠂" + - x: "BrailleChars(translate(substring-after(., ','), '0123456789', '\ue020\ue021\ue022\ue023\ue024\ue025\ue026\ue027\ue028\ue029'), 'Russian')" + - name: default tag: mn @@ -238,41 +574,17 @@ - name: functions - tag: mi - match: "IsInDefinition(., 'Speech', 'FunctionNames')" + tag: [mi, mtext] + 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: div-function + tag: [mi, mo, mtext] + match: "contains(., 'div')" + replace: + - t: "⠫⠙⠊⠧#" - name: default diff --git a/Rules/Braille/Russian/definitions.yaml b/Rules/Braille/Russian/definitions.yaml index 4d0c77efb..de0492043 100644 --- a/Rules/Braille/Russian/definitions.yaml +++ b/Rules/Braille/Russian/definitions.yaml @@ -1,2 +1,18 @@ --- - 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", + "sh": "\uE110", "sinh": "\uE110", "ch": "\uE111", "cosh": "\uE111", + "th": "\uE112", "tanh": "\uE112", "cth": "\uE113", "coth": "\uE113", + "log": "\uE108", "ln": "\uE109", "lg": "\uE10A", + "lim": "\uE10B", "min": "\uE10C", "max": "\uE10D", "exp": "\uE10E", + "arg": "\uE114", "sgn": "\uE115", "det": "\uE116", + "rank": "\uE117", "rg": "\uE118", + "Re": "\uE119", "re": "\uE119", "ℜ": "\uE119", + "Im": "\uE11A", "im": "\uE11A", "ℑ": "\uE11A", + "grad": "\uE11B", "gradient": "\uE11B", "rot": "\uE11C", "div": "\uE11D", + "gcd": "\uE11E", "lcm": "\uE11F" + } diff --git a/Rules/Braille/Russian/unicode.yaml b/Rules/Braille/Russian/unicode.yaml index 1f2c733c7..53fb06938 100644 --- a/Rules/Braille/Russian/unicode.yaml +++ b/Rules/Braille/Russian/unicode.yaml @@ -4,13 +4,20 @@ - ",": [t: "⠠⠂"] - "!": [t: "⠠⠖"] - "?": [t: "⠠⠢"] -- ":": [t: "⠠⠒"] +- ":": [t: "⠀⠲"] - ";": [t: "⠠⠆"] -- "{": [t: "⠠⠪"] -- "}": [t: "⠠⠕"] +- "[": [t: "⠷"] +- "]": [t: "⠾"] +- "{": [t: "⠪"] +- "}": [t: "⠕"] +- "/": [t: "⠠⠌"] +- "\\": [t: "⠀⠰⠤"] +- "|": [t: "⠸"] - "+": [t: "⠀⠖"] - "-": [t: "⠀⠤"] - "=": [t: "⠀⠶"] +- "(": [t: "⠣"] +- ")": [t: "⠜"] - ">": [t: "⠀⠕⠀"] - "<": [t: "⠀⠪⠀"] - "%": [t: "⠼⠴"] @@ -18,62 +25,72 @@ - ";": [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: "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⠗"] +- "′": + - test: + if: "preceding-sibling::*[1][self::m:mn or .='°' or .='′']" + then: [t: "⠨⠔"] + else: [t: "⠔"] +- "″": + - test: + if: "preceding-sibling::*[1][self::m:mn or .='°' or .='′']" + then: [t: "⠨⠔⠔"] + else: [t: "⠔⠔"] - "‴": [t: "⠔⠔⠔"] +- "°": [t: "⠨⠴"] +- "℃": [t: "⠨⠴⠨⠉"] - "∀": [t: "⠫⠄"] - "∂": [t: "⠹"] - "∃": [t: "⠫⠢"] @@ -92,6 +109,55 @@ - "∕": [t: "⠠⠌"] - "∖": [t: "⠀⠰⠤"] - "∗": [t: "⠔"] +- "·": [t: "⠄"] +- "×": [t: "⠀⠦"] +- "⋅": [t: "⠄"] +- "⁡": [t: ""] +- "⁢": [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: "⠫⠑#"] +- "\uE10F": [t: "⠂"] +- "\uE110": [t: "⠫⠎⠓#"] +- "\uE111": [t: "⠫⠉⠓#"] +- "\uE112": [t: "⠫⠞⠓#"] +- "\uE113": [t: "⠫⠉⠞⠓#"] +- "\uE114": [t: "⠫⠁⠗⠛#"] +- "\uE115": [t: "⠫⠎⠛⠝#"] +- "\uE116": [t: "⠫⠙⠑⠞#"] +- "\uE117": [t: "⠫⠗⠁⠝⠅#"] +- "\uE118": [t: "⠫⠗⠛#"] +- "\uE119": [t: "⠫⠗⠑#"] +- "\uE11A": [t: "⠫⠊⠍#"] +- "\uE11B": [t: "⠫⠛⠗⠁⠙#"] +- "\uE11C": [t: "⠫⠗⠕⠞#"] +- "\uE11D": [t: "⠫⠙⠊⠧#"] +- "\uE11E": [t: "⠫⠛⠉⠙#"] +- "\uE11F": [t: "⠫⠇⠉⠍#"] +- "\ue020": [t: "⠚"] +- "\ue021": [t: "⠁"] +- "\ue022": [t: "⠃"] +- "\ue023": [t: "⠉"] +- "\ue024": [t: "⠙"] +- "\ue025": [t: "⠑"] +- "\ue026": [t: "⠋"] +- "\ue027": [t: "⠛"] +- "\ue028": [t: "⠓"] +- "\ue029": [t: "⠊"] - "∘": [t: "⠴"] - "√": [t: "⠩⠱"] - "∛": [t: "⠩⠒⠱"] @@ -100,11 +166,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: "⠮⠮⠮"] @@ -153,62 +223,69 @@ - "⋰": [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⠵"] +- "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⠃"] @@ -220,6 +297,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/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/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/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/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/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/Rules/Languages/en/definitions.yaml b/Rules/Languages/en/definitions.yaml index 8bb43d7f3..0945f9395 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,51 +10,309 @@ # 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: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", + + + ### 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=div:divergence", + "gradient": "function=del: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", } # 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: { 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')" 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/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/Rules/definitions.yaml b/Rules/definitions.yaml index 6e164d71f..07681390c 100644 --- a/Rules/definitions.yaml +++ b/Rules/definitions.yaml @@ -38,8 +38,8 @@ "arctan", "arctang", "arctanh", "exp", "Exp", "expt", "arg", "im", "Im", "ℑ", re", "Re", "ℜ", "Pr", "cis", "gcd", "lcm", - "min", "max", "deg", "det", - "dim", "hom", "ker", "tr", + "min", "max", "deg", "det", "rank", "rg", "sgn", + "dim", "hom", "ker", "tr", "rot", "div", "Dim", "Hom", "Ker", "Tr", "inf", "lim", "liminf", "limsup", "sup", "injlim", "projlim", "inj lim", "proj lim", 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, поэтому скачивать дополнение не нужно. diff --git a/docs/ru/new_translators_guide_MathCAT_revised.md b/docs/ru/new_translators_guide_MathCAT_revised.md index a44bb6408..966cea36f 100644 --- a/docs/ru/new_translators_guide_MathCAT_revised.md +++ b/docs/ru/new_translators_guide_MathCAT_revised.md @@ -220,7 +220,7 @@ fn common_fraction_tenths() -> Result<()> { Есть два основных режима речи: 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». Здесь такие слова используются, поскольку знаменатель не является простым. +* SimpleSpeak компактно озвучивает простые выражения. Например, $\frac{x}{y} + 1$ на русском языке читается как «икс разделить на игрек плюс один». Слова, обозначающие начало и конец дроби, не используются, потому что числитель и знаменатель просты. Выражение $\frac{x}{y+1}$ на русском языке читается как «дробь, икс разделить на игрек плюс один, конец дроби». Здесь такие слова используются, поскольку знаменатель не является простым. Вы можете перевести оба стиля речи или сосредоточиться на одном. В зависимости от принятого способа произнесения математических выражений в целевом языке переведённые стили могут различаться сильнее или слабее английских. @@ -264,7 +264,7 @@ MathCAT также поддерживает уровни подробности ### Адаптация и создание собственных правил -Тесты покрывают большинство правил. Однако в португальском или другом языке могут существовать грамматические особенности, которых нет в английском и которые не охвачены тестами. Поэтому полезно просмотреть все правила и убедиться в их корректности. +Тесты покрывают большинство правил. Однако в португальском или другом языке могут существовать грамматические особенности, которых нет в английском языке и которые не охвачены тестами. Поэтому полезно просмотреть все правила и убедиться в их корректности. При грамматических различиях простой замены текстовых строк недостаточно. Необходимо изменить правила или добавить новые. Для совершенно новых правил полезно написать тесты, подтверждающие правильность результата. Не бойтесь пробовать разные варианты. @@ -297,7 +297,7 @@ MathCAT также поддерживает уровни подробности - t: "squared" # phrase(7 'squared' equals 49) ``` -Это правило означает: если степень имеет показатель 2 и настройка `ClearSpeak_Exponents` равна `Auto`, следует вывести «основание в квадрате». Например, $x^2$ озвучивается как «x squared». +Это правило означает: если степень имеет показатель 2 и настройка `ClearSpeak_Exponents` равна `Auto`, следует вывести «в квадрате». Например, $x^2$ на русском языке озвучивается как «икс в квадрате». Обратите внимание: @@ -344,9 +344,9 @@ MathCAT также поддерживает уровни подробности * числитель должен быть текстовой строкой длиной более одного символа или числом, неявно умноженным на такую строку, **и** * знаменатель должен удовлетворять тем же условиям, что и числитель. -Если все условия выполнены, дробь озвучивается как «числитель над знаменателем». Если `ClearSpeak_Fractions` имеет значение `EndFrac` или `OverEndFrac`, также добавляется короткая пауза перед фразой «конец дроби». +Если все условия выполнены, дробь озвучивается как «числитель разделить на знаменатель». Если `ClearSpeak_Fractions` имеет значение `EndFrac` или `OverEndFrac`, также добавляется короткая пауза перед фразой «конец дроби». -Например, выражение $\frac{\text{meter}}{\text{second}}$ озвучивается как «meter over second». +Например, выражение $\frac{\text{meter}}{\text{second}}$ озвучивается как «метр за секунду». --- @@ -506,7 +506,7 @@ MathCAT также поддерживает уровни подробности Пользовательское тестирование Брайля так же важно, как тестирование речи. -При работе над шведским переводом тестировщики использовали NVDA вместе со средством просмотра Брайля, чтобы контролировать результат во время удалённых сеансов. Это позволяло наблюдать вывод, даже если сами рецензенты читали его на физическом дисплее Брайля. +При работе над шведским переводом тестировщики использовали NVDA вместе с просмотрщиком Брайля, чтобы контролировать результат во время удалённых сеансов. Это позволяло наблюдать вывод, даже если сами рецензенты читали его на дисплее Брайля. Как и для речи, важна не только техническая правильность. Результат должен быть читаемым, согласованным и полезным в реальной работе. 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 +
+
+ + + diff --git a/src/braille.rs b/src/braille.rs index 6b259776f..2bcba4edc 100644 --- a/src/braille.rs +++ b/src/braille.rs @@ -2183,7 +2183,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" => "⠠", @@ -2195,9 +2196,37 @@ fn russian_cleanup(_pref_manager: Ref, raw_braille: String) - _ => "", } }); + let result = result.replace("⠠⠙⠊⠧", "⠫⠙⠊⠧"); 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/src/canonicalize.rs b/src/canonicalize.rs index 89b6c6c21..6117a5791 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; } @@ -3424,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 { @@ -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/infer_intent.rs b/src/infer_intent.rs index 429e1ddac..1112d859e 100644 --- a/src/infer_intent.rs +++ b/src/infer_intent.rs @@ -101,11 +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("=") { - 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()); - }; + 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()); + } }); } } @@ -140,6 +141,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 { @@ -922,7 +924,6 @@ mod tests { #[test] fn intent_with_literals() -> Result<()> { - init_logger(); let mathml = " x "; 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/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) } } diff --git a/src/main.rs b/src/main.rs index 3a086246b..e058fe83f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -193,9 +193,10 @@ fn main() { // "; let expr = r#" - 1 kg sec 1 m 1 - - "#; + + 𝟢 + + "#; // let instant = Instant::now(); // let rules_dir = "".to_string(); // Use MathCATRulesDir, potentially pointing to a zipped version @@ -235,39 +236,44 @@ 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("ToggleZoomLockUp".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") { + Err(e) => {eprintln!("Error: exiting -- {}", errors_to_string(&e)); exit(1);}, + Ok(speech) => info!("\nZoomIn speech: '{speech}'"), + } + info!("\n"); + // 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), // } 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) { diff --git a/src/speech.rs b/src/speech.rs index 1ca431506..2545d308a 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; } @@ -2563,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) } } @@ -2643,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 { @@ -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/>"); + } + } 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(()) + +} 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 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/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<()> { diff --git a/tests/Languages/en/shared.rs b/tests/Languages/en/shared.rs index 37fd45659..ee42fc3be 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 @@ -415,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(()); 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 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] 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(()); } diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index 869bf972f..859d230ad 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(()); } @@ -29,10 +29,82 @@ fn cyrillic_text() -> Result<()> { return Ok(()); } +#[test] +fn wikipedia_linear_parens_flat() -> 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"#; - 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(()); +} + +#[test] +fn source_gost_numbers_fractions_and_sets() -> Result<()> { + let expr = r#"0,56"#; + test_braille("Russian", expr, "⠼⠚⠂⠑⠋")?; + + let expr = r#"2/3"#; + test_braille("Russian", expr, "⠼⠃⠠⠌⠼⠉")?; + + let expr = r#"[1,4]\{4}=[1,4)"#; + test_braille("Russian", expr, "⠷⠼⠁⠠⠂⠼⠙⠾⠀⠰⠤⠪⠼⠙⠕⠀⠶⠷⠼⠁⠠⠂⠼⠙⠜")?; + + let expr = r#"36,6"#; + 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(()); } @@ -53,6 +125,131 @@ 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(()); +} + +#[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#"tanx+cotx"#; + test_braille("Russian", expr, "⠫⠞⠠⠭⠀⠖⠫⠉⠞⠠⠭")?; + + let expr = r#"logxylogyx=1"#; + test_braille("Russian", expr, "⠫⠇⠡⠠⠭⠱⠽⠄⠫⠇⠡⠠⠽⠱⠭⠀⠶⠼⠁")?; + + let expr = r#"shx+chx=sinhx+coshx"#; + test_braille("Russian", expr, "⠫⠎⠓⠠⠭⠀⠖⠫⠉⠓⠠⠭⠀⠶⠫⠎⠓⠠⠭⠀⠖⠫⠉⠓⠠⠭")?; + + let expr = r#"thxcthx=1"#; + test_braille("Russian", expr, "⠫⠞⠓⠠⠭⠄⠫⠉⠞⠓⠠⠭⠀⠶⠼⠁")?; + + let expr = r#"argz=0,sgnx"#; + test_braille("Russian", expr, "⠫⠁⠗⠛⠠⠵⠀⠶⠼⠚⠠⠂⠫⠎⠛⠝⠠⠭")?; + + let expr = r#"detA+rankA=rgA"#; + test_braille("Russian", expr, "⠫⠙⠑⠞⠨⠁⠀⠖⠫⠗⠁⠝⠅⠨⠁⠀⠶⠫⠗⠛⠨⠁")?; + + let expr = r#"Rez+Imz"#; + test_braille("Russian", expr, "⠫⠗⠑⠠⠵⠀⠖⠫⠊⠍⠠⠵")?; + + let expr = r#"gradφ+rotF+divF"#; + test_braille("Russian", expr, "⠫⠛⠗⠁⠙⠰⠋⠀⠖⠫⠗⠕⠞⠨⠋⠀⠖⠫⠙⠊⠧⠨⠋")?; + + let expr = r#"gcd(a,b)=lcm(c,d)"#; + test_braille("Russian", expr, "⠫⠛⠉⠙⠣⠠⠁⠠⠂⠃⠜⠀⠶⠫⠇⠉⠍⠣⠠⠉⠠⠂⠙⠜")?; + + let expr = r#"y(x)=f(x)"#; + test_braille("Russian", expr, "⠠⠽⠔⠣⠭⠜⠀⠶⠋⠣⠭⠜")?; + return Ok(()); +} + +#[test] +fn source_large_operator_limits() -> Result<()> { + let expr = r#"14x2dx"#; + test_braille("Russian", expr, "⠮⠡⠂⠌⠲⠠⠭⠌⠆⠙⠭")?; + + let expr = r#"i=1nai"#; + test_braille("Russian", expr, "⠸⠎⠨⠡⠠⠊⠀⠶⠼⠁⠱⠨⠌⠠⠝⠱⠁⠡⠊⠱")?; + + let expr = r#"0nbk"#; + 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#"a=2"#; + test_braille("Russian", expr, "⠠⠁⠒⠂⠀⠶⠼⠃")?; + + let expr = r#"AB+BC=AC"#; + test_braille("Russian", expr, "⠨⠁⠃⠨⠒⠂⠀⠖⠃⠉⠨⠒⠂⠀⠶⠁⠉⠨⠒⠂")?; + + let expr = r#"KL¯=4PQ¯"#; + test_braille("Russian", expr, "⠨⠅⠇⠨⠒⠀⠶⠼⠙⠄⠨⠏⠟⠨⠒")?; + + let expr = r#"EF=KL"#; + test_braille("Russian", expr, "⠸⠜⠨⠑⠋⠀⠶⠸⠜⠨⠅⠇")?; + + let expr = r#"(abcd)"#; + test_braille("Russian", expr, "⠣⠠⠁⠀⠃⠨⠳⠉⠀⠙⠜")?; + + let expr = r#"{x+y+z=1x+2y+3z=2x+3y+2z=5"#; + test_braille("Russian", expr, "⠏⠀⠠⠭⠀⠖⠽⠀⠖⠵⠀⠶⠼⠁⠨⠳⠇⠀⠠⠭⠀⠖⠼⠃⠠⠽⠀⠖⠼⠉⠠⠵⠀⠶⠼⠃⠨⠳⠧⠀⠠⠭⠀⠖⠼⠉⠠⠽⠀⠖⠼⠃⠠⠵⠀⠶⠼⠑")?; + + let expr = r#"f(x)={0x<0xx0"#; + 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(()); +} + +#[test] +fn source_chemical_reactions_and_charges() -> Result<()> { + let expr = r#"2HCl+2Na2NaCl+H2"#; + test_braille("Russian", expr, "⠼⠃⠨⠓⠉⠠⠇⠀⠖⠼⠃⠨⠝⠠⠁⠀⠒⠕⠼⠃⠨⠝⠠⠁⠨⠉⠠⠇⠀⠖⠨⠓⠡⠆")?; + + let expr = r#"H2(g)+I2(g)2HI(g)"#; + test_braille("Russian", expr, "⠨⠓⠡⠆⠣⠠⠛⠜⠀⠖⠨⠊⠡⠆⠣⠠⠛⠜⠀⠒⠕⠀⠦⠶⠼⠃⠨⠓⠊⠣⠠⠛⠜")?; + + let expr = r#"2Al2Al3++6e-"#; + test_braille("Russian", expr, "⠼⠃⠨⠁⠠⠇⠀⠒⠕⠼⠃⠨⠁⠠⠇⠌⠒⠖⠀⠖⠼⠋⠠⠑⠌⠤")?; + + let expr = r#"SO42-"#; + test_braille("Russian", expr, "⠨⠎⠕⠡⠲⠌⠆⠤")?; + + let expr = r#"HPO4--"#; + test_braille("Russian", expr, "⠨⠓⠏⠕⠡⠲⠌⠆⠤")?; + return Ok(()); +} + +#[test] +fn source_gost_logic_arrows() -> Result<()> { + let expr = r#"AB"#; + test_braille("Russian", expr, "⠨⠁⠀⠶⠜⠃")?; + + let expr = r#"BA"#; + test_braille("Russian", expr, "⠨⠃⠀⠦⠶⠁")?; + + let expr = r#"CD"#; + test_braille("Russian", expr, "⠨⠉⠀⠦⠶⠜⠙")?; return Ok(()); }