From bfa449b0f250bc0ed8a0ab2a184f3c7241bccc1a Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:04:35 +0300 Subject: [PATCH 01/47] First version of Russian Braille This commit adds first druft version for Russian Braille. --- Rules/Braille/Russian/Russian_Rules.yaml | 317 +++++++++++ Rules/Braille/Russian/definitions.yaml | 2 + Rules/Braille/Russian/unicode-full.yaml | 301 ++++++++++ Rules/Braille/Russian/unicode.yaml | 289 ++++++++++ src/braille.rs | 673 ++++++++++------------- tests/braille.rs | 4 +- tests/braille/Russian/russian.rs | 37 ++ 7 files changed, 1245 insertions(+), 378 deletions(-) create mode 100644 Rules/Braille/Russian/Russian_Rules.yaml create mode 100644 Rules/Braille/Russian/definitions.yaml create mode 100644 Rules/Braille/Russian/unicode-full.yaml create mode 100644 Rules/Braille/Russian/unicode.yaml create mode 100644 tests/braille/Russian/russian.rs diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml new file mode 100644 index 000000000..1effd8c44 --- /dev/null +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -0,0 +1,317 @@ +--- +# Russian mathematical braille rules. + +- + name: whitespace + tag: "!*" + match: "not(self::m:math) and not($MatchingWhitespace) and (@data-previous-space-width >= 0.25 or @data-following-space-width >= 0.25)" + replace: + - with: + variables: [MatchingWhitespace: "true()"] + replace: + - test: + - if: "@data-previous-space-width > 1.1" + then: [t: "⠀"] + - else_if: "@data-previous-space-width >= 0.25" + then: [t: "W"] + - x: "." + - test: + - if: "@data-following-space-width > 1.1" + then: [t: "⠀"] + - else_if: "@data-following-space-width >= 0.25" + then: [t: "W"] + +- + name: omission-intent + tag: "!*" + match: "contains(@intent, ':blank')" + replace: [t: "⠀"] + +- + name: unicode-override + tag: "*" + match: "@data-unicode" + replace: [x: "@data-unicode"] + +- + name: default + tag: math + match: "." + variables: + - RowStart: "''" + - RowEnd: "''" + - MatchingWhitespace: "false()" + replace: [x: "*"] + +- + name: default + tag: [mrow, mstyle, semantics] + match: "." + replace: [x: "*[1] | *[position()>1]"] + +- + name: no-content + tag: [math, mrow] + match: "not(*)" + replace: [t: "⠀"] + +- + name: default + tag: msqrt + match: "." + replace: + - t: "⠩⠱" + - x: "*[1]" + - t: "⠹" + +- + name: default + tag: mroot + match: "." + replace: + - t: "⠩" + - x: "*[2]" + - t: "⠱" + - x: "*[1]" + - t: "⠹" + +- + name: default + tag: mfrac + match: "." + replace: + - t: "⠆" + - x: "*[1]" + - t: "⠀⠳" + - x: "*[2]" + - t: "⠰" + +- + name: binomial-frac + tag: mrow + match: "IsBracketed(., '(', ')') and *[2][self::m:mfrac][@linethickness=0]" + replace: + - x: "*[1]" + - t: "⠨⠉⠡" + - x: "*[2]/*[2]" + - t: "⠌" + - x: "*[2]/*[1]" + - x: "*[3]" + +- + name: binomial-table + tag: mrow + match: "IsBracketed(., '(', ')') and *[2][self::m:mtable][count(*)=2 and count(*[1])=1] and contains(@intent, 'binomial(')" + replace: + - x: "*[1]" + - t: "⠨⠉⠡" + - x: "*[2]/*[2]/*[1]/*[1]" + - t: "⠌" + - x: "*[2]/*[1]/*[1]/*[1]" + - x: "*[3]" + +- + name: default-matrix + tag: mrow + variables: + - RowStart: "*[1]" + - RowEnd: "*[3]" + match: + - "*[2][self::m:mtable] and" + - (IsBracketed(., '(', ')') or IsBracketed(., '[', ']') or IsBracketed(., '|', '|')) + replace: [x: "*[2]"] + +- + name: default + tag: mtable + match: "." + replace: + - t: "⠞⠃⠇" + - x: "*" + +- + name: default + tag: [mtr, mlabeledtr] + match: "." + replace: + - test: + if: "preceding-sibling::*" + then: [t: "⠨⠳"] + - x: $RowStart + - test: + if: .[self::m:mlabeledtr] + then: [x: "*[position()>1]"] + else: [x: "*"] + - x: $RowEnd + +- + name: default + tag: mtd + match: "." + replace: + - test: + if: "*" + then: + - test: + if: "preceding-sibling::*" + then: [t: "W"] + - x: "*" + +- + name: single-char-exceptions + tag: msup + match: "*[2][translate(., \"'*`ª°²³´¹º′″‴‵‶‷⁗\",'')='']" + replace: [x: "*"] + +- + name: default + tag: [msub, munder] + match: "." + replace: + - x: "*[1]" + - test: + if: "self::m:munder" + then: [t: "⠨⠡"] + else: [t: "⠡"] + - x: "*[2]" + - t: "⠱" + +- + name: default + tag: [msup, mover] + match: "." + replace: + - x: "*[1]" + - test: + if: "self::m:mover" + then: [t: "⠨⠌"] + else: [t: "⠌"] + - x: "*[2]" + - t: "⠱" + +- + name: default + tag: [msubsup, munderover] + match: "count(*)=3" + replace: + - x: "*[1]" + - test: + if: "self::m:munderover" + then: [t: "⠨⠡"] + else: [t: "⠡"] + - x: "*[2]" + - test: + if: "self::m:munderover" + then: [t: "⠨⠌"] + else: [t: "⠌"] + - x: "*[3]" + - t: "⠱" + +- + name: default + tag: mmultiscripts + match: "." + replace: [x: "*"] + +- + name: default + tag: menclose + match: "." + replace: + - t: "⠣" + - x: "*" + - t: "⠜" + +- + name: default + tag: mo + match: "." + replace: + - x: "text()" + +- + name: default + tag: mn + match: "." + replace: + - x: "BrailleChars(., 'Russian')" + +- + name: functions + tag: mi + match: "IsInDefinition(., 'Speech', 'FunctionNames')" + replace: + - test: + - if: ".='arcsin'" + then: [t: "⠫⠁⠎"] + - else_if: ".='arccos'" + then: [t: "⠫⠁⠉"] + - else_if: ".='arctg' or .='arctan'" + then: [t: "⠫⠁⠞"] + - else_if: ".='arcctg' or .='arccotan' or .='arccot'" + then: [t: "⠫⠁⠉⠞"] + - else_if: ".='sin'" + then: [t: "⠫⠎"] + - else_if: ".='cos'" + then: [t: "⠫⠉"] + - else_if: ".='tg' or .='tan'" + then: [t: "⠫⠞"] + - else_if: ".='ctg' or .='cotan' or .='cot'" + then: [t: "⠫⠉⠞"] + - else_if: ".='log'" + then: [t: "⠫⠇"] + - else_if: ".='ln'" + then: [t: "⠫⠇⠝"] + - else_if: ".='lg'" + then: [t: "⠫⠇⠛"] + - else_if: ".='lim'" + then: [t: "⠫⠇⠍"] + - else_if: ".='min'" + then: [t: "⠫⠍⠝"] + - else_if: ".='max'" + then: [t: "⠫⠍⠭"] + - else_if: ".='exp'" + then: [t: "⠫⠑"] + else: [x: "BrailleChars(., 'Russian')"] + +- + name: default + tag: [mi, mtext] + match: "." + replace: + - x: "BrailleChars(., 'Russian')" + +- + name: default + tag: ms + match: "." + replace: + - test: + if: "string(@lquote)!=''" + then: [x: "@lquote"] + else: [t: "⠦"] + - x: "BrailleChars(., 'Russian')" + - test: + if: "string(@rquote)!=''" + then: [x: "@rquote"] + else: [t: "⠴"] + +- + name: default-children + tag: "*" + match: "*" + replace: + - x: "*" + +- + name: default-no-children + tag: "*" + match: "text()" + replace: + - x: "text()" + +- + name: default-no-text + tag: "*" + match: "." + replace: [t: ""] diff --git a/Rules/Braille/Russian/definitions.yaml b/Rules/Braille/Russian/definitions.yaml new file mode 100644 index 000000000..4d0c77efb --- /dev/null +++ b/Rules/Braille/Russian/definitions.yaml @@ -0,0 +1,2 @@ +--- +- include: "../definitions.yaml" diff --git a/Rules/Braille/Russian/unicode-full.yaml b/Rules/Braille/Russian/unicode-full.yaml new file mode 100644 index 000000000..f78feac35 --- /dev/null +++ b/Rules/Braille/Russian/unicode-full.yaml @@ -0,0 +1,301 @@ +--- + - "⋇": [tc: "1⠌⠯⠦"] # 0x22C7 (Division times) + - "⊩": [tc: "⠸⠳⠿⠸⠒"] # 0x22A9 (Forces) + - "ℏ": [tc: "⠈⠒⠓"] # t: "B"F (reduced Plank's constant) + - "ⅆ": [tc: "⠙"] # 0x2146 + - "ⅇ": [tc: "⠑"] # 0x2147 + - "ⅈ": [tc: "⠊"] # 0x2148 + - "⨯": [tc: "⠐⠦"] # U+2A2F(VECTOR OR CROSS PRODUCT) -- make the same as 0x00D7 (Multiplication sign) + + + - "¼": [tc: "#N⠁N⠌N⠙"] # 0x00BC (Vulgar Fraction One Quarter) + - "½": [tc: "#N⠁N⠌N⠃"] # 0x00BD (Vulgar Fraction One Half) + - "¾": [tc: "#N⠉N⠌N⠙"] # 0x00BE (Vulgar Fraction Three Quarters) + - "⅐": [tc: "#N⠁N⠌N⠛"] # 0x2150 (Vulgar Fraction One Seventh) + - "⅑": [tc: "#N⠁N⠌N⠊"] # 0x2151 (Vulgar Fraction One Ninth) + - "⅒": [tc: "#N⠁N⠌N⠁N⠚"] # 0x2152 (Vulgar Fraction One Tenth) + - "⅓": [tc: "#N⠁N⠌N⠉"] # 0x2153 (Vulgar Fraction One Third) + - "⅔": [tc: "#N⠃N⠌N⠉"] # 0x2154 (Vulgar Fraction Two Thirds) + - "⅕": [tc: "#N⠁N⠌N⠑"] # 0x2155 (Vulgar Fraction One Fifth) + - "⅖": [tc: "#N⠃N⠌N⠑"] # 0x2156 (Vulgar Fraction Two Fifths) + - "⅗": [tc: "#N⠉N⠌N⠑"] # 0x2157 (Vulgar Fraction Three Fifths) + - "⅘": [tc: "#N⠙N⠌N⠑"] # 0x2158 (Vulgar Fraction Four Fifths) + - "⅙": [tc: "#N⠁N⠌N⠋"] # 0x2159 (Vulgar Fraction One Sixth) + - "⅚": [tc: "#N⠑N⠌N⠋"] # 0x215A (Vulgar Fraction Five Sixths) + - "⅛": [tc: "#N⠁N⠌N⠓"] # 0x215B (Vulgar Fraction One Eighth) + - "⅜": [tc: "#N⠉N⠌N⠓"] # 0x215C (Vulgar Fraction Three Eighths) + - "⅝": [tc: "#N⠑N⠌N⠓"] # 0x215D (Vulgar Fraction Five Eighths) + - "⅞": [tc: "#N⠛N⠌N⠓"] # 0x215E (Vulgar Fraction Seven Eighths) + - "↉": [tc: "#N⠚N⠌N⠑"] # 0x2189 (Vulgar Fraction Zero Thirds) + + + + - "ℊ": [tc: "TL⠛⠁"] # 0x210a (Script Small G) + - "ℋ": [tc: "TCL⠓⠁"] # 0x210b (Script Capital H) + - "ℒ": [tc: "TCL⠇"] # 0x2113 (Script Capital L) + - "ℓ": [tc: "TL⠇"] # 0x2113 (Script Small L) + - "℘": [tc: "TCL⠏"] # 0x2118 (Script Capital P) + - "ℛ": [tc: "TCL⠗"] # 0x211B (Script Capital R) + - "ℯ": [tc: "TL⠑"] # 0x212F (Script Small E) + - "ℰ": [tc: "TCL⠑"] # 0x2130 (Script Capital E) + - "ℱ": [tc: "TCL⠋"] # 0x2131 (Script Capital F) + - "ℳ": [tc: "TCL⠍"] # 0x2133 (Script Capital M) + - "ℴ": [tc: "TL⠕"] # 0x21334 (Script Small O) + - "ℌ": [tc: "DCL⠓"] # 0x210C (Fraktur Capital H) + - "ℑ": [tc: "DCL⠊"] # 0x2111 (Fraktur Capital I) + - "ℜ": [tc: "DCL⠗"] # 0x211C (Fraktur Capital R) + - "ℨ": [tc: "DCL⠵"] # 0x2128 (Fraktur Capital Z) + - "ℭ": [tc: "DCL⠉"] # 0x22DC (Fraktur Capital C) + + - "ℂ": [tc: "𝔹CL⠉"] # 0x2102 + - "ℍ": [tc: "𝔹CL⠓"] # 0x210d + - "ℕ": [tc: "𝔹CL⠝"] # 0x2115 + - "ℙ": [tc: "𝔹CL⠏"] # 0x2119 + - "ℚ": [tc: "𝔹CL⠟"] # 0x211a + - "ℝ": [tc: "𝔹CL⠗"] # 0x211d + - "ℤ": [tc: "𝔹CL⠵"] # 0x2124 + + + - "𝚨-𝛀": # 0x1d6a8 - 0x1d6c0 + - tc: "B" + - spell: "translate('.', '𝛂𝛃𝛄𝛅𝛆𝛇𝛈𝛉𝛊𝛋𝛌𝛍𝛎𝛏𝛐𝛑𝛒𝛓𝛔𝛕𝛖𝛗𝛘𝛙𝛚', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + + - "𝛂-𝛚": # 0x1d6c2 - 0x1d6da + - tc: "BGL" + - spell: "translate('.', '𝛂𝛃𝛄𝛅𝛆𝛇𝛈𝛉𝛊𝛋𝛌𝛍𝛎𝛏𝛐𝛑𝛒𝛓𝛔𝛕𝛖𝛗𝛘𝛙𝛚', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + + - "𝔄-𝔜": # 0x1d504 - 0x1d51d ('z' version is reserved) + - tc: "DC" + - spell: "translate('.', '𝔄𝔅𝔆𝔇𝔈𝔉𝔊𝔋𝔌𝔍𝔎𝔏𝔐𝔑𝔒𝔓𝔔𝔕𝔖𝔗𝔘𝔙𝔚𝔛𝔜', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝔞-𝔷": # 0x1d51e - 0x1d537 + - tc: "D" + - spell: "translate('.', '𝔞𝔟𝔠𝔡𝔢𝔣𝔤𝔥𝔦𝔧𝔨𝔩𝔪𝔫𝔬𝔭𝔮𝔯𝔰𝔱𝔲𝔳𝔴𝔵𝔶𝔷', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝕬-𝖅": # 0x1D56C - 0x1D585 + - tc: "BDC" + - spell: "translate('.', '𝕬𝕭𝕮𝕯𝕰𝕱𝕲𝕳𝕴𝕵𝕶𝕷𝕸𝕹𝕺𝕻𝕼𝕽𝕾𝕿𝖀𝖁𝖂𝖃𝖄𝖅', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝖆-𝖟": # 0x1d586 - 0x1d59f + - tc: "D" + - spell: "translate('.', '𝖆𝖇𝖈𝖉𝖊𝖋𝖌𝖍𝖎𝖏𝖐𝖑𝖒𝖓𝖔𝖕𝖖𝖗𝖘𝖙𝖚𝖛𝖜𝖝𝖞𝖟', 'abcdefghijklmnopqrstuvwxyz')" + + # double struck (blackboard bold) chars in math alphabetic block and also MathType private use area + # Some of these are reserved because they were used in Plane 0 -- that shouldn't be an issue other than causing the other chars to not display + - "𝔸-𝕐": # 0x1d504 - 0x1d51d ('z' version is reserved) + - tc: "DC" + - spell: "translate('.', '𝔸𝔹𝔺𝔻𝔼𝔽𝔾𝔿𝕀𝕁𝕂𝕃𝕄𝕅𝕆𝕇𝕈𝕉𝕊𝕋𝕌𝕍𝕎𝕏𝕐', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝕒-𝕫": # 0x1d552 - 0x1d56b + - tc: "D" + - spell: "translate('.', '𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝟘-𝟡": # 0x1d7d8 - 0x1d7e1 + - tc: "D" + - spell: "translate('.', '𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡', '0123456789')" + + # script chars in math alphabetic block and also MathType private use area + - "𝒜-𝒵": # 0x1d49c - 0x1d4b5 + - tc: "TsC" + - spell: "translate('.', '𝒜𝒝𝒞𝒟𝒠𝒡𝒢𝒣𝒤𝒥𝒦𝒧𝒨𝒩𝒪𝒫𝒬𝒭𝒮𝒯𝒰𝒱𝒲𝒳𝒴𝒵', 'abcdefghijklmnopqrstuvwxyza')" + + - "𝒶-𝓏": # 0x1d4b6 - 0x1d4cf + - tc: "Ts" + - spell: "translate('.', '𝒶𝒷𝒸𝒹𝒺𝒻𝒼𝒽𝒾𝒿𝓀𝓁𝓂𝓃𝓄𝓅𝓆𝓇𝓈𝓉𝓊𝓋𝓌𝓍𝓎𝓏', 'abcdefghijklmnopqrstuvwxyz')" + + # bold script chars in math alphabetic block + - "𝓐-𝓩": # 0x1d4d0 - 0x1d4e9 + - tc: "BTs" + - spell: "translate('.', '𝓐𝓑𝓒𝓓𝓔𝓕𝓖𝓗𝓘𝓙𝓚𝓛𝓜𝓝𝓞𝓟𝓠𝓡𝓢𝓣𝓤𝓥𝓦𝓧𝓨𝓩', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝓪-𝔃": # 0x1d4ea - 0x1d503 + - tc: "BTs" + - spell: "translate('.', '𝓪𝓫𝓬𝓭𝓮𝓯𝓰𝓱𝓲𝓳𝓴𝓵𝓶𝓷𝓸𝓹𝓺𝓻𝓼𝓽𝓾𝓿𝔀𝔁𝔂𝔃', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝐀-𝐙": # 0x1d400 - 0x1d419 + - tc: "B" + - spell: "translate('.', '𝐀𝐁𝐂𝐃𝐄𝐅𝐆𝐇𝐈𝐉𝐊𝐋𝐌𝐍𝐎𝐏𝐐𝐑𝐒𝐓𝐔𝐕𝐖𝐗𝐘𝐙', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝐚-𝐳": # 0x1d41a - 0x1d433 + - tc: "B" + - spell: "translate('.', '𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝐴-𝑍": # 0x1d434 - 0x1d44d + # don't include italics + - tc: "C" + - spell: "translate('.', '𝐴𝐵𝐶𝐷𝐸𝐹𝐺𝐻𝐼𝐽𝐾𝐿𝑀𝑁𝑂𝑃𝑄𝑅𝑆𝑇𝑈𝑉𝑊𝑋𝑌𝑍', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝑎-𝑧": # 0x1d44e - 0x1d467 + # don't include italics + - spell: "translate('.', '𝑎𝑏𝑐𝑑𝑒𝑓𝑔𝑕𝑖𝑗𝑘𝑙𝑚𝑛𝑜𝑝𝑞𝑟𝑠𝑡𝑢𝑣𝑤𝑥𝑦𝑧', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝑨-𝒁": # 0x1d468 - 0x1d481 + - tc: "BIC" + - spell: "translate('.', '𝑨𝑩𝑪𝑫𝑬𝑭𝑮𝑯𝑰𝑱𝑲𝑳𝑴𝑵𝑶𝑷𝑸𝑹𝑺𝑻𝑼𝑽𝑾𝑿𝒀𝒁', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝒂-𝒛": # 0x1d482 - 0x1d49b + - spell: "translate('.', '𝒂𝒃𝒄𝒅𝒆𝒇𝒈𝒉𝒊𝒋𝒌𝒍𝒎𝒏𝒐𝒑𝒒𝒓𝒔𝒕𝒖𝒗𝒘𝒙𝒚𝒛', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝖠-𝖹": # 0x1d5a0 - 0x1d5b9 + - spell: "translate('.', '𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" + + - "𝖺-𝗓": # 0x1d5ba - 0x1d5d3 + - spell: "translate('.', '𝖺𝖻𝖼𝖽𝖾𝖿𝗀𝗁𝗂𝗃𝗄𝗅𝗆𝗇𝗈𝗉𝗊𝗋𝗌𝗍𝗎𝗏𝗐𝗑𝗒𝗓', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝗔-𝗭": # 0x1d5d4 - 0x1d5ed + - test: + if: "not($IgnoreBold)" + then: [tc: "B"] + - spell: "translate('.', '𝗔𝗕𝗖𝗗𝗘𝗙𝗚𝗛𝗜𝗝𝗞𝗟𝗠𝗡𝗢𝗣𝗤𝗥𝗦𝗧𝗨𝗩𝗪𝗫𝗬𝗭', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" + + - "𝗮-𝘇": # 0x1d5ee - 0x1d607 + - test: + if: "not($IgnoreBold)" + then: [tc: "B"] + - spell: "translate('.', '𝗮𝗯𝗰𝗱𝗲𝗳𝗴𝗵𝗶𝗷𝗸𝗹𝗺𝗻𝗼𝗽𝗾𝗿𝘀𝘁𝘂𝘃𝘄𝘅𝘆𝘇', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝘈-𝘡": # 0x1d608 - 0x1d621 + # - tc: "italic" + - spell: "translate('.', '𝘈𝘉𝘊𝘋𝘌𝘍𝘎𝘏𝘐𝘑𝘒𝘓𝘔𝘕𝘖𝘗𝘘𝘙𝘚𝘛𝘜𝘝𝘞𝘟𝘠𝘡', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" + + - "𝘢-𝘻": # 0x1d622 - 0x1d63b + # - tc: "italic" + - spell: "translate('.', '𝘢𝘣𝘤𝘥𝘦𝘧𝘨𝘩𝘪𝘫𝘬𝘭𝘮𝘯𝘰𝘱𝘲𝘳𝘴𝘵𝘶𝘷𝘸𝘹𝘺𝘻', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝘼-𝙕": # 0x1d63c - 0x1d655 + # - tc: "bold italic" + - test: + if: "$IgnoreBold" + then: [tc: I] + else: [tc: "BI"] + - spell: "translate('.', '𝘼𝘽𝘾𝘿𝙀𝙁𝙂𝙃𝙄𝙅𝙆𝙇𝙈𝙉𝙊𝙋𝙌𝙍𝙎𝙏𝙐𝙑𝙒𝙓𝙔𝙕', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" + + - "𝙖-𝙯": # 0x1d656 - 0x1d66f + # - tc: "bold italic" + - test: + if: "$IgnoreBold" + then: [tc: I] + else: [tc: "BI"] + - spell: "translate('.', '𝙖𝙗𝙘𝙙𝙚𝙛𝙜𝙝𝙞𝙟𝙠𝙡𝙢𝙣𝙤𝙥𝙦𝙧𝙨𝙩𝙪𝙫𝙬𝙭𝙮𝙯', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝙰-𝚉": # 0x1d670 - 0x1d689 + - tc: "C" + - spell: "translate('.', '𝙰𝙱𝙲𝙳𝙴𝙵𝙶𝙷𝙸𝙹𝙺𝙻𝙼𝙽𝙾𝙿𝚀𝚁𝚂𝚃𝚄𝚅𝚆𝚇𝚈𝚉', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝚊-𝚣": # 0x1d68a - 0x1d6a3 + - spell: "translate('.', '𝚊𝚋𝚌𝚍𝚎𝚏𝚐𝚑𝚒𝚓𝚔𝚕𝚖𝚗𝚘𝚙𝚚𝚛𝚜𝚝𝚞𝚟𝚠𝚡𝚢𝚣', 'abcdefghijklmnopqrstuvwxyz')" + + - "𝚤𝚥": # 0x1d6a4, 0x1d6a5 + - spell: "translate('.', '𝚤𝚥', 'ij')" # not sure what else these should be + + - "𝚨-𝛀": # 0x1d6a8 - 0x1d6c0 + - test: + if: "not($IgnoreBold)" + then: [tc: "B"] + - spell: "translate('.', '𝚨𝚩𝚪𝚫𝚬𝚭𝚮𝚯𝚰𝚱𝚲𝚳𝚴𝚵𝚶𝚷𝚸𝚹𝚺𝚻𝚼𝚽𝚾𝚿𝛀', 'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡ΢ΣΤΥΦΧΨΩ')" + + - "𝛂-𝛚": # 0x1d6c2 - 0x1d6da + - test: + if: "not($IgnoreBold)" + then: [tc: "B"] + - spell: "translate('.', '𝛂𝛃𝛄𝛅𝛆𝛇𝛈𝛉𝛊𝛋𝛌𝛍𝛎𝛏𝛐𝛑𝛒𝛓𝛔𝛕𝛖𝛗𝛘𝛙𝛚', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + - "𝛁": # 0x1d6c1 + - test: + if: "not($IgnoreBold)" + then: [tc: "B"] + - spell: "translate('.', '𝜵', '∇')" + + - "𝛛𝛜𝛝𝛞𝛟𝛠𝛡": # 0x1D6DB - 0x1D6E1 + - test: + if: "not($IgnoreBold)" + then: [tc: "B"] + - spell: "translate('.', '𝛛𝛜𝛝𝛞𝛟𝛠𝛡', '∂εθκφρπ')" + + - "𝛢-𝛺": # 0x1d6e2 - 0x1d6fa + - tc: "IC" + - spell: "translate('.', '𝛢𝛣𝛤𝛥𝛦𝛧𝛨𝛩𝛪𝛫𝛬𝛭𝛮𝛯𝛰𝛱𝛲𝛳𝛴𝛵𝛶𝛷𝛸𝛹𝛺', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + - "𝛼-𝜔": # 0x1d6fc - 0x1d714 + - spell: "translate('.', '𝛼𝛽𝛾𝛿𝜀𝜁𝜂𝜃𝜄𝜅𝜆𝜇𝜈𝜉𝜊𝜋𝜌𝜍𝜎𝜏𝜐𝜑𝜒𝜓𝜔', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + + - "𝛻": # 0x1d6fb + - spell: "translate('.', '𝜵', '∇')" + + - "𝜕𝜖𝜗𝜘𝜙𝜚𝜛": # 0x1d715 - 0x1d71b + # - tc: "italic" + - spell: "translate('.', '𝜕𝜖𝜗𝜘𝜙𝜚𝜛', '∂εθκφρπ')" + + - "𝜜-𝜴": # 0x1d71c - 0x1d734 + # - tc: "bold italic" + - tc: 'BIC⠁' + - spell: "translate('.', '𝜜𝜝𝜞𝜟𝜠𝜡𝜢𝜣𝜤𝜥𝜦𝜧𝜨𝜩𝜪𝜫𝜬𝜭𝜮𝜯𝜰𝜱𝜲𝜳𝜴', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + - "𝜶-𝝎": # 0x1d736 - 0x1d74e + - tc: "BI" + - spell: "translate('.', '𝜶𝜷𝜸𝜹𝜺𝜻𝜼𝜽𝜾𝜿𝝀𝝁𝝂𝝃𝝄𝝅𝝆𝝇𝝈𝝉𝝊𝝋𝝌𝝍𝝎', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + - "𝝏𝝐𝝑𝝒𝝓𝝔𝝕": # 0x1d74f - 0x1d755 + - tc: "BI" + - spell: "translate('.', '𝝏𝝐𝝑𝝒𝝓𝝔𝝕', '∂εκθφρπ')" + + - "𝜵": # 0x1d735 + - tc: "BI" + - spell: "translate('.', '𝜵', '∇')" + + - "𝝖-𝝮": # 0x1d756 - 0x1d76e + - tc: "BSC" + - spell: "translate('.', '𝝖𝝗𝝘𝝙𝝚𝝛𝝜𝝝𝝞𝝟𝝠𝝡𝝢𝝣𝝤𝝥𝝦𝝧𝝨𝝩𝝪𝝫𝝬𝝭𝝮', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + - "𝝰-𝞈": # 0x1d770 - 0x1d788 + - tc: "BS" + - spell: "translate('.', '𝝰𝝱𝝲𝝳𝝴𝝵𝝶𝝷𝝸𝝹𝝺𝝻𝝼𝝽𝝾𝝿𝞀𝞁𝞂𝞃𝞄𝞅𝞆𝞇𝞈', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + - "𝞉𝞊𝞋𝞌𝞍𝞎𝞏": # 0x1d789 - 0x1d78f + - tc: "B" + - spell: "translate('.', '𝞉𝞊𝞋𝞌𝞍𝞎𝞏', '∂εθκφρπ')" + + - "𝝯": # 0x1d76f + - tc: "B" + - spell: "translate('.', '𝜵', '∇')" + + - "𝞐-𝞨": # 0x1d790 - 0x1d7a8 + # - tc: "bold italic" + - test: + if: "$IgnoreBold" + then: [tc: I] + else: [tc: "BI"] + - spell: "translate('.', '𝞐𝞑𝞒𝞓𝞔𝞕𝞖𝞗𝞘𝞙𝞚𝞛𝞜𝞝𝞞𝞟𝞠𝞡𝞢𝞣𝞤𝞥𝞦𝞧𝞨', 'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡ΢ΣΤΥΦΧΨΩ')" + + - "𝞪-𝟂": # 0x1d7aa - 0x1d7c2 + # - tc: "bold italic" + - test: + if: "$IgnoreBold" + then: [tc: I] + else: [tc: "BI"] + - spell: "translate('.', '𝞪𝞫𝞬𝞭𝞮𝞯𝞰𝞱𝞲𝞳𝞴𝞵𝞶𝞷𝞸𝞹𝞺𝞻𝞼𝞽𝞾𝞿𝟀𝟁𝟂', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + - "𝟃𝟄𝟅𝟆𝟇𝟈𝟉": # 0x1d7c3 - 0x1d7c9 + - tc: "B" + - spell: "translate('.', '𝟃𝟄𝟅𝟆𝟇𝟈𝟉', '∂εθκφρπ')" + + - "𝞩": [tc: "B"] # 0x1d7a9 + + - "𝟎-𝟗": # 0x1d7ce - 0x1d7d7 + - tc: "B" + - spell: "translate('.', '𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗', '0123456789')" + + - "𝟬-𝟵": # 0x1D7EC - 0x1D7F5 + - tc: "BS" + - spell: "translate('.', '𝟬𝟭𝟮𝟯𝟰𝟱𝟲𝟳𝟴𝟵', '0123456789')" + + - "𝟢-𝟫": # 0x1d7e2 - 0x1d7eb + - tc: "S" + - spell: "translate('.', '𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿', '0123456789')" + + - "𝟶-𝟿": # 0x1d7f6 - 0x1d7ff + - spell: "translate('.', '𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿', '0123456789')" + diff --git a/Rules/Braille/Russian/unicode.yaml b/Rules/Braille/Russian/unicode.yaml new file mode 100644 index 000000000..1f2c733c7 --- /dev/null +++ b/Rules/Braille/Russian/unicode.yaml @@ -0,0 +1,289 @@ +--- +# Russian mathematical braille characters +- ".": [t: "⠠⠲"] +- ",": [t: "⠠⠂"] +- "!": [t: "⠠⠖"] +- "?": [t: "⠠⠢"] +- ":": [t: "⠠⠒"] +- ";": [t: "⠠⠆"] +- "{": [t: "⠠⠪"] +- "}": [t: "⠠⠕"] +- "+": [t: "⠀⠖"] +- "-": [t: "⠀⠤"] +- "=": [t: "⠀⠶"] +- ">": [t: "⠀⠕⠀"] +- "<": [t: "⠀⠪⠀"] +- "%": [t: "⠼⠴"] +- "±": [t: "⠀⠖⠤"] +- ";": [t: "⠠⠢"] +- "Ϳ": [t: "⠰⠊"] +- "·": [t: "⠰"] +- "α": [t: "⠰⠁"] +- "β": [t: "⠰⠃"] +- "γ": [t: "⠰⠛"] +- "δ": [t: "⠰⠙"] +- "ε": [t: "⠰⠑"] +- "ζ": [t: "⠰⠵"] +- "η": [t: "⠰⠚"] +- "θ": [t: "⠰⠓"] +- "ι": [t: "⠰⠊"] +- "κ": [t: "⠰⠅"] +- "λ": [t: "⠰⠇"] +- "μ": [t: "⠰⠍"] +- "ν": [t: "⠰⠝"] +- "ξ": [t: "⠰⠭"] +- "ο": [t: "⠰⠕"] +- "π": [t: "⠰⠏"] +- "ρ": [t: "⠰⠗"] +- "σ": [t: "⠰⠎"] +- "τ": [t: "⠰⠞"] +- "υ": [t: "⠰⠥"] +- "φ": [t: "⠰⠋"] +- "χ": [t: "⠰⠉"] +- "ψ": [t: "⠰⠽"] +- "ω": [t: "⠰⠺"] +- "Α": [t: "⠸⠁"] +- "Β": [t: "⠸⠃"] +- "Γ": [t: "⠸⠛"] +- "Δ": [t: "⠸⠙"] +- "Ε": [t: "⠸⠑"] +- "Ζ": [t: "⠸⠵"] +- "Η": [t: "⠸⠚"] +- "Θ": [t: "⠸⠓"] +- "Ι": [t: "⠸⠊"] +- "Κ": [t: "⠸⠅"] +- "Λ": [t: "⠸⠇"] +- "Μ": [t: "⠸⠍"] +- "Ν": [t: "⠸⠝"] +- "Ξ": [t: "⠸⠭"] +- "Ο": [t: "⠸⠕"] +- "Π": [t: "⠸⠏"] +- "Ρ": [t: "⠸⠗"] +- "Σ": [t: "⠸⠎"] +- "Τ": [t: "⠸⠞"] +- "Υ": [t: "⠸⠥"] +- "Φ": [t: "⠸⠋"] +- "Χ": [t: "⠸⠉"] +- "Ψ": [t: "⠸⠽"] +- "Ω": [t: "⠸⠺"] +- "ϐ": [t: "⠰⠃"] +- "ϑ": [t: "⠰⠓"] +- "ϕ": [t: "⠰⠋"] +- "ϖ": [t: "⠰⠏"] +- "ϱ": [t: "⠰⠗"] +- "′": [t: "⠔"] +- "″": [t: "⠔⠔"] +- "‴": [t: "⠔⠔⠔"] +- "∀": [t: "⠫⠄"] +- "∂": [t: "⠹"] +- "∃": [t: "⠫⠢"] +- "∅": [t: "⠈⠴"] +- "∇": [t: "⠫⠴"] +- "∈": [t: "⠀⠐⠪⠀"] +- "∉": [t: "⠘⠪"] +- "∋": [t: "⠀⠕⠂⠀"] +- "∌": [t: "⠈⠕⠂⠄"] +- "∎": [t: "⠀⠸⠇⠀"] +- "∏": [t: "⠸⠏"] +- "∑": [t: "⠸⠎"] +- "−": [t: "⠀⠤"] +- "∓": [t: "⠀⠤⠖"] +- "∔": [t: "⠈⠖"] +- "∕": [t: "⠠⠌"] +- "∖": [t: "⠀⠰⠤"] +- "∗": [t: "⠔"] +- "∘": [t: "⠴"] +- "√": [t: "⠩⠱"] +- "∛": [t: "⠩⠒⠱"] +- "∜": [t: "⠩⠲⠱"] +- "∞": [t: "⠻"] +- "∠": [t: "⠸⠪"] +- "∣": [t: "⠸"] +- "∤": [t: "⠀⠼⠀"] +- "∥": [t: "⠸⠸"] +- "∧": [t: "⠀⠰⠢"] +- "∨": [t: "⠀⠰⠔"] +- "∩": [t: "⠀⠰⠲"] +- "∪": [t: "⠀⠰⠴"] +- "∫": [t: "⠮"] +- "∬": [t: "⠮⠮"] +- "∭": [t: "⠮⠮⠮"] +- "∮": [t: "⠮⠴"] +- "∯": [t: "⠮⠮⠴"] +- "∴": [t: "⠀⠠⠡⠀"] +- "∵": [t: "⠀⠈⠌⠀"] +- "∶": [t: "⠀⠳"] +- "∸": [t: "⠈⠤"] +- "∼": [t: "⠀⠢"] +- "≅": [t: "⠀⠢"] +- "≈": [t: "⠀⠢⠢"] +- "≐": [t: "⠀⠒⠕"] +- "≔": [t: "⠀⠶⠒"] +- "≕": [t: "⠀⠶⠒"] +- "≖": [t: "⠀⠶⠴"] +- "≗": [t: "⠀⠶⠴"] +- "≛": [t: "⠀⠶⠆"] +- "≟": [t: "⠶⠢"] +- "≠": [t: "⠀⠾"] +- "≡": [t: "⠀⠰⠶"] +- "≢": [t: "⠀⠰⠾"] +- "≤": [t: "⠀⠪⠶"] +- "≥": [t: "⠀⠕⠶"] +- "≪": [t: "⠀⠪⠪⠀"] +- "≫": [t: "⠀⠕⠕⠀"] +- "≺": [t: "⠀⠒⠪⠀"] +- "≻": [t: "⠀⠕⠒⠀"] +- "≼": [t: "⠀⠒⠪⠶"] +- "≽": [t: "⠀⠕⠒⠶"] +- "⊂": [t: "⠀⠯⠀"] +- "⊃": [t: "⠀⠹⠀"] +- "⊄": [t: "⠈⠯"] +- "⊅": [t: "⠈⠹"] +- "⊆": [t: "⠀⠯⠶"] +- "⊇": [t: "⠀⠹⠶"] +- "⊕": [t: "⠀⠰⠖"] +- "⊥": [t: "⠼⠄"] +- "⊨": [t: "⠼⠎"] +- "⊼": [t: "⠰⠌"] +- "⊽": [t: "⠘⠡"] +- "⋅": [t: "⠄"] +- "⋆": [t: "⠔"] +- "⋮": [t: "⠠⠲⠲⠲"] +- "⋯": [t: "⠠⠲⠲⠲"] +- "⋰": [t: "⠠⠲⠲⠲"] +- "⋱": [t: "⠠⠲⠲⠲"] +- "△": [t: "⠸⠙"] +- "◻": [t: "⠶"] +- "♮": [t: "⠻⠨⠝⠻"] + +# Latin letters and digits used for mathematical identifiers. +- "a": [t: "⠁"] +- "A": [t: "C⠁"] +- "b": [t: "⠃"] +- "B": [t: "C⠃"] +- "c": [t: "⠉"] +- "C": [t: "C⠉"] +- "d": [t: "⠙"] +- "D": [t: "C⠙"] +- "e": [t: "⠑"] +- "E": [t: "C⠑"] +- "f": [t: "⠋"] +- "F": [t: "C⠋"] +- "g": [t: "⠛"] +- "G": [t: "C⠛"] +- "h": [t: "⠓"] +- "H": [t: "C⠓"] +- "i": [t: "⠊"] +- "I": [t: "C⠊"] +- "j": [t: "⠚"] +- "J": [t: "C⠚"] +- "k": [t: "⠅"] +- "K": [t: "C⠅"] +- "l": [t: "⠇"] +- "L": [t: "C⠇"] +- "m": [t: "⠍"] +- "M": [t: "C⠍"] +- "n": [t: "⠝"] +- "N": [t: "C⠝"] +- "o": [t: "⠕"] +- "O": [t: "C⠕"] +- "p": [t: "⠏"] +- "P": [t: "C⠏"] +- "q": [t: "⠟"] +- "Q": [t: "C⠟"] +- "r": [t: "⠗"] +- "R": [t: "C⠗"] +- "s": [t: "⠎"] +- "S": [t: "C⠎"] +- "t": [t: "⠞"] +- "T": [t: "C⠞"] +- "u": [t: "⠥"] +- "U": [t: "C⠥"] +- "v": [t: "⠧"] +- "V": [t: "C⠧"] +- "w": [t: "⠺"] +- "W": [t: "C⠺"] +- "x": [t: "⠭"] +- "X": [t: "C⠭"] +- "y": [t: "⠽"] +- "Y": [t: "C⠽"] +- "z": [t: "⠵"] +- "Z": [t: "C⠵"] +- "0": [t: "N⠚"] +- "1": [t: "N⠁"] +- "2": [t: "N⠃"] +- "3": [t: "N⠉"] +- "4": [t: "N⠙"] +- "5": [t: "N⠑"] +- "6": [t: "N⠋"] +- "7": [t: "N⠛"] +- "8": [t: "N⠓"] +- "9": [t: "N⠊"] + +# Russian literary braille letters for mtext and Cyrillic identifiers. +- "а": [t: "⠁"] +- "А": [t: "C⠁"] +- "б": [t: "⠃"] +- "Б": [t: "C⠃"] +- "в": [t: "⠺"] +- "В": [t: "C⠺"] +- "г": [t: "⠛"] +- "Г": [t: "C⠛"] +- "д": [t: "⠙"] +- "Д": [t: "C⠙"] +- "е": [t: "⠑"] +- "Е": [t: "C⠑"] +- "ё": [t: "⠡"] +- "Ё": [t: "C⠡"] +- "ж": [t: "⠚"] +- "Ж": [t: "C⠚"] +- "з": [t: "⠵"] +- "З": [t: "C⠵"] +- "и": [t: "⠊"] +- "И": [t: "C⠊"] +- "й": [t: "⠯"] +- "Й": [t: "C⠯"] +- "к": [t: "⠅"] +- "К": [t: "C⠅"] +- "л": [t: "⠇"] +- "Л": [t: "C⠇"] +- "м": [t: "⠍"] +- "М": [t: "C⠍"] +- "н": [t: "⠝"] +- "Н": [t: "C⠝"] +- "о": [t: "⠕"] +- "О": [t: "C⠕"] +- "п": [t: "⠏"] +- "П": [t: "C⠏"] +- "р": [t: "⠗"] +- "Р": [t: "C⠗"] +- "с": [t: "⠎"] +- "С": [t: "C⠎"] +- "т": [t: "⠞"] +- "Т": [t: "C⠞"] +- "у": [t: "⠥"] +- "У": [t: "C⠥"] +- "ф": [t: "⠋"] +- "Ф": [t: "C⠋"] +- "х": [t: "⠓"] +- "Х": [t: "C⠓"] +- "ц": [t: "⠉"] +- "Ц": [t: "C⠉"] +- "ч": [t: "⠟"] +- "Ч": [t: "C⠟"] +- "ш": [t: "⠱"] +- "Ш": [t: "C⠱"] +- "щ": [t: "⠭"] +- "Щ": [t: "C⠭"] +- "ъ": [t: "⠷"] +- "Ъ": [t: "C⠷"] +- "ы": [t: "⠮"] +- "Ы": [t: "C⠮"] +- "ь": [t: "⠾"] +- "Ь": [t: "C⠾"] +- "э": [t: "⠪"] +- "Э": [t: "C⠪"] +- "ю": [t: "⠳"] +- "Ю": [t: "C⠳"] +- "я": [t: "⠫"] +- "Я": [t: "C⠫"] diff --git a/src/braille.rs b/src/braille.rs index 66f579a73..1c6ff0d05 100644 --- a/src/braille.rs +++ b/src/braille.rs @@ -1,8 +1,7 @@ #![allow(clippy::needless_return)] use strum_macros::Display; -use sxd_document_no_unsafe::dom::{Element, ChildOfElement}; -use sxd_document_no_unsafe::Package; -use sxd_document_no_unsafe::as_str; +use sxd_document::dom::{Element, ChildOfElement}; +use sxd_document::Package; use crate::definitions::SPEECH_DEFINITIONS; use crate::errors::*; use crate::pretty_print::mml_to_string; @@ -44,10 +43,17 @@ pub fn braille_mathml(mathml: Element, nav_node_id: &str) -> Result<(String, usi let pref_manager = rules_with_context.get_rules().pref_manager.borrow(); let highlight_style = pref_manager.pref_to_string("BrailleNavHighlight"); let braille_code = pref_manager.pref_to_string("BrailleCode"); - let braille = match get_braille_code(&braille_code) { - Some(code) => code.cleanup(pref_manager, braille_string), - // probably needs cleanup if someone has another code, but this will have to get added by hand - None => braille_string.trim_matches('⠀').to_string(), + let braille = match braille_code.as_str() { + "Nemeth" => nemeth_cleanup(pref_manager, braille_string), + "UEB" => ueb_cleanup(pref_manager, braille_string), + "Vietnam" => vietnam_cleanup(pref_manager, braille_string), + "CMU" => cmu_cleanup(pref_manager, braille_string), + "Finnish" => finnish_cleanup(pref_manager, braille_string), + "Swedish" => swedish_cleanup(pref_manager, braille_string), + "Russian" => russian_cleanup(pref_manager, braille_string), + "LaTeX" => LaTeX_cleanup(pref_manager, braille_string), + "ASCIIMath" => ASCIIMath_cleanup(pref_manager, braille_string), + _ => braille_string.trim_matches('⠀').to_string(), // probably needs cleanup if someone has another code, but this will have to get added by hand }; return Ok( @@ -110,15 +116,19 @@ pub fn braille_mathml(mathml: Element, nav_node_id: &str) -> Result<(String, usi // need to highlight (optional) capital/number, language, and style (max 2 chars) also in that (rev) order let mut prefix_ch_index = std::cmp::max(0, start_index as isize - 5*3) as usize; - if prefix_ch_index == 0 { - // don't count the word or passage mode as part of a indicator (UEB); other codes return 0 - prefix_ch_index = get_braille_code(braille_code).map_or(0, |code| code.highlight_word_passage_prefix(braille)); + if prefix_ch_index == 0 && braille_code == "UEB" { + // don't count the word or passage mode as part of a indicator + if braille.starts_with("⠰⠰⠰") { + prefix_ch_index = 9; + } else if braille.starts_with("⠰⠰") { + prefix_ch_index = 6; + } } let indicators = &braille[prefix_ch_index..start_index]; // chars to be examined - // treat unknown codes like UEB because they probably have similar number and letter prefixes - let n_indicator_chars = get_braille_code(braille_code) - .map_or_else(|| i_start_ueb(indicators), |code| code.highlight_first_indicator_len(indicators, first_ch)); - let i_byte_start = start_index - 3 * n_indicator_chars; + let i_byte_start = start_index - 3 * match braille_code { + "Nemeth" => i_start_nemeth(indicators, first_ch), + _ => i_start_ueb(indicators), // treat all the other like UEB because they probably have similar number and letter prefixes + }; if i_byte_start < start_index { // remove old highlight as long as we don't wipe out the end highlight if start_index < end_index { @@ -137,229 +147,107 @@ pub fn braille_mathml(mathml: Element, nav_node_id: &str) -> Result<(String, usi } /// Return the byte indexes of the first and last place to highlight - /// Currently, this only does something for CMU braille (see `Cmu::expand_highlight`) + /// Currently, this only does something for CMU braille fn expand_highlight(braille: &mut String, braille_code: &str, start_index: usize, end_index: usize) -> Option<(usize, usize)> { - if start_index == 0 || end_index == braille.len() { + // For CMU, we want to expand mrows to include the opening and closing grouping indicators if they exist + if start_index == 0 || end_index == braille.len() || braille_code != "CMU" { return None; } - return get_braille_code(braille_code)?.expand_highlight(braille, start_index, end_index); - } - } -} -/// Given a position in a Nemeth string, what is the position character that starts it (e.g, the prev char for capital letter) -fn i_start_nemeth(braille_prefix: &str, first_ch: char) -> usize { - fn is_nemeth_number(ch: char) -> bool { - matches!(ch, '⠂' | '⠆' | '⠒' | '⠲' | '⠢' | '⠖' | '⠶' | '⠦' | '⠔' | '⠴' | '⠨') - } - let mut n_chars = 0; - let prefix = &mut braille_prefix.chars().rev().peekable(); - if prefix.peek() == Some(&'⠠') || // cap indicator - (prefix.peek() == Some(&'⠼') && is_nemeth_number(first_ch)) || // number indicator - [Some(&'⠸'), Some(&'⠈'), Some(&'⠨')].contains(&prefix.peek()) { // bold, script/blackboard, italic indicator - n_chars += 1; - prefix.next(); - } - - if [Some(&'⠰'), Some(&'⠸'), Some(&'⠨')].contains(&prefix.peek()) { // English, German, Greek - n_chars += 1; - } else if prefix.peek() == Some(&'⠈') { - let ch = prefix.next(); // Russian/Greek Variant - if ch == Some('⠈') || ch == Some('⠨') { - n_chars += 2; - } - } else if prefix.peek() == Some(&'⠠') { // Hebrew - let ch = prefix.next(); // Russian/Greek Variant - if ch == Some('⠠') { - n_chars += 2; - } - }; - return n_chars; -} + let first_ch = unhighlight(braille_at(braille, start_index)); + let last_ch = unhighlight(braille_at(braille, end_index-3)); + // We need to be careful not to expand the selection if we are already on a grouping indicator + if first_ch == '⠢' && last_ch == '⠔'{ + return None; + } + let preceding_ch = braille_at(braille, start_index-3); + if preceding_ch != '⠢' { + return None; + } -/// Given a position in a UEB string, what is the position character that starts it (e.g, the prev char for capital letter) -fn i_start_ueb(braille_prefix: &str) -> usize { - let prefix = &mut braille_prefix.chars().rev().peekable(); - let mut n_chars = 0; - while let Some(ch) = prefix.next() { - if is_ueb_prefix(ch) { - n_chars += 1; - } else if ch == '⠆' { - let n_typeform_chars = check_for_typeform(prefix); - if n_typeform_chars > 0 { - n_chars += n_typeform_chars; - } else { - break; + let following_ch = braille_at(braille, end_index); + if following_ch != '⠔' { + return None; } - } else { - break; + + let preceding_ch = highlight(preceding_ch); + braille.replace_range(start_index-3..start_index+3, format!("{preceding_ch}{first_ch}").as_str()); + let following_ch = highlight(following_ch); + braille.replace_range(end_index-3..end_index+3, format!("{last_ch}{following_ch}").as_str()); + return Some( (start_index-3, end_index + 3) ); } } - return n_chars; -} + /// Given a position in a Nemeth string, what is the position character that starts it (e.g, the prev char for capital letter) + fn i_start_nemeth(braille_prefix: &str, first_ch: char) -> usize { + fn is_nemeth_number(ch: char) -> bool { + matches!(ch, '⠂' | '⠆' | '⠒' | '⠲' | '⠢' | '⠖' | '⠶' | '⠦' | '⠔' | '⠴' | '⠨') + } + let mut n_chars = 0; + let prefix = &mut braille_prefix.chars().rev().peekable(); + if prefix.peek() == Some(&'⠠') || // cap indicator + (prefix.peek() == Some(&'⠼') && is_nemeth_number(first_ch)) || // number indicator + [Some(&'⠸'), Some(&'⠈'), Some(&'⠨')].contains(&prefix.peek()) { // bold, script/blackboard, italic indicator + n_chars += 1; + prefix.next(); + } -fn check_for_typeform(prefix: &mut dyn std::iter::Iterator) -> usize { - fn is_ueb_typeform_prefix(ch: char) -> bool { - matches!(ch, '⠈' | '⠘' | '⠸' | '⠨') + if [Some(&'⠰'), Some(&'⠸'), Some(&'⠨')].contains(&prefix.peek()) { // English, German, Greek + n_chars += 1; + } else if prefix.peek() == Some(&'⠈') { + let ch = prefix.next(); // Russian/Greek Variant + if ch == Some('⠈') || ch == Some('⠨') { + n_chars += 2; + } + } else if prefix.peek() == Some(&'⠠') { // Hebrew + let ch = prefix.next(); // Russian/Greek Variant + if ch == Some('⠠') { + n_chars += 2; + } + }; + return n_chars; } - if let Some(typeform_indicator) = prefix.next() { - if is_ueb_typeform_prefix(typeform_indicator) { - return 2; - } else if typeform_indicator == '⠼' && - let Some(user_defined_typeform_indicator) = prefix.next() && - (is_ueb_typeform_prefix(user_defined_typeform_indicator) || user_defined_typeform_indicator == '⠐') { - return 3; + /// Given a position in a UEB string, what is the position character that starts it (e.g, the prev char for capital letter) + fn i_start_ueb(braille_prefix: &str) -> usize { + let prefix = &mut braille_prefix.chars().rev().peekable(); + let mut n_chars = 0; + while let Some(ch) = prefix.next() { + if is_ueb_prefix(ch) { + n_chars += 1; + } else if ch == '⠆' { + let n_typeform_chars = check_for_typeform(prefix); + if n_typeform_chars > 0 { + n_chars += n_typeform_chars; + } else { + break; } + } else { + break; + } + } + return n_chars; } - return 0; -} - -/// A braille code (Nemeth, UEB, ...). -/// -/// Each code encapsulates its per-code post-processing ("cleanup"), leaf-char generation, -/// grouping decision, and navigation-highlight knobs. To add a new code: create a unit struct, -/// implement this trait, and register it in `braille_code()`. The four dispatch sites -/// (cleanup in `braille_mathml`, `BrailleChars::get_braille_chars`, `NeedsToBeGrouped`, -/// and `highlight_braille_chars`) all go through the registry, so no other code needs to change. -trait BrailleCode: Sync { - /// Name of the code, as used by the `BrailleCode` preference and in rule files. - fn name(&self) -> &'static str; - - /// Post-process the raw (rule-generated) braille into the final braille string. - fn cleanup(&self, pref_manager: Ref, raw_braille: String) -> String; - - /// Braille chars for a *leaf* node (mn/mi/mo/mtext/ms). Default: the code has no leaf handling. - fn get_braille_chars(&self, _node: Element, _text_range: Option>) -> Result { - bail!("get_braille_chars: braille code '{}' does not implement leaf char generation", self.name()); - } - - /// Whether `mathml` needs grouping indicators around it. Default: the code doesn't use grouping. - fn needs_grouping(&self, _mathml: Element, _is_base: bool) -> StdResult { - return Err(XPathError::Other { what: format!( - "NeedsToBeGrouped: braille code arg '{}' is not a known code ('UEB', 'CMU', or 'Swedish')", self.name()) }); - } - - // --- navigation-highlight knobs (UEB-like defaults; override per code as needed) --- - - /// Number of prefix braille cells (before the first highlighted char) that belong to that char. - fn highlight_first_indicator_len(&self, indicators: &str, _first_ch: char) -> usize { - return i_start_ueb(indicators); - } - /// Number of leading cells that are a word/passage indicator and should not be counted as an - /// indicator run (only relevant when the highlight starts at the very beginning). UEB only. - fn highlight_word_passage_prefix(&self, _braille: &str) -> usize { return 0; } - /// Expand the highlight to include grouping indicators around the selection. CMU only. - fn expand_highlight(&self, _braille: &mut String, _start_index: usize, _end_index: usize) -> Option<(usize, usize)> { - return None; - } -} - -/// Look up the implementation for a braille code name. Returns `None` for unknown codes. -fn get_braille_code(code: &str) -> Option<&'static dyn BrailleCode> { - return Some(match code { - "Nemeth" => &Nemeth, - "UEB" => &Ueb, - "Vietnam" => &Vietnam, - "CMU" => &Cmu, - "Finnish" => &Finnish, - "Swedish" => &Swedish, - "LaTeX" => &LaTeX, - "ASCIIMath" => &AsciiMath, - _ => return None, - }); -} - -struct Nemeth; -struct Ueb; -struct Vietnam; -struct Cmu; -struct Finnish; -struct Swedish; -#[allow(non_camel_case_types)] -struct LaTeX; -struct AsciiMath; - -impl BrailleCode for Nemeth { - fn name(&self) -> &'static str { "Nemeth" } - fn cleanup(&self, pref_manager: Ref, raw_braille: String) -> String { nemeth_cleanup(pref_manager, raw_braille) } - fn get_braille_chars(&self, node: Element, text_range: Option>) -> Result { BrailleChars::get_braille_nemeth_chars(node, text_range) } - fn highlight_first_indicator_len(&self, indicators: &str, first_ch: char) -> usize { i_start_nemeth(indicators, first_ch) } -} - -impl BrailleCode for Ueb { - fn name(&self) -> &'static str { "UEB" } - fn cleanup(&self, pref_manager: Ref, raw_braille: String) -> String { ueb_cleanup(pref_manager, raw_braille) } - fn get_braille_chars(&self, node: Element, text_range: Option>) -> Result { BrailleChars::get_braille_ueb_chars(node, text_range) } - fn needs_grouping(&self, mathml: Element, is_base: bool) -> StdResult { Ok(NeedsToBeGrouped::needs_grouping_for_ueb(mathml, is_base)) } - fn highlight_word_passage_prefix(&self, braille: &str) -> usize { - // don't count the word or passage mode as part of an indicator - if braille.starts_with("⠰⠰⠰") { return 9; } else if braille.starts_with("⠰⠰") { return 6; } else { return 0; } - } -} -impl BrailleCode for Vietnam { - fn name(&self) -> &'static str { "Vietnam" } - fn cleanup(&self, pref_manager: Ref, raw_braille: String) -> String { vietnam_cleanup(pref_manager, raw_braille) } - fn get_braille_chars(&self, node: Element, text_range: Option>) -> Result { BrailleChars::get_braille_vietnam_chars(node, text_range) } -} - -impl BrailleCode for Cmu { - fn name(&self) -> &'static str { "CMU" } - fn cleanup(&self, pref_manager: Ref, raw_braille: String) -> String { cmu_cleanup(pref_manager, raw_braille) } - fn get_braille_chars(&self, node: Element, text_range: Option>) -> Result { BrailleChars::get_braille_cmu_chars(node, text_range) } - fn needs_grouping(&self, mathml: Element, is_base: bool) -> StdResult { Ok(NeedsToBeGrouped::needs_grouping_for_cmu(mathml, is_base)) } - fn expand_highlight(&self, braille: &mut String, start_index: usize, end_index: usize) -> Option<(usize, usize)> { - // For CMU, we want to expand mrows to include the opening and closing grouping indicators if they exist - let first_ch = unhighlight(braille_at(braille, start_index)); - let last_ch = unhighlight(braille_at(braille, end_index-3)); - // We need to be careful not to expand the selection if we are already on a grouping indicator - if first_ch == '⠢' && last_ch == '⠔'{ - return None; - } - let preceding_ch = braille_at(braille, start_index-3); - if preceding_ch != '⠢' { - return None; - } - - let following_ch = braille_at(braille, end_index); - if following_ch != '⠔' { - return None; - } - - let preceding_ch = highlight(preceding_ch); - braille.replace_range(start_index-3..start_index+3, format!("{preceding_ch}{first_ch}").as_str()); - let following_ch = highlight(following_ch); - braille.replace_range(end_index-3..end_index+3, format!("{last_ch}{following_ch}").as_str()); - return Some( (start_index-3, end_index + 3) ); + + fn check_for_typeform(prefix: &mut dyn std::iter::Iterator) -> usize { + fn is_ueb_typeform_prefix(ch: char) -> bool { + matches!(ch, '⠈' | '⠘' | '⠸' | '⠨') + } + + if let Some(typeform_indicator) = prefix.next() { + if is_ueb_typeform_prefix(typeform_indicator) { + return 2; + } else if typeform_indicator == '⠼' && + let Some(user_defined_typeform_indicator) = prefix.next() && + (is_ueb_typeform_prefix(user_defined_typeform_indicator) || user_defined_typeform_indicator == '⠐') { + return 3; + } + } + return 0; } } -impl BrailleCode for Finnish { - fn name(&self) -> &'static str { "Finnish" } - fn cleanup(&self, pref_manager: Ref, raw_braille: String) -> String { finnish_cleanup(pref_manager, raw_braille) } - fn get_braille_chars(&self, node: Element, text_range: Option>) -> Result { BrailleChars::get_braille_ueb_chars(node, text_range) } // FIX: need to figure out what to implement - fn needs_grouping(&self, mathml: Element, is_base: bool) -> StdResult { Ok(NeedsToBeGrouped::needs_grouping_for_finnish(mathml, is_base)) } -} - -impl BrailleCode for Swedish { - fn name(&self) -> &'static str { "Swedish" } - fn cleanup(&self, pref_manager: Ref, raw_braille: String) -> String { swedish_cleanup(pref_manager, raw_braille) } - fn get_braille_chars(&self, node: Element, text_range: Option>) -> Result { BrailleChars::get_braille_ueb_chars(node, text_range) } // FIX: need to figure out what to implement - fn needs_grouping(&self, mathml: Element, is_base: bool) -> StdResult { Ok(NeedsToBeGrouped::needs_grouping_for_swedish(mathml, is_base)) } -} - -impl BrailleCode for LaTeX { - fn name(&self) -> &'static str { "LaTeX" } - fn cleanup(&self, pref_manager: Ref, raw_braille: String) -> String { LaTeX_cleanup(pref_manager, raw_braille) } -} - -impl BrailleCode for AsciiMath { - fn name(&self) -> &'static str { "ASCIIMath" } - fn cleanup(&self, pref_manager: Ref, raw_braille: String) -> String { ASCIIMath_cleanup(pref_manager, raw_braille) } -} - // FIX: if 8-dot braille is needed, perhaps the highlights can be shifted to a "highlighted" 256 char block in private space // they would need to be unshifted for the external world fn is_highlighted(ch: char) -> bool { @@ -438,8 +326,7 @@ pub fn get_navigation_node_from_braille_position(mathml: Element, position: usiz /// find the navigation node that most tightly encapsulates the target position (0-based) /// 'node' is the current node we are on inside of 'mathml' fn find_navigation_node<'e>(mathml: Element<'e>, node: Element<'e>, target_position: usize) -> Result> { - let raw_node_id = node.attribute_value("id"); - let node_id = match raw_node_id.as_deref() { + let node_id = match node.attribute_value("id") { Some(id) => id, None => bail!("'id' is not present on mathml: {}", mml_to_string(node)), }; @@ -557,7 +444,7 @@ pub fn get_navigation_node_from_braille_position(mathml: Element, position: usiz return BRAILLE_DEFINITIONS.with(|definitions| { let definitions = definitions.borrow(); let comparison_operators = definitions.get_hashset("ComparisonOperators").unwrap(); - return comparison_operators.contains(as_str!(as_text(node))); + return comparison_operators.contains(as_text(node)); }); } @@ -591,7 +478,7 @@ pub fn get_navigation_node_from_braille_position(mathml: Element, position: usiz fn estimate_braille_chars(child: ChildOfElement, n_number_indicator: usize) -> usize { let node = as_element(child); - let leaf_name = as_str!(name(node)); + let leaf_name = name(node); if is_leaf(node) { let text = as_text(node); // len() is close since mn's probably have ASCII digits and lower case vars are common (count as) and other chars need extra braille chars @@ -599,16 +486,11 @@ pub fn get_navigation_node_from_braille_position(mathml: Element, position: usiz if text == "\u{2061}" || text == "\u{2062}" { // invisible function apply/times (most common by far) return 0; } - // We can't know what encoding the user is using for LaTeX and ASCIIMath -- assume 8-dot braille - let braille_code = PreferenceManager::get().borrow().pref_to_string("BrailleCode"); - if braille_code == "LaTeX" || braille_code == "ASCIIMath" { - return text.len(); - } else { - return match leaf_name { - "mn" => n_number_indicator + text.len(), - "mo" => 2, // could do better by actually brailling char, but that is more expensive - _ => text.len(), - } + // FIX: this assumption is bad for 8-dot braille + return match leaf_name { + "mn" => n_number_indicator + text.len(), + "mo" => 2, // could do better by actually brailling char, but that is more expensive + _ => text.len(), } } let mut estimate = if leaf_name == "mrow" {0} else {node.children().len() + 1}; // guess extra chars need for mfrac, msub, etc (start+intermediate+end). @@ -623,7 +505,7 @@ pub fn get_navigation_node_from_braille_position(mathml: Element, position: usiz } } -fn nemeth_cleanup(_pref_manager: Ref, raw_braille: String) -> String { +fn nemeth_cleanup(pref_manager: Ref, raw_braille: String) -> String { // Typeface: S: sans-serif, B: bold, T: script/blackboard, I: italic, R: Roman // Language: E: English, D: German, G: Greek, V: Greek variants, H: Hebrew, U: Russian // Indicators: C: capital, N: number, P: punctuation, M: multipurpose @@ -635,7 +517,7 @@ fn nemeth_cleanup(_pref_manager: Ref, raw_braille: String) -> static NEMETH_INDICATOR_REPLACEMENTS: phf::Map<&str, &str> = phf_map! { "S" => "⠠⠨", // sans-serif "B" => "⠸", // bold - "𝔹" => "⠠⠸", // blackboard + "𝔹" => "⠨", // blackboard "T" => "⠈", // script "I" => "⠨", // italic (mapped to be the same a blackboard) "R" => "", // roman @@ -826,10 +708,25 @@ fn nemeth_cleanup(_pref_manager: Ref, raw_braille: String) -> let result = REMOVE_AFTER_PUNCT_IND.replace_all(&result, "$1$2"); // debug!("Punct38: \"{}\"", &result); + // these typeforms need to get pulled from user-prefs as they are transcriber-defined + let sans_serif = pref_manager.pref_to_string("Nemeth_SansSerif"); + let bold = pref_manager.pref_to_string("Nemeth_Bold"); + let double_struck = pref_manager.pref_to_string("Nemeth_DoubleStruck"); + let script = pref_manager.pref_to_string("Nemeth_Script"); + let italic = pref_manager.pref_to_string("Nemeth_Italic"); + let result = REPLACE_INDICATORS.replace_all(&result, |cap: &Captures| { - match NEMETH_INDICATOR_REPLACEMENTS.get(&cap[0]) { - None => {error!("REPLACE_INDICATORS and NEMETH_INDICATOR_REPLACEMENTS are not in sync"); ""}, - Some(&ch) => ch, + let matched_char = &cap[0]; + match matched_char { + "S" => &sans_serif, + "B" => &bold, + "𝔹" => &double_struck, + "T" => &script, + "I" => &italic, + _ => match NEMETH_INDICATOR_REPLACEMENTS.get(&cap[0]) { + None => {error!("REPLACE_INDICATORS and NEMETH_INDICATOR_REPLACEMENTS are not in sync"); ""}, + Some(&ch) => ch, + } } }); @@ -953,52 +850,6 @@ fn is_letter_prefix(ch: char) -> bool { static REPLACE_INDICATORS: LazyLock = LazyLock::new(|| Regex::new(r"([1𝟙SB𝔹TIREDGVHP𝐶𝑐CLMNW𝐖swe,.-—―#ocb])").unwrap()); static COLLAPSE_SPACES: LazyLock = LazyLock::new(|| Regex::new(r"⠀⠀+").unwrap()); -/// Transcriber-defined typeforms pulled from prefs (their braille is configurable), used by the -/// UEB-family indicator replacement for blackboard `𝔹`, sans-serif `S`, fraktur `D`, Greek variant `V`. -struct UserTypeforms { - double_struck: String, - sans_serif: String, - fraktur: String, - greek_variant: String, -} - -impl UserTypeforms { - /// Read the four typeforms from prefs named `_DoubleStruck`, `_SansSerif`, etc. - fn from_prefs(pref_manager: &Ref, prefix: &str) -> UserTypeforms { - return UserTypeforms { - double_struck: pref_manager.pref_to_string(&format!("{prefix}_DoubleStruck")), - sans_serif: pref_manager.pref_to_string(&format!("{prefix}_SansSerif")), - fraktur: pref_manager.pref_to_string(&format!("{prefix}_Fraktur")), - greek_variant: pref_manager.pref_to_string(&format!("{prefix}_GreekVariant")), - }; - } -} - -/// Replace the symbolic indicator chars (matched by `regex`) with their braille cells using `map`, -/// pulling the transcriber-defined typeforms from `typeforms`. Shared by the UEB-family codes -/// (UEB, Vietnam, Finnish, Swedish). `map_name` is only used in the "out of sync" error message. -fn apply_indicator_replacements( - raw_braille: &str, - regex: &Regex, - map: &phf::Map<&'static str, &'static str>, - map_name: &str, - typeforms: &UserTypeforms, -) -> String { - return regex.replace_all(raw_braille, |cap: &Captures| { - let matched_char = &cap[0]; - match matched_char { - "𝔹" => typeforms.double_struck.as_str(), - "S" => typeforms.sans_serif.as_str(), - "D" => typeforms.fraktur.as_str(), - "V" => typeforms.greek_variant.as_str(), - _ => match map.get(matched_char) { - None => {error!("REPLACE_INDICATORS and {map_name} are not in sync: missing '{matched_char}'"); ""}, - Some(&ch) => ch, - }, - } - }).to_string(); -} - fn is_short_form(chars: &[char]) -> bool { let chars_as_string = chars.iter().map(|ch| ch.to_string()).collect::(); return SHORT_FORMS.contains(&chars_as_string); @@ -1020,9 +871,24 @@ fn ueb_cleanup(pref_manager: Ref, raw_braille: String) -> Str let result = result.replace("tW", "W"); // these typeforms need to get pulled from user-prefs as they are transcriber-defined - let typeforms = UserTypeforms::from_prefs(&pref_manager, "UEB"); - let result = apply_indicator_replacements(&result, &REPLACE_INDICATORS, &UEB_INDICATOR_REPLACEMENTS, - "UEB_INDICATOR_REPLACEMENTS", &typeforms); + let double_struck = pref_manager.pref_to_string("UEB_DoubleStruck"); + let sans_serif = pref_manager.pref_to_string("UEB_SansSerif"); + let fraktur = pref_manager.pref_to_string("UEB_Fraktur"); + let greek_variant = pref_manager.pref_to_string("UEB_GreekVariant"); + + let result = REPLACE_INDICATORS.replace_all(&result, |cap: &Captures| { + let matched_char = &cap[0]; + match matched_char { + "𝔹" => &double_struck, + "S" => &sans_serif, + "D" => &fraktur, + "V" => &greek_variant, + _ => match UEB_INDICATOR_REPLACEMENTS.get(matched_char) { + None => {error!("REPLACE_INDICATORS and UEB_INDICATOR_REPLACEMENTS are not in sync: missing '{matched_char}'"); ""}, + Some(&ch) => ch, + }, + } + }); // Remove unicode blanks at start and end -- do this after the substitutions because ',' introduces spaces // let result = result.trim_start_matches('⠀').trim_end_matches('⠀'); @@ -1990,14 +1856,28 @@ fn vietnam_cleanup(pref_manager: Ref, raw_braille: String) -> // debug!(" after UY={}", &result); // these typeforms need to get pulled from user-prefs as they are transcriber-defined - let typeforms = UserTypeforms::from_prefs(&pref_manager, "Vietnam"); + let double_struck = pref_manager.pref_to_string("Vietnam_DoubleStruck"); + let sans_serif = pref_manager.pref_to_string("Vietnam_SansSerif"); + let fraktur = pref_manager.pref_to_string("Vietnam_Fraktur"); + let greek_variant = pref_manager.pref_to_string("Vietnam_GreekVariant"); // This reuses the code just for getting rid of unnecessary "L"s and "N"s let result = remove_unneeded_mode_changes(&result, UEB_Mode::Grade1, UEB_Duration::Passage); - let result = apply_indicator_replacements(&result, &REPLACE_INDICATORS, &VIETNAM_INDICATOR_REPLACEMENTS, - "VIETNAM_INDICATOR_REPLACEMENTS", &typeforms); + let result = REPLACE_INDICATORS.replace_all(&result, |cap: &Captures| { + let matched_char = &cap[0]; + match matched_char { + "𝔹" => &double_struck, + "S" => &sans_serif, + "D" => &fraktur, + "V" => &greek_variant, + _ => match VIETNAM_INDICATOR_REPLACEMENTS.get(matched_char) { + None => {error!("REPLACE_INDICATORS and VIETNAM_INDICATOR_REPLACEMENTS are not in sync: missing '{matched_char}'"); ""}, + Some(&ch) => ch, + }, + } + }); // Remove unicode blanks at start and end -- do this after the substitutions because ',' introduces spaces // let result = result.trim_start_matches('⠀').trim_end_matches('⠀'); @@ -2207,17 +2087,30 @@ fn finnish_cleanup(pref_manager: Ref, raw_braille: String) -> // debug!(" after typeface/caps={}", &result); - // these typeforms need to get pulled from user-prefs as they are transcriber-defined (Finnish reuses the Vietnam prefs) - let typeforms = UserTypeforms::from_prefs(&pref_manager, "Vietnam"); + // these typeforms need to get pulled from user-prefs as they are transcriber-defined + let double_struck = pref_manager.pref_to_string("Vietnam_DoubleStruck"); + let sans_serif = pref_manager.pref_to_string("Vietnam_SansSerif"); + let fraktur = pref_manager.pref_to_string("Vietnam_Fraktur"); + let greek_variant = pref_manager.pref_to_string("Vietnam_GreekVariant"); // This reuses the code just for getting rid of unnecessary "L"s and "N"s let result = remove_unneeded_mode_changes(&result, UEB_Mode::Grade1, UEB_Duration::Passage); // debug!(" remove_unneeded_mode_changes={}", &result); - // Note: the "out of sync" message intentionally references SWEDISH (pre-existing quirk) - let result = apply_indicator_replacements(&result, &REPLACE_INDICATORS, &FINNISH_INDICATOR_REPLACEMENTS, - "SWEDISH_INDICATOR_REPLACEMENTS", &typeforms); + let result = REPLACE_INDICATORS.replace_all(&result, |cap: &Captures| { + let matched_char = &cap[0]; + match matched_char { + "𝔹" => &double_struck, + "S" => &sans_serif, + "D" => &fraktur, + "V" => &greek_variant, + _ => match FINNISH_INDICATOR_REPLACEMENTS.get(matched_char) { + None => {error!("REPLACE_INDICATORS and SWEDISH_INDICATOR_REPLACEMENTS are not in sync: missing '{matched_char}'"); ""}, + Some(&ch) => ch, + }, + } + }); // Remove unicode blanks at start and end -- do this after the substitutions because ',' introduces spaces // let result = result.trim_start_matches('⠀').trim_end_matches('⠀'); @@ -2241,8 +2134,11 @@ fn swedish_cleanup(pref_manager: Ref, raw_braille: String) -> // debug!(" after typeface/caps={}", &result); - // these typeforms need to get pulled from user-prefs as they are transcriber-defined (Swedish reuses the Vietnam prefs) - let typeforms = UserTypeforms::from_prefs(&pref_manager, "Vietnam"); + // these typeforms need to get pulled from user-prefs as they are transcriber-defined + let double_struck = pref_manager.pref_to_string("Vietnam_DoubleStruck"); + let sans_serif = pref_manager.pref_to_string("Vietnam_SansSerif"); + let fraktur = pref_manager.pref_to_string("Vietnam_Fraktur"); + let greek_variant = pref_manager.pref_to_string("Vietnam_GreekVariant"); // This reuses the code just for getting rid of unnecessary "L"s and "N"s let result = remove_unneeded_mode_changes(&result, UEB_Mode::Grade1, UEB_Duration::Passage); @@ -2250,8 +2146,19 @@ fn swedish_cleanup(pref_manager: Ref, raw_braille: String) -> let result = EMPTY_BASE.replace_all(&result, "$1"); - let result = apply_indicator_replacements(&result, &REPLACE_INDICATORS, &SWEDISH_INDICATOR_REPLACEMENTS, - "SWEDISH_INDICATOR_REPLACEMENTS", &typeforms); + let result = REPLACE_INDICATORS.replace_all(&result, |cap: &Captures| { + let matched_char = &cap[0]; + match matched_char { + "𝔹" => &double_struck, + "S" => &sans_serif, + "D" => &fraktur, + "V" => &greek_variant, + _ => match SWEDISH_INDICATOR_REPLACEMENTS.get(matched_char) { + None => {error!("REPLACE_INDICATORS and SWEDISH_INDICATOR_REPLACEMENTS are not in sync: missing '{matched_char}'"); ""}, + Some(&ch) => ch, + }, + } + }); // Remove unicode blanks at start and end -- do this after the substitutions because ',' introduces spaces // let result = result.trim_start_matches('⠀').trim_end_matches('⠀'); @@ -2260,6 +2167,38 @@ fn swedish_cleanup(pref_manager: Ref, raw_braille: String) -> return result.to_string(); } +fn russian_cleanup(_pref_manager: Ref, raw_braille: String) -> String { + static REPLACE_INDICATORS: LazyLock = LazyLock::new(|| Regex::new(r"([BCILNW#])").unwrap()); + static COLLAPSE_SPACES: LazyLock = LazyLock::new(|| Regex::new(r"⠀+").unwrap()); + + let mut raw_braille_without_repeated_number_indicators = String::with_capacity(raw_braille.len()); + let mut previous_char_was_digit = false; + for ch in raw_braille.chars() { + if ch == 'N' && previous_char_was_digit { + previous_char_was_digit = false; + continue; + } + raw_braille_without_repeated_number_indicators.push(ch); + previous_char_was_digit = matches!(ch, '⠚' | '⠁' | '⠃' | '⠉' | '⠙' | '⠑' | '⠋' | '⠛' | '⠓' | '⠊'); + } + + let result = REPLACE_INDICATORS.replace_all(&raw_braille_without_repeated_number_indicators, |cap: &Captures| { + match &cap[0] { + "B" => "⠸", + "C" => "⠠", + "I" => "⠨", + "L" => "", + "N" => "⠼", + "W" => "⠀", + "#" => "", + _ => "", + } + }); + return COLLAPSE_SPACES.replace_all(&result, "⠀") + .trim_matches('⠀') + .to_string(); +} + #[allow(non_snake_case)] fn LaTeX_cleanup(_pref_manager: Ref, raw_braille: String) -> String { static REMOVE_SPACE: LazyLock = LazyLock::new(|| Regex::new(r" ([\^_,;)\]}])").unwrap()); // '^', '_', ',', ';', ')', ']', '}' @@ -2303,10 +2242,10 @@ fn ASCIIMath_cleanup(_pref_manager: Ref, raw_braille: String) use crate::canonicalize::{as_element, as_text, name}; use crate::xpath_functions::{is_leaf, validate_one_node, IsBracketed}; use std::result::Result as StdResult; -use sxd_document_no_unsafe::dom::ParentOfChild; -use sxd_xpath_no_unsafe::function::Error as XPathError; -use sxd_xpath_no_unsafe::function::{Args, Function}; -use sxd_xpath_no_unsafe::{context, nodeset::*, Value}; +use sxd_document::dom::ParentOfChild; +use sxd_xpath::function::Error as XPathError; +use sxd_xpath::function::{Args, Function}; +use sxd_xpath::{context, nodeset::*, Value}; pub struct NemethNestingChars; const NEMETH_FRAC_LEVEL: &str = "data-nemeth-frac-level"; // name of attr where value is cached @@ -2331,7 +2270,7 @@ impl NemethNestingChars { max_value += repeat_char; node.set_attribute_value(NEMETH_FRAC_LEVEL, &max_value); return max_value; - } else if FIRST_CHILD_ONLY.contains(&as_str!(name)) { + } else if FIRST_CHILD_ONLY.contains(&name) { // only look at the base -- ignore scripts/index return NemethNestingChars::nemeth_frac_value(as_element(children[0]), repeat_char); } else { @@ -2366,11 +2305,11 @@ impl NemethNestingChars { if let ParentOfChild::Element(e) = parent_of_child { parent = e; } else { - return Err( sxd_xpath_no_unsafe::function::Error::Other { what: "Internal error in nemeth_root_value: didn't find 'math' tag".to_string() } ); + return Err( sxd_xpath::function::Error::Other("Internal error in nemeth_root_value: didn't find 'math' tag".to_string()) ); } } } - return Err( XPathError::Other { what: "Internal error in nemeth_root_value: didn't find 'math' tag".to_string() } ); + return Err( XPathError::Other("Internal error in nemeth_root_value: didn't find 'math' tag".to_string()) ); } } @@ -2401,7 +2340,7 @@ impl Function for NemethNestingChars { } else if name == "msqrt" || name == "mroot" { return Ok( Value::String( NemethNestingChars::nemeth_root_value(el, &repeat_char)? ) ); } else { - return Err(XPathError::Other { what: format!("NestingChars chars should be used only on 'mfrac'. '{}' was passed in", name) }); + return Err(XPathError::Other(format!("NestingChars chars should be used only on 'mfrac'. '{}' was passed in", name))); } } else { // not an element, so nothing to do @@ -2416,13 +2355,19 @@ impl BrailleChars { // this string follows the Nemeth rules typefaces and deals with mathvariant // which has partially turned chars to the alphanumeric block fn get_braille_chars(node: Element, code: &str, text_range: Option>) -> StdResult { - let result = match get_braille_code(code) { - Some(braille_code) => braille_code.get_braille_chars(node, text_range), - None => return Err(sxd_xpath_no_unsafe::function::Error::Other { what: format!("get_braille_chars: unknown braille code '{code}'") }), + let result = match code { + "Nemeth" => BrailleChars::get_braille_nemeth_chars(node, text_range), + "UEB" => BrailleChars:: get_braille_ueb_chars(node, text_range), + "CMU" => BrailleChars:: get_braille_cmu_chars(node, text_range), + "Vietnam" => BrailleChars:: get_braille_vietnam_chars(node, text_range), + "Swedish" => BrailleChars:: get_braille_ueb_chars(node, text_range), // FIX: need to figure out what to implement + "Finnish" => BrailleChars:: get_braille_ueb_chars(node, text_range), // FIX: need to figure out what to implement + "Russian" => BrailleChars:: get_braille_ueb_chars(node, text_range), + _ => return Err(sxd_xpath::function::Error::Other(format!("get_braille_chars: unknown braille code '{code}'"))) }; return match result { Ok(string) => Ok(make_quoted_string(string)), - Err(err) => return Err(sxd_xpath_no_unsafe::function::Error::Other { what: err.to_string() }), + Err(err) => return Err(sxd_xpath::function::Error::Other(err.to_string())), } } @@ -2434,8 +2379,8 @@ impl BrailleChars { static PICK_APART_CHAR: LazyLock = LazyLock::new(|| { Regex::new(r"(?P[SB𝔹TIR]*)(?P[EDGVHU]?)(?PC?)(?PL?)(?P[N]?)(?P.)").unwrap() }); - let raw_math_variant = node.attribute_value("mathvariant"); - let math_variant = raw_math_variant.as_deref(); + let math_variant = node.attribute_value("mathvariant"); + // FIX: cover all the options -- use phf::Map let attr_typeface = match math_variant { None => "R", Some(variant) => match variant { @@ -2448,7 +2393,7 @@ impl BrailleChars { _ => "R", // normal and unknown }, }; - let text = BrailleChars::substring(as_str!(as_text(node)), &text_range); + let text = BrailleChars::substring(as_text(node), &text_range); let braille_chars = braille_replace_chars(&text, node)?; // debug!("Nemeth chars: text='{}', braille_chars='{}'", &text, &braille_chars); @@ -2510,9 +2455,8 @@ impl BrailleChars { Regex::new(r"(?PB??)(?PI??)(?P[S𝔹TD]??)s??(?PC??)(?PG??)(?P[NL].)").unwrap() }); - let raw_math_variant = node.attribute_value("mathvariant"); - let math_variant = raw_math_variant.as_deref(); - let text = BrailleChars::substring(as_str!(as_text(node)), &text_range); + let math_variant = node.attribute_value("mathvariant"); + let text = BrailleChars::substring(as_text(node), &text_range); let mut braille_chars = braille_replace_chars(&text, node)?; // debug!("get_braille_ueb_chars: before/after unicode.yaml: '{}'/'{}'", text, braille_chars); @@ -2564,9 +2508,8 @@ impl BrailleChars { Regex::new(r"(?PB??)(?PI??)(?P[S𝔹TD]??)s??(?PC??)(?PG??)(?P[NL].)").unwrap() }); - let raw_math_variant = node.attribute_value("mathvariant"); - let math_variant = raw_math_variant.as_deref(); - let text = BrailleChars::substring(as_str!(as_text(node)), &text_range); + let math_variant = node.attribute_value("mathvariant"); + let text = BrailleChars::substring(as_text(node), &text_range); let text = add_separator(text); let braille_chars = braille_replace_chars(&text, node)?; @@ -2699,14 +2642,13 @@ impl BrailleChars { return false; fn child_meets_conditions(node: Element) -> bool { - let name = as_str!(name(node)); + let name = name(node); return match name { "mi" | "mn" => true, "mo" => !crate::canonicalize::is_relational_op(node), "mtext" => { - let raw_text = as_text(node); - let text = raw_text.trim(); - return text=="?" || text=="-?-" || text.is_empty(); + let text = as_text(node).trim(); + return text=="?" || text=="-?-" || text.is_empty(); // various forms of "fill in missing content" (see also Nemeth_RULEs.yaml, "omissions") }, "mrow" => { if IsBracketed::is_bracketed(node, "", "", false, false) { @@ -2721,7 +2663,7 @@ impl BrailleChars { true }, "menclose" => { - if let Some(notation) = node.attribute_value("notation").as_deref() { + if let Some(notation) = node.attribute_value("notation") { if notation != "bottom" || notation != "box" { return false; } @@ -2766,7 +2708,7 @@ impl Function for BrailleChars { use crate::canonicalize::create_mathml_element; let mut args = Args(args); if let Err(e) = args.exactly(2).or_else(|_| args.exactly(4)) { - return Err( XPathError::Other { what: format!("BrailleChars requires 2 or 4 args: {e}") }); + return Err( XPathError::Other(format!("BrailleChars requires 2 or 4 args: {e}"))); }; let range = if args.len() == 4 { @@ -2798,7 +2740,7 @@ impl Function for BrailleChars { }; if !is_leaf(node) { - return Err( XPathError::Other { what: format!("BrailleChars called on non-leaf element '{}'", mml_to_string(node)) } ); + return Err( XPathError::Other(format!("BrailleChars called on non-leaf element '{}'", mml_to_string(node))) ); } return Ok( Value::String( BrailleChars::get_braille_chars(node, &braille_code, range)? ) ); } @@ -2864,7 +2806,7 @@ impl NeedsToBeGrouped { /// FIX: what needs to be implemented? fn needs_grouping_for_finnish(mathml: Element, is_base: bool) -> bool { use crate::xpath_functions::IsInDefinition; - let mut node_name = as_str!(name(mathml)); + let mut node_name = name(mathml); if mathml.attribute_value("data-roman-numeral").is_some() { node_name = "mi"; // roman numerals don't follow number rules } @@ -2895,7 +2837,7 @@ impl NeedsToBeGrouped { } }, "mi" | "mo" | "mtext" => { - let text = as_str!(as_text(mathml)); + let text = as_text(mathml); let parent = get_parent(mathml); // there is always a "math" node let parent_name = name(parent); // there is always a "math" node if is_base && (parent_name == "msub" || parent_name == "msup" || parent_name == "msubsup") && !text.contains([' ', '\u{00A0}']) { @@ -2955,7 +2897,7 @@ impl NeedsToBeGrouped { // if the number is irregular, return the ordinal form, otherwise return 'None'. fn needs_grouping_for_swedish(mathml: Element, is_base: bool) -> bool { use crate::xpath_functions::IsInDefinition; - let mut node_name = as_str!(name(mathml)); + let mut node_name = name(mathml); if mathml.attribute_value("data-roman-numeral").is_some() { node_name = "mi"; // roman numerals don't follow number rules } @@ -2963,7 +2905,7 @@ impl NeedsToBeGrouped { match node_name { "mn" => return false, "mi" | "mo" | "mtext" => { - let text = as_str!(as_text(mathml)); + let text = as_text(mathml); let parent = get_parent(mathml); // there is always a "math" node let parent_name = name(parent); // there is always a "math" node if is_base && (parent_name == "msub" || parent_name == "msup" || parent_name == "msubsup") && !text.contains([' ', '\u{00A0}']) { @@ -3022,7 +2964,7 @@ impl NeedsToBeGrouped { // 8. If none of the foregoing apply, the item is simply the [this element's] individual symbol. use crate::xpath_functions::IsInDefinition; - let mut node_name = as_str!(name(mathml)); + let mut node_name = name(mathml); if mathml.attribute_value("data-roman-numeral").is_some() { node_name = "mi"; // roman numerals don't follow number rules } @@ -3051,7 +2993,7 @@ impl NeedsToBeGrouped { } }, "mi" | "mo" | "mtext" => { - let text = as_str!(as_text(mathml)); + let text = as_text(mathml); let parent = get_parent(mathml); // there is always a "math" node let parent_name = name(parent); // there is always a "math" node if is_base && (parent_name == "msub" || parent_name == "msup" || parent_name == "msubsup") && !text.contains([' ', '\u{00A0}']) { @@ -3102,14 +3044,17 @@ impl Function for NeedsToBeGrouped { let braille_code = args.pop_string()?; let node = validate_one_node(args.pop_nodeset()?, "NeedsToBeGrouped")?; if let Node::Element(e) = node { - let answer = match get_braille_code(&braille_code) { - Some(code) => code.needs_grouping(e, is_base)?, - None => return Err(XPathError::Other { what: format!("NeedsToBeGrouped: braille code arg '{braille_code:?}' is not a known code ('UEB', 'CMU', or 'Swedish')") }), + let answer = match braille_code.as_str() { + "CMU" => NeedsToBeGrouped::needs_grouping_for_cmu(e, is_base), + "UEB" => NeedsToBeGrouped::needs_grouping_for_ueb(e, is_base), + "Finnish" => NeedsToBeGrouped::needs_grouping_for_finnish(e, is_base), + "Swedish" => NeedsToBeGrouped::needs_grouping_for_swedish(e, is_base), + _ => return Err(XPathError::Other(format!("NeedsToBeGrouped: braille code arg '{braille_code:?}' is not a known code ('UEB', 'CMU', or 'Swedish')"))), }; return Ok( Value::Boolean( answer ) ); } - return Err(XPathError::Other { what: format!("NeedsToBeGrouped: first arg '{node:?}' is not a node") }); + return Err(XPathError::Other(format!("NeedsToBeGrouped: first arg '{node:?}' is not a node"))); } } @@ -3123,30 +3068,8 @@ mod tests { use crate::interface::*; use log::debug; - fn braille_test(f: F) -> Result<()> - where - F: FnOnce() -> Result<()> + std::panic::UnwindSafe, - { - use std::panic::{catch_unwind, AssertUnwindSafe}; - init_panic_handler(); - let result = catch_unwind(AssertUnwindSafe(f)); - return report_any_panic(result); - } - - fn init_braille_mathml(mathml: &str) -> Result<()> { - use std::panic::{catch_unwind, AssertUnwindSafe}; - init_panic_handler(); - let result = catch_unwind(AssertUnwindSafe(|| { - set_rules_dir(super::super::abs_rules_dir_path())?; - set_mathml(mathml)?; - return Ok( () ); - })); - return report_any_panic(result); - } - #[test] fn ueb_highlight_24() -> Result<()> { // issue 24 - return braille_test(|| { let mathml_str = " 4 @@ -3156,9 +3079,10 @@ mod tests { c "; - init_braille_mathml(mathml_str)?; - set_preference("BrailleCode", "UEB")?; - set_preference("BrailleNavHighlight", "All")?; + crate::interface::set_rules_dir(super::super::abs_rules_dir_path()).unwrap(); + set_mathml(mathml_str).unwrap(); + set_preference("BrailleCode", "UEB").unwrap(); + set_preference("BrailleNavHighlight", "All").unwrap(); let braille = get_braille("id-2")?; assert_eq!("⣼⣙⠰⠁⠉", braille); set_navigation_node("id-2", 0)?; @@ -3169,13 +3093,11 @@ mod tests { set_navigation_node("id-4", 0)?; assert_eq!( get_braille_position()?, (2,4)); return Ok( () ); - }); } #[test] // This test probably should be repeated for each braille code and be taken out of here fn find_mathml_from_braille() -> Result<()> { - return braille_test(|| { use std::time::Instant; let mathml_str = " @@ -3213,10 +3135,11 @@ mod tests { "; - init_braille_mathml(mathml_str)?; - set_preference("BrailleNavHighlight", "Off")?; + crate::interface::set_rules_dir(super::super::abs_rules_dir_path()).unwrap(); + set_mathml(mathml_str).unwrap(); + set_preference("BrailleNavHighlight", "Off").unwrap(); - set_preference("BrailleCode", "Nemeth")?; + set_preference("BrailleCode", "Nemeth").unwrap(); let _braille = get_braille("")?; let answers= &[2, 3, 3, 3, 3, 4, 7, 8, 9, 9, 10, 13, 12, 14, 12, 15, 17, 19, 21, 10, 4, 23, 25, 4]; let answers = answers.map(|num| format!("id-{}", num)); @@ -3230,7 +3153,7 @@ mod tests { assert_eq!(*answer, id, "\nNemeth test ith position={}", i); } - set_preference("BrailleCode", "UEB")?; + set_preference("BrailleCode", "UEB").unwrap(); let _braille = get_braille("")?; let answers= &[0, 0, 0, 2, 3, 3, 3, 3, 4, 7, 7, 8, 9, 9, 10, 13, 12, 14, 14, 15, 15, 17, 17, 19, 19, 21, 10, 4, 4, 23, 23, 25, 25, 4, 0, 0]; let answers = answers.map(|num| format!("id-{}", num)); @@ -3243,7 +3166,7 @@ mod tests { debug!("Time taken: {}ms", instant.elapsed().as_millis()); assert_eq!(*answer, id, "\nUEB test ith position={}", i); } - set_preference("BrailleCode", "CMU")?; + set_preference("BrailleCode", "CMU").unwrap(); let braille = get_braille("")?; let answers= &[2, 3, 5, 7, 8, 9, 9, 9, 10, 10, 11, 13, 12, 14, 14, 15, 17, 17, 19, 19, 21, 11, 5, 4, 22, 23, 23, 25, 25, 22,]; let answers = answers.map(|num| format!("id-{}", num)); @@ -3258,23 +3181,21 @@ mod tests { assert_eq!(*answer, id, "\nCMU test ith position={}", i); } return Ok( () ); - }); } #[test] #[allow(non_snake_case)] fn test_UEB_start_mode() -> Result<()> { - return braille_test(|| { let mathml_str = "xn"; - init_braille_mathml(mathml_str)?; - set_preference("BrailleCode", "UEB")?; - set_preference("UEB_START_MODE", "Grade2")?; + crate::interface::set_rules_dir(super::super::abs_rules_dir_path()).unwrap(); + set_mathml(mathml_str).unwrap(); + set_preference("BrailleCode", "UEB").unwrap(); + set_preference("UEB_START_MODE", "Grade2").unwrap(); let braille = get_braille("")?; assert_eq!("⠭⠰⠔⠝", braille, "Grade2"); - set_preference("UEB_START_MODE", "Grade1")?; + set_preference("UEB_START_MODE", "Grade1").unwrap(); let braille = get_braille("")?; assert_eq!("⠭⠔⠝", braille, "Grade1"); return Ok( () ); - }); } } diff --git a/tests/braille.rs b/tests/braille.rs index 865f159b7..2fc04fffc 100644 --- a/tests/braille.rs +++ b/tests/braille.rs @@ -24,8 +24,8 @@ mod braille { mod vi; } - mod Swedish { - mod swedish; + mod Russian { + mod russian; } mod LaTeX { diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs new file mode 100644 index 000000000..2897a3717 --- /dev/null +++ b/tests/braille/Russian/russian.rs @@ -0,0 +1,37 @@ +use crate::common::*; +use anyhow::Result; + +#[test] +fn numbers_and_operators() -> Result<()> { + let expr = r#"5+12=17"#; + test_braille("Russian", expr, "⠼⠑⠀⠖⠼⠁⠃⠀⠶⠼⠁⠛")?; + return Ok(()); +} + +#[test] +fn fraction() -> Result<()> { + let expr = r#"x2"#; + test_braille("Russian", expr, "⠆⠭⠀⠳⠼⠃⠰")?; + return Ok(()); +} + +#[test] +fn scripts_and_root() -> Result<()> { + let expr = r#"x2+y"#; + test_braille("Russian", expr, "⠭⠌⠼⠃⠱⠀⠖⠩⠱⠽⠹")?; + return Ok(()); +} + +#[test] +fn cyrillic_text() -> Result<()> { + let expr = r#"угол"#; + test_braille("Russian", expr, "⠥⠛⠕⠇")?; + return Ok(()); +} + +#[test] +fn nested_fraction_and_root() -> Result<()> { + let expr = r#"x+1yx-y"#; + test_braille("Russian", expr, "⠆⠭⠀⠖⠩⠱⠆⠼⠁⠀⠳⠽⠰⠹⠀⠳⠭⠀⠤⠽⠰")?; + return Ok(()); +} From ae72c65e0a349068b72c6c461e7e96d941462796 Mon Sep 17 00:00:00 2001 From: nsoiffer Date: Mon, 1 Jun 2026 12:03:21 -0700 Subject: [PATCH 02/47] typo in example --- tests/braille/Russian/russian.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index 2897a3717..869bf972f 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -35,3 +35,24 @@ fn nested_fraction_and_root() -> Result<()> { test_braille("Russian", expr, "⠆⠭⠀⠖⠩⠱⠆⠼⠁⠀⠳⠽⠰⠹⠀⠳⠭⠀⠤⠽⠰")?; return Ok(()); } + +#[test] +fn wikipedia_times_divide() -> Result<()> { + let expr = r#"6×7:14=3"#; + test_braille("Russian", expr, "⠼⠋⠀⠦⠼⠛⠀⠲⠼⠁⠙⠀⠶⠼⠉")?; + return Ok(()); +} + +#[test] +fn wikipedia_linear_parens() -> Result<()> { + let expr = r#"3·(97)=6"#; + test_braille("Russian", expr, "⠼⠉⠄⠣⠼⠊⠀⠤⠼⠛⠜⠀⠶⠼⠋")?; + return Ok(()); +} + +#[test] +fn wikipedia_sqrt() -> Result<()> { + let expr = r#"10000<101"#; + test_braille("Russian", expr, "⠩⠱⠼⠁⠚⠚⠚⠚⠀⠪⠀⠼⠁⠚⠁")?; + return Ok(()); +} From b6ef68d7c7f6fdd7fdce3fd1fb681bce0d82741f Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:20:37 +0300 Subject: [PATCH 03/47] Russian Braille fixes This commit adds fixes to Russian Braille. Now the test with parentheses is passed. Also there are devision and multiplication signs have been modified according to the Russian braille specs. --- Rules/Braille/Russian/unicode.yaml | 8 +++++++- tests/braille/Russian/russian.rs | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Rules/Braille/Russian/unicode.yaml b/Rules/Braille/Russian/unicode.yaml index 1f2c733c7..a0b0dc3cd 100644 --- a/Rules/Braille/Russian/unicode.yaml +++ b/Rules/Braille/Russian/unicode.yaml @@ -4,13 +4,15 @@ - ",": [t: "⠠⠂"] - "!": [t: "⠠⠖"] - "?": [t: "⠠⠢"] -- ":": [t: "⠠⠒"] +- ":": [t: "⠀⠲"] - ";": [t: "⠠⠆"] - "{": [t: "⠠⠪"] - "}": [t: "⠠⠕"] - "+": [t: "⠀⠖"] - "-": [t: "⠀⠤"] - "=": [t: "⠀⠶"] +- "(": [t: "⠣"] +- ")": [t: "⠜"] - ">": [t: "⠀⠕⠀"] - "<": [t: "⠀⠪⠀"] - "%": [t: "⠼⠴"] @@ -92,6 +94,10 @@ - "∕": [t: "⠠⠌"] - "∖": [t: "⠀⠰⠤"] - "∗": [t: "⠔"] +- "·": [t: "⠄"] +- "×": [t: "⠀⠦"] +- "⋅": [t: "⠄"] +- "⁢": [t: "⠄"] - "∘": [t: "⠴"] - "√": [t: "⠩⠱"] - "∛": [t: "⠩⠒⠱"] diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index 869bf972f..7ccb33d48 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -29,6 +29,26 @@ fn cyrillic_text() -> Result<()> { return Ok(()); } +#[test] +fn wikipedia_linear_parens() -> Result<()> { + let expr = r#"3(9-7)=6"#; + test_braille("Russian", expr, "⠼⠉⠄⠣⠼⠊⠀⠤⠼⠛⠜⠀⠶⠼⠋")?; + return Ok(()); +} + +#[test] +fn source_arithmetic_examples() -> Result<()> { + let expr = r#"2481=1944"#; + test_braille("Russian", expr, "⠼⠃⠙⠄⠼⠓⠁⠀⠶⠼⠁⠊⠙⠙")?; + + let expr = r#"783:9=87"#; + test_braille("Russian", expr, "⠼⠛⠓⠉⠀⠲⠼⠊⠀⠶⠼⠓⠛")?; + + let expr = r#"12×35=420"#; + test_braille("Russian", expr, "⠼⠁⠃⠀⠦⠼⠉⠑⠀⠶⠼⠙⠃⠚")?; + return Ok(()); +} + #[test] fn nested_fraction_and_root() -> Result<()> { let expr = r#"x+1yx-y"#; From b2de4cadcd763e87aaff4e80cd4e81726d28a42b Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:45:55 +0300 Subject: [PATCH 04/47] Braille - Latin and Greek letters fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds a layer of Latin and Greek alphabets for Russian mathematical Braille. * In unicode.yaml Latin and Greek letters now go through the internal markers of the alphabetic mode. • In braille.rs russian_cleanup turns these markers into real signs: ◦ Latin lowercase: ⠠ ◦ Latin uppercase: ⠨ ◦ Greek lowercase: ⠰ ◦ Greek uppercase: ⠸ Added tests to russian.rs to x+A+y+B, the Latin letter after the number. --- Rules/Braille/Russian/unicode.yaml | 212 ++++++++++++++--------------- src/braille.rs | 30 +++- tests/braille/Russian/russian.rs | 30 +++- 3 files changed, 160 insertions(+), 112 deletions(-) diff --git a/Rules/Braille/Russian/unicode.yaml b/Rules/Braille/Russian/unicode.yaml index a0b0dc3cd..8b72b2ba6 100644 --- a/Rules/Braille/Russian/unicode.yaml +++ b/Rules/Braille/Russian/unicode.yaml @@ -20,59 +20,59 @@ - ";": [t: "⠠⠢"] - "Ϳ": [t: "⠰⠊"] - "·": [t: "⠰"] -- "α": [t: "⠰⠁"] -- "β": [t: "⠰⠃"] -- "γ": [t: "⠰⠛"] -- "δ": [t: "⠰⠙"] -- "ε": [t: "⠰⠑"] -- "ζ": [t: "⠰⠵"] -- "η": [t: "⠰⠚"] -- "θ": [t: "⠰⠓"] -- "ι": [t: "⠰⠊"] -- "κ": [t: "⠰⠅"] -- "λ": [t: "⠰⠇"] -- "μ": [t: "⠰⠍"] -- "ν": [t: "⠰⠝"] -- "ξ": [t: "⠰⠭"] -- "ο": [t: "⠰⠕"] -- "π": [t: "⠰⠏"] -- "ρ": [t: "⠰⠗"] -- "σ": [t: "⠰⠎"] -- "τ": [t: "⠰⠞"] -- "υ": [t: "⠰⠥"] -- "φ": [t: "⠰⠋"] -- "χ": [t: "⠰⠉"] -- "ψ": [t: "⠰⠽"] -- "ω": [t: "⠰⠺"] -- "Α": [t: "⠸⠁"] -- "Β": [t: "⠸⠃"] -- "Γ": [t: "⠸⠛"] -- "Δ": [t: "⠸⠙"] -- "Ε": [t: "⠸⠑"] -- "Ζ": [t: "⠸⠵"] -- "Η": [t: "⠸⠚"] -- "Θ": [t: "⠸⠓"] -- "Ι": [t: "⠸⠊"] -- "Κ": [t: "⠸⠅"] -- "Λ": [t: "⠸⠇"] -- "Μ": [t: "⠸⠍"] -- "Ν": [t: "⠸⠝"] -- "Ξ": [t: "⠸⠭"] -- "Ο": [t: "⠸⠕"] -- "Π": [t: "⠸⠏"] -- "Ρ": [t: "⠸⠗"] -- "Σ": [t: "⠸⠎"] -- "Τ": [t: "⠸⠞"] -- "Υ": [t: "⠸⠥"] -- "Φ": [t: "⠸⠋"] -- "Χ": [t: "⠸⠉"] -- "Ψ": [t: "⠸⠽"] -- "Ω": [t: "⠸⠺"] -- "ϐ": [t: "⠰⠃"] -- "ϑ": [t: "⠰⠓"] -- "ϕ": [t: "⠰⠋"] -- "ϖ": [t: "⠰⠏"] -- "ϱ": [t: "⠰⠗"] +- "α": [t: "g⠁"] +- "β": [t: "g⠃"] +- "γ": [t: "g⠛"] +- "δ": [t: "g⠙"] +- "ε": [t: "g⠑"] +- "ζ": [t: "g⠵"] +- "η": [t: "g⠚"] +- "θ": [t: "g⠓"] +- "ι": [t: "g⠊"] +- "κ": [t: "g⠅"] +- "λ": [t: "g⠇"] +- "μ": [t: "g⠍"] +- "ν": [t: "g⠝"] +- "ξ": [t: "g⠭"] +- "ο": [t: "g⠕"] +- "π": [t: "g⠏"] +- "ρ": [t: "g⠗"] +- "σ": [t: "g⠎"] +- "τ": [t: "g⠞"] +- "υ": [t: "g⠥"] +- "φ": [t: "g⠋"] +- "χ": [t: "g⠉"] +- "ψ": [t: "g⠽"] +- "ω": [t: "g⠺"] +- "Α": [t: "v⠁"] +- "Β": [t: "v⠃"] +- "Γ": [t: "v⠛"] +- "Δ": [t: "v⠙"] +- "Ε": [t: "v⠑"] +- "Ζ": [t: "v⠵"] +- "Η": [t: "v⠚"] +- "Θ": [t: "v⠓"] +- "Ι": [t: "v⠊"] +- "Κ": [t: "v⠅"] +- "Λ": [t: "v⠇"] +- "Μ": [t: "v⠍"] +- "Ν": [t: "v⠝"] +- "Ξ": [t: "v⠭"] +- "Ο": [t: "v⠕"] +- "Π": [t: "v⠏"] +- "Ρ": [t: "v⠗"] +- "Σ": [t: "v⠎"] +- "Τ": [t: "v⠞"] +- "Υ": [t: "v⠥"] +- "Φ": [t: "v⠋"] +- "Χ": [t: "v⠉"] +- "Ψ": [t: "v⠽"] +- "Ω": [t: "v⠺"] +- "ϐ": [t: "g⠃"] +- "ϑ": [t: "g⠓"] +- "ϕ": [t: "g⠋"] +- "ϖ": [t: "g⠏"] +- "ϱ": [t: "g⠗"] - "′": [t: "⠔"] - "″": [t: "⠔⠔"] - "‴": [t: "⠔⠔⠔"] @@ -97,7 +97,7 @@ - "·": [t: "⠄"] - "×": [t: "⠀⠦"] - "⋅": [t: "⠄"] -- "⁢": [t: "⠄"] +- "⁢": [t: ""] - "∘": [t: "⠴"] - "√": [t: "⠩⠱"] - "∛": [t: "⠩⠒⠱"] @@ -163,58 +163,58 @@ - "♮": [t: "⠻⠨⠝⠻"] # Latin letters and digits used for mathematical identifiers. -- "a": [t: "⠁"] -- "A": [t: "C⠁"] -- "b": [t: "⠃"] -- "B": [t: "C⠃"] -- "c": [t: "⠉"] -- "C": [t: "C⠉"] -- "d": [t: "⠙"] -- "D": [t: "C⠙"] -- "e": [t: "⠑"] -- "E": [t: "C⠑"] -- "f": [t: "⠋"] -- "F": [t: "C⠋"] -- "g": [t: "⠛"] -- "G": [t: "C⠛"] -- "h": [t: "⠓"] -- "H": [t: "C⠓"] -- "i": [t: "⠊"] -- "I": [t: "C⠊"] -- "j": [t: "⠚"] -- "J": [t: "C⠚"] -- "k": [t: "⠅"] -- "K": [t: "C⠅"] -- "l": [t: "⠇"] -- "L": [t: "C⠇"] -- "m": [t: "⠍"] -- "M": [t: "C⠍"] -- "n": [t: "⠝"] -- "N": [t: "C⠝"] -- "o": [t: "⠕"] -- "O": [t: "C⠕"] -- "p": [t: "⠏"] -- "P": [t: "C⠏"] -- "q": [t: "⠟"] -- "Q": [t: "C⠟"] -- "r": [t: "⠗"] -- "R": [t: "C⠗"] -- "s": [t: "⠎"] -- "S": [t: "C⠎"] -- "t": [t: "⠞"] -- "T": [t: "C⠞"] -- "u": [t: "⠥"] -- "U": [t: "C⠥"] -- "v": [t: "⠧"] -- "V": [t: "C⠧"] -- "w": [t: "⠺"] -- "W": [t: "C⠺"] -- "x": [t: "⠭"] -- "X": [t: "C⠭"] -- "y": [t: "⠽"] -- "Y": [t: "C⠽"] -- "z": [t: "⠵"] -- "Z": [t: "C⠵"] +- "a": [t: "l⠁"] +- "A": [t: "u⠁"] +- "b": [t: "l⠃"] +- "B": [t: "u⠃"] +- "c": [t: "l⠉"] +- "C": [t: "u⠉"] +- "d": [t: "l⠙"] +- "D": [t: "u⠙"] +- "e": [t: "l⠑"] +- "E": [t: "u⠑"] +- "f": [t: "l⠋"] +- "F": [t: "u⠋"] +- "g": [t: "l⠛"] +- "G": [t: "u⠛"] +- "h": [t: "l⠓"] +- "H": [t: "u⠓"] +- "i": [t: "l⠊"] +- "I": [t: "u⠊"] +- "j": [t: "l⠚"] +- "J": [t: "u⠚"] +- "k": [t: "l⠅"] +- "K": [t: "u⠅"] +- "l": [t: "l⠇"] +- "L": [t: "u⠇"] +- "m": [t: "l⠍"] +- "M": [t: "u⠍"] +- "n": [t: "l⠝"] +- "N": [t: "u⠝"] +- "o": [t: "l⠕"] +- "O": [t: "u⠕"] +- "p": [t: "l⠏"] +- "P": [t: "u⠏"] +- "q": [t: "l⠟"] +- "Q": [t: "u⠟"] +- "r": [t: "l⠗"] +- "R": [t: "u⠗"] +- "s": [t: "l⠎"] +- "S": [t: "u⠎"] +- "t": [t: "l⠞"] +- "T": [t: "u⠞"] +- "u": [t: "l⠥"] +- "U": [t: "u⠥"] +- "v": [t: "l⠧"] +- "V": [t: "u⠧"] +- "w": [t: "l⠺"] +- "W": [t: "u⠺"] +- "x": [t: "l⠭"] +- "X": [t: "u⠭"] +- "y": [t: "l⠽"] +- "Y": [t: "u⠽"] +- "z": [t: "l⠵"] +- "Z": [t: "u⠵"] - "0": [t: "N⠚"] - "1": [t: "N⠁"] - "2": [t: "N⠃"] diff --git a/src/braille.rs b/src/braille.rs index 1c6ff0d05..9fd64b4cc 100644 --- a/src/braille.rs +++ b/src/braille.rs @@ -2182,7 +2182,8 @@ fn russian_cleanup(_pref_manager: Ref, raw_braille: String) - previous_char_was_digit = matches!(ch, '⠚' | '⠁' | '⠃' | '⠉' | '⠙' | '⠑' | '⠋' | '⠛' | '⠓' | '⠊'); } - let result = REPLACE_INDICATORS.replace_all(&raw_braille_without_repeated_number_indicators, |cap: &Captures| { + let result = add_russian_alphabet_indicators(&raw_braille_without_repeated_number_indicators); + let result = REPLACE_INDICATORS.replace_all(&result, |cap: &Captures| { match &cap[0] { "B" => "⠸", "C" => "⠠", @@ -2197,6 +2198,33 @@ fn russian_cleanup(_pref_manager: Ref, raw_braille: String) - return COLLAPSE_SPACES.replace_all(&result, "⠀") .trim_matches('⠀') .to_string(); + + fn add_russian_alphabet_indicators(raw_braille: &str) -> String { + let mut result = String::with_capacity(raw_braille.len()); + let mut alphabet_mode = None; + for ch in raw_braille.chars() { + match ch { + 'l' | 'u' | 'g' | 'v' => { + if alphabet_mode != Some(ch) { + result.push_str(match ch { + 'l' => "⠠", // Latin lowercase: dots 6 + 'u' => "⠨", // Latin uppercase: dots 4-6 + 'g' => "⠰", // Greek lowercase: dots 5-6 + 'v' => "⠸", // Greek uppercase: dots 4-5-6 + _ => unreachable!(), + }); + alphabet_mode = Some(ch); + } + }, + 'C' | 'N' | '#' => { + alphabet_mode = None; + result.push(ch); + }, + _ => result.push(ch), + } + } + return result; + } } #[allow(non_snake_case)] diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index 7ccb33d48..589ab9863 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -11,14 +11,14 @@ fn numbers_and_operators() -> Result<()> { #[test] fn fraction() -> Result<()> { let expr = r#"x2"#; - test_braille("Russian", expr, "⠆⠭⠀⠳⠼⠃⠰")?; + test_braille("Russian", expr, "⠆⠠⠭⠀⠳⠼⠃⠰")?; return Ok(()); } #[test] fn scripts_and_root() -> Result<()> { let expr = r#"x2+y"#; - test_braille("Russian", expr, "⠭⠌⠼⠃⠱⠀⠖⠩⠱⠽⠹")?; + test_braille("Russian", expr, "⠠⠭⠌⠼⠃⠱⠀⠖⠩⠱⠠⠽⠹")?; return Ok(()); } @@ -30,7 +30,7 @@ fn cyrillic_text() -> Result<()> { } #[test] -fn wikipedia_linear_parens() -> Result<()> { +fn wikipedia_linear_parens_flat() -> Result<()> { let expr = r#"3(9-7)=6"#; test_braille("Russian", expr, "⠼⠉⠄⠣⠼⠊⠀⠤⠼⠛⠜⠀⠶⠼⠋")?; return Ok(()); @@ -52,7 +52,27 @@ fn source_arithmetic_examples() -> Result<()> { #[test] fn nested_fraction_and_root() -> Result<()> { let expr = r#"x+1yx-y"#; - test_braille("Russian", expr, "⠆⠭⠀⠖⠩⠱⠆⠼⠁⠀⠳⠽⠰⠹⠀⠳⠭⠀⠤⠽⠰")?; + test_braille("Russian", expr, "⠆⠠⠭⠀⠖⠩⠱⠆⠼⠁⠀⠳⠠⠽⠰⠹⠀⠳⠭⠀⠤⠽⠰")?; + return Ok(()); +} + +#[test] +fn latin_alphabet_indicators() -> Result<()> { + let expr = r#"x+A+y+B=x+y+A+B"#; + test_braille("Russian", expr, "⠠⠭⠀⠖⠨⠁⠀⠖⠠⠽⠀⠖⠨⠃⠀⠶⠠⠭⠀⠖⠽⠀⠖⠨⠁⠀⠖⠃")?; + return Ok(()); +} + +#[test] +fn alphabet_indicators_after_numbers_and_greek() -> Result<()> { + let expr = r#"2x+15=23"#; + test_braille("Russian", expr, "⠼⠃⠄⠠⠭⠀⠖⠼⠁⠑⠀⠶⠼⠃⠉")?; + + let expr = r#"L=2πr"#; + test_braille("Russian", expr, "⠨⠇⠀⠶⠼⠃⠰⠏⠠⠗")?; + + let expr = r#"α+β=Α+Β"#; + test_braille("Russian", expr, "⠰⠁⠀⠖⠃⠀⠶⠸⠁⠀⠖⠃")?; return Ok(()); } @@ -73,6 +93,6 @@ fn wikipedia_linear_parens() -> Result<()> { #[test] fn wikipedia_sqrt() -> Result<()> { let expr = r#"10000<101"#; - test_braille("Russian", expr, "⠩⠱⠼⠁⠚⠚⠚⠚⠀⠪⠀⠼⠁⠚⠁")?; + test_braille("Russian", expr, "⠩⠱⠼⠁⠚⠚⠚⠚⠹⠀⠪⠀⠼⠁⠚⠁")?; return Ok(()); } From 80f1edd6a17cbe7d3fc3658080cf69834b337484 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:00:38 +0300 Subject: [PATCH 05/47] Braille - Roots, powers and scripts This commit adds Roots, powers and scripts according to the Russian math Braille specs. --- Rules/Braille/Russian/Russian_Rules.yaml | 63 +++++++++++++++++++++++- Rules/Braille/Russian/unicode.yaml | 12 +++++ tests/braille/Russian/russian.rs | 22 +++++++-- 3 files changed, 93 insertions(+), 4 deletions(-) diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index 1effd8c44..f717e6b68 100644 --- a/Rules/Braille/Russian/Russian_Rules.yaml +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -67,7 +67,7 @@ - name: default tag: mroot - match: "." + match: "not(*[2][self::m:mn and translate(., '0123456789', '')=''])" replace: - t: "⠩" - x: "*[2]" @@ -75,6 +75,43 @@ - x: "*[1]" - t: "⠹" +- + name: integer-index + tag: mroot + match: "*[2][self::m:mn and translate(., '0123456789', '')='']" + replace: + - t: "⠩" + - x: "BrailleChars(translate(*[2], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - t: "⠱" + - x: "*[1]" + - t: "⠹" + +- + name: simple-numeric + tag: mfrac + match: + - "*[1][self::m:mn and translate(., '0123456789', '')=''] and" + - "*[2][self::m:mn and translate(., '0123456789', '')='']" + replace: + - x: "*[1]" + - x: "BrailleChars(translate(*[2], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + +- + name: simple-algebraic + tag: mfrac + match: + - "(*[1][self::m:mi] or *[1][self::m:mn and translate(., '0123456789', '')='']) and" + - "(*[2][self::m:mi] or *[2][self::m:mn and translate(., '0123456789', '')=''])" + replace: + - x: "*[1]" + - t: "⠳" + - test: + if: "*[2][self::m:mn]" + then: + - x: "BrailleChars(translate(*[2], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + else: + - x: "*[2]" + - name: default tag: mfrac @@ -163,6 +200,18 @@ match: "*[2][translate(., \"'*`ª°²³´¹º′″‴‵‶‷⁗\",'')='']" replace: [x: "*"] +- + name: integer + tag: [msub, munder] + match: "*[2][self::m:mn and translate(., '0123456789', '')='']" + replace: + - x: "*[1]" + - test: + if: "self::m:munder" + then: [t: "⠨⠡"] + else: [t: "⠡"] + - x: "BrailleChars(translate(*[2], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - name: default tag: [msub, munder] @@ -176,6 +225,18 @@ - x: "*[2]" - t: "⠱" +- + name: integer + tag: [msup, mover] + match: "*[2][self::m:mn and translate(., '0123456789', '')='']" + replace: + - x: "*[1]" + - test: + if: "self::m:mover" + then: [t: "⠨⠌"] + else: [t: "⠌"] + - x: "BrailleChars(translate(*[2], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - name: default tag: [msup, mover] diff --git a/Rules/Braille/Russian/unicode.yaml b/Rules/Braille/Russian/unicode.yaml index 8b72b2ba6..4407eb8ec 100644 --- a/Rules/Braille/Russian/unicode.yaml +++ b/Rules/Braille/Russian/unicode.yaml @@ -226,6 +226,18 @@ - "8": [t: "N⠓"] - "9": [t: "N⠊"] +# Lowered digits used in denominators and integer scripts. +- "\ue000": [t: "⠴"] +- "\ue001": [t: "⠂"] +- "\ue002": [t: "⠆"] +- "\ue003": [t: "⠒"] +- "\ue004": [t: "⠲"] +- "\ue005": [t: "⠢"] +- "\ue006": [t: "⠖"] +- "\ue007": [t: "⠶"] +- "\ue008": [t: "⠦"] +- "\ue009": [t: "⠔"] + # Russian literary braille letters for mtext and Cyrillic identifiers. - "а": [t: "⠁"] - "А": [t: "C⠁"] diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index 589ab9863..4cdf97962 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -11,14 +11,14 @@ fn numbers_and_operators() -> Result<()> { #[test] fn fraction() -> Result<()> { let expr = r#"x2"#; - test_braille("Russian", expr, "⠆⠠⠭⠀⠳⠼⠃⠰")?; + test_braille("Russian", expr, "⠠⠭⠳⠆")?; return Ok(()); } #[test] fn scripts_and_root() -> Result<()> { let expr = r#"x2+y"#; - test_braille("Russian", expr, "⠠⠭⠌⠼⠃⠱⠀⠖⠩⠱⠠⠽⠹")?; + test_braille("Russian", expr, "⠠⠭⠌⠆⠀⠖⠩⠱⠽⠹")?; return Ok(()); } @@ -52,7 +52,23 @@ fn source_arithmetic_examples() -> Result<()> { #[test] fn nested_fraction_and_root() -> Result<()> { let expr = r#"x+1yx-y"#; - test_braille("Russian", expr, "⠆⠠⠭⠀⠖⠩⠱⠆⠼⠁⠀⠳⠠⠽⠰⠹⠀⠳⠭⠀⠤⠽⠰")?; + test_braille("Russian", expr, "⠆⠠⠭⠀⠖⠩⠱⠼⠁⠳⠠⠽⠹⠀⠳⠭⠀⠤⠽⠰")?; + return Ok(()); +} + +#[test] +fn source_simple_fractions_scripts_roots() -> Result<()> { + let expr = r#"12"#; + test_braille("Russian", expr, "⠼⠁⠆")?; + + let expr = r#"a3"#; + test_braille("Russian", expr, "⠠⠁⠳⠒")?; + + let expr = r#"b7"#; + test_braille("Russian", expr, "⠠⠃⠡⠶")?; + + let expr = r#"x3"#; + test_braille("Russian", expr, "⠩⠒⠱⠠⠭⠹")?; return Ok(()); } From efe10da4ec744969ba86cb2bdbb9eb36bf7127d1 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:14:46 +0300 Subject: [PATCH 06/47] Update Russian Braille rules and mappings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix YAML replace syntax and extend Russian braille rules: add chemical-formula-operator-bond and prefix-geometry-ops rules, broaden function-name matching (include tg/ctg variants) and add separator markers. Update unicode mappings (degree sign, invisible characters, conditional ∪ mapping, parallel/arrow glyphs) and adjust several function/operator braille outputs. Add/expand tests to cover functions, geometry, matrices and chemical formulas to validate the changes. --- Rules/Braille/Russian/Russian_Rules.yaml | 62 ++++++++++++++++-------- Rules/Braille/Russian/unicode.yaml | 16 +++++- tests/braille/Russian/russian.rs | 35 +++++++++++++ 3 files changed, 92 insertions(+), 21 deletions(-) diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index f717e6b68..6ac752bc6 100644 --- a/Rules/Braille/Russian/Russian_Rules.yaml +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -162,9 +162,7 @@ name: default tag: mtable match: "." - replace: - - t: "⠞⠃⠇" - - x: "*" + replace: [x: "*"] - name: default @@ -283,6 +281,30 @@ - x: "*" - t: "⠜" +- + name: chemical-formula-operator-bond + tag: mo + match: "@data-chemical-bond" + replace: + - test: + - if: ".='-' or .=':'" + then: [t: "⠤"] + - else_if: ".='=' or .='∷'" + then: [t: "⠦"] + - else_if: ".='≡'" + then: [t: "⠿"] + else: [x: "text()"] + +- + name: prefix-geometry-ops + tag: mo + match: + - "parent::*[count(*) = 2 and name()='mrow' and not(preceding-sibling::*)] and" + - "( .='∠' or .='△' or .='∪' )" + replace: + - x: "text()" + - t: "#" + - name: default tag: mo @@ -300,39 +322,41 @@ - name: functions tag: mi - match: "IsInDefinition(., 'Speech', 'FunctionNames')" + match: + - "IsInDefinition(., 'Speech', 'FunctionNames') or" + - ".='tg' or .='ctg' or .='arctg' or .='arcctg'" replace: - test: - if: ".='arcsin'" - then: [t: "⠫⠁⠎"] + then: [t: "⠫⠁⠎#"] - else_if: ".='arccos'" - then: [t: "⠫⠁⠉"] + then: [t: "⠫⠁⠉#"] - else_if: ".='arctg' or .='arctan'" - then: [t: "⠫⠁⠞"] + then: [t: "⠫⠁⠞#"] - else_if: ".='arcctg' or .='arccotan' or .='arccot'" - then: [t: "⠫⠁⠉⠞"] + then: [t: "⠫⠁⠉⠞#"] - else_if: ".='sin'" - then: [t: "⠫⠎"] + then: [t: "⠫⠎#"] - else_if: ".='cos'" - then: [t: "⠫⠉"] + then: [t: "⠫⠉#"] - else_if: ".='tg' or .='tan'" - then: [t: "⠫⠞"] + then: [t: "⠫⠞#"] - else_if: ".='ctg' or .='cotan' or .='cot'" - then: [t: "⠫⠉⠞"] + then: [t: "⠫⠉⠞#"] - else_if: ".='log'" - then: [t: "⠫⠇"] + then: [t: "⠫⠇#"] - else_if: ".='ln'" - then: [t: "⠫⠇⠝"] + then: [t: "⠫⠇⠝#"] - else_if: ".='lg'" - then: [t: "⠫⠇⠛"] + then: [t: "⠫⠇⠛#"] - else_if: ".='lim'" - then: [t: "⠫⠇⠍"] + then: [t: "⠫⠇⠍#"] - else_if: ".='min'" - then: [t: "⠫⠍⠝"] + then: [t: "⠫⠍⠝#"] - else_if: ".='max'" - then: [t: "⠫⠍⠭"] + then: [t: "⠫⠍⠭#"] - else_if: ".='exp'" - then: [t: "⠫⠑"] + then: [t: "⠫⠑#"] else: [x: "BrailleChars(., 'Russian')"] - diff --git a/Rules/Braille/Russian/unicode.yaml b/Rules/Braille/Russian/unicode.yaml index 4407eb8ec..885f51a6b 100644 --- a/Rules/Braille/Russian/unicode.yaml +++ b/Rules/Braille/Russian/unicode.yaml @@ -76,6 +76,7 @@ - "′": [t: "⠔"] - "″": [t: "⠔⠔"] - "‴": [t: "⠔⠔⠔"] +- "°": [t: "⠨⠴"] - "∀": [t: "⠫⠄"] - "∂": [t: "⠹"] - "∃": [t: "⠫⠢"] @@ -97,7 +98,10 @@ - "·": [t: "⠄"] - "×": [t: "⠀⠦"] - "⋅": [t: "⠄"] +- "⁡": [t: ""] - "⁢": [t: ""] +- "⁣": [t: ""] +- "⁤": [t: ""] - "∘": [t: "⠴"] - "√": [t: "⠩⠱"] - "∛": [t: "⠩⠒⠱"] @@ -106,11 +110,15 @@ - "∠": [t: "⠸⠪"] - "∣": [t: "⠸"] - "∤": [t: "⠀⠼⠀"] -- "∥": [t: "⠸⠸"] +- "∥": [t: "⠸⠸#"] - "∧": [t: "⠀⠰⠢"] - "∨": [t: "⠀⠰⠔"] - "∩": [t: "⠀⠰⠲"] -- "∪": [t: "⠀⠰⠴"] +- "∪": + - test: + if: "following-sibling::*[1][self::m:mi]" + then: [t: "⠸⠜"] + else: [t: "⠀⠰⠴"] - "∫": [t: "⠮"] - "∬": [t: "⠮⠮"] - "∭": [t: "⠮⠮⠮"] @@ -159,6 +167,10 @@ - "⋰": [t: "⠠⠲⠲⠲"] - "⋱": [t: "⠠⠲⠲⠲"] - "△": [t: "⠸⠙"] +- "→": [t: "⠀⠒⠕"] +- "⟶": [t: "⠀⠒⠕"] +- "←": [t: "⠀⠦⠶"] +- "⟵": [t: "⠀⠦⠶"] - "◻": [t: "⠶"] - "♮": [t: "⠻⠨⠝⠻"] diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index 4cdf97962..313c45326 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -112,3 +112,38 @@ fn wikipedia_sqrt() -> Result<()> { test_braille("Russian", expr, "⠩⠱⠼⠁⠚⠚⠚⠚⠹⠀⠪⠀⠼⠁⠚⠁")?; return Ok(()); } + +#[test] +fn source_functions_logs_derivatives() -> Result<()> { + let expr = r#"cosα"#; + test_braille("Russian", expr, "⠫⠉⠰⠁")?; + + let expr = r#"tgxctgx=1"#; + test_braille("Russian", expr, "⠫⠞⠠⠭⠄⠫⠉⠞⠠⠭⠀⠶⠼⠁")?; + + let expr = r#"logxylogyx=1"#; + test_braille("Russian", expr, "⠫⠇⠡⠠⠭⠱⠽⠄⠫⠇⠡⠠⠽⠱⠭⠀⠶⠼⠁")?; + + let expr = r#"y(x)=f(x)"#; + test_braille("Russian", expr, "⠠⠽⠔⠣⠭⠜⠀⠶⠋⠣⠭⠜")?; + return Ok(()); +} + +#[test] +fn source_geometry_matrix_chemistry() -> Result<()> { + let expr = r#"ABC=15°3012"#; + test_braille("Russian", expr, "⠸⠪⠨⠁⠃⠉⠀⠶⠼⠁⠑⠨⠴⠼⠉⠚⠔⠼⠁⠃⠔⠔")?; + + let expr = r#"ABCD"#; + test_braille("Russian", expr, "⠨⠁⠃⠸⠸⠨⠉⠙")?; + + let expr = r#"(abcd)"#; + test_braille("Russian", expr, "⠣⠠⠁⠀⠃⠨⠳⠉⠀⠙⠜")?; + + let expr = r#"H2O"#; + test_braille("Russian", expr, "⠨⠓⠡⠆⠕")?; + + let expr = r#"H2C=CH2"#; + test_braille("Russian", expr, "⠨⠓⠡⠆⠉⠦⠉⠓⠡⠆")?; + return Ok(()); +} From e298a6d6cce53c94d96b003d6a70bf378ba75152 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:13:16 +0300 Subject: [PATCH 07/47] Add Russian language to docs Add Russian (ru) to the list of supported speech languages and update Russian documentation and translation guidance. - docs/User_guide_for_MathCAT_en.md, docs/index.md: add Russian (ru) to supported languages lists. - docs/ru/User_guide_for_MathCAT_ru.md: include Russian in language options, reorder entries and apply minor wording fixes. - docs/ru/helpers.md: clarify wording, update examples and test instructions to reference ru instead of fr, and improve translation guidance and examples (including test and cargo commands). - docs/ru/index.md: reorder supported languages and polish phrasing. These edits enable and document Russian support and improve clarity for translators and developers working on the Russian localization. --- docs/User_guide_for_MathCAT_en.md | 1 + docs/index.md | 2 +- docs/ru/User_guide_for_MathCAT_ru.md | 7 +++-- docs/ru/helpers.md | 46 ++++++++++++++-------------- docs/ru/index.md | 4 +-- 5 files changed, 31 insertions(+), 29 deletions(-) diff --git a/docs/User_guide_for_MathCAT_en.md b/docs/User_guide_for_MathCAT_en.md index c63431a81..80e19314d 100644 --- a/docs/User_guide_for_MathCAT_en.md +++ b/docs/User_guide_for_MathCAT_en.md @@ -39,6 +39,7 @@ You can choose between the following settings for speech. Below each setting you - \[English (en)\] - Spanish (es) - Indonesian (id) + - Russian (ru) - Swedish (sv) - Vietnamese (vi) - Chinese, traditional (zh-tw) diff --git a/docs/index.md b/docs/index.md index c4c713172..64b82251d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -33,7 +33,7 @@ MathCAT uses a number of heuristics that try to repair poor MathML and put it in MathCAT is under active development. DAISY is actively participating in the development effort, and contributions are welcome. MathCAT is open source, and the [GitHub repository is available here](https://github.com/daisy/MathCAT). [NVDA-specific problems with the MathCAT add-on can be reported here](https://github.com/daisy/MathCATForPython/issues). * MathCAT currently supports speech and navigation for -English, German, Spanish, Finnish, Indonesian, Norwegian, Swedish, Vietnamese, and Chinese (Traditional). +English, German, Spanish, Finnish, Indonesian, Norwegian, Russian, Swedish, Vietnamese, and Chinese (Traditional). * MathCAT's braille support includes Nemeth, UEB, CMU, Vietnamese, German/Austrian LaTeX and ASCIIMath. * An [NVDA add-on](https://addons.nvda-project.org/addons/MathCAT.en.html) exists. It should be usable as a MathPlayer replacement for those using the English version or one of the supported translations. As of NVDA 2026.1, MathCAT is built into NVDA and no download is needed. diff --git a/docs/ru/User_guide_for_MathCAT_ru.md b/docs/ru/User_guide_for_MathCAT_ru.md index 43e230816..fec001990 100644 --- a/docs/ru/User_guide_for_MathCAT_ru.md +++ b/docs/ru/User_guide_for_MathCAT_ru.md @@ -43,11 +43,12 @@ MathCAT можно настроить в соответствии с вашим - Трудности в обучении. Более краткое озвучивание. - Язык: (по умолчанию используется язык программы экранного доступа) - \[Английский (en)\] - - Испанский (es) - - Индонезийский (id) - - Шведский (sv) - Вьетнамский (vi) + - Индонезийский (id) + - Испанский (es) - Китайский, традиционное письмо (zh-tw) + - Русский (ru) + - Шведский (sv) - Стиль речи: - \[ClearSpeak.\] Выражения озвучиваются примерно так, как их произнёс бы учитель на уроке. - SimpleSpeak. Выражения озвучиваются более кратко. Иногда такое чтение может быть неоднозначным. diff --git a/docs/ru/helpers.md b/docs/ru/helpers.md index bd63b7c4e..be36a6116 100644 --- a/docs/ru/helpers.md +++ b/docs/ru/helpers.md @@ -20,19 +20,19 @@ title: Руководство переводчика и разработчика ## Переводчикам на другие языки Если вы хотите перевести MathCAT, свяжитесь с @NSoiffer. Он подготовит начальный вариант перевода, который может значительно сэкономить время. В результате будут созданы файлы в каталоге `Rules/Languages/xx`, где `xx` — код языка, например `fr`, `de` или `el`. Перевод выполняется в этом каталоге. Необходимо отредактировать четыре категории файлов: -1. `definitions.yaml`: содержит переводы числительных, в том числе количественных и порядковых. Проверьте начальный перевод и внесите необходимые исправления. Эти числительные используются, например, при озвучивании фразы «три пятых». На некотором этапе образование числительных в языках становится регулярным, поэтому некоторые списки в файле можно сократить, а некоторые следует дополнить. Подробнее см. английские комментарии в файле. +1. `definitions.yaml`: содержит переводы числительных, в том числе количественных и порядковых. Проверьте начальный перевод и внесите необходимые исправления. Эти числительные используются, например, при озвучивании фразы «три пятых». На некотором этапе образование числительных в языках становится регулярным, поэтому некоторые списки в файле можно сократить, а некоторые следует дополнить. Подробнее об этом можно узнать из английских комментариев в файле. 2. Файлы `xxx_Rules.yaml`, сейчас это `ClearSpeak_Rules.yaml` и `SimpleSpeak_Rules.yaml`. Они соответствуют разным стилям речи. Настоятельно рекомендуется сначала выбрать только один стиль. Эти файлы обычно содержат слова, описывающие структуру выражения, например «дробь» и «степень», а также связующие слова. Поскольку стили речи во многом похожи, существует также каталог `SharedRules` с общими файлами правил. Они подключаются в `ClearSpeak_Rules.yaml` и `SimpleSpeak_Rules.yaml` с помощью правил `- include: file_name`. Их тоже необходимо перевести.

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

Эти файлы содержат автоматически созданные начальные переводы. Хотя текст уже переведён, используется ключ `t:`, а не `T:` с прописной буквы. Каждый перевод необходимо проверить и только после этого изменить ключ на вариант с прописной буквы. Подробнее об автоматическом переводе см. ниже. * В некоторых языках нет смысла произносить эквиваленты слов «the» и, возможно, «of» во фразе «the square root of x». В таком случае замените их пустыми строками. - * В некоторых языках меняется порядок слов. Переставляйте слова свободно, но внимательно следите за отступами: в YAML они значимы. + * В некоторых языках меняется порядок слов. Переставляйте слова свободно, но внимательно следите за отступами: в YAML они имеют значение. * В некоторых языках могут потребоваться слова, отсутствующие в английской версии, до или после существующих фраз. Добавляйте их при необходимости. Условное добавление выполняется с помощью `test`. Если нужна помощь, свяжитесь с @NSoiffer. - * Паузы между словами и фразами могут значительно улучшить понятность речи. Исходные паузы выбраны для английского языка. Настройте их в соответствии со звучанием синтезаторов вашего языка. Паузы легко добавлять, удалять и изменять. Их длительность масштабируется в соответствии с текущей скоростью речи. + * Паузы между словами и фразами могут значительно улучшить понятность речи. Исходные паузы выбраны для английского языка. Настройте их в соответствии со звучанием синтезаторов речи вашего языка. Паузы легко добавлять, удалять и изменять. Их длительность масштабируется в соответствии с текущей скоростью речи. 3. Файлы Unicode: `unicode.yaml` и `unicode-full.yaml`. Они содержат такие символы, как `<` и `∫`. * Начните с перевода `unicode.yaml`. В нём находится подавляющее большинство используемых математических символов. Сейчас список основан на практическом опыте, но в дальнейшем планируется уточнить его по статистике из реальных книг. В `unicode.yaml` около 270 символов, примерно 50 из которых — греческие буквы. Как и в файлах правил речи, здесь есть автоматически созданные начальные переводы. Проверьте их и измените `t:` на `T:`. Подробнее об автоматическом переводе см. ниже. * В `unicode-full.yaml` тысячи строк. Вернитесь к нему после завершения остальных переводов и работайте столько, сколько сможете: большинство этих символов встречаются только в сложной математике и даже там используются редко. Наиболее важны: @@ -57,15 +57,15 @@ __ПРИМЕЧАНИЕ__: Сейчас почти завершён перехо then: [t: "is"] - t: "greater than" ``` -При переводе на французский язык слова после `t:` заменяются примерно так: +При переводе на русский язык слова после `t:` заменяются примерно так: ``` - - "=": [T: "égale"] # 0x3d + - "=": [T: "равно"] # 0x3d - ">": # 0x3e - test: if: "$Verbosity!='Terse'" - then: [T: "est"] - - T: "supérieur à" + then: [T: ""] + - T: "больше" ``` Примечание: иногда значение `IfThenElse` не требует перевода, но ключ всё равно следует изменить, чтобы было видно, что строка проверена. В следующем примере перевод не нужен, поскольку части `then` и `else`, соответственно `count(*/*[1])` и `$LineCountTry`, не являются словами: @@ -84,15 +84,15 @@ __ПРИМЕЧАНИЕ__: Сейчас почти завершён перехо Если переводы MathPlayer и SRE различаются, выбирается вариант, совпадающий с Google Translate, а другой вариант включается в комментарий. Например: ``` - else: [t: "parenthèse gauche"] # (en: 'left paren', MathPlayer: 'parenthèse ouvrante') + else: [t: "левая круглая скобка"] # (en: 'left paren', MathPlayer: 'открывающая круглая скобка') ``` Если не совпадает ни один перевод, выбирается один из вариантов, а остальные приводятся в комментарии. Например: ``` - else: [t: "parenthèse gauche"] # (en: 'open paren', MathPlayer: 'parenthèse ouvrante', google: 'parenthèse ouverte') + else: [t: "левая круглая скобка"] # (en: 'open paren', MathPlayer: 'открывающая круглая скобка', google: 'открытая скобка') ``` Наконец, если перевод отсутствует, используется Google Translate и добавляется комментарий `google translation`. Вероятность неудачного перевода в таком случае заметно выше, поэтому внимательно проверяйте эти строки. Пример, где доступен только Google Translate: ``` - then: [t: "ligne verticale"] # (en: 'vertical line', google translation) + then: [t: "вертикальная линия"] # (en: 'vertical line', google translation) ``` @@ -100,7 +100,7 @@ __ПРИМЕЧАНИЕ__: Сейчас почти завершён перехо Если вы используете NVDA, начатый перевод можно сразу проверить. Предполагается, что дополнение MathCAT уже установлено: 1. Скопируйте новый каталог перевода в `%AppData%\nvda\addons\MathCAT\globalPlugins\MathCAT\Rules\Languages`. 2. Запустите NVDA и откройте меню настроек MathCAT: параметры NVDA, затем «Настройки MathCAT...». -3. Выберите новый язык в раскрывающемся списке `Languages`. +3. Выберите новый язык в комбинированном списке `Languages`. 4. Проверьте озвучивание. Хорошим источником примеров служат страницы Википедии. 5. Если произошла ошибка, часто выражающаяся в отсутствии речи, откройте журнал NVDA из подменю «Сервис». Ошибка должна быть указана там. Объяснение сообщений об ошибках приведено ниже. 6. MathCAT должен заметить изменение файла и перезагрузить его. Сейчас это не работает для файлов, подключённых с помощью `include`, например для файлов каталога `Shared`. После изменения такого файла перезагрузите MathCAT через «Сервис: Перезагрузить плагины» в NVDA или перезапустите NVDA. @@ -110,16 +110,16 @@ __ПРИМЕЧАНИЕ__: Сейчас почти завершён перехо ### Автоматические тесты перевода Тестирование очень важно. MathCAT написан на Rust и содержит множество автоматических тестов, использующих встроенную систему тестирования Rust. Чтобы писать и проверять собственные тесты, [скачайте и установите Rust](https://www.rust-lang.org/tools/install). Знать Rust необязательно: достаточно заменить некоторые английские строки ожидаемыми строками на вашем языке. -Предположим, что вы переводите на французский язык с кодом `fr`. +Предположим, что вы переводите на русский язык с кодом `ru`. -Сначала откройте файл `languages.rs` в каталоге тестов и добавьте строку `mod fr;` после `mod en;` или аналогичной строки другого языка. +Сначала откройте файл `languages.rs` в каталоге тестов и добавьте строку `mod ru;` после `mod en;` или аналогичной строки другого языка. В каталоге `tests\Languages` находятся файл `en.rs` и каталог `en`. -1. Скопируйте `en.rs` в `fr.rs`. -2. Скопируйте каталог `en` в `fr`. -3. Если вы выбрали только один стиль речи, например SimpleSpeak, отредактируйте `fr.rs`: удалите строки, начиная с `mod ClearSpeak {` и заканчивая соответствующей строкой `}`. Удалите подкаталог `ClearSpeak` из каталога `fr`. -4. Желательно перевести все файлы, но на начальном этапе можно ограничиться несколькими. В `fr.rs` закомментируйте каждый непереведённый файл, добавив `//` в начало строки. Например, если вы не перевели файл SimpleSpeak `geometry.yaml`, строка должна выглядеть так: `// mod geometry;`. -5. Начните редактировать файлы: сначала глобально замените `"en"` на `"fr"`, а затем замените английские строки подходящими французскими или другими строками. +1. Скопируйте `en.rs` в `ru.rs`. +2. Скопируйте каталог `en` в `ru`. +3. Если вы выбрали только один стиль речи, например SimpleSpeak, отредактируйте `ru.rs`: удалите строки, начиная с `mod ClearSpeak {` и заканчивая соответствующей строкой `}`. Удалите подкаталог `ClearSpeak` из каталога `ru`. +4. Желательно перевести все файлы, но на начальном этапе можно ограничиться несколькими. В `ru.rs` закомментируйте каждый непереведённый файл, добавив `//` в начало строки. Например, если вы не перевели файл SimpleSpeak `geometry.yaml`, строка должна выглядеть так: `// mod geometry;`. +5. Начните редактировать файлы: сначала глобально замените `"en"` на `"ru"`, а затем замените английские строки подходящими русскими или другими строками. Пример теста: ``` @@ -131,9 +131,9 @@ fn common_fraction_half() { test("en", "SimpleSpeak", expr, "1 half"); } ``` -Для французского языка строка `test` будет выглядеть так: +Для русского языка строка `test` будет выглядеть так: ``` - test("fr", "SimpleSpeak", expr, "un demi"); + test("fr", "SimpleSpeak", expr, "одна вторая"); ``` После перевода нескольких тестов запустите автоматическую проверку. @@ -143,9 +143,9 @@ cargo test Languages::en ``` Затем запустите свои тесты. В нашем примере: ``` -cargo test Languages::fr +cargo test Languages::ru ``` -MathCAT добавляет паузы. В ожидаемых строках тестов они записываются символами `,` и `;`. При необходимости добавьте или удалите эти символы в ожидаемом результате. Если паузы звучат неуместно, добавьте или удалите `pause: xxx` в соответствующем месте одного из файлов `Rules\fr`. +MathCAT добавляет паузы. В ожидаемых строках тестов они записываются символами `,` и `;`. При необходимости добавьте или удалите эти символы в ожидаемом результате. Если паузы звучат неуместно, добавьте или удалите `pause: xxx` в соответствующем месте одного из файлов `Rules\ru`. __Совет__: возможно, быстрее всего сначала запустить тесты на своём языке, не меняя ожидаемые результаты. Все тесты завершатся с ошибкой, но в сообщениях будет показана речь, созданная MathCAT на вашем языке. _Если она верна_, просто замените ей английский текст. После обработки всех ошибок снова запустите тесты. В идеальном случае ошибок больше не будет. diff --git a/docs/ru/index.md b/docs/ru/index.md index 8d06d475f..25c7d1b5a 100644 --- a/docs/ru/index.md +++ b/docs/ru/index.md @@ -8,7 +8,7 @@ title: MathCAT — библиотека, которая преобразует MathML в: -* текст для озвучивания со встроенными командами речевого синтезатора; +* текст для озвучивания со встроенными командами синтезатора речи; * Брайль (код Немета, технический вариант UEB, а в дальнейшем и другие математические системы записи Брайля); * представление для навигации по математическим выражениям, в том числе с возможностью обзора. @@ -38,7 +38,7 @@ MathCAT применяет несколько эвристик, чтобы ис MathCAT активно развивается. DAISY принимает деятельное участие в разработке, и вклад новых участников приветствуется. MathCAT распространяется с открытым исходным кодом. [Репозиторий проекта доступен на GitHub](https://github.com/daisy/MathCAT). [О проблемах дополнения MathCAT, относящихся к NVDA, можно сообщить здесь](https://github.com/daisy/MathCATForPython/issues). -* MathCAT поддерживает озвучивание и навигацию для английского, немецкого, испанского, финского, индонезийского, норвежского, шведского, вьетнамского и китайского языков (традиционное письмо). +* MathCAT поддерживает озвучивание и навигацию для английского, вьетнамского, индонезийского, испанского, китайского (традиционное письмо), немецкого, норвежского, русского, финского и шведского языков. * MathCAT поддерживает код Немета, UEB, CMU, вьетнамскую систему записи Брайля, а также немецкую и австрийскую системы записи LaTeX и ASCIIMath. * Существует [дополнение NVDA](https://addons.nvda-project.org/addons/MathCAT.en.html). Оно может заменить MathPlayer для пользователей английского языка и поддерживаемых переводов. Начиная с NVDA 2026.1 MathCAT встроен в NVDA, поэтому скачивать дополнение не нужно. From 067770f5fa49369fc478585d4a9795a8ea52de9f Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Fri, 12 Jun 2026 07:54:03 +0300 Subject: [PATCH 08/47] Refactor Russian function braille mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace hardcoded function-name branches in Russian_Rules.yaml with a DefinitionValue lookup that pulls from a new RussianFunctionBraille mapping. Add RussianFunctionBraille entries to definitions.yaml (private-use codepoints U+E100–U+E10E) and map those codepoints to braille strings in unicode.yaml. Add a test covering tan/cot rendering. This centralizes function braille representations and simplifies the rule logic. --- Rules/Braille/Russian/Russian_Rules.yaml | 37 ++---------------------- Rules/Braille/Russian/definitions.yaml | 8 +++++ Rules/Braille/Russian/unicode.yaml | 15 ++++++++++ tests/braille/Russian/russian.rs | 3 ++ 4 files changed, 28 insertions(+), 35 deletions(-) diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index 6ac752bc6..2848c62f4 100644 --- a/Rules/Braille/Russian/Russian_Rules.yaml +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -322,42 +322,9 @@ - name: functions tag: mi - match: - - "IsInDefinition(., 'Speech', 'FunctionNames') or" - - ".='tg' or .='ctg' or .='arctg' or .='arcctg'" + match: "DefinitionValue(., 'Braille', 'RussianFunctionBraille') != ''" replace: - - test: - - if: ".='arcsin'" - then: [t: "⠫⠁⠎#"] - - else_if: ".='arccos'" - then: [t: "⠫⠁⠉#"] - - else_if: ".='arctg' or .='arctan'" - then: [t: "⠫⠁⠞#"] - - else_if: ".='arcctg' or .='arccotan' or .='arccot'" - then: [t: "⠫⠁⠉⠞#"] - - else_if: ".='sin'" - then: [t: "⠫⠎#"] - - else_if: ".='cos'" - then: [t: "⠫⠉#"] - - else_if: ".='tg' or .='tan'" - then: [t: "⠫⠞#"] - - else_if: ".='ctg' or .='cotan' or .='cot'" - then: [t: "⠫⠉⠞#"] - - else_if: ".='log'" - then: [t: "⠫⠇#"] - - else_if: ".='ln'" - then: [t: "⠫⠇⠝#"] - - else_if: ".='lg'" - then: [t: "⠫⠇⠛#"] - - else_if: ".='lim'" - then: [t: "⠫⠇⠍#"] - - else_if: ".='min'" - then: [t: "⠫⠍⠝#"] - - else_if: ".='max'" - then: [t: "⠫⠍⠭#"] - - else_if: ".='exp'" - then: [t: "⠫⠑#"] - else: [x: "BrailleChars(., 'Russian')"] + - x: "DefinitionValue(., 'Braille', 'RussianFunctionBraille')" - name: default diff --git a/Rules/Braille/Russian/definitions.yaml b/Rules/Braille/Russian/definitions.yaml index 4d0c77efb..c2fe639ed 100644 --- a/Rules/Braille/Russian/definitions.yaml +++ b/Rules/Braille/Russian/definitions.yaml @@ -1,2 +1,10 @@ --- - include: "../definitions.yaml" + +- RussianFunctionBraille: { + "arcsin": "\uE100", "arccos": "\uE101", "arctg": "\uE102", "arctan": "\uE102", + "arcctg": "\uE103", "arccotan": "\uE103", "arccot": "\uE103", + "sin": "\uE104", "cos": "\uE105", "tg": "\uE106", "tan": "\uE106", "ctg": "\uE107", "cotan": "\uE107", "cot": "\uE107", + "log": "\uE108", "ln": "\uE109", "lg": "\uE10A", + "lim": "\uE10B", "min": "\uE10C", "max": "\uE10D", "exp": "\uE10E" + } diff --git a/Rules/Braille/Russian/unicode.yaml b/Rules/Braille/Russian/unicode.yaml index 885f51a6b..37d64bd97 100644 --- a/Rules/Braille/Russian/unicode.yaml +++ b/Rules/Braille/Russian/unicode.yaml @@ -102,6 +102,21 @@ - "⁢": [t: ""] - "⁣": [t: ""] - "⁤": [t: ""] +- "\uE100": [t: "⠫⠁⠎#"] +- "\uE101": [t: "⠫⠁⠉#"] +- "\uE102": [t: "⠫⠁⠞#"] +- "\uE103": [t: "⠫⠁⠉⠞#"] +- "\uE104": [t: "⠫⠎#"] +- "\uE105": [t: "⠫⠉#"] +- "\uE106": [t: "⠫⠞#"] +- "\uE107": [t: "⠫⠉⠞#"] +- "\uE108": [t: "⠫⠇#"] +- "\uE109": [t: "⠫⠇⠝#"] +- "\uE10A": [t: "⠫⠇⠛#"] +- "\uE10B": [t: "⠫⠇⠍#"] +- "\uE10C": [t: "⠫⠍⠝#"] +- "\uE10D": [t: "⠫⠍⠭#"] +- "\uE10E": [t: "⠫⠑#"] - "∘": [t: "⠴"] - "√": [t: "⠩⠱"] - "∛": [t: "⠩⠒⠱"] diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index 313c45326..e19a963bc 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -121,6 +121,9 @@ fn source_functions_logs_derivatives() -> Result<()> { let expr = r#"tgxctgx=1"#; test_braille("Russian", expr, "⠫⠞⠠⠭⠄⠫⠉⠞⠠⠭⠀⠶⠼⠁")?; + let expr = r#"tanx+cotx"#; + test_braille("Russian", expr, "⠫⠞⠠⠭⠀⠖⠫⠉⠞⠠⠭")?; + let expr = r#"logxylogyx=1"#; test_braille("Russian", expr, "⠫⠇⠡⠠⠭⠱⠽⠄⠫⠇⠡⠠⠽⠱⠭⠀⠶⠼⠁")?; From 9720f49791dd54d463294e046c6b22d532d7978b Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:37:03 +0300 Subject: [PATCH 09/47] Refine Russian wording in translators guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clarify and naturalize Russian phrasing in the translators guide: add explicit "на русском языке" to SimpleSpeak examples; change exponent wording to "в квадрате" with example "икс в квадрате"; make fraction wording more natural ("числитель разделить на знаменатель" and example "метр за секунду"); fix "английском" → "английском языке"; and update Braille tooling phrasing to "просмотрщиком Брайля" and "дисплее Брайля" for consistency. --- docs/ru/new_translators_guide_MathCAT_revised.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 вместе с просмотрщиком Брайля, чтобы контролировать результат во время удалённых сеансов. Это позволяло наблюдать вывод, даже если сами рецензенты читали его на дисплее Брайля. Как и для речи, важна не только техническая правильность. Результат должен быть читаемым, согласованным и полезным в реальной работе. From 70eed4e665d6427f66da2123500ff65c1e18a7d1 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Mon, 1 Jun 2026 13:29:02 +0300 Subject: [PATCH 10/47] Addition of Russian documentation This commit adds Russian documentation for users, developers and translators. --- docs/ru/User_guide_for_MathCAT_ru.md | 7 ++- docs/ru/helpers.md | 46 +++++++++---------- docs/ru/index.md | 4 +- .../new_translators_guide_MathCAT_revised.md | 12 ++--- 4 files changed, 34 insertions(+), 35 deletions(-) diff --git a/docs/ru/User_guide_for_MathCAT_ru.md b/docs/ru/User_guide_for_MathCAT_ru.md index fec001990..43e230816 100644 --- a/docs/ru/User_guide_for_MathCAT_ru.md +++ b/docs/ru/User_guide_for_MathCAT_ru.md @@ -43,12 +43,11 @@ MathCAT можно настроить в соответствии с вашим - Трудности в обучении. Более краткое озвучивание. - Язык: (по умолчанию используется язык программы экранного доступа) - \[Английский (en)\] - - Вьетнамский (vi) - - Индонезийский (id) - Испанский (es) - - Китайский, традиционное письмо (zh-tw) - - Русский (ru) + - Индонезийский (id) - Шведский (sv) + - Вьетнамский (vi) + - Китайский, традиционное письмо (zh-tw) - Стиль речи: - \[ClearSpeak.\] Выражения озвучиваются примерно так, как их произнёс бы учитель на уроке. - SimpleSpeak. Выражения озвучиваются более кратко. Иногда такое чтение может быть неоднозначным. diff --git a/docs/ru/helpers.md b/docs/ru/helpers.md index be36a6116..bd63b7c4e 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: "равно"] # 0x3d + - "=": [T: "égale"] # 0x3d - ">": # 0x3e - test: if: "$Verbosity!='Terse'" - then: [T: ""] - - T: "больше" + then: [T: "est"] + - T: "supérieur à" ``` Примечание: иногда значение `IfThenElse` не требует перевода, но ключ всё равно следует изменить, чтобы было видно, что строка проверена. В следующем примере перевод не нужен, поскольку части `then` и `else`, соответственно `count(*/*[1])` и `$LineCountTry`, не являются словами: @@ -84,15 +84,15 @@ __ПРИМЕЧАНИЕ__: Сейчас почти завершён перехо Если переводы MathPlayer и SRE различаются, выбирается вариант, совпадающий с Google Translate, а другой вариант включается в комментарий. Например: ``` - else: [t: "левая круглая скобка"] # (en: 'left paren', MathPlayer: 'открывающая круглая скобка') + else: [t: "parenthèse gauche"] # (en: 'left paren', MathPlayer: 'parenthèse ouvrante') ``` Если не совпадает ни один перевод, выбирается один из вариантов, а остальные приводятся в комментарии. Например: ``` - else: [t: "левая круглая скобка"] # (en: 'open paren', MathPlayer: 'открывающая круглая скобка', google: 'открытая скобка') + else: [t: "parenthèse gauche"] # (en: 'open paren', MathPlayer: 'parenthèse ouvrante', google: 'parenthèse ouverte') ``` Наконец, если перевод отсутствует, используется Google Translate и добавляется комментарий `google translation`. Вероятность неудачного перевода в таком случае заметно выше, поэтому внимательно проверяйте эти строки. Пример, где доступен только Google Translate: ``` - then: [t: "вертикальная линия"] # (en: 'vertical line', google translation) + then: [t: "ligne verticale"] # (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 необязательно: достаточно заменить некоторые английские строки ожидаемыми строками на вашем языке. -Предположим, что вы переводите на русский язык с кодом `ru`. +Предположим, что вы переводите на французский язык с кодом `fr`. -Сначала откройте файл `languages.rs` в каталоге тестов и добавьте строку `mod ru;` после `mod en;` или аналогичной строки другого языка. +Сначала откройте файл `languages.rs` в каталоге тестов и добавьте строку `mod fr;` после `mod en;` или аналогичной строки другого языка. В каталоге `tests\Languages` находятся файл `en.rs` и каталог `en`. -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"`, а затем замените английские строки подходящими русскими или другими строками. +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"`, а затем замените английские строки подходящими французскими или другими строками. Пример теста: ``` @@ -131,9 +131,9 @@ fn common_fraction_half() { test("en", "SimpleSpeak", expr, "1 half"); } ``` -Для русского языка строка `test` будет выглядеть так: +Для французского языка строка `test` будет выглядеть так: ``` - test("fr", "SimpleSpeak", expr, "одна вторая"); + test("fr", "SimpleSpeak", expr, "un demi"); ``` После перевода нескольких тестов запустите автоматическую проверку. @@ -143,9 +143,9 @@ cargo test Languages::en ``` Затем запустите свои тесты. В нашем примере: ``` -cargo test Languages::ru +cargo test Languages::fr ``` -MathCAT добавляет паузы. В ожидаемых строках тестов они записываются символами `,` и `;`. При необходимости добавьте или удалите эти символы в ожидаемом результате. Если паузы звучат неуместно, добавьте или удалите `pause: xxx` в соответствующем месте одного из файлов `Rules\ru`. +MathCAT добавляет паузы. В ожидаемых строках тестов они записываются символами `,` и `;`. При необходимости добавьте или удалите эти символы в ожидаемом результате. Если паузы звучат неуместно, добавьте или удалите `pause: xxx` в соответствующем месте одного из файлов `Rules\fr`. __Совет__: возможно, быстрее всего сначала запустить тесты на своём языке, не меняя ожидаемые результаты. Все тесты завершатся с ошибкой, но в сообщениях будет показана речь, созданная MathCAT на вашем языке. _Если она верна_, просто замените ей английский текст. После обработки всех ошибок снова запустите тесты. В идеальном случае ошибок больше не будет. diff --git a/docs/ru/index.md b/docs/ru/index.md index 25c7d1b5a..8d06d475f 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 966cea36f..a44bb6408 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$ на русском языке читается как «икс разделить на игрек плюс один». Слова, обозначающие начало и конец дроби, не используются, потому что числитель и знаменатель просты. Выражение $\frac{x}{y+1}$ на русском языке читается как «дробь, икс разделить на игрек плюс один, конец дроби». Здесь такие слова используются, поскольку знаменатель не является простым. +* SimpleSpeak компактно озвучивает простые выражения. Например, $\frac{x}{y} + 1$ читается как «x over y plus one». Слова, обозначающие начало и конец дроби, не используются, потому что числитель и знаменатель просты. Выражение $\frac{x}{y+1}$ читается как «fraction, x over y plus one, end fraction». Здесь такие слова используются, поскольку знаменатель не является простым. Вы можете перевести оба стиля речи или сосредоточиться на одном. В зависимости от принятого способа произнесения математических выражений в целевом языке переведённые стили могут различаться сильнее или слабее английских. @@ -264,7 +264,7 @@ MathCAT также поддерживает уровни подробности ### Адаптация и создание собственных правил -Тесты покрывают большинство правил. Однако в португальском или другом языке могут существовать грамматические особенности, которых нет в английском языке и которые не охвачены тестами. Поэтому полезно просмотреть все правила и убедиться в их корректности. +Тесты покрывают большинство правил. Однако в португальском или другом языке могут существовать грамматические особенности, которых нет в английском и которые не охвачены тестами. Поэтому полезно просмотреть все правила и убедиться в их корректности. При грамматических различиях простой замены текстовых строк недостаточно. Необходимо изменить правила или добавить новые. Для совершенно новых правил полезно написать тесты, подтверждающие правильность результата. Не бойтесь пробовать разные варианты. @@ -297,7 +297,7 @@ MathCAT также поддерживает уровни подробности - t: "squared" # phrase(7 'squared' equals 49) ``` -Это правило означает: если степень имеет показатель 2 и настройка `ClearSpeak_Exponents` равна `Auto`, следует вывести «в квадрате». Например, $x^2$ на русском языке озвучивается как «икс в квадрате». +Это правило означает: если степень имеет показатель 2 и настройка `ClearSpeak_Exponents` равна `Auto`, следует вывести «основание в квадрате». Например, $x^2$ озвучивается как «x squared». Обратите внимание: @@ -344,9 +344,9 @@ MathCAT также поддерживает уровни подробности * числитель должен быть текстовой строкой длиной более одного символа или числом, неявно умноженным на такую строку, **и** * знаменатель должен удовлетворять тем же условиям, что и числитель. -Если все условия выполнены, дробь озвучивается как «числитель разделить на знаменатель». Если `ClearSpeak_Fractions` имеет значение `EndFrac` или `OverEndFrac`, также добавляется короткая пауза перед фразой «конец дроби». +Если все условия выполнены, дробь озвучивается как «числитель над знаменателем». Если `ClearSpeak_Fractions` имеет значение `EndFrac` или `OverEndFrac`, также добавляется короткая пауза перед фразой «конец дроби». -Например, выражение $\frac{\text{meter}}{\text{second}}$ озвучивается как «метр за секунду». +Например, выражение $\frac{\text{meter}}{\text{second}}$ озвучивается как «meter over second». --- @@ -506,7 +506,7 @@ MathCAT также поддерживает уровни подробности Пользовательское тестирование Брайля так же важно, как тестирование речи. -При работе над шведским переводом тестировщики использовали NVDA вместе с просмотрщиком Брайля, чтобы контролировать результат во время удалённых сеансов. Это позволяло наблюдать вывод, даже если сами рецензенты читали его на дисплее Брайля. +При работе над шведским переводом тестировщики использовали NVDA вместе со средством просмотра Брайля, чтобы контролировать результат во время удалённых сеансов. Это позволяло наблюдать вывод, даже если сами рецензенты читали его на физическом дисплее Брайля. Как и для речи, важна не только техническая правильность. Результат должен быть читаемым, согласованным и полезным в реальной работе. From 1afd36f3f9aca122fb2e2a91e793e03ec9fa18ca Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:04:35 +0300 Subject: [PATCH 11/47] First version of Russian Braille This commit adds first druft version for Russian Braille. --- src/braille.rs | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/src/braille.rs b/src/braille.rs index 9fd64b4cc..6b259776f 100644 --- a/src/braille.rs +++ b/src/braille.rs @@ -53,6 +53,7 @@ pub fn braille_mathml(mathml: Element, nav_node_id: &str) -> Result<(String, usi "Russian" => russian_cleanup(pref_manager, braille_string), "LaTeX" => LaTeX_cleanup(pref_manager, braille_string), "ASCIIMath" => ASCIIMath_cleanup(pref_manager, braille_string), + "ASCIIMath-fi" => ASCIIMath_cleanup(pref_manager, braille_string), _ => braille_string.trim_matches('⠀').to_string(), // probably needs cleanup if someone has another code, but this will have to get added by hand }; @@ -2182,8 +2183,7 @@ fn russian_cleanup(_pref_manager: Ref, raw_braille: String) - previous_char_was_digit = matches!(ch, '⠚' | '⠁' | '⠃' | '⠉' | '⠙' | '⠑' | '⠋' | '⠛' | '⠓' | '⠊'); } - let result = add_russian_alphabet_indicators(&raw_braille_without_repeated_number_indicators); - let result = REPLACE_INDICATORS.replace_all(&result, |cap: &Captures| { + let result = REPLACE_INDICATORS.replace_all(&raw_braille_without_repeated_number_indicators, |cap: &Captures| { match &cap[0] { "B" => "⠸", "C" => "⠠", @@ -2198,33 +2198,6 @@ fn russian_cleanup(_pref_manager: Ref, raw_braille: String) - return COLLAPSE_SPACES.replace_all(&result, "⠀") .trim_matches('⠀') .to_string(); - - fn add_russian_alphabet_indicators(raw_braille: &str) -> String { - let mut result = String::with_capacity(raw_braille.len()); - let mut alphabet_mode = None; - for ch in raw_braille.chars() { - match ch { - 'l' | 'u' | 'g' | 'v' => { - if alphabet_mode != Some(ch) { - result.push_str(match ch { - 'l' => "⠠", // Latin lowercase: dots 6 - 'u' => "⠨", // Latin uppercase: dots 4-6 - 'g' => "⠰", // Greek lowercase: dots 5-6 - 'v' => "⠸", // Greek uppercase: dots 4-5-6 - _ => unreachable!(), - }); - alphabet_mode = Some(ch); - } - }, - 'C' | 'N' | '#' => { - alphabet_mode = None; - result.push(ch); - }, - _ => result.push(ch), - } - } - return result; - } } #[allow(non_snake_case)] From d16e42b1b49706e5fb3fd59ca1af1b43b6f5d08e Mon Sep 17 00:00:00 2001 From: nsoiffer Date: Mon, 1 Jun 2026 12:03:21 -0700 Subject: [PATCH 12/47] typo in example --- tests/braille/Russian/russian.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index e19a963bc..604a2880c 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -150,3 +150,24 @@ fn source_geometry_matrix_chemistry() -> Result<()> { test_braille("Russian", expr, "⠨⠓⠡⠆⠉⠦⠉⠓⠡⠆")?; return Ok(()); } + +#[test] +fn wikipedia_times_divide() -> Result<()> { + let expr = r#"6×7:14=3"#; + test_braille("Russian", expr, "⠼⠋⠀⠦⠼⠛⠀⠲⠼⠁⠙⠀⠶⠼⠉")?; + return Ok(()); +} + +#[test] +fn wikipedia_linear_parens() -> Result<()> { + let expr = r#"3·(97)=6"#; + test_braille("Russian", expr, "⠼⠉⠄⠣⠼⠊⠀⠤⠼⠛⠜⠀⠶⠼⠋")?; + return Ok(()); +} + +#[test] +fn wikipedia_sqrt() -> Result<()> { + let expr = r#"10000<101"#; + test_braille("Russian", expr, "⠩⠱⠼⠁⠚⠚⠚⠚⠀⠪⠀⠼⠁⠚⠁")?; + return Ok(()); +} From 3cd86341c5a241ce50ab4216b48be87fad9aeb02 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Thu, 25 Jun 2026 08:47:04 +0300 Subject: [PATCH 13/47] Add alphabet indicators in russian_cleanup Insert a preprocessing step that injects braille alphabet indicators before running REPLACE_INDICATORS. A new helper add_russian_alphabet_indicators scans the raw braille and emits specific indicators for 'l' (Latin lowercase), 'u' (Latin uppercase), 'g' (Greek lowercase) and 'v' (Greek uppercase), avoiding repeated indicators by tracking the current alphabet mode. The helper also resets the mode on 'C', 'N', and '#'. This keeps the existing indicator replacement and space-collapsing logic intact while preventing redundant alphabet indicators. --- src/braille.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/braille.rs b/src/braille.rs index 6b259776f..ec26cb2da 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" => "⠠", @@ -2198,6 +2199,33 @@ fn russian_cleanup(_pref_manager: Ref, raw_braille: String) - return COLLAPSE_SPACES.replace_all(&result, "⠀") .trim_matches('⠀') .to_string(); + + fn add_russian_alphabet_indicators(raw_braille: &str) -> String { + let mut result = String::with_capacity(raw_braille.len()); + let mut alphabet_mode = None; + for ch in raw_braille.chars() { + match ch { + 'l' | 'u' | 'g' | 'v' => { + if alphabet_mode != Some(ch) { + result.push_str(match ch { + 'l' => "⠠", // Latin lowercase: dots 6 + 'u' => "⠨", // Latin uppercase: dots 4-6 + 'g' => "⠰", // Greek lowercase: dots 5-6 + 'v' => "⠸", // Greek uppercase: dots 4-5-6 + _ => unreachable!(), + }); + alphabet_mode = Some(ch); + } + }, + 'C' | 'N' | '#' => { + alphabet_mode = None; + result.push(ch); + }, + _ => result.push(ch), + } + } + return result; + } } #[allow(non_snake_case)] From 5d430b8b67c52e4bc51da901704b23e523a8dc9a Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Thu, 25 Jun 2026 09:22:20 +0300 Subject: [PATCH 14/47] Russian braille: add GOST symbols and decimals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add GOST-style number and symbol support for Russian braille: introduce a decimal-comma rule that emits BrailleChars for integer and fractional parts with a decimal indicator, using PUA digit placeholders. Extend unicode mappings with bracket/brace/slash/backslash/pipe characters, Celsius, context-sensitive prime (′/″) handling, PUA digit-to-braille mappings, and a few arrow/connective symbols. Update tests to cover decimal numbers, fractions, set notation, Celsius, and logical arrows, and adjust expected output for geometry primes to reflect the new context-aware prime handling. --- Rules/Braille/Russian/Russian_Rules.yaml | 9 ++++++ Rules/Braille/Russian/unicode.yaml | 36 +++++++++++++++++++++--- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index 2848c62f4..157142474 100644 --- a/Rules/Braille/Russian/Russian_Rules.yaml +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -312,6 +312,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 diff --git a/Rules/Braille/Russian/unicode.yaml b/Rules/Braille/Russian/unicode.yaml index 37d64bd97..674ebe35a 100644 --- a/Rules/Braille/Russian/unicode.yaml +++ b/Rules/Braille/Russian/unicode.yaml @@ -6,8 +6,13 @@ - "?": [t: "⠠⠢"] - ":": [t: "⠀⠲"] - ";": [t: "⠠⠆"] -- "{": [t: "⠠⠪"] -- "}": [t: "⠠⠕"] +- "[": [t: "⠷"] +- "]": [t: "⠾"] +- "{": [t: "⠪"] +- "}": [t: "⠕"] +- "/": [t: "⠠⠌"] +- "\\": [t: "⠀⠰⠤"] +- "|": [t: "⠸"] - "+": [t: "⠀⠖"] - "-": [t: "⠀⠤"] - "=": [t: "⠀⠶"] @@ -73,10 +78,19 @@ - "ϕ": [t: "g⠋"] - "ϖ": [t: "g⠏"] - "ϱ": [t: "g⠗"] -- "′": [t: "⠔"] -- "″": [t: "⠔⠔"] +- "′": + - 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: "⠫⠢"] @@ -117,6 +131,17 @@ - "\uE10C": [t: "⠫⠍⠝#"] - "\uE10D": [t: "⠫⠍⠭#"] - "\uE10E": [t: "⠫⠑#"] +- "\uE10F": [t: "⠂"] +- "\ue020": [t: "⠚"] +- "\ue021": [t: "⠁"] +- "\ue022": [t: "⠃"] +- "\ue023": [t: "⠉"] +- "\ue024": [t: "⠙"] +- "\ue025": [t: "⠑"] +- "\ue026": [t: "⠋"] +- "\ue027": [t: "⠛"] +- "\ue028": [t: "⠓"] +- "\ue029": [t: "⠊"] - "∘": [t: "⠴"] - "√": [t: "⠩⠱"] - "∛": [t: "⠩⠒⠱"] @@ -186,6 +211,9 @@ - "⟶": [t: "⠀⠒⠕"] - "←": [t: "⠀⠦⠶"] - "⟵": [t: "⠀⠦⠶"] +- "⇒": [t: "⠀⠶⠜"] +- "⇐": [t: "⠀⠦⠶"] +- "⇔": [t: "⠀⠦⠶⠜"] - "◻": [t: "⠶"] - "♮": [t: "⠻⠨⠝⠻"] From 8319ee72e97d00b22cded66b078ba3204a1c0631 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Thu, 25 Jun 2026 09:35:52 +0300 Subject: [PATCH 15/47] Add vector/bar/arc mover rules and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce three new mover rules in Rules/Braille/Russian/Russian_Rules.yaml: vector-arrow, vector-bar, and geometry-arc. Each rule detects specific accent characters (arrows: → ⟶ ⇀ ⃗; bars: ¯ ‾ _  ̄; arcs: ⌒ ⏜ ︵) and emits the appropriate braille indicators, using a different form when the base has more than one character. Add corresponding tests in tests/braille/Russian/russian.rs to cover single- and multi-character movers and combined expressions. --- Rules/Braille/Russian/Russian_Rules.yaml | 35 ++++++++++++++++++++++++ tests/braille/Russian/russian.rs | 12 ++++++++ 2 files changed, 47 insertions(+) diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index 157142474..5b586bdc5 100644 --- a/Rules/Braille/Russian/Russian_Rules.yaml +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -198,6 +198,41 @@ match: "*[2][translate(., \"'*`ª°²³´¹º′″‴‵‶‷⁗\",'')='']" replace: [x: "*"] +- + 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] diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index 604a2880c..233cbe5bf 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -140,6 +140,18 @@ fn source_geometry_matrix_chemistry() -> Result<()> { 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, "⠣⠠⠁⠀⠃⠨⠳⠉⠀⠙⠜")?; From 75a3a43af9bec6544a648546da9f8f56f7b58f6a Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Thu, 25 Jun 2026 09:58:59 +0300 Subject: [PATCH 16/47] Add Russian braille math functions and limits Extend Russian braille rules to support additional math functions and large operator limits. Added new rules for large-op-limits, operator/functions (including special handling for div), and div-specific replacements in Rules/Braille/Russian/*. Updated Russian definitions and unicode mappings to include sinh/cosh/tanh/coth, arg/sgn/det/rank/rg, Re/Im, grad/rot/div, gcd/lcm and related braille codes. Updated global Rules/definitions.yaml to recognize new function names. Fixed braille cleanup in src/braille.rs to normalize div encoding. Added unit tests exercising the new function mappings and large operator limits. --- Rules/Braille/Russian/Russian_Rules.yaml | 54 +++++++++++++++++++++++- Rules/Braille/Russian/definitions.yaml | 10 ++++- Rules/Braille/Russian/unicode.yaml | 16 +++++++ Rules/definitions.yaml | 4 +- src/braille.rs | 1 + tests/braille/Russian/russian.rs | 34 +++++++++++++++ 6 files changed, 115 insertions(+), 4 deletions(-) diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index 5b586bdc5..d9c522d09 100644 --- a/Rules/Braille/Russian/Russian_Rules.yaml +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -283,6 +283,37 @@ - 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: default tag: [msubsup, munderover] @@ -340,6 +371,20 @@ - 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 @@ -365,11 +410,18 @@ - name: functions - tag: mi + tag: [mi, mtext] match: "DefinitionValue(., 'Braille', 'RussianFunctionBraille') != ''" replace: - x: "DefinitionValue(., 'Braille', 'RussianFunctionBraille')" +- + name: div-function + tag: [mi, mo, mtext] + match: "contains(., 'div')" + replace: + - t: "⠫⠙⠊⠧#" + - name: default tag: [mi, mtext] diff --git a/Rules/Braille/Russian/definitions.yaml b/Rules/Braille/Russian/definitions.yaml index c2fe639ed..de0492043 100644 --- a/Rules/Braille/Russian/definitions.yaml +++ b/Rules/Braille/Russian/definitions.yaml @@ -5,6 +5,14 @@ "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" + "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 674ebe35a..53fb06938 100644 --- a/Rules/Braille/Russian/unicode.yaml +++ b/Rules/Braille/Russian/unicode.yaml @@ -132,6 +132,22 @@ - "\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: "⠃"] 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/src/braille.rs b/src/braille.rs index ec26cb2da..2bcba4edc 100644 --- a/src/braille.rs +++ b/src/braille.rs @@ -2196,6 +2196,7 @@ fn russian_cleanup(_pref_manager: Ref, raw_braille: String) - _ => "", } }); + let result = result.replace("⠠⠙⠊⠧", "⠫⠙⠊⠧"); return COLLAPSE_SPACES.replace_all(&result, "⠀") .trim_matches('⠀') .to_string(); diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index 233cbe5bf..889641128 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -127,11 +127,45 @@ fn source_functions_logs_derivatives() -> Result<()> { 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"#; From 14f61d99573e3b7b77a157a04e436e1506c5317f Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:11:38 +0300 Subject: [PATCH 17/47] Support system left brace in Russian braille Introduce handling for system left-brace matrices in the Russian braille rules: add InSystemLeftBrace variable, a system-left-brace-opener rule and a system-left-brace-table rule to detect brace-enclosed mtable as a system. Update row-start logic to emit appropriate braille tokens when InSystemLeftBrace is set. Add tests exercising curly-brace matrices, piecewise function (brace with conditions), and vertical-bar matrices to verify the new behavior. --- Rules/Braille/Russian/Russian_Rules.yaml | 27 +++++++++++++++++++++++- tests/braille/Russian/russian.rs | 9 ++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index d9c522d09..bea1a15d5 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] @@ -147,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 @@ -172,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]"] diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index 889641128..1b0aac365 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -189,6 +189,15 @@ fn source_geometry_matrix_chemistry() -> Result<()> { 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, "⠨⠓⠡⠆⠕")?; From 68e73a77d1b9a8c94778a0ca16b2872cfe648543 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:26:37 +0300 Subject: [PATCH 18/47] Add Russian chemistry braille rules and tests Extend Russian braille rules to handle chemical notation: add rules for equilibrium-arrow, reaction operators, repeated charges, charges, and charge-with-subscript mappings (chemical-reaction-operator, chemistry-mhchem-equilibrium-arrow, chemistry-repeated-charge, chemistry-charge, chemistry-charge-with-subscript). Update Rules/Braille/Russian/Russian_Rules.yaml to map arrows, +/-, superscripts/subscripts and reaction operators to appropriate braille sequences. Add tests in tests/braille/Russian/russian.rs (source_chemical_reactions_and_charges) covering reaction arrows, charges, subscripts/superscripts and combined examples. --- Rules/Braille/Russian/Russian_Rules.yaml | 139 +++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index bea1a15d5..4ab7154a5 100644 --- a/Rules/Braille/Russian/Russian_Rules.yaml +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -223,6 +223,15 @@ 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 @@ -283,6 +292,59 @@ - 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] @@ -339,6 +401,52 @@ - 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] @@ -386,6 +494,37 @@ 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 From c533887226a7dd0a773e438ae1f6248cb732f69b Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Thu, 25 Jun 2026 22:27:27 +0300 Subject: [PATCH 19/47] Add signed-integer and nested-fraction rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add handling for nested fractions and signed integers inside script positions and adjust grouping behavior for scripts in Russian braille rules. Changes introduce a nested-fraction rule for mfrac and new signed-integer-number / signed-integer rules for msub/munder and msup/mover, plus a small grouping test (⠐) when scripts contain grouped mrow with '+' or a fraction. Also add a comprehensive regression test (script_grouping_regressions) covering many sup/sub cases to ensure correct braille output. Files updated: Rules/Braille/Russian/Russian_Rules.yaml and tests/braille/Russian/russian.rs. --- Rules/Braille/Russian/Russian_Rules.yaml | 87 +++++++++++++++++++ tests/braille/Russian/russian.rs | 102 +++++++++++++++++++++++ 2 files changed, 189 insertions(+) diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index 4ab7154a5..a691101e8 100644 --- a/Rules/Braille/Russian/Russian_Rules.yaml +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -93,6 +93,17 @@ - x: "*[1]" - t: "⠹" +- + name: nested-fraction + tag: mfrac + match: "parent::m:mfrac" + replace: + - t: "⠆" + - x: "*[1]" + - t: "⠀⠳" + - x: "*[2]" + - t: "⠰" + - name: simple-numeric tag: mfrac @@ -267,6 +278,41 @@ - t: "#" - x: "*[1]" +- + name: signed-integer-number + tag: [msub, munder] + match: "*[2][self::m:mn and (substring(., 1, 1)='-' or substring(., 1, 1)='−' or substring(., 1, 1)='+') and translate(substring(., 2), '0123456789', '')='']" + replace: + - x: "*[1]" + - test: + if: "self::m:munder" + then: [t: "⠨⠡"] + else: [t: "⠡"] + - test: + if: "substring(*[2], 1, 1)='+'" + then: [t: "⠖"] + else: [t: "⠤"] + - x: "BrailleChars(translate(substring(*[2], 2), '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + +- + name: signed-integer + tag: [msub, munder] + match: + - "*[2][self::m:mrow and count(*)=2 and" + - " *[1][self::m:mo][.='-' or .='−' or .='+'] and" + - " *[2][self::m:mn and translate(., '0123456789', '')='']]" + replace: + - x: "*[1]" + - test: + if: "self::m:munder" + then: [t: "⠨⠡"] + else: [t: "⠡"] + - test: + if: "*[2]/*[1][.='+']" + then: [t: "⠖"] + else: [t: "⠤"] + - x: "BrailleChars(translate(*[2]/*[2], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - name: integer tag: [msub, munder] @@ -289,9 +335,47 @@ if: "self::m:munder" then: [t: "⠨⠡"] else: [t: "⠡"] + - test: + if: "*[2][self::m:mrow[m:mo[.='+']] or self::m:mfrac]" + then: [t: "⠐"] - x: "*[2]" - t: "⠱" +- + name: signed-integer-number + tag: [msup, mover] + match: "*[2][self::m:mn and (substring(., 1, 1)='-' or substring(., 1, 1)='−' or substring(., 1, 1)='+') and translate(substring(., 2), '0123456789', '')='']" + replace: + - x: "*[1]" + - test: + if: "self::m:mover" + then: [t: "⠨⠌"] + else: [t: "⠌"] + - test: + if: "substring(*[2], 1, 1)='+'" + then: [t: "⠖"] + else: [t: "⠤"] + - x: "BrailleChars(translate(substring(*[2], 2), '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + +- + name: signed-integer + tag: [msup, mover] + match: + - "*[2][self::m:mrow and count(*)=2 and" + - " *[1][self::m:mo][.='-' or .='−' or .='+'] and" + - " *[2][self::m:mn and translate(., '0123456789', '')='']]" + replace: + - x: "*[1]" + - test: + if: "self::m:mover" + then: [t: "⠨⠌"] + else: [t: "⠌"] + - test: + if: "*[2]/*[1][.='+']" + then: [t: "⠖"] + else: [t: "⠤"] + - x: "BrailleChars(translate(*[2]/*[2], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - name: chemistry-repeated-charge tag: msup @@ -457,6 +541,9 @@ if: "self::m:munderover" then: [t: "⠨⠡"] else: [t: "⠡"] + - test: + if: "*[2][self::m:mrow[m:mo[.='+']] or self::m:mfrac]" + then: [t: "⠐"] - x: "*[2]" - test: if: "self::m:munderover" diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index 1b0aac365..d5aae2769 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -1,6 +1,108 @@ use crate::common::*; use anyhow::Result; +#[test] +fn script_grouping_regressions() -> Result<()> { + let cases = vec![ + ("sup_x", r#"ax"#), + ("sup_zero", r#"x0"#), + ("sup_ten", r#"x10"#), + ("sup_minus_x", r#"a-x"#), + ("sup_minus_2", r#"a-2"#), + ("sup_x_plus_1", r#"ax+1"#), + ("sup_sub_x2", r#"ax2"#), + ("sup_nested", r#"ax2"#), + ("sup_frac", r#"a12"#), + ("sup_sqrt", r#"ax"#), + ("sup_follow_letter", r#"x2y"#), + ("sup_follow_number", r#"x23"#), + ("sub_x", r#"ax"#), + ("sub_zero", r#"x0"#), + ("sub_ten", r#"x10"#), + ("sub_minus_x", r#"a-x"#), + ("sub_minus_2", r#"a-2"#), + ("sub_x_plus_1", r#"ax+1"#), + ("sub_nested", r#"ax2"#), + ("sub_follow_letter", r#"x2y"#), + ("sub_follow_number", r#"x23"#), + ("subsup_numeric", r#"x12"#), + ("subsup_i_n", r#"xin"#), + ("subsup_grouped_sub", r#"xi+12"#), + ("subsup_grouped_sup", r#"x0n-1"#), + ("subsup_nested_sup", r#"x0n2"#), + ("negative_base_sup", r#"-x2"#), + ("paren_base_sup", r#"(x+1)2"#), + ("frac_base_sup", r#"1x2"#), + ("root_base_sup", r#"x2"#), + ("sup_on_function", r#"sin2x"#), + ("log_sub_sup", r#"log23x"#), + ("root_index_group", r#"xn+1"#), + ("root_index_sub", r#"xn2"#), + ("frac_num_group", r#"x+1y"#), + ("frac_den_group", r#"xy+1"#), + ("nested_frac", r#"1x1y"#), + ("sup_after_fraction", r#"x+1y-12"#), + ("sub_after_fraction", r#"x+1y-1i"#), + ("tensor_like", r#"Tijxjk"#), + ("pre_negative_power", r#"2x-1"#), + ("power_of_power_follow", r#"x23y"#), + ("subscripted_power_follow", r#"x23y"#), + ("power_subscript_follow", r#"x23y"#), + ]; + + let expected = std::collections::HashMap::from([ + ("sup_x", "⠠⠁⠌⠭⠱"), + ("sup_zero", "⠠⠭⠌⠴"), + ("sup_ten", "⠠⠭⠌⠂⠴"), + ("sup_minus_x", "⠠⠁⠌⠀⠤⠭⠱"), + ("sup_minus_2", "⠠⠁⠌⠤⠆"), + ("sup_x_plus_1", "⠠⠁⠌⠭⠀⠖⠼⠁⠱"), + ("sup_sub_x2", "⠠⠁⠌⠭⠡⠆⠱"), + ("sup_nested", "⠠⠁⠌⠭⠌⠆⠱"), + ("sup_frac", "⠠⠁⠌⠼⠁⠆⠱"), + ("sup_sqrt", "⠠⠁⠌⠩⠱⠭⠹⠱"), + ("sup_follow_letter", "⠠⠭⠌⠆⠽"), + ("sup_follow_number", "⠠⠭⠌⠆⠼⠉"), + ("sub_x", "⠠⠁⠡⠭⠱"), + ("sub_zero", "⠠⠭⠡⠴"), + ("sub_ten", "⠠⠭⠡⠂⠴"), + ("sub_minus_x", "⠠⠁⠡⠀⠤⠭⠱"), + ("sub_minus_2", "⠠⠁⠡⠤⠆"), + ("sub_x_plus_1", "⠠⠁⠡⠐⠭⠀⠖⠼⠁⠱"), + ("sub_nested", "⠠⠁⠡⠭⠡⠆⠱"), + ("sub_follow_letter", "⠠⠭⠡⠆⠽"), + ("sub_follow_number", "⠠⠭⠡⠆⠼⠉"), + ("subsup_numeric", "⠠⠭⠡⠼⠁⠌⠼⠃⠱"), + ("subsup_i_n", "⠠⠭⠡⠊⠌⠝⠱"), + ("subsup_grouped_sub", "⠠⠭⠡⠐⠊⠀⠖⠼⠁⠌⠼⠃⠱"), + ("subsup_grouped_sup", "⠠⠭⠡⠼⠚⠌⠠⠝⠀⠤⠼⠁⠱"), + ("subsup_nested_sup", "⠠⠭⠡⠼⠚⠌⠠⠝⠌⠆⠱"), + ("negative_base_sup", "⠤⠠⠭⠌⠆"), + ("paren_base_sup", "⠣⠠⠭⠀⠖⠼⠁⠜⠌⠆"), + ("frac_base_sup", "⠼⠁⠳⠠⠭⠌⠆"), + ("root_base_sup", "⠩⠱⠠⠭⠹⠌⠆"), + ("sup_on_function", "⠫⠎⠌⠆⠠⠭"), + ("log_sub_sup", "⠫⠇⠡⠼⠃⠌⠼⠉⠱⠠⠭"), + ("root_index_group", "⠩⠠⠝⠀⠖⠼⠁⠱⠠⠭⠹"), + ("root_index_sub", "⠩⠠⠝⠡⠆⠱⠭⠹"), + ("frac_num_group", "⠆⠠⠭⠀⠖⠼⠁⠀⠳⠠⠽⠰"), + ("frac_den_group", "⠆⠠⠭⠀⠳⠽⠀⠖⠼⠁⠰"), + ("nested_frac", "⠆⠆⠼⠁⠀⠳⠠⠭⠰⠀⠳⠆⠼⠁⠀⠳⠠⠽⠰⠰"), + ("sup_after_fraction", "⠆⠠⠭⠀⠖⠼⠁⠀⠳⠠⠽⠀⠤⠼⠁⠰⠌⠆"), + ("sub_after_fraction", "⠆⠠⠭⠀⠖⠼⠁⠀⠳⠠⠽⠀⠤⠼⠁⠰⠡⠠⠊⠱"), + ("tensor_like", "⠨⠞⠡⠠⠊⠌⠚⠱⠭⠡⠚⠌⠅⠱"), + ("pre_negative_power", "⠼⠃⠠⠭⠌⠤⠂"), + ("power_of_power_follow", "⠠⠭⠌⠆⠌⠒⠽"), + ("subscripted_power_follow", "⠠⠭⠡⠆⠌⠒⠽"), + ("power_subscript_follow", "⠠⠭⠌⠆⠡⠒⠽"), + ]); + + for (label, expr) in cases { + test_braille("Russian", expr, expected[label])?; + } + return Ok(()); +} + #[test] fn numbers_and_operators() -> Result<()> { let expr = r#"5+12=17"#; From 1f6de0d621261a85c302653342af7a4be42971da Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Thu, 25 Jun 2026 21:07:10 +0300 Subject: [PATCH 20/47] Enhance Russian Braille support with mathematical symbols and rules (#589) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refine matrix rule handling by adding support for augmented matrices; update unit tests to include 2x3 and 3x4 augmented matrix scenarios. (#555) * remove logging statement * removed uneeded eprintlin!() * Alternative fix to PR #540. Fixes #528. Includes the tests in the PR. * Add Russian translation * Fix #585 -- SSML was being escaped. I earlier added code to prevent a SSML injection attack by escaping HTML. It was done at the wrong point and escape legal SSML. I have moved the check and conversion down to the leaves. I was very surprised there were no useful tests with SSML. All the main tests set `TTS=None`. I added a test that catches bad escaping. I also added some tests to check for leaf-based SSML attacks and also attribute-based ones. * bump beta number * The range for MATHEMATICAL SANS-SERIF DIGITs had MATHEMATICAL MONOSPACE DIGITs as the match part of translate, so if one of the sans-serif digits were used, you ended up in a loop (no match, to it returned itself). * comment out some debug statements * Add 02DA (˚) to list of circle-like chars -- I thought I had done this already, so it might in some other branch * Add source language option to translation audit tool Allow audit-translations to compare a target language against any source language via --source, defaulting to English for existing workflows. Update CLI output, docs, and tests for dynamic source/target labels, including Swedish/Norwegian comparison coverage. Normalize Rich ANSI output in tests so golden and string assertions remain stable when terminal color is forced. * fix ruff * fix ruff * fix ruff * Core concept names additions and tests -- issue 381 (#592) * initial addition of core concept names * defnitions and tests, with a few failing * minus, plus, volume fixes. Other defined, not appearing in site * fix clippy warnings * use "ruamel.yaml>=0.19.1" in uv.lock and pyproject.toml * handle canonicalize failure in containerized environments (#574) * add 1 week dependency cooldown for uv (#599) * fix rules for digits in german (#596) * update navigation rules for traditional chinese (#588) * copy english version of navigate.yaml from NVDA2026.1 and translate * 更新 definitions.yaml 與 navigate.yaml * update unicode-full.yaml * 區分完全/已完全放大/縮小 * 移至->移到, 表->表格, 數學式->數學, 退出->離開, 不能->無法, 當前->目前。 overview line 47, SimpleSpeak_Rules.yaml line 39, "根號"->""。mroot.rs 對應修改 * 移到上一格 -> 往上一格,移到下一格 -> 往下一格, 佔位符 -> 書籤,設定佔位符 -> 設定書籤,已是最上層 -> 不在別的式子裡面,上一層是 -> 在後面式子裡面,字元模式 開n 次方根 的讀法,基底 -> 基、底 * 向量 -> 射線 * 基 -> 基底,底 -> 基底 * Internally in definitions.yaml, MathCAT uses "nofix", but that's not a MathML airity property. Remove it. * don't say "indexed by", say "sub" * add some terse and verbose forms of speech * fix typo * Added an html file with all the test exprs so that translators have examples to test. * Fix remaining erros after `definitions.yaml` was updated -- needed to add `function:` * fix clippy warning * minor cleanup: remove ASCIIMath-fi reference * Refactor braille.rs to a BrailleCode trait + registry Replace the four scattered string-match dispatch sites (cleanup, get_braille_chars, NeedsToBeGrouped, and highlight knobs) with a single get_braille_code() registry and a BrailleCode trait implemented per code. Extract the duplicated UEB/Vietnam/Finnish/Swedish indicator-replacement closures into one apply_indicator_replacements helper plus a UserTypeforms struct. Behavior is unchanged (all tests/braille pass); adding a new code now means one registry entry + one impl block. Co-authored-by: Cursor * improved counting for (likey) 8 bit codes when figuring out braille position * make sure this is part of repo * add BrailleDocs to repo (stop ignoring) * updated data, does not include ASCII letters/digits * First version of Russian Braille This commit adds first druft version for Russian Braille. * typo in example * Russian Braille fixes This commit adds fixes to Russian Braille. Now the test with parentheses is passed. Also there are devision and multiplication signs have been modified according to the Russian braille specs. * Braille - Latin and Greek letters fixes This commit adds a layer of Latin and Greek alphabets for Russian mathematical Braille. * In unicode.yaml Latin and Greek letters now go through the internal markers of the alphabetic mode. • In braille.rs russian_cleanup turns these markers into real signs: ◦ Latin lowercase: ⠠ ◦ Latin uppercase: ⠨ ◦ Greek lowercase: ⠰ ◦ Greek uppercase: ⠸ Added tests to russian.rs to x+A+y+B, the Latin letter after the number. * Braille - Roots, powers and scripts This commit adds Roots, powers and scripts according to the Russian math Braille specs. * Update Russian Braille rules and mappings Fix YAML replace syntax and extend Russian braille rules: add chemical-formula-operator-bond and prefix-geometry-ops rules, broaden function-name matching (include tg/ctg variants) and add separator markers. Update unicode mappings (degree sign, invisible characters, conditional ∪ mapping, parallel/arrow glyphs) and adjust several function/operator braille outputs. Add/expand tests to cover functions, geometry, matrices and chemical formulas to validate the changes. * Add Russian language to docs Add Russian (ru) to the list of supported speech languages and update Russian documentation and translation guidance. - docs/User_guide_for_MathCAT_en.md, docs/index.md: add Russian (ru) to supported languages lists. - docs/ru/User_guide_for_MathCAT_ru.md: include Russian in language options, reorder entries and apply minor wording fixes. - docs/ru/helpers.md: clarify wording, update examples and test instructions to reference ru instead of fr, and improve translation guidance and examples (including test and cargo commands). - docs/ru/index.md: reorder supported languages and polish phrasing. These edits enable and document Russian support and improve clarity for translators and developers working on the Russian localization. * Refactor Russian function braille mapping Replace hardcoded function-name branches in Russian_Rules.yaml with a DefinitionValue lookup that pulls from a new RussianFunctionBraille mapping. Add RussianFunctionBraille entries to definitions.yaml (private-use codepoints U+E100–U+E10E) and map those codepoints to braille strings in unicode.yaml. Add a test covering tan/cot rendering. This centralizes function braille representations and simplifies the rule logic. * Refine Russian wording in translators guide Clarify and naturalize Russian phrasing in the translators guide: add explicit "на русском языке" to SimpleSpeak examples; change exponent wording to "в квадрате" with example "икс в квадрате"; make fraction wording more natural ("числитель разделить на знаменатель" and example "метр за секунду"); fix "английском" → "английском языке"; and update Braille tooling phrasing to "просмотрщиком Брайля" and "дисплее Брайля" for consistency. * Add alphabet indicators in russian_cleanup Insert a preprocessing step that injects braille alphabet indicators before running REPLACE_INDICATORS. A new helper add_russian_alphabet_indicators scans the raw braille and emits specific indicators for 'l' (Latin lowercase), 'u' (Latin uppercase), 'g' (Greek lowercase) and 'v' (Greek uppercase), avoiding repeated indicators by tracking the current alphabet mode. The helper also resets the mode on 'C', 'N', and '#'. This keeps the existing indicator replacement and space-collapsing logic intact while preventing redundant alphabet indicators. * Russian braille: add GOST symbols and decimals Add GOST-style number and symbol support for Russian braille: introduce a decimal-comma rule that emits BrailleChars for integer and fractional parts with a decimal indicator, using PUA digit placeholders. Extend unicode mappings with bracket/brace/slash/backslash/pipe characters, Celsius, context-sensitive prime (′/″) handling, PUA digit-to-braille mappings, and a few arrow/connective symbols. Update tests to cover decimal numbers, fractions, set notation, Celsius, and logical arrows, and adjust expected output for geometry primes to reflect the new context-aware prime handling. * Add vector/bar/arc mover rules and tests Introduce three new mover rules in Rules/Braille/Russian/Russian_Rules.yaml: vector-arrow, vector-bar, and geometry-arc. Each rule detects specific accent characters (arrows: → ⟶ ⇀ ⃗; bars: ¯ ‾ _  ̄; arcs: ⌒ ⏜ ︵) and emits the appropriate braille indicators, using a different form when the base has more than one character. Add corresponding tests in tests/braille/Russian/russian.rs to cover single- and multi-character movers and combined expressions. * Add Russian braille math functions and limits Extend Russian braille rules to support additional math functions and large operator limits. Added new rules for large-op-limits, operator/functions (including special handling for div), and div-specific replacements in Rules/Braille/Russian/*. Updated Russian definitions and unicode mappings to include sinh/cosh/tanh/coth, arg/sgn/det/rank/rg, Re/Im, grad/rot/div, gcd/lcm and related braille codes. Updated global Rules/definitions.yaml to recognize new function names. Fixed braille cleanup in src/braille.rs to normalize div encoding. Added unit tests exercising the new function mappings and large operator limits. * Support system left brace in Russian braille Introduce handling for system left-brace matrices in the Russian braille rules: add InSystemLeftBrace variable, a system-left-brace-opener rule and a system-left-brace-table rule to detect brace-enclosed mtable as a system. Update row-start logic to emit appropriate braille tokens when InSystemLeftBrace is set. Add tests exercising curly-brace matrices, piecewise function (brace with conditions), and vertical-bar matrices to verify the new behavior. * Add Russian chemistry braille rules and tests Extend Russian braille rules to handle chemical notation: add rules for equilibrium-arrow, reaction operators, repeated charges, charges, and charge-with-subscript mappings (chemical-reaction-operator, chemistry-mhchem-equilibrium-arrow, chemistry-repeated-charge, chemistry-charge, chemistry-charge-with-subscript). Update Rules/Braille/Russian/Russian_Rules.yaml to map arrows, +/-, superscripts/subscripts and reaction operators to appropriate braille sequences. Add tests in tests/braille/Russian/russian.rs (source_chemical_reactions_and_charges) covering reaction arrows, charges, subscripts/superscripts and combined examples. --------- Co-authored-by: Moritz Groß Co-authored-by: nsoiffer Co-authored-by: Tuomas Pyorre <82783219+ttpyorre@users.noreply.github.com> Co-authored-by: V3nky Co-authored-by: Hon-Jang Yang Co-authored-by: nsoiffer Co-authored-by: Cursor --- .gitignore | 2 - Cargo.toml | 22 +- Rules/Braille/UEB/unicode-full.yaml | 35 ++- Rules/Languages/de/unicode-full.yaml | 225 ++++++++++++++++++ Rules/Languages/en/SharedRules/default.yaml | 29 +-- Rules/Languages/en/unicode-full.yaml | 185 +++++++++++++- Rules/Languages/zh/tw/unicode-full.yaml | 195 +++++++++++++++ docs/ru/User_guide_for_MathCAT_ru.md | 7 +- docs/ru/helpers.md | 46 ++-- docs/ru/index.md | 4 +- .../new_translators_guide_MathCAT_revised.md | 12 +- src/infer_intent.rs | 53 ++--- src/interface.rs | 109 +++++++++ src/main.rs | 161 ++++++------- src/speech.rs | 155 ++++++------ src/tts.rs | 191 ++++++--------- tests/Languages/en/intent.rs | 1 + 17 files changed, 1018 insertions(+), 414 deletions(-) diff --git a/.gitignore b/.gitignore index b4a54ec2c..fef5d915c 100644 --- a/.gitignore +++ b/.gitignore @@ -57,5 +57,3 @@ Rules/*/*/*.zip docs/_site docs/.jekyll-cache docs/Gemfile.lock - -docs/nb_of_norway/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index f499124b6..37c55108a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mathcat" -version = "0.7.6-beta.8" +version = "0.7.6-beta.5" authors = ["Neil Soiffer "] license = "MIT" description = "MathCAT: Math Capable Assistive Technology ('Speech and braille from MathML')" @@ -8,33 +8,32 @@ repository = "https://github.com/NSoiffer/MathCAT" homepage = "https://nsoiffer.github.io/MathCAT/" documentation = "https://nsoiffer.github.io/MathCAT/" edition = "2024" -exclude = ["src/main.rs", "docs", "BrailleDocs", "PythonScripts"] # should have "Rules/", but then one can't run build.rs to build the zip file +exclude = ["src/main.rs", "docs", "PythonScripts"] # should have "Rules/", but then one can't run build.rs to build the zip file [features] "include-zip" = [] "enable-logs" = ["android_logger"] "tts" = [ "natural-tts" ] -"no-unsafe" = ["sxd-document-no-unsafe/no-unsafe", "sxd-xpath-no-unsafe/no-unsafe"] [dependencies] -sxd-document-no-unsafe = "0.4.2" -sxd-xpath-no-unsafe = "0.5.1" +sxd-document = "0.3" +sxd-xpath = "0.4" yaml-rust = "0.4" # yaml-rust = { version = "0.11", package = "yaml-rust2" } -strum = "0.28" -strum_macros = "0.28" +strum = "0.27.2" +strum_macros = "0.27.2" anyhow = "1.0" -regex = "1.12.3" +regex = "1.10" dirs = "6.0" -bitflags = "2.11" +bitflags = "2.10" phf = { version = "0.13", features = ["macros"] } roman-numerals-rs = "4.1.0" radix_fmt = "1.0" unicode-script = "0.5" log = "0.4" -env_logger = "0.11.9" +env_logger = "0.11.8" cfg-if = "1.0.1" fastrand = { version = "2.3.0" } clap = { version = "4.5.60", features = ["derive"] } @@ -54,9 +53,6 @@ bitflags = "2.6" # build.rs picks DEFLATE/BZIP2 based on the *target* family, not the host. zip = { version = "8.2", default-features = false, features = ["deflate", "bzip2"] } -[dev-dependencies] -tempfile = "3.23" - [lib] name = "libmathcat" crate-type = ["rlib", "cdylib"] diff --git a/Rules/Braille/UEB/unicode-full.yaml b/Rules/Braille/UEB/unicode-full.yaml index df7bf2274..eeee8b309 100644 --- a/Rules/Braille/UEB/unicode-full.yaml +++ b/Rules/Braille/UEB/unicode-full.yaml @@ -1,24 +1,6 @@ --- - - - " ": [t: "W"] # 0x2000 - - " ": [t: "W"] # 0x2001 - - " ": [t: "W"] # 0x2002 - - " ": [t: "W"] # 0x2003 - - " ": [t: "W"] # 0x2004 - - " ": [t: "W"] # 0x2005 - - " ": [t: "W"] # 0x2006 - - " ": [t: "W"] # 0x2007 - - " ": [t: "W"] # 0x2008 - - " ": [t: "W"] # 0x2009 - - " ": [t: "W"] # 0x200a - - "​": [t: "W"] # 0x200b - - "
": [t: "W"] # 0x2028 - - "
": [t: "W"] # 0x2029 - - " ": [t: "W"] # 0x202f - - " ": [t: "W"] # 0x205f - - "": [t: "W"] # 0xfeff - - + - "⋇": [tc: "1⠌⠯⠦"] # 0x22C7 (Division times) + - "⊩": [tc: "⠸⠳⠿⠸⠒"] # 0x22A9 (Forces) - "ℏ": [tc: "⠈⠒⠓"] # t: "B"F (reduced Plank's constant) - "ⅆ": [tc: "⠙"] # 0x2146 - "ⅇ": [tc: "⠑"] # 0x2147 @@ -47,6 +29,7 @@ - "↉": [tc: "#N⠚N⠌N⠑"] # 0x2189 (Vulgar Fraction Zero Thirds) + - "ℊ": [tc: "TL⠛⠁"] # 0x210a (Script Small G) - "ℋ": [tc: "TCL⠓⠁"] # 0x210b (Script Capital H) - "ℒ": [tc: "TCL⠇"] # 0x2113 (Script Capital L) @@ -73,6 +56,16 @@ - "ℤ": [tc: "𝔹CL⠵"] # 0x2124 + - "𝚨-𝛀": # 0x1d6a8 - 0x1d6c0 + - tc: "B" + - spell: "translate('.', '𝛂𝛃𝛄𝛅𝛆𝛇𝛈𝛉𝛊𝛋𝛌𝛍𝛎𝛏𝛐𝛑𝛒𝛓𝛔𝛕𝛖𝛗𝛘𝛙𝛚', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + + - "𝛂-𝛚": # 0x1d6c2 - 0x1d6da + - tc: "BGL" + - spell: "translate('.', '𝛂𝛃𝛄𝛅𝛆𝛇𝛈𝛉𝛊𝛋𝛌𝛍𝛎𝛏𝛐𝛑𝛒𝛓𝛔𝛕𝛖𝛗𝛘𝛙𝛚', 'αβγδεζηθικλμνξοπρςστυφχψω')" + + - "𝔄-𝔜": # 0x1d504 - 0x1d51d ('z' version is reserved) - tc: "DC" - spell: "translate('.', '𝔄𝔅𝔆𝔇𝔈𝔉𝔊𝔋𝔌𝔍𝔎𝔏𝔐𝔑𝔒𝔓𝔔𝔕𝔖𝔗𝔘𝔙𝔚𝔛𝔜', 'abcdefghijklmnopqrstuvwxyz')" @@ -106,7 +99,7 @@ # script chars in math alphabetic block and also MathType private use area - "𝒜-𝒵": # 0x1d49c - 0x1d4b5 - tc: "TsC" - - spell: "translate('.', '𝒜𝒝𝒞𝒟𝒠𝒡𝒢𝒣𝒤𝒥𝒦𝒧𝒨𝒩𝒪𝒫𝒬𝒭𝒮𝒯𝒰𝒱𝒲𝒳𝒴𝒵', 'abcdefghijklmnopqrstuvwxyz')" + - spell: "translate('.', '𝒜𝒝𝒞𝒟𝒠𝒡𝒢𝒣𝒤𝒥𝒦𝒧𝒨𝒩𝒪𝒫𝒬𝒭𝒮𝒯𝒰𝒱𝒲𝒳𝒴𝒵', 'abcdefghijklmnopqrstuvwxyza')" - "𝒶-𝓏": # 0x1d4b6 - 0x1d4cf - tc: "Ts" diff --git a/Rules/Languages/de/unicode-full.yaml b/Rules/Languages/de/unicode-full.yaml index 6e41bf9c5..1d1387f0d 100644 --- a/Rules/Languages/de/unicode-full.yaml +++ b/Rules/Languages/de/unicode-full.yaml @@ -10,6 +10,7 @@ - "¨": [T: "diaeresis"] # 0xa8 (en: 'double dot') - "©": [T: "Copyright"] # 0xa9 (en: 'copyright') - "ª": [T: "weibliches Ordnungszeichen"] # 0xaa (en: 'feminine ordinal indicator') + - "¬": [T: "nicht"] # 0xac (en: 'not') - "«": [T: "Nach links zeigendes doppeltes spitzes Anführungszeichen"] # 0xab (en: 'left-pointing double angle quote mark', MathPlayer: 'left-pointing double angle quote mark', google: 'links zeigende doppelwinkel-zitatmarke') - "¯": # 0xaf - test: @@ -18,9 +19,24 @@ else: [T: "Überstrich"] # (en: 'line', MathPlayer: 'macron', google: 'linie') - "²": [T: "Quadrat"] # 0xb2 (en: 'two', MathPlayer: 'zum Quadrat', google: 'zwei') - "³": [T: "Kubik"] # 0xb3 (en: 'three', MathPlayer: 'hoch drei', google: 'drei') + - "´": [T: "Akutakzent"] # 0xb4 (en: 'acute', MathPlayer: 'acute accent', google: 'akut') - "µ": [T: "Mikrozeichen"] # 0xb5 (en: 'micro', MathPlayer: 'micro sign', google: 'mikro') - "¹": [T: "hoch eins"] # 0xb9 (en: 'one', google: 'eins') - "º": [T: "Männliches Ordnungszeichen"] # 0xb9 (en: 'masculine ordinal indicator') + - "·": + - test: + if: "$SpeechStyle != 'ClearSpeak' or $ClearSpeak_MultSymbolDot = 'Auto'" + then: [T: "mal"] # (en: 'times', google translation) + else: [T: "mal"] # (en: 'dot', MathPlayer: 'Punkt mittig', google: 'punkt') + - "×": # 0xd7 + - test: + if: "$SpeechStyle != 'ClearSpeak' or $ClearSpeak_MultSymbolX = 'Auto'" + then: [T: "mal"] # (en: 'times', google translation) + else_test: + if: $ClearSpeak_MultSymbolX = 'By' + then: [T: "von"] # (en: 'by', google translation) + else: [T: "mal"] # (en: 'cross', MathPlayer: 'times', google: 'kreuzen') + - "÷": [T: "geteilt durch"] # 0xf7 (en: 'divided by', MathPlayer: 'divides') - "¡": [T: "umgekehrtes ausrufezeichen"] # 0xa1 (en: 'inverted exclamation mark') - "¶": [T: "absatzzeichen"] # 0xb6 (en: 'paragraph mark') - "¿": [T: "umgekehrtes fragezeichen"] # 0xbf (en: 'inverted question mark') @@ -108,6 +124,14 @@ - "˽": [T: "modifikatorregal"] # 0x2fd (en: 'modifier shelf', google translation) - "˾": [T: "modifikator offenes regal"] # 0x2fe (en: 'modifier open shelf', google translation) - "˿": [T: "modifikator niedriger links pfeil"] # 0x2ff (en: 'modifier low left arrow', google translation) + - "̀": [T: "gravisakzentverzierung"] # 0x300 (en: 'grave accent embellishment', google: 'grabakzentverzierung') + - "́": [T: "akutakzentverzierung"] # 0x301 (en: 'acute accent embellishment', google: 'akute akzentverzierung') + - "̂": [T: "Hutverzierung"] # 0x302 (en: 'circumflex accent embellishment', google: 'zirma akzentverzierung') + - "̃": [T: "tildeverzierung"] # 0x303 (google: 'tilde verzierung') + - "̄": [T: "makronverzierung"] # 0x304 (en: 'macron embellishment', google: 'makron -verzierung') + - "̅": [T: "Überstreifenverzierung"] # 0x305 (google: 'überbärerverzierung') + - "̆": [T: "kombinierendes Breve"] # 0x306 (en: 'breve embellishment', google: 'breve -verzierung') + - "̇": [T: "Punkt über Verzierung"] # 0x307 (google: 'punkt über verzierung') - "̈": [T: "zwei Punkte oberhalb"] # 0x308 (en: 'diaeresis embellishment', google: 'diaeresis -verzierung') - "̉": [T: "haken über verzierung"] # 0x309 (en: 'hook above embellishment', google translation) - "̊": [T: "ring über verzierung"] # 0x30a (en: 'ring above embellishment', google: 'ring über verzierung') @@ -197,9 +221,13 @@ - "ϒ": [T: "Variante Upsilon mit Haken"] # 0x3d2 (en: 'upsilon with hook', google: 'upsilon mit haken') - "ϓ": [T: "Upsilon mit Akutakzent und Haken"] # 0x3d3 (en: 'upsilon with acute and hook', google translation) - "ϔ": [T: "Upsilon mit Diaeresis und Haken"] # 0x3d4 (en: 'upsilon with diaeresis and hook', google translation) + - "ϕ": [T: "gerades Phi"] # 0x3d5 (en: 'phi', google: 'phi') + - "ϖ": [T: "Variante Pi"] # 0x3d6 (en: 'pi', google: 'pi') - "ϗ": [T: "kai"] # 0x3d7 (MathPlayer: 'variant kai') - "Ϙ": [T: "großes archaisches koppa"] # 0x3d8 (en: 'cap archaic koppa') - "ϙ": [T: "archaisches koppa"] # 0x3d9 (en: 'archaic koppa') + - "ϵ": [T: "epsilon"] # 0x3f5 + - "϶": [T: "umgekehrtes epsilon"] # 0x3f6 (en: 'reversed epsilon', MathPlayer: 'reversed epsilon', google: 'umgekehrter epsilon') - "А-Я": # 0x410 - 0x42f - test: if: "$CapitalLetters_Beep" @@ -256,6 +284,10 @@ - "‐": [T: "Bindestrich"] # 0x2010 (en: 'hyphen', MathPlayer: 'minus', google: 'bindestrich') - "‑": [T: "nicht-trennender Bindestrich"] # 0x2011 (en: 'hyphen', google: 'bindestrich') - "‒": [T: "Figurenstrich"] # 0x2012 (en: 'figure dash', google: 'figur dash') + - "–": [T: "Halbgeviertstrich"] # 0x2013 (SRE: 'En Dash') + - "—": [T: "Geviertstrich"] # 0x2014 (SRE: 'EM Dash') + - "―": [T: "Horizontale Linie"] # 0x2015 (en: 'horizontal bar', MathPlayer: 'horizontal bar', google: 'horizontale linie') + - "‖": [T: "Doppelte vertikale Linie"] # 0x2016 (en: 'double vertical line', MathPlayer: 'double vertical bar', google: 'doppelte vertikale linie') - "†": [T: "Dolch"] # 0x2020 (en: 'dagger', MathPlayer: 'dagger', google: 'dolch') - "‡": [T: "Doppelter Dolch"] # 0x2021 (en: 'double dagger', MathPlayer: 'double dagger', google: 'doppeldolch') @@ -267,8 +299,24 @@ then: [T: "punkt"] # (en: 'dot', google translation) else: [T: "Kugel"] # (en: 'bullet', MathPlayer: 'bullet', google: 'kugel') + - "…": # 0x2026 + test: + if: + - "$SpeechStyle != 'ClearSpeak' or $ClearSpeak_Ellipses = 'Auto' or" + # must be ClearSpeak and $ClearSpeak_Ellipses = 'AndSoOn' + # speak '…' as 'and so on...' unless expr starts with '…' + - "../*[1][text()='…']" + then: [T: "punkt punkt punkt"] # (en: 'dot dot dot', google translation) + else_test: # must have $ClearSpeak_Ellipses = 'AndSoOn' + if: "count(following-sibling::*) = 0" + then: [T: "und so weiter"] # (en: 'and so on', google translation) + else: [T: "horizontale Ellipse"] # (en: 'and so on up to', MathPlayer: 'dot dot dot', google: 'und so weiter bis') + - "‰": [T: "promille"] # 0x2030 (en: 'per mille', MathPlayer: 'per mille sign', google: 'pro mille') - "‱": [T: "pro Zehntausend"] # 0x2031 (en: 'per ten thousand', MathPlayer: 'per ten thousand sign', google: 'pro zehntausend') + - "′": [T: "Strich"] # 0x2032 (en: 'prime', MathPlayer: 'strich', google: 'prime') + - "″": [T: "zwei Striche"] # 0x2033 (en: 'double prime', MathPlayer: 'zwei-strich', google: 'double prime') + - "‴": [T: "drei Striche"] # 0x2034 (en: 'triple prime', MathPlayer: 'drei-strich', google: 'triple prime') - "‵": [T: "umgekehrter Strich"] # 0x2035 (en: 'reversed prime', MathPlayer: 'reversed prime', google: 'umgekehrt prime') - "‶": [T: "zwei umgekehrte Striche"] # 0x2036 (en: 'reversed double prime', MathPlayer: 'reversed double prime', google: 'umgekehrt doppelte prime') - "‷": [T: "drei umgekehrte Striche"] # 0x2037 (en: 'reversed triple prime', google: 'triple prime umgekehrt') @@ -398,6 +446,7 @@ - "℆": [T: "cada una"] # 0x2106 (google translation) - "ℇ": [T: "Euler-Konstante"] # 0x2107 (en: 'euler's constant', google: 'eulers konstante') - "℈": [T: "skrupel"] # 0x2108 (en: 'scruples', google translation) + - "℉": [T: "Grad Fahrenheit"] # 0x2109 (google: 'grad fahrenheit') - "ℊ": [T: "Skript G"] # 0x210a (google: 'skript g') - "ℌℑℨℭ": # 0x210c, 0x2111, 0x2128, 0x212d - T: "Fraktur" # (en: 'fraktur', google: 'fraktur') @@ -409,6 +458,7 @@ then: [T: "doppelt geschlagen"] # (en: 'double struck', google translation) - spell: "translate('.', 'ℍℙℾℿ', 'HPΓΠ')" + - "ℎ": [T: "Planck-Konstante"] # 0x210e (en: 'planck constant', MathPlayer: 'planck constant', google: 'planck konstant') - "ℏ": # 0x210f - test: if: "($Verbosity='Terse')" @@ -419,11 +469,15 @@ - T: "Skript" # (en: 'script', MathPlayer: 'h bar', google: 'skript') - spell: "translate('.', 'ℐℒ℘ℬℰℱℳ', 'ILPBEFM')" + - "ℓ": [T: "Schreibschrift l"] # 0x2113 (en: 'script l', MathPlayer: 'script l', google: 'skript l') - "℔": [T: "L B Balkensymbol"] # 0x2114 (en: 'pounds', google: 'pfund') - "№": [T: "Numero-Zeichen"] # 0x2116 (en: 'number', MathPlayer: 'numero sign', google: 'nummer') - "℥": [T: "Unze-Zeichen"] # 0x2125 (en: 'ounces', MathPlayer: 'ounce sign', google: 'unzen') + - "Ω": [T: "Ohm-Zeichen"] # 0x2126 (en: 'ohms', MathPlayer: 'ohm sign', google: 'ohm') - "℧": [T: "Umgekehrtes Ohm-Zeichen"] # 0x2127 (en: 'mhos', MathPlayer: 'inverted ohm', google: 'mhos') - "℩": [T: "gedrehtes Iota"] # 0x2129 (google: 'drehte iota') + - "K": [T: "Kelvin-Zeichen"] # 0x212a (en: 'kelvin', MathPlayer: 'degrees kelvin', google: 'kelvin') + - "Å": [T: "Angstrom-Zeichen"] # 0x212b (en: 'angstroms', MathPlayer: 'angstroms', google: 'angstrome') - "ℯ": [T: "Skript E"] # 0x212f (google: 'skript e') # coalesced some chars that use cap letters @@ -501,6 +555,10 @@ - "ⅾ": [T: "ⅾ"] # 0x217e (en: 'D', google translation) - "ⅿ": [T: "ⅿ"] # 0x217f (en: 'M', google translation) - "↉": [T: "null drittel"] # 0x2189 (en: 'zero thirds') + - "←": [T: "Pfeil nach links"] # 0x2190 (en: 'leftwards arrow', MathPlayer: 'leftwards arrow', google: 'pfeil nach links') + - "↑": [T: "Aufwärtspfeil"] # 0x2191 (en: 'upwards arrow', MathPlayer: 'upwards arrow', google: 'nach oben pfeil') + - "→": [T: "Pfeil nach rechts"] # 0x2192 (en: 'rightwards arrow', MathPlayer: 'rightwards arrow', google: 'richtiger pfeil') + - "↓": [T: "Pfeil nach unten"] # 0x2193 (en: 'downwards arrow', MathPlayer: 'downwards arrow', google: 'nach unten pfeil') - "↔": [T: "Pfeil nach links und rechts"] # 0x2194 (en: 'left right arrow', MathPlayer: 'left right arrow', google: 'links rechts pfeil') - "↕": [T: "Pfeil nach oben und unten"] # 0x2195 (en: 'up down arrow', MathPlayer: 'up down arrow', google: 'auf pfeil runter') - "↖": [T: "Nordwestpfeil"] # 0x2196 (en: 'north west arrow', MathPlayer: 'north west arrow', google: 'nordwestpfeil') @@ -573,6 +631,7 @@ - "⇏": [T: "Doppelpfeil nach rechts mit Strich"] # 0x21cf (en: 'rightwards double arrow with stroke', MathPlayer: 'rightwards double arrow with stroke', google: 'richtiger doppelpfeil mit schlaganfall') - "⇐": [T: "Doppelpfeil nach links"] # 0x21d0 (en: 'leftwards double arrow', MathPlayer: 'leftwards double arrow', google: 'links doppelpfeil') - "⇑": [T: "Doppelpfeil nach oben"] # 0x21d1 (en: 'upwards double arrow', MathPlayer: 'upwards double arrow', google: 'double -pfeil nach oben') + - "⇒": [T: "Doppelpfeil nach rechts"] # 0x21d2 (en: 'rightwards double arrow', MathPlayer: 'rightwards double arrow', google: 'richtiger doppelpfeil') - "⇓": [T: "Doppelpfeil nach unten"] # 0x21d3 (en: 'downwards double arrow', MathPlayer: 'downwards double arrow', google: 'downwards doppelpfeil') - "⇔": [T: "Linker rechter Doppelpfeil"] # 0x21d4 (en: 'left right double arrow', MathPlayer: 'left right double arrow', google: 'links rechts doppelpfeil') - "⇕": [T: "Doppelpfeil nach oben und unten"] # 0x21d5 (en: 'up down double arrow', MathPlayer: 'up down double arrow', google: 'doppelpfeil nach oben') @@ -624,18 +683,107 @@ if: "$Verbosity!='Terse'" then: [T: "die"] # (en: 'the', google translation) - T: "Komplement" # (en: 'complement of') + - "∂": # 0x2202 + - test: + if: "$Verbosity='Terse'" + then: [T: "partiell"] # (en: 'partial', google translation) + else: [T: "partielle Ableitung"] # (en: 'partial derivative', google: 'partielle ableitung') - "∃": [T: "es gibt"] # 0x2203 (en: 'there exists') - "∄": [T: "es gibt nicht"] # 0x2204 (en: 'there does not exist') - "∅": [T: "leere Menge"] # 0x2205 (en: 'empty set') + - "∆": # 0x2206 + - test: + if: "$Verbosity!='Terse'" + then: [T: "die"] # (en: 'the', google translation) + - T: "Laplace-Operator von" # (en: 'laplacian of', MathPlayer: 'increment', google: 'laplace von') + - "∇": # 0x2207 + - test: + if: "$Verbosity!='Terse'" + then: [T: "die"] # (en: 'the', google translation) + - T: "Nabla" # (en: 'gradient of', google: 'gradient von') + - "∈": # 0x2208 + - test: + if: "$SpeechStyle != 'ClearSpeak'" + then: [T: "ein Element von"] # (en: 'an element of', google translation) + # Several options for speaking elements in ClearSpeak -- they split between being inside a set or not and then the option + else_test: + if: "../../self::m:set or ../../../self::m:set" # inside a set + then_test: + - if: $ClearSpeak_SetMemberSymbol = 'Auto' or $ClearSpeak_SetMemberSymbol = 'In' + then: [T: "in"] # (google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'Member' + then: [T: "Element von"] # (en: 'member of', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'Element' + then: [T: "Element von"] # (en: 'element of', google translation) + - else: [T: "Element von"] # $ClearSpeak_SetMemberSymbol = 'Belongs' (en: 'belonging to') + else_test: + - if: $ClearSpeak_SetMemberSymbol = 'Auto' or $ClearSpeak_SetMemberSymbol = 'Member' + then: [T: "ist ein Element von"] # (en: 'is a member of', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'Element' + then: [T: "ist ein Element von"] # (en: 'is an element of', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'In' + then: [T: "ist in"] # (en: 'is in', google translation) + - else: [T: "Element von"] # $ClearSpeak_SetMemberSymbol = 'Belongs' (en: 'belongs to') + - "∉": # 0x2209 + # rule is identical to 0x2208 + - test: + if: "$SpeechStyle != 'ClearSpeak'" + then: [T: "ist kein Element von"] # (en: 'is not an element of', google translation) + # Several options for speaking elements in ClearSpeak -- they split between being inside a set or not and then the option + else_test: + if: "../../self::m:set or ../../../self::m:set" # inside a set + then_test: + - if: $ClearSpeak_SetMemberSymbol = 'Auto' or $ClearSpeak_SetMemberSymbol = 'In' + then: [T: "nicht in"] # (en: 'not in', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'Member' + then: [T: "kein Element von"] # (en: 'not member of', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'Element' + then: [T: "kein Element von"] # (en: 'not element of', google translation) + - else: [T: "nicht Element von"] # $ClearSpeak_SetMemberSymbol = 'Belongs' (en: 'not belonging to') + else_test: + - if: $ClearSpeak_SetMemberSymbol = 'Auto' or $ClearSpeak_SetMemberSymbol = 'Member' + then: [T: "ist kein Element von"] # (en: 'is not a member of', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'Element' + then: [T: "ist kein Element von"] # (en: 'is not an element of', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'In' + then: [T: "ist nicht in"] # (en: 'is not in', google translation) + - else: [T: "nicht Element von"] # $ClearSpeak_SetMemberSymbol = 'Belongs' (en: 'does not belong to') + - "∊": # 0x220a + - test: + if: "$SpeechStyle != 'ClearSpeak'" + then: [T: "ist ein Element von"] # (en: 'is an element of', google translation) + # Several options for speaking elements in ClearSpeak -- they split between being inside a set or not and then the option + else_test: + if: "../../self::m:set or ../../../self::m:set" # inside a set + then_test: + - if: $ClearSpeak_SetMemberSymbol = 'Auto' or $ClearSpeak_SetMemberSymbol = 'In' + then: [T: "in"] # (google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'Member' + then: [T: "Element von"] # (en: 'member of', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'Element' + then: [T: "Element von"] # (en: 'element of', google translation) + - else: [T: "Element von"] # $ClearSpeak_SetMemberSymbol = 'Belongs' (en: 'belonging to') + else_test: + - if: $ClearSpeak_SetMemberSymbol = 'Auto' or $ClearSpeak_SetMemberSymbol = 'Member' + then: [T: "ist ein Element von"] # (en: 'is a member of', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'Element' + then: [T: "ist ein Element von"] # (en: 'is an element of', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'In' + then: [T: "ist in"] # (en: 'is in', google translation) + - else: [T: "Element von"] # $ClearSpeak_SetMemberSymbol = 'Belongs' (en: 'belongs to') - "∋": [T: "hat als Element"] # 0x220b (en: 'contains the member') - "∌": [T: "hat nicht als Element"] # 0x220c (en: 'does not contain the member') - "∍": [T: "hat als Element"] # 0x220d (en: 'contains the member') - "∎": [T: "Ende Beweis"] # 0x220e (en: 'end of proof') + - "∏": [T: "Produkt"] # 0x220f (en: 'product', MathPlayer: 'Product', google: 'produkt') + - "∐": [T: "Koprodukt"] # 0x2210 (en: 'coproduct', MathPlayer: 'coproduct', google: 'koprodukt') + - "∑": [T: "Summe"] # 0x2211 (en: 'sum') - "−": [T: "minus"] # 0x2212 - "∓": [T: "minus plus"] # 0x2213 (en: 'minus or plus') - "∔": [T: "Plus mit Punkt"] # 0x2214 (en: 'dot plus', MathPlayer: 'dot plus', google: 'punkt plus') - "∕": [T: "Division"] # 0x2215 (en: 'divided by', MathPlayer: 'Schrägstrich (Division)', google: 'geteilt durch') - "∖": [T: "Mengendifferenz"] # 0x2216 (en: 'set minus', MathPlayer: 'Mengen-Minus', google: 'minus einstellen') + - "∗": [T: "Sternoperator"] # 0x2217 (en: 'times', MathPlayer: 'asterisk operator', google: 'mal') - "∘": [T: "verknüpft mit"] # 0x2218 (en: 'composed with') - "∙": # 0x2219 - test: @@ -643,6 +791,11 @@ then: [T: "punkt"] # (en: 'dot', google translation) else: [T: "Kreisoperator"] # (en: 'times', MathPlayer: 'bullet operator', google: 'mal') + - "√": # 0x221a + - test: + if: "$Verbosity!='Terse'" + then: [T: "die"] # (en: 'the', google translation) + - T: "Quadratwurzel" # (en: 'square root of', MathPlayer: 'radical', google: 'quadratwurzel von') - "∛": # 0x221b - test: if: "$Verbosity!='Terse'" @@ -653,12 +806,26 @@ if: "$Verbosity!='Terse'" then: [T: "die"] # (en: 'the', google translation) - T: "vierte Wurzel" # (en: 'fourth root of') + - "∝": # 0x221d + - test: + if: "$Verbosity!='Terse'" + then: [T: "ist"] # (en: 'is', google translation) + - T: "proportional zu" # (en: 'proportional to') - "∞": [T: "unendlich"] # 0x221e (en: 'infinity') + - "∟": [T: "rechter Winkel"] # 0x221f (en: 'right angle') + - "∠": [T: "Winkel"] # 0x2220 (en: 'angle') + - "∡": [T: "gerichteter Winkel"] # 0x2221 (en: 'measured angle', MathPlayer: 'measured angle', google: 'gemessener winkel') - "∢": [T: "Raumwinkel"] # 0x2222 (en: 'spherical angle', MathPlayer: 'spherical angle', google: 'sphärischer winkel') - "∣": [T: "teilt"] # 0x2223 (en: 'divides') - "∤": [T: "teilt nicht"] # 0x2224 (en: 'does not divide') - "∧": [T: "und"] # 0x2227 (en: 'and') - "∨": [T: "oder"] # 0x2228 (en: 'or') + - "∩": [T: "Durchschnitt"] # 0x2229 (en: 'intersection') + - "∪": [T: "Vereinigung"] # 0x222a (en: 'union') + - "∫": [T: "Integral"] # 0x222b (en: 'integral') + - "∬": [T: "Doppelintegral"] # 0x222c (en: 'double integral') + - "∭": [T: "Dreifachintegral"] # 0x222d (en: 'triple integral', MathPlayer: 'Dreifaches Integral', google: 'dreifachintegral') + - "∮": [T: "Randintegral"] # 0x222e (en: 'contour integral', MathPlayer: 'Kontur-Integral', google: 'konturintegral') - "∯": [T: "Oberflächenintegral"] # 0x222f (en: 'surface integral', MathPlayer: 'Flächenintegral', google: 'oberflächenintegral') - "∰": [T: "Volumenintegral"] # 0x2230 (en: 'volume integral', MathPlayer: 'Volumsintegral', google: 'volumenintegral') - "∱": [T: "Integral im Uhrzeigersinn"] # 0x2231 (en: 'clockwise integral') @@ -666,6 +833,12 @@ - "∳": [T: "Kurvenintegral im Gegenuhrzeigersinn"] # 0x2233 (en: 'anticlockwise contour integral', MathPlayer: 'Kontur-Integral im Gegenuhrzeigersinn', google: 'konturintegral gegen den gegenwart') - "∴": [T: "folglich"] # 0x2234 (en: 'therefore', MathPlayer: 'deshalb', google: 'daher') - "∵": [T: "weil"] # 0x2235 (en: 'because') + - "∶": # 0x2236 + - test: + if: "$Verbosity!='Terse'" + then: [T: "ist"] # (en: 'is', google translation) + - T: "Verhältnis" # (en: 'to') + - "∷": [T: "Proportion"] # 0x2237 (en: 'as') - "∸": [T: "Minus mit Punkt"] # 0x2238 (en: 'dot minus', MathPlayer: 'dot minus', google: 'punkt minus') - "∹": [T: "Überschuss"] # 0x2239 (en: 'has excess compared to', MathPlayer: 'excess', google: 'hat überschüssig im vergleich zu') - "∺": # 0x223a @@ -678,6 +851,14 @@ if: "$Verbosity!='Terse'" then: [T: "ist"] # (en: 'is', google translation) - T: "homothetisch" # (en: 'homothetic to', MathPlayer: 'homothetic', google: 'homothetisch zu') + - "∼": [T: "Tildeoperator"] # 0x223c (en: 'varies with', MathPlayer: 'tilde operator', google: 'variiert mit') + - "∽": [T: "Umgekehrte Tilde"] # 0x223d (en: 'reversed tilde', MathPlayer: 'reversed tilde', google: 'umgekehrte tilde') + - "∾": # 0x223e + - test: + if: "$Verbosity!='Terse'" + then: [T: "ist"] # (en: 'is', google translation) + - T: "Umgekehrtes stummes S" # (en: 'most positive', MathPlayer: 'inverted lazy s', google: 'am positivsten') + - "∿": [T: "Sinuswelle"] # 0x223f (en: 'sine wave', MathPlayer: 'sine wave', google: 'sinus') - "≀": [T: "Kranzprodukt"] # 0x2240 (en: 'wreath product', MathPlayer: 'wreath product', google: 'kranzprodukt') - "≁": [T: "durchgestrichene Tilde"] # 0x2241 (en: 'not tilde', MathPlayer: 'not tilde', google: 'nicht tilde') - "≂": [T: "minus tilde"] # 0x2242 (SRE: 'Minus über Tilde') @@ -778,6 +959,16 @@ then: [T: "ist"] # (en: 'is', google translation) - T: "gemessen mit" # (en: 'measured by', MathPlayer: 'measured by') - "≟": [T: "vielleicht gleich"] # 0x225f (en: 'has an unknown relationship with', MathPlayer: 'questioned equal to', google: 'hat eine unbekannte beziehung zu') + - "≠": # 0x2260 + - test: + if: "$Verbosity!='Terse'" + then: [T: "ist"] # (en: 'is', google translation) + - T: "ungleich" # (en: 'not equal to') + - "≡": # 0x2261 + - test: + if: "$Verbosity!='Terse'" + then: [T: "ist"] # (en: 'is', google translation) + - T: "identisch mit" # (en: 'identical to', SRE: 'kongruent mit') - "≢": # 0x2262 - test: if: "$Verbosity!='Terse'" @@ -888,6 +1079,36 @@ - "≿": [T: "nachfolgend oder äquivalent"] # 0x227f (en: 'succeeds or is equivalent to', MathPlayer: 'succeeds or equivalent to', google: 'erfolg oder ist gleichbedeutend mit') - "⊀": [T: "nicht vorangehend"] # 0x2280 (en: 'does not precede', MathPlayer: 'does not precede', google: 'vorausgegangen nicht voraus') - "⊁": [T: "nicht nachfolgend"] # 0x2281 (en: 'does not succeed', MathPlayer: 'does not succeed', google: 'gelingt es nicht') + - "⊂": # 0x2282 + - test: + if: "$Verbosity!='Terse'" + then: [T: "ist ein"] # (en: 'is a', google translation) + - T: "echte Teilmenge von" # (en: 'subset of', MathPlayer: 'Teilmenge von', google: 'untergruppe von') + - "⊃": # 0x2283 + - test: + if: "$Verbosity!='Terse'" + then: [T: "ist ein"] # (en: 'is a', google translation) + - T: "echte Obermenge von" # (en: 'superset of', MathPlayer: 'Obermenge von', google: 'superset von') + - "⊄": # 0x2284 + - test: + if: "$Verbosity!='Terse'" + then: [T: "ist"] # (en: 'is', google translation) + - T: "keine echte Teilmenge von" # (en: 'not a subset of', MathPlayer: 'nicht Teilmenge von', google: 'keine teilmenge von') + - "⊅": # 0x2285 + - test: + if: "$Verbosity!='Terse'" + then: [T: "ist"] # (en: 'is', google translation) + - T: "keine echte Obermenge von" # (en: 'not a superset of', MathPlayer: 'nicht Obermenge von', google: 'kein superset von') + - "⊆": # 0x2286 + - test: + if: "$Verbosity!='Terse'" + then: [T: "ist ein"] # (en: 'is a', google translation) + - T: "Teilmenge oder gleich" # (en: 'subset of or equal to', MathPlayer: 'Teilmenge von odre gleich', google: 'teilmenge von oder gleich') + - "⊇": # 0x2287 + - test: + if: "$Verbosity!='Terse'" + then: [T: "ist ein"] # (en: 'is a', google translation) + - T: "Obermenge oder gleich" # (en: 'superset of or equal to', MathPlayer: 'Obermenge von oder gleich', google: 'superset von oder gleich') - "⊈": # 0x2288 - test: if: "$Verbosity!='Terse'" @@ -2846,8 +3067,11 @@ - "": [T: "digamma"] # 0xed03 (google translation) - "ϝ": [T: "digamma"] # 0x3dd - "": [T: ""] # 0xed10 (en: 'd', google translation) + - "ⅆ": [T: "d kursiv mit Doppelstrich"] # 0x2146 (en: 'd', MathPlayer: 'd', google: 'ⅆ') - "": [T: ""] # 0xed11 (en: 'e', google translation) + - "ⅇ": [T: "e kursiv mit Doppelstrich"] # 0x2147 (en: 'e', MathPlayer: 'e', google: 'ⅇ') - "": [T: ""] # 0xed12 (en: 'i', google translation) + - "ⅈ": [T: "i kursiv mit Doppelstrich"] # 0x2148 (en: 'i', MathPlayer: 'i', google: 'ⅈ') - "": [T: ""] # 0xed13 (en: 'j', google translation) - "ⅅ": - spell: "translate('.', 'ⅅ', 'DD')" # 0xed16, 0x2145 @@ -2896,6 +3120,7 @@ - " ": [T: ""] # 0x205f - "": [T: ""] # 0xef04 - "": [T: ""] # 0xef05 + - " ": [T: ""] # 0x2003 - "": [T: ""] # 0xef06 - "": [T: ""] # 0xef07 - "": [T: ""] # 0xef08 diff --git a/Rules/Languages/en/SharedRules/default.yaml b/Rules/Languages/en/SharedRules/default.yaml index 21cfc256b..c1f07ec7d 100644 --- a/Rules/Languages/en/SharedRules/default.yaml +++ b/Rules/Languages/en/SharedRules/default.yaml @@ -62,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 > 0 or @data-roman-numeral" # 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) @@ -237,26 +237,12 @@ - x: "*[2]" - t: "below" # phrase(x with z 'below' it) -# FIX: this only works for up to four dots (which is realistically all we need) -- name: dot-over - tag: modified-variable - match: "*[1][self::m:mi] and (*[2][translate(., '.', '')=''])" - replace: - - x: "*[1]" - - x: "IfThenElse(string-length(*[2])=1, 'dot', - IfThenElse(string-length(*[2])=2, 'double dot', - IfThenElse(string-length(*[2])=3, 'triple dot', 'quadruple dot') - ))" - - pause: short - -# FIX: the 'translate' list is a little different from 'modified-variable', so we also include 'mover' (unify?) - name: diacriticals - tag: [mover, modified-variable] + tag: mover match: "*[1][self::m:mi] and *[2][translate(., '\u0306\u030c.\u00A8\u02D9\u20DB\u20DC`^ˇ~→¯_', '')='']" replace: - x: "*[1]" - x: "*[2]" - - pause: short - name: default tag: mover @@ -436,23 +422,22 @@ - t: "end scripts" # phrase(At this point 'end scripts' occurs) - name: default - tag: [mtable, array] + tag: mtable variables: - IsColumnSilent: "false()" - - NumColumns: "CountTableColumns(.)" - - NumRows: "CountTableRows(.)" + - NumColumns: "count(*[1]/*) - IfThenElse(*/self::m:mlabeledtr, 1, 0)" match: "." replace: - t: "table with" # phrase(the 'table with' 3 rows) - - x: "$NumRows" + - x: count(*) - test: - if: "$NumRows=1" + if: count(*)=1 then: [t: "row"] # phrase(the table with 1 'row') else: [t: "rows"] # phrase(the table with 3 'rows') - t: "and" # phrase(the table with 3 rows 'and' 4 columns) - x: "$NumColumns" - test: - if: "$NumColumns=1" + if: "NumColumns=1" then: [t: "column"] # phrase(the table with 3 rows and 1 'column') else: [t: "columns"] # phrase(the table with 3 rows and 4 'columns') - pause: long diff --git a/Rules/Languages/en/unicode-full.yaml b/Rules/Languages/en/unicode-full.yaml index 271da2b67..1a566e8b3 100644 --- a/Rules/Languages/en/unicode-full.yaml +++ b/Rules/Languages/en/unicode-full.yaml @@ -10,6 +10,7 @@ - "¨": [t: "double dot"] # 0xa8 - "©": [t: "copyright"] # 0xa9 - "ª": [t: "feminine ordinal indicator"] # 0xaa + - "¬": [t: "not"] # 0xac - "«": [t: "left-pointing double angle quote mark"] # 0xab - "¯": # 0xaf - test: @@ -18,9 +19,24 @@ else: [t: "line"] - "²": [t: "two"] # 0xb2 - "³": [t: "three"] # 0xb3 + - "´": [t: "acute"] # 0xb4 - "µ": [t: "micro"] # 0xb5 - "¹": [t: "one"] # 0xb9 - "º": [t: "masculine ordinal indicator"] # 0xb9 + - "·": + - test: + if: "$SpeechStyle != 'ClearSpeak' or $ClearSpeak_MultSymbolDot = 'Auto'" + then: [t: "times"] + else: [t: "dot"] + - "×": # 0xd7 + - test: + if: "$SpeechStyle != 'ClearSpeak' or $ClearSpeak_MultSymbolX = 'Auto'" + then: [t: "times"] + else_test: + if: $ClearSpeak_MultSymbolX = 'By' + then: [t: "by"] + else: [t: "cross"] + - "÷": [t: "divided by"] # 0xf7 - "¡": [t: "inverted exclamation mark"] # 0xa1 - "¶": [t: "paragraph mark"] # 0xb6 - "¿": [t: "inverted question mark"] # 0xbf @@ -108,6 +124,14 @@ - "˽": [t: "modifier shelf"] # 0x2fd - "˾": [t: "modifier open shelf"] # 0x2fe - "˿": [t: "modifier low left arrow"] # 0x2ff + - "̀": [t: "grave accent embellishment"] # 0x300 + - "́": [t: "acute accent embellishment"] # 0x301 + - "̂": [t: "circumflex accent embellishment"] # 0x302 + - "̃": [t: "tilde embellishment"] # 0x303 + - "̄": [t: "macron embellishment"] # 0x304 + - "̅": [t: "overbar embellishment"] # 0x305 + - "̆": [t: "breve embellishment"] # 0x306 + - "̇": [t: "dot above embellishment"] # 0x307 - "̈": [t: "diaeresis embellishment"] # 0x308 - "̉": [t: "hook above embellishment"] # 0x309 - "̊": [t: "ring above embellishment"] # 0x30a @@ -197,9 +221,13 @@ - "ϒ": [t: "upsilon with hook"] # 0x3d2 - "ϓ": [t: "upsilon with acute and hook"] # 0x3d3 - "ϔ": [t: "upsilon with diaeresis and hook"] # 0x3d4 + - "ϕ": [t: "phi"] # 0x3d5 + - "ϖ": [t: "pi"] # 0x3d6 - "ϗ": [t: "kai"] # 0x3d7 - "Ϙ": [t: "cap archaic koppa"] # 0x3d8 - "ϙ": [t: "archaic koppa"] # 0x3d9 + - "ϵ": [t: "epsilon"] # 0x3f5 + - "϶": [t: "reversed epsilon"] # 0x3f6 - "А-Я": # 0x410 - 0x42f - test: if: "$CapitalLetters_Beep" @@ -271,6 +299,19 @@ then: [t: "dot"] else: [t: "bullet"] + - "…": # 0x2026 + test: + if: + - "$SpeechStyle != 'ClearSpeak' or $ClearSpeak_Ellipses = 'Auto' or" + # must be ClearSpeak and $ClearSpeak_Ellipses = 'AndSoOn' + # speak '…' as 'and so on...' unless expr starts with '…' + - "../*[1][.='…']" + then: [t: "dot dot dot"] + else_test: # must have $ClearSpeak_Ellipses = 'AndSoOn' + if: "count(following-sibling::*) = 0" + then: [t: "and so on"] + else: [t: "and so on up to"] + - "‰": [t: "per mille"] # 0x2030 - "‱": [t: "per ten thousand"] # 0x2031 - "′": [t: "prime"] # 0x2032 @@ -427,6 +468,7 @@ - t: "script" - spell: "translate('.', 'ℐℒ℘ℬℰℱℳ', 'ILPBEFM')" + - "ℓ": [t: "script l"] # 0x2113 - "℔": [t: "pounds"] # 0x2114 - "№": [t: "number"] # 0x2116 - "℥": [t: "ounces"] # 0x2125 @@ -584,6 +626,7 @@ - "⇏": [t: "right double arrow with stroke"] # 0x21cf - "⇐": [t: "left double arrow"] # 0x21d0 - "⇑": [t: "up double arrow"] # 0x21d1 + - "⇒": [t: "implies"] # 0x21d2 - "⇓": [t: "down double arrow"] # 0x21d3 - "⇔": [t: "if and only if"] # 0x21d4 - "⇕": [t: "up down double arrow"] # 0x21d5 @@ -635,15 +678,81 @@ if: "$Verbosity!='Terse'" then: [ot: "the"] - t: "complement of" + - "∂": # 0x2202 + - test: + if: "$Verbosity='Terse'" + then: [t: "partial"] + else: [t: "partial derivative"] - "∃": [t: "there exists"] # 0x2203 - "∄": [t: "there does not exist"] # 0x2204 - "∅": [t: "empty set"] # 0x2205 + - "∆": # 0x2206 + - test: + if: "following-sibling::*" + then_test: + if: "$Verbosity!='Terse'" + then: [t: "the LahPlahsian of"] # "LahPlahsian" sounds better than "laplacian" in speech engines tested + else: [t: "LahPlahsian"] + else: [t: "LahPlahsian"] + - "∇": [t: "nahblah"] # 0x2207 + - "∉": # 0x2209 + # rule is identical to 0x2208 + - test: + if: "$SpeechStyle != 'ClearSpeak'" + then: [t: "is not an element of"] + # Several options for speaking elements in ClearSpeak -- they split between being inside a set or not and then the option + else_test: + if: "../../self::m:set or ../../../self::m:set" # inside a set + then_test: + - if: $ClearSpeak_SetMemberSymbol = 'Auto' or $ClearSpeak_SetMemberSymbol = 'In' + then: [t: "not in"] + - else_if: $ClearSpeak_SetMemberSymbol = 'Member' + then: [t: "not member of"] + - else_if: $ClearSpeak_SetMemberSymbol = 'Element' + then: [t: "not element of"] + - else: [t: "not belonging to"] # $ClearSpeak_SetMemberSymbol = 'Belongs' + else_test: + - if: $ClearSpeak_SetMemberSymbol = 'Auto' or $ClearSpeak_SetMemberSymbol = 'Member' + then: [t: "is not a member of"] + - else_if: $ClearSpeak_SetMemberSymbol = 'Element' + then: [t: "is not an element of"] + - else_if: $ClearSpeak_SetMemberSymbol = 'In' + then: [t: "is not in"] + - else: [t: "does not belong to"] # $ClearSpeak_SetMemberSymbol = 'Belongs' + - "∊": # 0x220a + - test: + if: "$SpeechStyle != 'ClearSpeak'" + then: + - test: + if: "$Verbosity!='Terse' and not(parent::m:set)" # "the set x is an element of ..." sounds bad + then: [t: "is"] + - t: "an element of" + # Several options for speaking elements in ClearSpeak -- they split between being inside a set or not and then the option + else_test: + if: "../../self::m:set or ../../../self::m:set" # inside a set + then_test: + - if: $ClearSpeak_SetMemberSymbol = 'Auto' or $ClearSpeak_SetMemberSymbol = 'In' + then: [t: "in"] + - else_if: $ClearSpeak_SetMemberSymbol = 'Member' + then: [t: "member of"] + - else_if: $ClearSpeak_SetMemberSymbol = 'Element' + then: [t: "element of"] + - else: [t: "belonging to"] # $ClearSpeak_SetMemberSymbol = 'Belongs' + else_test: + - if: $ClearSpeak_SetMemberSymbol = 'Auto' or $ClearSpeak_SetMemberSymbol = 'Member' + then: [t: "is a member of"] + - else_if: $ClearSpeak_SetMemberSymbol = 'Element' + then: [t: "is an element of"] + - else_if: $ClearSpeak_SetMemberSymbol = 'In' + then: [t: "is in"] + - else: [t: "belongs to"] # $ClearSpeak_SetMemberSymbol = 'Belongs' - "∋": [t: "contains the member"] # 0x220b - "∌": [t: "does not contain the member"] # 0x220c - "∍": [t: "contains the member"] # 0x220d - "∎": [t: "end of proof"] # 0x220e - "∏": [t: "product"] # 0x220f + - "∐": [t: "coproduct"] # 0x2210 - "∑": [t: "sum"] # 0x2211 - "−": [t: "minus"] # 0x2212 - "∓": [t: "minus or plus"] # 0x2213 @@ -658,6 +767,11 @@ then: [t: "dot"] else: [t: "times"] + - "√": # 0x221a + - test: + if: "$Verbosity!='Terse'" + then: [ot: "the"] + - t: "square root of" - "∛": # 0x221b - test: if: "$Verbosity!='Terse'" @@ -668,6 +782,11 @@ if: "$Verbosity!='Terse'" then: [ot: "the"] - t: "fourth root of" + - "∝": # 0x221d + - test: + if: "$Verbosity!='Terse'" + then: [t: "is"] + - t: "proportional to" - "∞": [t: "infinity"] # 0x221e - "∟": [t: "right angle"] # 0x221f - "∠": [t: "angle"] # 0x2220 @@ -690,6 +809,11 @@ - "∳": [t: "anticlockwise contour integral"] # 0x2233 - "∴": [t: "therefore"] # 0x2234 - "∵": [t: "because"] # 0x2235 + - "∶": # 0x2236 + - test: + if: "$Verbosity!='Terse'" + then: [t: "is"] + - t: "to" - "∷": [t: "as"] # 0x2237 - "∸": [t: "dot minus"] # 0x2238 - "∹": [t: "has excess compared to"] # 0x2239 @@ -705,6 +829,11 @@ - t: "homothetic to" - "∼": [t: "varies with"] # 0x223c - "∽": [t: "reversed tilde"] # 0x223d + - "∾": # 0x223e + - test: + if: "$Verbosity!='Terse'" + then: [t: "is"] + - t: "most positive" - "∿": [t: "sine wave"] # 0x223f - "≀": [t: "wreath product"] # 0x2240 - "≁": [t: "not tilde"] # 0x2241 @@ -806,6 +935,16 @@ then: [t: "is"] - t: "measured by" - "≟": [t: "has an unknown relationship with"] # 0x225f + - "≠": # 0x2260 + - test: + if: "$Verbosity!='Terse'" + then: [t: "is"] + - t: "not equal to" + - "≡": # 0x2261 + - test: + if: "$Verbosity!='Terse'" + then: [t: "is"] + - t: "identical to" - "≢": # 0x2262 - test: if: "$Verbosity!='Terse'" @@ -916,6 +1055,36 @@ - "≿": [t: "succeeds or is equivalent to"] # 0x227f - "⊀": [t: "does not precede"] # 0x2280 - "⊁": [t: "does not succeed"] # 0x2281 + - "⊂": # 0x2282 + - test: + if: "$Verbosity!='Terse'" + then: [t: "is a"] + - t: "subset of" + - "⊃": # 0x2283 + - test: + if: "$Verbosity!='Terse'" + then: [t: "is a"] + - t: "superset of" + - "⊄": # 0x2284 + - test: + if: "$Verbosity!='Terse'" + then: [t: "is"] + - t: "not a subset of" + - "⊅": # 0x2285 + - test: + if: "$Verbosity!='Terse'" + then: [t: "is"] + - t: "not a superset of" + - "⊆": # 0x2286 + - test: + if: "$Verbosity!='Terse'" + then: [t: "is a"] + - t: "subset of or equal to" + - "⊇": # 0x2287 + - test: + if: "$Verbosity!='Terse'" + then: [t: "is a"] + - t: "superset of or equal to" - "⊈": # 0x2288 - test: if: "$Verbosity!='Terse'" @@ -1234,7 +1403,7 @@ - "⏠": [t: "top tortoise shell bracket"] # 0x23e0 - "⏡": [t: "bottom tortoise shell bracket"] # 0x23e1 - "⏢": [t: "white trapezium"] # 0x23e2 - - "⏣": [t: "benzene ring with circle"] # 0x23e3 + - "⏣": [t: "benzene right with circle"] # 0x23e3 - "⏤": [t: "straightness"] # 0x23e4 - "⏥": [t: "flatness"] # 0x23e5 - "⏦": # 0x23e6 @@ -1286,7 +1455,7 @@ - t: "parenthesized" - spell: "translate('.', '⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵', 'abcdefghijklmnopqrstuvwxyz')" - - "Ⓐ-Ⓩ": # 0x24b6 - 0x24cf + - "Ⓐ-ⓩ": # 0x24b6 - 0x24cf - t: "circled" - spell: "translate('.', 'ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏ', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" @@ -1297,7 +1466,6 @@ - "ⓐ-ⓩ": # 0x24d0 - 0x24e9 - t: "circled" - spell: "translate('.', 'ⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ', 'abcdefghijklmnopqrstuvwxyz')" - - "⓪": [t: "circled zero"] # 0x24ea - "⓫": [t: "black circled eleven"] # 0x24eb - "⓬": [t: "black circled twelve"] # 0x24ec @@ -1449,7 +1617,7 @@ - "⚆": [t: "white circle with dot right"] # 0x2686 - "⚇": [t: "white circle wiht two dots"] # 0x2687 - "⚈": [t: "black circle with dot right"] # 0x2688 - - "⚉": [t: "black circle with two dots"] # 0x2689 + - "⚉": [t: "black circle wiht two dots"] # 0x2689 - "⚪": [t: "medium white circle"] # 0x26aa - "⚫": [t: "medium black circle"] # 0x26ab - "⚬": [t: "medium small white circle"] # 0x26ac @@ -2879,8 +3047,11 @@ - "": [t: "digamma"] # 0xed03 - "ϝ": [t: "digamma"] # 0x3dd - "": [t: "d"] # 0xed10 + - "ⅆ": [t: "d"] # 0x2146 - "": [t: "e"] # 0xed11 + - "ⅇ": [t: "e"] # 0x2147 - "": [t: "i"] # 0xed12 + - "ⅈ": [t: "i"] # 0x2148 - "": [t: "j"] # 0xed13 - "ⅅ": - spell: "translate('.', 'ⅅ', 'DD')" # 0xed16, 0x2145 @@ -3495,6 +3666,12 @@ then: [t: "bold"] - spell: "translate('.', '𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗', '0123456789')" + - "-": # 0xf52e - 0xf537 (old MathType) + - test: + if: "not($IgnoreBold)" + then: [t: "bold"] + - spell: "translate('.', '', '0123456789')" + - "𝟬-𝟵": # 0x1D7EC - 0x1D7F5 - test: if: "not($IgnoreBold)" diff --git a/Rules/Languages/zh/tw/unicode-full.yaml b/Rules/Languages/zh/tw/unicode-full.yaml index 49f872354..5f88573ac 100644 --- a/Rules/Languages/zh/tw/unicode-full.yaml +++ b/Rules/Languages/zh/tw/unicode-full.yaml @@ -10,6 +10,7 @@ - "¨": [T: "double dot"] # 0xa8 (en: 'double dot', google translation) - "©": [T: "版權"] # 0xa9 (en: 'copyright', google translation) - "ª": [t: "feminine ordinal indicator"] # 0xaa (en: 'feminine ordinal indicator', google translation) + - "¬": [T: "非"] # 0xac (en: 'not', google translation) - "«": [t: "left-pointing double angle quote mark"] # 0xab (en: 'left-pointing double angle quote mark', google translation) - "¯": # 0xaf - test: @@ -18,9 +19,24 @@ else: [T: "橫線"] # (en: 'line') - "²": [T: "二"] # 0xb2 (en: 'two', google translation) - "³": [T: "三"] # 0xb3 (en: 'three', google translation) + - "´": [t: "acute"] # 0xb4 (en: 'acute', google translation) - "µ": [T: "微"] # 0xb5 (en: 'micro', google translation) - "¹": [T: "一"] # 0xb9 (en: 'one', google translation) - "º": [t: "masculine ordinal indicator"] # 0xb9 (en: 'masculine ordinal indicator', google translation) + - "·": + - test: + if: "$SpeechStyle != 'ClearSpeak' or $ClearSpeak_MultSymbolDot = 'Auto'" + then: [T: "乘"] # (en: 'times', google translation) + else: [T: "內積"] # (en: 'dot') + - "×": # 0xd7 + - test: + if: "$SpeechStyle != 'ClearSpeak' or $ClearSpeak_MultSymbolX = 'Auto'" + then: [T: "乘"] # (en: 'times', google translation) + else_test: + if: $ClearSpeak_MultSymbolX = 'By' + then: [T: "乘"] # (en: 'by', google translation) + else: [T: "乘"] # (en: 'cross') + - "÷": [T: "除以"] # 0xf7 (en: 'divided by') - "ʰ": [t: "modifier small h"] # 0x2b0 (en: 'modifier small h', google translation) - "ʱ": [t: "modifier small h with hook"] # 0x2b1 (en: 'modifier small h with hook', google translation) - "ʲ": [t: "modifier small j"] # 0x2b2 (en: 'modifier small j', google translation) @@ -104,6 +120,14 @@ - "˽": [t: "modifier shelf"] # 0x2fd (en: 'modifier shelf', google translation) - "˾": [t: "modifier open shelf"] # 0x2fe (en: 'modifier open shelf', google translation) - "˿": [t: "modifier low left arrow"] # 0x2ff (en: 'modifier low left arrow', google translation) + - "̀": [t: "grave accent embellishment"] # 0x300 (en: 'grave accent embellishment', google translation) + - "́": [t: "acute accent embellishment"] # 0x301 (en: 'acute accent embellishment', google translation) + - "̂": [t: "circumflex accent embellishment"] # 0x302 (en: 'circumflex accent embellishment', google translation) + - "̃": [t: "tilde embellishment"] # 0x303 (en: 'tilde embellishment', google translation) + - "̄": [t: "macron embellishment"] # 0x304 (en: 'macron embellishment', google translation) + - "̅": [t: "overbar embellishment"] # 0x305 (en: 'overbar embellishment', google translation) + - "̆": [t: "breve embellishment"] # 0x306 (en: 'breve embellishment', google translation) + - "̇": [t: "dot above embellishment"] # 0x307 (en: 'dot above embellishment', google translation) - "̈": [t: "diaeresis embellishment"] # 0x308 (en: 'diaeresis embellishment', google translation) - "̉": [t: "hook above embellishment"] # 0x309 (en: 'hook above embellishment', google translation) - "̊": [t: "ring above embellishment"] # 0x30a (en: 'ring above embellishment', google translation) @@ -192,7 +216,11 @@ - "ϒ": [t: "upsilon with hook"] # 0x3d2 (en: 'upsilon with hook') - "ϓ": [t: "upsilon with acute and hook"] # 0x3d3 (en: 'upsilon with acute and hook') - "ϔ": [t: "upsilon with diaeresis and hook"] # 0x3d4 (en: 'upsilon with diaeresis and hook') + - "ϕ": [T: "phi"] # 0x3d5 (en: 'phi') + - "ϖ": [t: "pi"] # 0x3d6 (en: 'pi') - "ϗ": [t: "kai"] # 0x3d7 (en: 'kai') + - "ϵ": [T: "epsilon"] # 0x3f5 (en: 'epsilon') + - "϶": [t: "reversed epsilon"] # 0x3f6 (en: 'reversed epsilon') - "А-Я": # 0x410 - 0x42f - test: if: "$CapitalLetters_Beep" @@ -246,6 +274,9 @@ - "‐": [T: "連字符"] # 0x2010 (en: 'hyphen', google translation) - "‑": [T: "連字符"] # 0x2011 (en: 'hyphen', google translation) - "‒": [t: "圖破折號"] # 0x2012 (en: 'figure dash', google translation) + - "–": [t: "en dash"] # 0x2013 (google translation) + - "—": [t: "em dash"] # 0x2014 (google translation) + - "―": [T: "橫線"] # 0x2015 (en: 'horizontal bar', google translation) - "‖": [T: "雙垂直線"] # 0x2016 (en: 'double vertical line', google translation) - "†": [t: "dagger"] # 0x2020 (en: 'dagger', google translation) - "‡": [t: "double dagger"] # 0x2021 (en: 'double dagger', google translation) @@ -256,6 +287,19 @@ then: [T: "dot"] # (en: 'dot', google translation) else: [T: "bullet"] # (en: 'bullet', google translation) + - "…": # 0x2026 + test: + if: + - "$SpeechStyle != 'ClearSpeak' or $ClearSpeak_Ellipses = 'Auto' or" + # must be ClearSpeak and $ClearSpeak_Ellipses = 'AndSoOn' + # speak '…' as 'and so on...' unless expr starts with '…' + - "../*[1][.='…']" + then: [T: "點點點"] # (en: 'dot dot dot', google translation) + else_test: # must have $ClearSpeak_Ellipses = 'AndSoOn' + if: "count(following-sibling::*) = 0" + then: [T: "等等"] # (en: 'and so on', google translation) + else: [T: "等等"] # (en: 'and so on up to', google translation) + - "‰": [T: "千分"] # 0x2030 (en: 'per mille', google translation) - "‱": [T: "萬分"] # 0x2031 (en: 'per ten thousand', google translation) - "′": [T: "prime"] # 0x2032 @@ -402,6 +446,7 @@ - T: "草體" # (en: 'script', google translation) - SPELL: "translate('.', 'ℐℒ℘ℬℰℱℳ', 'ILPBEFM')" + - "ℓ": [T: "草體l"] # 0x2113 (en: 'script l', google translation) - "℔": [T: "磅"] # 0x2114 (en: 'pounds', google translation) - "№": [T: "數字"] # 0x2116 (en: 'number', google translation) - "℥": [T: "盎司"] # 0x2125 (en: 'ounces', google translation) @@ -486,6 +531,7 @@ - "↉": [T: "三分之0"] # 0x2189 (en: 'zero thirds', google translation) - "←": [T: "左箭頭"] # 0x2190 (en: 'leftwards arrow', google translation) - "↑": [T: "向上箭頭"] # 0x2191 (en: 'upwards arrow', google translation) + - "→": [T: "右箭頭"] # 0x2192 (en: 'rightwards arrow') - "↓": [T: "向下箭頭"] # 0x2193 (en: 'downwards arrow', google translation) - "↔": [T: "左右雙箭頭"] # 0x2194 (en: 'left right arrow') - "↕": [T: "上下雙箭頭"] # 0x2195 (en: 'up down arrow', google translation) @@ -559,6 +605,7 @@ - "⇏": [t: "帶撇向右粗箭頭"] # 0x21cf (en: 'rightwards double arrow with stroke', google translation) - "⇐": [t: "向左粗箭頭"] # 0x21d0 (en: 'leftwards double arrow', google translation) - "⇑": [t: "向上粗箭頭"] # 0x21d1 (en: 'upwards double arrow', google translation) + - "⇒": [t: "向右粗箭頭"] # 0x21d2 (en: 'rightwards double arrow', google translation) - "⇓": [t: "向下粗箭頭"] # 0x21d3 (en: 'downwards double arrow', google translation) - "⇔": [t: "左右粗箭頭"] # 0x21d4 (en: 'left right double arrow', google translation) - "⇕": [t: "上下粗箭頭"] # 0x21d5 (en: 'up down double arrow', google translation) @@ -604,19 +651,100 @@ - "⇽": [t: "向左開頭箭頭"] # 0x21fd (en: 'leftwards open headed arrow', google translation) - "⇾": [t: "向右開頭箭頭"] # 0x21fe (en: 'rightwards open headed arrow', google translation) - "⇿": [t: "左右開頭箭頭"] # 0x21ff (en: 'left right open headed arrow', google translation) + - "∀": [T: "對所有的"] # 0x2200 (en: 'for all') - "∁": # 0x2201 - test: if: "$Verbosity!='Terse'" then: [T: ""] # (en: 'the', google translation) - T: "補集" # (en: 'complement of') + - "∂": # 0x2202 + - test: + if: "$Verbosity='Terse'" + then: [T: "偏微分"] # (en: 'partial', google translation) + else: [T: "偏微分"] # (en: 'partial derivative') - "∃": [T: "存在"] # 0x2203 (en: 'there exists') - "∄": [T: "不存在"] # 0x2204 (en: 'there does not exist') - "∅": [T: "空集合"] # 0x2205 (en: 'empty set') + - "∆": # 0x2206 + - test: + if: "$Verbosity!='Terse'" + then: [T: ""] # (en: 'the', google translation) + - T: "變化量" # (en: 'laplacian of') - "∇": # 0x2207 - test: if: "$Verbosity!='Terse'" then: [T: ""] # (en: 'the', google translation) - T: "梯度" # (en: 'gradient of') + - "∈": # 0x2208 + - test: + if: "$SpeechStyle != 'ClearSpeak'" + then: [T: "屬於"] # (en: 'an element of', google translation) + # Several options for speaking elements in ClearSpeak -- they split between being inside a set or not and then the option + else_test: + if: "../../self::m:set or ../../../self::m:set" # inside a set + then_test: + - if: $ClearSpeak_SetMemberSymbol = 'Auto' or $ClearSpeak_SetMemberSymbol = 'In' + then: [T: "在"] # (en: 'in', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'Member' + then: [T: "成員"] # (en: 'member of', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'Element' + then: [T: "元素"] # (en: 'element of', google translation) + - else: [T: "屬於"] # $ClearSpeak_SetMemberSymbol = 'Belongs' (en: 'belonging to') + else_test: + - if: $ClearSpeak_SetMemberSymbol = 'Auto' or $ClearSpeak_SetMemberSymbol = 'Member' + then: [T: "是"] # (en: 'is a member of', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'Element' + then: [T: "是一個元素"] # (en: 'is an element of', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'In' + then: [T: "在"] # (en: 'is in', google translation) + - else: [T: "屬於"] # $ClearSpeak_SetMemberSymbol = 'Belongs' (en: 'belongs to') + - "∉": # 0x2209 + # rule is identical to 0x2208 + - test: + if: "$SpeechStyle != 'ClearSpeak'" + then: [T: "不屬於"] # (en: 'is not an element of', google translation) + # Several options for speaking elements in ClearSpeak -- they split between being inside a set or not and then the option + else_test: + if: "../../self::m:set or ../../../self::m:set" # inside a set + then_test: + - if: $ClearSpeak_SetMemberSymbol = 'Auto' or $ClearSpeak_SetMemberSymbol = 'In' + then: [T: "不在"] # (en: 'not in', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'Member' + then: [T: "不是成員"] # (en: 'not member of', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'Element' + then: [T: "不是"] # (en: 'not element of', google translation) + - else: [T: "不屬於"] # $ClearSpeak_SetMemberSymbol = 'Belongs' (en: 'not belonging to') + else_test: + - if: $ClearSpeak_SetMemberSymbol = 'Auto' or $ClearSpeak_SetMemberSymbol = 'Member' + then: [T: "不是成員"] # (en: 'is not a member of', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'Element' + then: [T: "不是一個元素"] # (en: 'is not an element of', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'In' + then: [T: "不在"] # (en: 'is not in', google translation) + - else: [T: "不屬於"] # $ClearSpeak_SetMemberSymbol = 'Belongs' (en: 'does not belong to') + - "∊": # 0x220a + - test: + if: "$SpeechStyle != 'ClearSpeak'" + then: [T: "屬於"] # (en: 'is an element of', google translation) + # Several options for speaking elements in ClearSpeak -- they split between being inside a set or not and then the option + else_test: + if: "../../self::m:set or ../../../self::m:set" # inside a set + then_test: + - if: $ClearSpeak_SetMemberSymbol = 'Auto' or $ClearSpeak_SetMemberSymbol = 'In' + then: [T: "在"] # (en: 'in', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'Member' + then: [T: "成員"] # (en: 'member of', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'Element' + then: [T: "元素"] # (en: 'element of', google translation) + - else: [T: "屬於"] # $ClearSpeak_SetMemberSymbol = 'Belongs' (en: 'belonging to') + else_test: + - if: $ClearSpeak_SetMemberSymbol = 'Auto' or $ClearSpeak_SetMemberSymbol = 'Member' + then: [T: "是"] # (en: 'is a member of', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'Element' + then: [T: "是一個元素"] # (en: 'is an element of', google translation) + - else_if: $ClearSpeak_SetMemberSymbol = 'In' + then: [T: "在"] # (en: 'is in', google translation) + - else: [T: "屬於"] # $ClearSpeak_SetMemberSymbol = 'Belongs' (en: 'belongs to') - "∋": [T: "包含"] # 0x220b (en: 'contains the member') - "∌": [T: "不包含"] # 0x220c (en: 'does not contain the member') - "∍": [T: "包含"] # 0x220d (en: 'contains the member') @@ -637,6 +765,11 @@ then: [T: "dot"] # (en: 'dot', google translation) else: [T: "乘"] # (en: 'times') + - "√": # 0x221a + - test: + if: "$Verbosity!='Terse'" + then: [T: ""] # (en: 'the', google translation) + - T: "開平方根" # (en: 'square root of') - "∛": # 0x221b - test: if: "$Verbosity!='Terse'" @@ -647,6 +780,11 @@ if: "$Verbosity!='Terse'" then: [T: ""] # (en: 'the', google translation) - T: "開四次方根" # (en: 'fourth root of') + - "∝": # 0x221d + - test: + if: "$Verbosity!='Terse'" + then: [T: ""] # (en: 'is', google translation) + - T: "正比於" # (en: 'proportional to') - "∞": [T: "無限大"] # 0x221e (en: 'infinity') - "∟": [T: "直角"] # 0x221f (en: 'right angle') - "∠": [T: "角"] # 0x2220 (en: 'angle') @@ -654,6 +792,8 @@ - "∢": [T: "球面角"] # 0x2222 (en: 'spherical angle') - "∣": [T: "整除"] # 0x2223 (en: 'divides') - "∤": [T: "不整除"] # 0x2224 (en: 'does not divide') + - "∧": [T: "邏輯與"] # 0x2227 (en: 'and') + - "∨": [T: "邏輯或"] # 0x2228 (en: 'or') - "∩": [T: "交集"] # 0x2229 (en: 'intersection') - "∪": [T: "聯集"] # 0x222a (en: 'union') - "∫": [T: "積分"] # 0x222b (en: 'integral') @@ -667,6 +807,11 @@ - "∳": [T: "逆時針輪廓積分"] # 0x2233 (en: 'anticlockwise contour integral') - "∴": [T: "所以"] # 0x2234 (en: 'therefore') - "∵": [T: "因為"] # 0x2235 (en: 'because') + - "∶": # 0x2236 + - test: + if: "$Verbosity!='Terse'" + then: [T: ""] # (en: 'is', google translation) + - T: "比" # (en: 'to') - "∷": [T: "當成"] # 0x2237 (en: 'as') - "∸": [T: "點負號"] # 0x2238 (en: 'dot minus') - "∹": [T: "超出"] # 0x2239 (en: 'has excess compared to') @@ -682,6 +827,11 @@ - T: "同位" # (en: 'homothetic to') - "∼": [T: "波浪符"] # 0x223c (en: 'varies with') - "∽": [T: "反波浪符"] # 0x223d (en: 'reversed tilde') + - "∾": # 0x223e + - test: + if: "$Verbosity!='Terse'" + then: [T: ""] # (en: 'is', google translation) + - T: "正無限大" # (en: 'most positive') - "∿": [T: "正弦波型"] # 0x223f (en: 'sine wave') - "≀": [t: "wreath product"] # 0x2240 (en: 'wreath product') - "≁": [t: "not tilde"] # 0x2241 (en: 'not tilde') @@ -783,6 +933,16 @@ then: [T: ""] # (en: 'is', google translation) - T: "測量" # (en: 'measured by') - "≟": [T: "問號等於"] # 0x225f (en: 'has an unknown relationship with') + - "≠": # 0x2260 + - test: + if: "$Verbosity!='Terse'" + then: [T: ""] # (en: 'is', google translation) + - T: "不等於" # (en: 'not equal to') + - "≡": # 0x2261 + - test: + if: "$Verbosity!='Terse'" + then: [T: ""] # (en: 'is', google translation) + - T: "恆等於" # (en: 'identical to') - "≢": # 0x2262 - test: if: "$Verbosity!='Terse'" @@ -793,6 +953,8 @@ if: "$Verbosity!='Terse'" then: [T: ""] # (en: 'is', google translation) - T: "完全等於" # (en: 'strictly equivalent to') + - "≦": [T: "小於等於"] # 0x2266 (en: 'less than over equal to') + - "≧": [T: "大於等於"] # 0x2267 (en: 'greater than over equal to') - "≨": # 0x2268 - test: if: "$Verbosity!='Terse'" @@ -891,6 +1053,36 @@ - "≿": [T: "後於或等價於"] # 0x227f (en: 'succeeds or is equivalent to') - "⊀": [T: "不先於"] # 0x2280 (en: 'does not precede') - "⊁": [T: "不後於"] # 0x2281 (en: 'does not succeed') + - "⊂": # 0x2282 + - test: + if: "$Verbosity!='Terse'" + then: [T: ""] # (en: 'is a', google translation) + - T: "包含於" # (en: 'subset of') + - "⊃": # 0x2283 + - test: + if: "$Verbosity!='Terse'" + then: [T: ""] # (en: 'is a', google translation) + - T: "包含" # (en: 'superset of') + - "⊄": # 0x2284 + - test: + if: "$Verbosity!='Terse'" + then: [T: ""] # (en: 'is', google translation) + - T: "不包含於" # (en: 'not a subset of') + - "⊅": # 0x2285 + - test: + if: "$Verbosity!='Terse'" + then: [T: ""] # (en: 'is', google translation) + - T: "不包含" # (en: 'not a superset of') + - "⊆": # 0x2286 + - test: + if: "$Verbosity!='Terse'" + then: [T: ""] # (en: 'is a', google translation) + - T: "包含於或等於" # (en: 'subset of or equal to') + - "⊇": # 0x2287 + - test: + if: "$Verbosity!='Terse'" + then: [T: ""] # (en: 'is a', google translation) + - T: "包含或等於" # (en: 'superset of or equal to') - "⊈": # 0x2288 - test: if: "$Verbosity!='Terse'" @@ -2791,8 +2983,11 @@ - "": [t: "digamma"] # 0xed03 (google translation) - "ϝ": [t: "Diggmma"] # 0x3dd (en: 'digamma') - "": [T: "d"] # 0xed10 (en: 'd', google translation) + - "ⅆ": [T: "d"] # 0x2146 (en: 'd', google translation) - "": [T: "e"] # 0xed11 (en: 'e', google translation) + - "ⅇ": [T: "e"] # 0x2147 (en: 'e', google translation) - "": [T: "i"] # 0xed12 (en: 'i', google translation) + - "ⅈ": [T: "i"] # 0x2148 (en: 'i', google translation) - "": [T: "j"] # 0xed13 (en: 'j', google translation) - "ⅅ": - SPELL: "translate('.', 'ⅅ', 'DD')" # 0xed16, 0x2145 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/src/infer_intent.rs b/src/infer_intent.rs index 3c15a7874..1112d859e 100644 --- a/src/infer_intent.rs +++ b/src/infer_intent.rs @@ -5,8 +5,7 @@ //! The implementation of the module is on hold until the MathML committee figures out how it wants to do this. #![allow(clippy::needless_return)] -use sxd_document_no_unsafe::dom::{Element, Document, ChildOfElement}; -use sxd_document_no_unsafe::as_str; +use sxd_document::dom::{Element, Document, ChildOfElement}; use crate::prefs::PreferenceManager; use crate::speech::SpeechRulesWithContext; use crate::canonicalize::{as_element, as_text, name, create_mathml_element, set_mathml_name, INTENT_ATTR, MATHML_FROM_NAME_ATTR}; @@ -39,11 +38,11 @@ pub fn infer_intent<'r, 'c, 's:'c, 'm:'c>(rules_with_context: &'r mut SpeechRule .context("Pattern match/replacement failure!") { Err(e) => Err(e), Ok(intent) => { - intent.set_attribute_value(INTENT_ATTR, as_str!(saved_intent_attr)); // so attr can be potentially be viewed later + intent.set_attribute_value(INTENT_ATTR, saved_intent_attr); // so attr can be potentially be viewed later Ok(intent) }, }; - mathml.set_attribute_value(INTENT_ATTR, as_str!(saved_intent_attr)); + mathml.set_attribute_value(INTENT_ATTR, saved_intent_attr); return intent_tree; } } @@ -95,14 +94,13 @@ pub fn simplify_fixity_properties(properties: &str) -> String { /// Given the intent add the fixity property for the intent if it isn't given (and one exists) fn add_fixity(intent: Element) { - let raw_properties = intent.attribute_value(INTENT_PROPERTY); - let properties = raw_properties.as_deref().unwrap_or_default(); + let properties = intent.attribute_value(INTENT_PROPERTY).unwrap_or_default(); if properties.split(":").all(|property| !FIXITIES.contains(property)) { let intent_name = name(intent); crate::definitions::SPEECH_DEFINITIONS.with(|definitions| { let definitions = definitions.borrow(); // debug!(" add_fixity: intent_name: {}, ", intent_name); - if let Some(definition) = definitions.get_hashmap("IntentMappings").unwrap().get(as_str!(intent_name)) && + if let Some(definition) = definitions.get_hashmap("IntentMappings").unwrap().get(intent_name) && let Some((fixity, _)) = definition.split_once("=") && fixity != "nofix" { let new_properties = (if properties.is_empty() {":"} else {properties}).to_string() + fixity + ":"; @@ -136,18 +134,16 @@ pub fn add_fixity_children(intent: Element) -> Element { return mathml; } // we also exclude fixity on mtable because they mess up the counts (see 'en::mtable::unknown_mtable_property') - if mathml.attribute_value(MATHML_FROM_NAME_ATTR).as_deref().unwrap_or_default() == "mtable" { + if mathml.attribute_value(MATHML_FROM_NAME_ATTR).unwrap_or_default() == "mtable" { return mathml; } let doc = mathml.document(); - let raw_properties = mathml.attribute_value(INTENT_PROPERTY); - let properties = raw_properties.as_deref().unwrap_or_default(); + 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 = as_str!(name(mathml)); + let intent_name = name(mathml); // debug!("add_fixity_child: fixity '{}', intent_name '{}'", fixity, intent_name); - let raw_op_id = mathml.attribute_value("id"); - let op_name_id = raw_op_id.as_deref().unwrap_or("new-id"); + let op_name_id = mathml.attribute_value("id").unwrap_or("new-id"); match fixity { "infix" => { let mut new_children = Vec::with_capacity(2*children.len()-1); @@ -280,7 +276,7 @@ static ARG_REF: LazyLock = LazyLock::new(|| { }); static NUMBER: LazyLock = LazyLock::new(|| Regex::new(r#"^-?[0-9]+(\.[0-9]+)?"#).unwrap()); -static TERMINALS_AS_U8: [u8; 3] = *b"(,)"; +static TERMINALS_AS_U8: [u8; 3] = [b'(', b',', b')']; // static TERMINALS: [char; 3] = ['(', ',',')']; // 'i -- "i" for the lifetime of the INTENT_ATTR string @@ -416,18 +412,18 @@ fn build_intent<'b, 'r, 'c, 's:'c, 'm:'c>(rules_with_context: &'r mut SpeechRule // Note: to avoid infinite loop, we need to remove the 'intent' so we don't end up back here; we put it back later let properties = get_properties(lex_state)?; // advance state to see if funcall if lex_state.is_terminal("(") { - intent = create_mathml_element(&doc, as_str!(name(mathml))); + intent = create_mathml_element(&doc, name(mathml)); intent.set_attribute_value(INTENT_PROPERTY, &properties); - intent.set_attribute_value(MATHML_FROM_NAME_ATTR, as_str!(name(mathml))); - intent.set_attribute_value("id", as_str!(mathml.attribute_value("id") - .ok_or_else(|| anyhow!("no id on intent function name"))?)); + intent.set_attribute_value(MATHML_FROM_NAME_ATTR, name(mathml)); + intent.set_attribute_value("id", mathml.attribute_value("id") + .ok_or_else(|| anyhow!("no id on intent function name"))?); } else { let saved_intent = mathml.attribute_value(INTENT_ATTR).unwrap(); mathml.remove_attribute(INTENT_ATTR); mathml.set_attribute_value(INTENT_PROPERTY, &properties); // needs to be set before the pattern match intent = rules_with_context.match_pattern::>(mathml)?; // debug!("Intent after pattern match:\n{}", mml_to_string(intent)); - mathml.set_attribute_value(INTENT_ATTR, as_str!(saved_intent)); + mathml.set_attribute_value(INTENT_ATTR, saved_intent); } add_fixity(intent); return Ok(intent); // if we start with properties, then there can only be properties @@ -436,7 +432,7 @@ fn build_intent<'b, 'r, 'c, 's:'c, 'm:'c>(rules_with_context: &'r mut SpeechRule let (leaf_name, leaf_text) = if let Token::Number(_) = lex_state.token { ("mn", word) } else if let Token::ConceptOrLiteral(word) = lex_state.token && is_concept_name(lex_state.remaining_str) { - (word, if is_leaf(mathml) { as_str!(as_text(mathml)) } else { "" }) + (word, if is_leaf(mathml) {as_text(mathml)} else {""}) } else { ("mi", word) @@ -444,10 +440,9 @@ fn build_intent<'b, 'r, 'c, 's:'c, 'm:'c>(rules_with_context: &'r mut SpeechRule intent = create_mathml_element(&doc, leaf_name); // if the str is part of a larger intent and not the head (e.g., "a" in "f($x, a)", but not the "f" in it), then it is "made up" // debug!(" Token::ConceptOrLiteral, word={}, leaf_name={}", word, leaf_name); - let mathml_name = name(mathml); - let raw_intent_attr = mathml.attribute_value(INTENT_ATTR); - intent.set_attribute_value(MATHML_FROM_NAME_ATTR, - if word == raw_intent_attr.as_deref().unwrap_or_default() {as_str!(mathml_name)} else {leaf_name}); + // debug!(" token={}", lex_state); + intent.set_attribute_value(MATHML_FROM_NAME_ATTR, + if word == mathml.attribute_value(INTENT_ATTR).unwrap_or_default() {name(mathml)} else {leaf_name}); intent.set_text(leaf_text); // '-' and '_' get removed by the rules. if let Some(id) = mathml.attribute_value("id") { intent.set_attribute_value("id", &format!("{}-literal-{}", id, intent_offset)); @@ -542,7 +537,7 @@ fn build_function<'b, 'r, 'c, 's:'c, 'm:'c>( // application := intent '(' arguments? S ')' where 'function_name' is 'intent' assert!(lex_state.is_terminal("(")); let mut function = function_name; - function.set_attribute_value(MATHML_FROM_NAME_ATTR, as_str!(name(mathml))); + function.set_attribute_value(MATHML_FROM_NAME_ATTR, name(mathml)); while lex_state.is_terminal("(") { lex_state.get_next()?; if lex_state.is_terminal(")") { @@ -596,11 +591,11 @@ fn lift_function_name<'m>(doc: Document<'m>, function_name: Element<'m>, childre if name(function_name) == "mi" || name(function_name) == "mn" { // FIX -- really want to test for all leaves, but not "data-from-mathml" // simple/normal case of f(x,y) // don't want to say that this is a leaf -- doing so messes up because it potentially has children - set_mathml_name(function_name, as_str!(as_text(function_name))); + set_mathml_name(function_name, as_text(function_name)); function_name.set_text(""); function_name.replace_children(children); - if name(function_name).find(|ch: char| ch!='_' && ch!='-').is_none() { - let properties = function_name.attribute_value(INTENT_PROPERTY).as_deref().unwrap_or(":").to_owned(); + if name(function_name).find(|ch| ch!='_' && ch!='-').is_none() { + let properties = function_name.attribute_value(INTENT_PROPERTY).unwrap_or(":").to_owned(); function_name.set_attribute_value(INTENT_PROPERTY, &(properties + "silent:")); } return function_name; @@ -672,7 +667,7 @@ mod tests { use crate::errors::Result; #[allow(unused_imports)] use log::debug; - use sxd_document_no_unsafe::parser; + use sxd_document::parser; use std::panic::{catch_unwind, AssertUnwindSafe}; diff --git a/src/interface.rs b/src/interface.rs index 93a84c389..7744a96db 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -1489,4 +1489,113 @@ mod tests { return Ok(()); }); } + + + + fn setup_speech_ssml() { + set_rules_dir(super::super::abs_rules_dir_path()).unwrap(); + set_preference("Language", "en").unwrap(); + set_preference("TTS", "SSML").unwrap(); + set_preference("MathRate", "80").unwrap(); + set_preference("SpeechStyle", "SimpleSpeak").unwrap(); + set_preference("Verbosity", "Medium").unwrap(); + } + + #[test] + fn test_no_escaping() -> Result<()> { + setup_speech_ssml(); + let expr = " + + x+y + x-y + + "; + set_mathml(&expr)?; + let speech = get_spoken_text()?; + // Rule-generated SSML must pass through verbatim (not XML-entity-encoded). + assert!(!speech.contains("<")); + assert!(!speech.contains(">")); + assert!(!speech.contains("&lt;")); + return Ok(()); + } + + /// The attack payload must not pass through verbatim (rule-generated SSML may contain `"#; + /// Same bytes as `PAYLOAD`, entity-encoded so attribute values are well-formed XML. + const PAYLOAD_ATTR_XML: &str = "<break time="50000ms"/>"; + /// Entity-encoded payload plus trailing literal text (well-formed in leaf element text). + const PAYLOAD_LEAF_XML: &str = "<break time="50000ms"/>note"; + + #[test] + /// User-supplied leaf text must not inject SSML when TTS is SSML. + fn leaf_text_ssml_attack_neutralized_in_speech() -> Result<()> { + setup_speech_ssml(); + // Entity-encoded payload: valid XML through set_mathml (no CDATA), decodes to PAYLOAD + "note". + let mathml = format!( + r#"{PAYLOAD_LEAF_XML}+ + {PAYLOAD_LEAF_XML}+ + {PAYLOAD_LEAF_XML}+ + {PAYLOAD_LEAF_XML}"# + ); + set_mathml(&mathml)?; + let speech = get_spoken_text()?; + assert_ssml_attack_neutralized(&speech, PAYLOAD); + assert!(speech.contains("note") || speech.contains("<")); + let mathml = format!( + "{PAYLOAD_LEAF_XML}+1" + ); + set_mathml(&mathml)?; + let speech = get_spoken_text()?; + assert_ssml_attack_neutralized(&speech, PAYLOAD); + assert!(speech.contains("note") || speech.contains("<")); + return Ok(()); + } + + #[test] + /// Attribute values read via xpath must not inject SSML when TTS is SSML. + fn attribute_ssml_attack_neutralized_in_speech() -> Result<()> { + use crate::speech::{SpeechRulesWithContext, SPEECH_RULES}; + + setup_speech_ssml(); + let mathml = format!( + r#"x"# + ); + set_mathml(&mathml)?; + let speech = get_spoken_text()?; + assert_ssml_attack_neutralized(&speech, PAYLOAD); + + // XPath Attribute nodes use replace_chars (same path as replace_nodes_string). + SPEECH_RULES.with(|rules| { + rules.borrow_mut().read_files()?; + let rules_ref = rules.borrow(); + let package = parser::parse(&mathml)?; + let math = get_element(&package); + let attr = math + .attribute("data-ssml-attack") + .expect("data-ssml-attack attribute"); + let work_package = Package::new(); + let mut ctx = + SpeechRulesWithContext::new(&rules_ref, work_package.as_document(), "", 0); + let from_attr = ctx.replace_chars(attr.value(), math)?; + assert_ssml_attack_neutralized(&from_attr, PAYLOAD); + assert!( + from_attr.contains("<"), + "attribute value should be XML-escaped for SSML: {from_attr}" + ); + Ok::<(), Error>(()) + })?; + return Ok(()); + } } diff --git a/src/main.rs b/src/main.rs index 506bd15d7..e058fe83f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,41 +35,41 @@ fn main() { .format_level(false) .init(); - let expr = r#" - - - - e - - - - 1 - 2 - - - - - ( - - - - x - - μ - - σ - - - ) - - - 2 - - - - - -"#; +// let expr = r#" +// +// +// +// e +// +// +// +// 1 +// 2 +// +// +// +// +// ( +// +// +// +// x +// +// μ +// +// σ +// +// +// ) +// +// +// 2 +// +// +// +// +// +// "#; // let expr = " // // x @@ -192,14 +192,11 @@ fn main() { // // "; -// let expr = r#" -// -// N2 -// Haber processH2 -// N -// H3 -// -// "#; + let expr = r#" + + 𝟢 + + "#; // let instant = Instant::now(); // let rules_dir = "".to_string(); // Use MathCATRulesDir, potentially pointing to a zipped version @@ -308,7 +305,7 @@ fn main() { // info!("#xpath = {}; duplicates = {}", xpath_counts.0, xpath_counts.1); // info!("Time taken (second time for speech + braille): {}ms", instant.elapsed().as_millis()); // debug!("Hashmap sizes:\n{}", libmathcat::speech::SpeechRules::print_sizes()); - timing_test(expr, 5000); + timing_test(expr, 000); } @@ -334,45 +331,45 @@ fn timing_test(expr: &str, n_loops: usize) { } info!("Time taken (time for set, speech, {} braille averaged over {} loops): {}ms", get_preference("BrailleCode").unwrap(), n_loops, instant.elapsed().as_millis() as f64/n_loops_float); - // let instant = Instant::now(); - // for _ in 0..n_loops { - // if let Err(e) = set_mathml(expr) { - // eprintln!("Error: exiting -- {}", errors_to_string(&e)); - // }; - // } - // info!("Time taken (time for set averaged over {} loops): {}ms", n_loops, instant.elapsed().as_millis() as f64/n_loops_float); + let instant = Instant::now(); + for _ in 0..n_loops { + if let Err(e) = set_mathml(expr) { + eprintln!("Error: exiting -- {}", errors_to_string(&e)); + }; + } + info!("Time taken (time for set averaged over {} loops): {}ms", n_loops, instant.elapsed().as_millis() as f64/n_loops_float); - // let instant = Instant::now(); - // for _ in 0..n_loops { - // match get_spoken_text() { - // Ok(_) =>( ), - // Err(e) => {eprintln!("{}", errors_to_string(&e)); exit(1);}, - // } - // } - // info!("Time taken (time for get_spoken_text() averaged over {} loops): {}ms", n_loops, instant.elapsed().as_millis() as f64/n_loops_float); + let instant = Instant::now(); + for _ in 0..n_loops { + match get_spoken_text() { + Ok(_) =>( ), + Err(e) => {eprintln!("{}", errors_to_string(&e)); exit(1);}, + } + } + info!("Time taken (time for get_spoken_text() averaged over {} loops): {}ms", n_loops, instant.elapsed().as_millis() as f64/n_loops_float); - // set_preference("BrailleCode", "UEB").unwrap(); - // get_braille("").unwrap(); - // let instant = Instant::now(); - // for _ in 0..n_loops { - // match get_braille("") { - // Ok(_) => (), - // Err(e) => {eprintln!("{}", errors_to_string(&e)); exit(1);}, - // } - // } - // info!("Time taken (time for {} braille averaged over {} loops): {}ms", get_preference("BrailleCode").unwrap(), n_loops, instant.elapsed().as_millis() as f64/n_loops_float); + set_preference("BrailleCode", "UEB").unwrap(); + get_braille("").unwrap(); + let instant = Instant::now(); + for _ in 0..n_loops { + match get_braille("") { + Ok(_) => (), + Err(e) => {eprintln!("{}", errors_to_string(&e)); exit(1);}, + } + } + info!("Time taken (time for {} braille averaged over {} loops): {}ms", get_preference("BrailleCode").unwrap(), n_loops, instant.elapsed().as_millis() as f64/n_loops_float); - // if let Err(e) = set_mathml(expr) { - // eprintln!("Error: exiting -- {}", errors_to_string(&e)); - // }; - // set_preference("BrailleCode", "Nemeth").unwrap(); - // get_braille("").unwrap(); - // let instant = Instant::now(); - // for _ in 0..n_loops { - // match get_braille("") { - // Ok(_) => (), - // Err(e) => {eprintln!("{}", errors_to_string(&e)); exit(1);} , - // } - // } - // info!("Time taken (time for {} braille averaged over {} loops): {}ms", get_preference("BrailleCode").unwrap(), n_loops, instant.elapsed().as_millis() as f64/n_loops_float); + if let Err(e) = set_mathml(expr) { + eprintln!("Error: exiting -- {}", errors_to_string(&e)); + }; + set_preference("BrailleCode", "Nemeth").unwrap(); + get_braille("").unwrap(); + let instant = Instant::now(); + for _ in 0..n_loops { + match get_braille("") { + Ok(_) => (), + Err(e) => {eprintln!("{}", errors_to_string(&e)); exit(1);} , + } + } + info!("Time taken (time for {} braille averaged over {} loops): {}ms", get_preference("BrailleCode").unwrap(), n_loops, instant.elapsed().as_millis() as f64/n_loops_float); } diff --git a/src/speech.rs b/src/speech.rs index 8ef2949d7..2545d308a 100644 --- a/src/speech.rs +++ b/src/speech.rs @@ -7,13 +7,11 @@ use std::path::PathBuf; use std::collections::HashMap; use std::cell::{RefCell, RefMut}; use std::sync::LazyLock; -use std::fmt::Debug; -use sxd_document_no_unsafe::dom::{ChildOfElement, Document, Element}; -use sxd_document_no_unsafe::{Package, QName}; -use sxd_document_no_unsafe::{as_str, as_qname}; -use sxd_xpath_no_unsafe::context::Evaluation; -use sxd_xpath_no_unsafe::{Factory, Value, XPath}; -use sxd_xpath_no_unsafe::nodeset::Node; +use sxd_document::dom::{ChildOfElement, Document, Element}; +use sxd_document::{Package, QName}; +use sxd_xpath::context::Evaluation; +use sxd_xpath::{Factory, Value, XPath}; +use sxd_xpath::nodeset::Node; use std::fmt; use std::time::SystemTime; use crate::definitions::read_definitions_file; @@ -97,7 +95,7 @@ fn intent_rules<'m>(rules: &'static std::thread::LocalKey>, let should_set_literal_intent = rules.pref_manager.borrow().pref_to_string("SpeechStyle").as_str() == "LiteralSpeak"; let original_intent = mathml.attribute_value("intent"); if should_set_literal_intent { - if let Some(ref intent) = original_intent { + if let Some(intent) = original_intent { let intent = if intent.contains('(') {intent.replace('(', ":literal(")} else {intent.to_string() + ":literal"}; mathml.set_attribute_value("intent", &intent); } else { @@ -115,7 +113,7 @@ fn intent_rules<'m>(rules: &'static std::thread::LocalKey>, }; if should_set_literal_intent { if let Some(original_intent) = original_intent { - mathml.set_attribute_value("intent", as_str!(original_intent)); + mathml.set_attribute_value("intent", original_intent); } else { mathml.remove_attribute("intent"); } @@ -152,8 +150,7 @@ fn speak_rules(rules: &'static std::thread::LocalKey>, math // 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 - let raw_intent_attr = mathml.attribute_value("data-intent-property"); - let intent_attr = raw_intent_attr.as_deref().unwrap_or_default(); + let intent_attr = mathml.attribute_value("data-intent-property").unwrap_or_default(); if let Some(start) = speech_string.find("[[") { match speech_string[start+2..].find("]]") { None => bail!("Internal error: looking for '[[...]]' during navigation -- only found '[[' in '{}'", speech_string), @@ -324,7 +321,7 @@ pub fn process_include(current_file: &Path, new_file_name: &str, mut read_new /// As the name says, TreeOrString is either a Tree (Element) or a String /// It is used to share code during pattern matching -pub trait TreeOrString<'c, 'm:'c, T: Debug> : Debug { +pub trait TreeOrString<'c, 'm:'c, T> { fn from_element(e: Element<'m>) -> Result; fn from_string(s: String, doc: Document<'m>) -> Result; fn replace_tts<'s:'c, 'r>(tts: &TTS, command: &TTSCommandRule, prefs: &PreferenceManager, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result; @@ -516,7 +513,7 @@ struct InsertChildren { #[cfg_attr(coverage, coverage(off))] impl fmt::Display for InsertChildren { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - return write!(f, "InsertChildren:\n nodes {}\n replacements {}", self.xpath, self.replacements); + return write!(f, "InsertChildren:\n nodes {}\n replacements {}", self.xpath, &self.replacements); } } @@ -554,7 +551,7 @@ impl InsertChildren { // This is slower than the alternatives, but reuses a bunch of code and hence is less complicated. fn replace<'c, 's:'c, 'm: 'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result { let result = self.xpath.evaluate(&rules_with_context.context_stack.base, mathml) - .with_context(||format!("in '{}' replacing after pattern match", self.xpath.rc.string) )?; + .with_context(||format!("in '{}' replacing after pattern match", &self.xpath.rc.string) )?; match result { Value::Nodeset(nodes) => { if nodes.size() == 0 { @@ -618,7 +615,7 @@ impl fmt::Display for Intent { return write!(f, "intent: {}: {}, attrs='{}'>\n children: {}", if self.name.is_some() {"name"} else {"xpath-name"}, name, self.attrs, - self.children); + &self.children); } } @@ -659,17 +656,17 @@ impl Intent { result = temp; } if let Some(intent_name) = &self.name { - result.set_attribute_value(MATHML_FROM_NAME_ATTR, as_str!(name(mathml))); + result.set_attribute_value(MATHML_FROM_NAME_ATTR, name(mathml)); set_mathml_name(result, intent_name.as_str()); } if let Some(my_xpath) = &self.xpath{ // self.xpath_name must be != None let xpath_value = my_xpath.evaluate(rules_with_context.get_context(), mathml)?; match xpath_value { Value::String(intent_name) => { - result.set_attribute_value(MATHML_FROM_NAME_ATTR, as_str!(name(mathml))); + result.set_attribute_value(MATHML_FROM_NAME_ATTR, name(mathml)); set_mathml_name(result, intent_name.as_str()) }, - _ => bail!("'xpath-name' value '{}' was not a string", my_xpath), + _ => bail!("'xpath-name' value '{}' was not a string", &my_xpath), } } if self.name.is_none() && self.xpath.is_none() { @@ -677,7 +674,7 @@ impl Intent { }; for attr in mathml.attributes() { - result.set_attribute_value(as_qname!(attr.name()), as_str!(attr.value())); + result.set_attribute_value(attr.name(), attr.value()); } // can't test against name == "math" because intent might a new element @@ -748,7 +745,7 @@ struct With { #[cfg_attr(coverage, coverage(off))] impl fmt::Display for With { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - return write!(f, "with:\n variables: {}\n replace: {}", self.variables, self.replacements); + return write!(f, "with:\n variables: {}\n replace: {}", &self.variables, &self.replacements); } } @@ -794,7 +791,7 @@ struct SetVariables { #[cfg_attr(coverage, coverage(off))] impl fmt::Display for SetVariables { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - return write!(f, "SetVariables: variables {}", self.variables); + return write!(f, "SetVariables: variables {}", &self.variables); } } @@ -826,7 +823,7 @@ struct TranslateExpression { #[cfg_attr(coverage, coverage(off))] impl fmt::Display for TranslateExpression { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - return write!(f, "speak: {}", self.xpath); + return write!(f, "speak: {}", &self.xpath); } } @@ -947,7 +944,7 @@ impl ReplacementArray { let after = if i+1 == replacement_strings.len() {""} else {&replacement_strings[i+1]}; replacement_strings[i] = replacement_strings[i].replace( PAUSE_AUTO_STR, - &rules_with_context.speech_rules.pref_manager.borrow().get_tts().compute_auto_pause(&rules_with_context.speech_rules.pref_manager.borrow(), before, after)?); + &rules_with_context.speech_rules.pref_manager.borrow().get_tts().compute_auto_pause(&rules_with_context.speech_rules.pref_manager.borrow(), before, after)); } } @@ -1107,8 +1104,12 @@ impl MyXPath { let compiled_xpath = factory.build(&xpath_with_debug_info) .with_context(|| format!( "Could not compile XPath for pattern:\n{}{}", - xpath, more_details(xpath)))?; - return Ok(compiled_xpath); + &xpath, more_details(xpath)))?; + return match compiled_xpath { + Some(xpath) => Ok(xpath), + None => bail!("Problem compiling Xpath for pattern:\n{}{}", + &xpath, more_details(xpath)), + }; fn more_details(xpath: &str) -> String { @@ -1225,7 +1226,7 @@ impl MyXPath { } } - fn is_true(&self, context: &sxd_xpath_no_unsafe::Context, mathml: Element) -> Result { + fn is_true(&self, context: &sxd_xpath::Context, mathml: Element) -> Result { // return true if there is no condition or if the condition evaluates to true return Ok( match self.evaluate(context, mathml)? { @@ -1242,7 +1243,7 @@ impl MyXPath { } let result = self.evaluate(&rules_with_context.context_stack.base, mathml) - .with_context(|| format!("in '{}' replacing after pattern match", self.rc.string) )?; + .with_context(|| format!("in '{}' replacing after pattern match", &self.rc.string) )?; let string = match result { Value::Nodeset(nodes) => { if nodes.size() == 0 { @@ -1260,7 +1261,7 @@ impl MyXPath { return T::from_string(result, rules_with_context.doc ); } - pub fn evaluate<'c>(&self, context: &sxd_xpath_no_unsafe::Context<'c>, mathml: Element<'c>) -> Result> { + pub fn evaluate<'c>(&self, context: &sxd_xpath::Context<'c>, mathml: Element<'c>) -> Result> { // debug!("evaluate: {}", self); let result = self.rc.xpath.evaluate(context, mathml); return match result { @@ -1333,14 +1334,14 @@ impl SpeechPattern { Err(e) => return Err( e.context( format!("tag name '{}' is not a string in:\n{}", - yaml_to_string(&tag_array.as_vec().unwrap()[i], 0), - yaml_to_string(dict, 1))) + &yaml_to_string(&tag_array.as_vec().unwrap()[i], 0), + &yaml_to_string(dict, 1))) ), Ok(str) => tag_names.push(str), }; } } else { - bail!("Errors trying to find 'tag' in:\n{}", yaml_to_string(dict, 1)); + bail!("Errors trying to find 'tag' in:\n{}", &yaml_to_string(dict, 1)); } } } @@ -1349,7 +1350,7 @@ impl SpeechPattern { if dict.is_null() { bail!("Error trying to find 'name': empty value (two consecutive '-'s?"); } else { - bail!("Errors trying to find 'name' in:\n{}", yaml_to_string(dict, 1)); + bail!("Errors trying to find 'name' in:\n{}", &yaml_to_string(dict, 1)); }; }; let pattern_name = pattern_name.unwrap().to_string(); @@ -1406,8 +1407,8 @@ impl SpeechPattern { return Ok(None); } - fn is_match(&self, context: &sxd_xpath_no_unsafe::Context, mathml: Element) -> Result { - if self.tag_name != as_qname!(mathml.name()).local_part() && self.tag_name != "*" && self.tag_name != "!*" { + fn is_match(&self, context: &sxd_xpath::Context, mathml: Element) -> Result { + if self.tag_name != mathml.name().local_part() && self.tag_name != "*" && self.tag_name != "!*" { return Ok( false ); } @@ -1593,7 +1594,7 @@ impl fmt::Display for Test { } impl Test { - fn is_true(&self, context: &sxd_xpath_no_unsafe::Context, mathml: Element) -> Result { + fn is_true(&self, context: &sxd_xpath::Context, mathml: Element) -> Result { return match self.condition.as_ref() { None => Ok( false ), // trivially false -- want to do else part Some(condition) => condition.is_true(context, mathml) @@ -1725,7 +1726,7 @@ impl VariableDefinitions { struct ContextStack<'c> { // Note: values are generated by calling value_of on an Evaluation -- that makes the two lifetimes the same old_values: Vec>, // store old values so they can be set on pop - base: sxd_xpath_no_unsafe::Context<'c> // initial context -- contains all the function defs and pref variables + base: sxd_xpath::Context<'c> // initial context -- contains all the function defs and pref variables } impl fmt::Display for ContextStack<'_> { @@ -1754,8 +1755,8 @@ impl<'c, 'r> ContextStack<'c> { return context_stack; } - fn base_context(var_defs: PreferenceHashMap) -> sxd_xpath_no_unsafe::Context<'c> { - let mut context = sxd_xpath_no_unsafe::Context::new(); + fn base_context(var_defs: PreferenceHashMap) -> sxd_xpath::Context<'c> { + let mut context = sxd_xpath::Context::new(); context.set_namespace("m", "http://www.w3.org/1998/Math/MathML"); crate::xpath_functions::add_builtin_functions(&mut context); for (key, value) in var_defs { @@ -1799,7 +1800,7 @@ impl<'c, 'r> ContextStack<'c> { // set the new value let new_value = match def.value.evaluate(&self.base, mathml) { Ok(val) => val, - Err(_) => Value::Nodeset(sxd_xpath_no_unsafe::nodeset::Nodeset::new()), + Err(_) => Value::Nodeset(sxd_xpath::nodeset::Nodeset::new()), }; let qname = QName::new(def.name.as_str()); self.base.set_variable(qname, new_value); @@ -1916,10 +1917,8 @@ impl UnicodeDef { for ch in first..last+1 { let ch_as_str = char::from_u32(ch).unwrap().to_string(); - if unicode_table.insert(ch, ReplacementArray::build(&substitute_ch(replacements, &ch_as_str)) - .with_context(|| format!("In definition of char: '{def_range}'"))?.replacements).is_some() { - error!("*** Character '{}' (0x{:X}) is repeated", char::from_u32(ch).unwrap(), ch); - } + unicode_table.insert(ch, ReplacementArray::build(&substitute_ch(replacements, &ch_as_str)) + .with_context(|| format!("In definition of char: '{def_range}'"))?.replacements); }; return Ok(None) @@ -2134,11 +2133,11 @@ impl fmt::Display for SpeechRules { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "SpeechRules '{}'\n{})", self.name, self.pref_manager.borrow())?; let mut rules_vec: Vec<(&String, &Vec>)> = self.rules.iter().collect(); - rules_vec.sort_by_key(|(tag_name, _)| *tag_name); + rules_vec.sort_by_key(|(tag_name, _)| tag_name.as_str()); for (tag_name, rules) in rules_vec { writeln!(f, " {}: #patterns {}", tag_name, rules.len())?; }; - return writeln!(f, " {}+{} unicode entries", self.unicode_short.borrow().len(), self.unicode_full.borrow().len()); + return writeln!(f, " {}+{} unicode entries", &self.unicode_short.borrow().len(), &self.unicode_full.borrow().len()); } } @@ -2159,14 +2158,14 @@ pub struct SpeechRulesWithContext<'c, 's:'c, 'm:'c> { impl<'c, 's:'c, 'm:'c> fmt::Display for SpeechRulesWithContext<'c, 's,'m> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "SpeechRulesWithContext \n{})", self.speech_rules)?; - return writeln!(f, " {} context entries, nav node id '({}, {})'", self.context_stack, self.nav_node_id, self.nav_node_offset); + return writeln!(f, " {} context entries, nav node id '({}, {})'", &self.context_stack, self.nav_node_id, self.nav_node_offset); } } thread_local!{ /// SPEECH_UNICODE_SHORT is shared among several rules, so "RC" is used static SPEECH_UNICODE_SHORT: UnicodeTable = - Rc::new( RefCell::new( HashMap::with_capacity(700) ) ); + Rc::new( RefCell::new( HashMap::with_capacity(500) ) ); /// SPEECH_UNICODE_FULL is shared among several rules, so "RC" is used static SPEECH_UNICODE_FULL: UnicodeTable = @@ -2178,7 +2177,7 @@ thread_local!{ /// BRAILLE_UNICODE_FULL is shared among several rules, so "RC" is used static BRAILLE_UNICODE_FULL: UnicodeTable = - Rc::new( RefCell::new( HashMap::with_capacity(4000) ) ); + Rc::new( RefCell::new( HashMap::with_capacity(5000) ) ); /// SPEECH_DEFINITION_FILES_AND_TIMES is shared among several rules, so "RC" is used static SPEECH_DEFINITION_FILES_AND_TIMES: FilesAndTimesShared = @@ -2460,7 +2459,7 @@ impl<'c, 's:'c, 'r, 'm:'c> SpeechRulesWithContext<'c, 's,'m> { ); } - pub fn get_context(&mut self) -> &mut sxd_xpath_no_unsafe::Context<'c> { + pub fn get_context(&mut self) -> &mut sxd_xpath::Context<'c> { return &mut self.context_stack.base; } @@ -2475,8 +2474,7 @@ impl<'c, 's:'c, 'r, 'm:'c> SpeechRulesWithContext<'c, 's,'m> { pub fn match_pattern>(&'r mut self, mathml: Element<'c>) -> Result { // debug!("Looking for a match for: \n{}", mml_to_string(mathml)); - let raw_name = mathml.name(); - let tag_name = as_qname!(raw_name).local_part(); + let tag_name = mathml.name().local_part(); let rules = &self.speech_rules.rules; // start with priority rules that apply to any node (should be a very small number) @@ -2519,7 +2517,7 @@ impl<'c, 's:'c, 'r, 'm:'c> SpeechRulesWithContext<'c, 's,'m> { self.context_stack.push(pattern.var_defs.clone(), mathml)?; } let result = if self.nav_node_offset > 0 && - self.nav_node_id == mathml.attribute_value("id").as_deref().unwrap_or_default() && is_leaf(mathml) { + self.nav_node_id == mathml.attribute_value("id").unwrap_or_default() && is_leaf(mathml) { let ch = crate::canonicalize::as_text(mathml).chars().nth(self.nav_node_offset-1).unwrap_or_default(); let ch = self.replace_single_char(ch, mathml)?; // debug!("find_match: ch={} from '{}'; matched pattern name/tag: {}/{} with nav_node_offset={}", @@ -2538,7 +2536,7 @@ impl<'c, 's:'c, 'r, 'm:'c> SpeechRulesWithContext<'c, 's,'m> { if self.nav_node_id.is_empty() { Ok( Some(s) ) } else { - if self.nav_node_id == mathml.attribute_value("id").as_deref().unwrap_or_default() {debug!("Matched pattern name/tag: {}/{}", pattern.pattern_name, pattern.tag_name)}; + if self.nav_node_id == mathml.attribute_value("id").unwrap_or_default() {debug!("Matched pattern name/tag: {}/{}", pattern.pattern_name, pattern.tag_name)}; Ok ( Some(self.nav_node_adjust(s, mathml)) ) } }, @@ -2579,11 +2577,10 @@ impl<'c, 's:'c, 'r, 'm:'c> SpeechRulesWithContext<'c, 's,'m> { fn nav_node_adjust>(&self, speech: T, mathml: Element<'c>) -> T { if let Some(id) = mathml.attribute_value("id") && self.nav_node_id == id { - let raw_offset = mathml.attribute_value(crate::navigate::ID_OFFSET); - let offset = raw_offset.as_deref().unwrap_or("0"); - debug!("nav_node_adjust: id/name='{}/{}' offset?='{}'", id, name(mathml), - self.nav_node_offset.to_string().as_str() == offset - ); + 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 + // ); 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"); @@ -2718,13 +2715,13 @@ impl<'c, 's:'c, 'r, 'm:'c> SpeechRulesWithContext<'c, 's,'m> { Node::Element(n) => self.match_pattern::>(n)?, Node::Text(t) => { let leaf = create_mathml_element(&self.doc, "TEMP_NAME"); - leaf.set_text(as_str!(t.text())); + leaf.set_text(t.text()); leaf }, Node::Attribute(attr) => { // debug!(" from attr with text '{}'", attr.value()); let leaf = create_mathml_element(&self.doc, "TEMP_NAME"); - leaf.set_text(as_str!(attr.value())); + leaf.set_text(attr.value()); leaf }, _ => { @@ -2752,8 +2749,8 @@ 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(as_str!(t.text()), mathml)?, - Node::Attribute(attr) => self.replace_chars(as_str!(attr.value()), 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!!!"), }; result += &matched; @@ -2820,35 +2817,23 @@ impl<'c, 's:'c, 'r, 'm:'c> SpeechRulesWithContext<'c, 's,'m> { info!("*** Loading full unicode {} for char '{}'/{:#06x}", rules.name, ch, ch_as_u32); rules.unicode_full.borrow_mut().clear(); rules.unicode_full_files.borrow_mut().set_files_and_times(rules.read_unicode(None, false)?); - // when debugging, run a check across the short and full tables to ensure no characters are repeated - if cfg!(debug_assertions) { - let unicode_full = rules.unicode_full.borrow(); - for ch in unicode.keys() { - if unicode_full.get(ch).is_some() { - error!("*** Character '{}' (0x{:X}) is repeated in both short and full unicode tables", *ch, *ch); - } - } - } info!("# Unicode defs = {}/{}", rules.unicode_short.borrow().len(), rules.unicode_full.borrow().len()); } unicode = rules.unicode_full.borrow(); replacements = unicode.get( &ch_as_u32 ); if replacements.is_none() { - 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(self.escape_string_for_safety(String::from(ch))); - } else { - let ch_as_int = ch as u32; - if ('\u{2800}'..='\u{28ff}').contains(&ch) { // braille -- leave as braille - return Ok(self.escape_string_for_safety(String::from(ch))); - } else { // Emulate what NVDA does: generate (including single quotes) '\xhhhh' or '\yhhhhhh' - let prefix_indicator = if ch_as_int < 1<<16 {'x'} else {'y'}; - return self.replace_chars( &format!("'\\{prefix_indicator}{:06x}'", ch_as_int), mathml); - } - } + 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(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; + let prefix_indicator = if ch_as_int < 1<<16 {'x'} else {'y'}; + return self.replace_chars( &format!("'\\{prefix_indicator}{:06x}'", ch_as_int), mathml); } - }; + } + }; // map across all the parts of the replacement, collect them up into a Vec, and then concat them together let result = replacements.unwrap() diff --git a/src/tts.rs b/src/tts.rs index 34856813d..521715381 100644 --- a/src/tts.rs +++ b/src/tts.rs @@ -68,7 +68,7 @@ #![allow(clippy::needless_return)] use crate::{errors::*, prefs::PreferenceManager, speech::ReplacementArray}; -use sxd_document_no_unsafe::dom::Element; +use sxd_document::dom::Element; use yaml_rust::Yaml; use std::fmt; @@ -78,7 +78,7 @@ use std::str::FromStr; use strum_macros::{Display, EnumString}; use regex::Regex; use std::sync::LazyLock; -use sxd_xpath_no_unsafe::Value; +use sxd_xpath::Value; use html_escape::encode_safe; const MIN_PAUSE:f64 = 50.0; // ms -- avoids clutter of putting out pauses that probably can't be heard @@ -174,7 +174,7 @@ impl Pronounce { } } if text.is_empty() { - bail!("'text' key/value is required for 'pronounce' -- it can not be an empty string.") + bail!("'text' key/value is required for 'pronounce' -- it is used is the speech engine is unknown.") } return Ok( Pronounce{ text: text.to_string(), @@ -196,25 +196,26 @@ pub enum TTSCommandValue { } impl TTSCommandValue { - fn get_num(&self) -> Result { + fn get_num(&self) -> f64 { match self { - TTSCommandValue::Number(n) => Ok(*n), - _ => bail!("Internal error: TTSCommandValue is not a number"), + TTSCommandValue::Number(n) => return *n, + _ => panic!("Internal error: TTSCommandValue is not a number"), } } - fn get_string(&self) -> Result<&String> { + fn get_string(&self) -> &String { match self { - TTSCommandValue::String(s) => Ok(s), - _ => bail!("Internal error: TTSCommandValue is not a string"), + TTSCommandValue::String(s) => return s, + _ => panic!("Internal error: TTSCommandValue is not a string"), } } - fn get_pronounce(&self) -> Result<&Pronounce> { + fn get_pronounce(&self) -> &Pronounce { match self { - TTSCommandValue::Pronounce(p) => Ok(p), - _ => bail!("Internal error: TTSCommandValue is not a 'pronounce' command"), + TTSCommandValue::Pronounce(p) => return p, + _ => panic!("Internal error: TTSCommandValue is not a 'pronounce' command'"), } + } } @@ -385,7 +386,7 @@ impl TTS { match command.value { TTSCommandValue::XPath(xpath) => { let value = xpath.evaluate(rules_with_context.get_context(), mathml) - .with_context(|| format!("in 'spell': can't evaluate xpath \"{}\"", xpath) )?; + .with_context(|| format!("in 'spell': can't evaluate xpath \"{}\"", &xpath.to_string()) )?; let value_string = match value { Value::String(s) => s, Value::Nodeset(nodes) if nodes.size() == 1 => { @@ -396,14 +397,14 @@ impl TTS { if crate::xpath_functions::is_leaf(el) { crate::canonicalize::as_text(el).to_string() } else { - bail!("in 'spell': value returned from xpath '{}' does not evaluate to a string", xpath); + bail!("in 'spell': value returned from xpath '{}' does not evaluate to a string", &xpath.to_string()); } } else { bail!("in 'spell': value returned from xpath '{}' does not evaluate to a string, it is {} nodes", - xpath, nodes.size()); + &xpath.to_string(), nodes.size()); } }, - _ => bail!("in 'spell': value returned from xpath '{}' does not evaluate to a string", xpath), + _ => bail!("in 'spell': value returned from xpath '{}' does not evaluate to a string", &xpath.to_string()), }; // Chemistry wants to spell elements like "Na". But we also have the issue of capitalization (SpeechOverrides_CapitalLetters) // so the "N" need to use that. The logic for that is already in unicode.yaml. We could replicate that here. @@ -455,16 +456,16 @@ impl TTS { // small optimization to avoid generating tags that do nothing - if ((command.command == TTSCommand::Pitch || command.command == TTSCommand::Volume || command.command == TTSCommand::Pause) && command.value.get_num()? == 0.0) || - (command.command == TTSCommand::Rate && command.value.get_num()? == 100.0) { + if ((command.command == TTSCommand::Pitch || command.command == TTSCommand::Volume || command.command == TTSCommand::Pause) && command.value.get_num() == 0.0) || + (command.command == TTSCommand::Rate && command.value.get_num() == 100.0) { return command.replacements.replace::(rules_with_context, mathml); } let mut result = String::with_capacity(255); result += &match self { - TTS::None => self.get_string_none(&command, prefs, true)?, - TTS::SSML => self.get_string_ssml(&command, prefs, true)?, - TTS::SAPI5 => self.get_string_sapi5(&command, prefs, true)?, + TTS::None => self.get_string_none(&command, prefs, true), + TTS::SSML => self.get_string_ssml(&command, prefs, true), + TTS::SAPI5 => self.get_string_sapi5(&command, prefs, true), }; @@ -477,9 +478,9 @@ impl TTS { } 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)?, + 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() { @@ -503,13 +504,13 @@ impl TTS { // auto pausing can't be known until neighboring strings are computed // we create a unique string in this case and compute the real value later - fn get_string_none(&self, command: &TTSCommandRule, prefs: &PreferenceManager, is_start_tag: bool) -> Result { + fn get_string_none(&self, command: &TTSCommandRule, prefs: &PreferenceManager, is_start_tag: bool) -> String { // they only thing to do is handle "pause" with some punctuation hacks along with 'spell' if is_start_tag { if command.command == TTSCommand::Pause { - let amount = command.value.get_num()?; + let amount = command.value.get_num(); // only ',' and ';' are used as '.' didn't seem to reliably generate pauses in tests - return Ok(crate::speech::CONCAT_INDICATOR.to_string() + ( + return crate::speech::CONCAT_INDICATOR.to_string() + ( if amount == PAUSE_AUTO { PAUSE_AUTO_STR } else { @@ -522,22 +523,22 @@ impl TTS { ";" } } - )); + ); } else if command.command == TTSCommand::Spell { // debug!("spell rule: {}", command.value.get_string()); - return Ok(command.value.get_string()?.to_string()); + return command.value.get_string().to_string(); } else if let TTSCommandValue::Pronounce(p) = &command.value { - return Ok(crate::speech::CONCAT_INDICATOR.to_string() + &p.text); + return crate::speech::CONCAT_INDICATOR.to_string() + &p.text; } }; - return Ok("".to_string()); + return "".to_string(); } - fn get_string_sapi5(&self, command: &TTSCommandRule, prefs: &PreferenceManager, is_start_tag: bool) -> Result { + fn get_string_sapi5(&self, command: &TTSCommandRule, prefs: &PreferenceManager, is_start_tag: bool) -> String { return match &command.command { TTSCommand::Pause => if is_start_tag { - let amount = command.value.get_num()?; - Ok(if amount == PAUSE_AUTO { + let amount = command.value.get_num(); + if amount == PAUSE_AUTO { PAUSE_AUTO_STR.to_string() } else { let amount = amount * TTS::get_pause_multiplier(prefs); @@ -546,60 +547,35 @@ impl TTS { } else { "".to_string() } - }) + } } else { - Ok("".to_string()) + "".to_string() }, // pitch must be in [-10, 10], logarithmic based on octaves // note MathPlayer uses 'absmiddle' (requires keeping a stack) -- could be 'middle' is not well supported - TTSCommand::Pitch => if is_start_tag { - Ok(format!("", (24.0*(1.0+command.value.get_num()?/100.0).log2()).round())) - } else { - Ok(String::from("")) - }, + TTSCommand::Pitch => if is_start_tag {format!("", (24.0*(1.0+command.value.get_num()/100.0).log2()).round())} else {String::from("")}, // rate must be in [-10, 10], but we get relative %s. 300% => 10 (see comments at top of file) - TTSCommand::Rate => if is_start_tag { - Ok(format!("", 10.0*(0.01*command.value.get_num()?).log(3.0))) - } else { - Ok(String::from("")) - }, - TTSCommand::Volume => if is_start_tag { - Ok(format!("", command.value.get_num()?)) - } else { - Ok(String::from("")) - }, - TTSCommand::Audio => Ok("".to_string()), // SAPI5 doesn't support audio - TTSCommand::Gender => if is_start_tag { - Ok(format!("", command.value.get_string()?)) - } else { - Ok(String::from("")) - }, - TTSCommand::Voice => if is_start_tag { - Ok(format!("", command.value.get_string()?)) - } else { - Ok(String::from("")) - }, - TTSCommand::Spell => if is_start_tag { - Ok(format!("{}", command.value.get_string()?)) - } else { - Ok(String::from("")) - }, - TTSCommand::Pronounce => if is_start_tag { - let pronounce = command.value.get_pronounce()?; - Ok(format!("{}", pronounce.sapi5, pronounce.text)) - } else { - Ok(String::from("")) - }, - TTSCommand::Bookmark => bail!("Internal error: bookmarks should have been handled earlier"), + TTSCommand::Rate => if is_start_tag {format!("", 10.0*(0.01*command.value.get_num()).log(3.0))} else {String::from("")}, + TTSCommand::Volume =>if is_start_tag {format!("", command.value.get_num())} else {String::from("")}, + TTSCommand::Audio => "".to_string(), // SAPI5 doesn't support audio + TTSCommand::Gender =>if is_start_tag {format!("", command.value.get_string())} else {String::from("")}, + TTSCommand::Voice =>if is_start_tag {format!("", command.value.get_string())} else {String::from("")}, + TTSCommand::Spell =>if is_start_tag {format!("{}", command.value.get_string())} else {String::from("")}, + TTSCommand::Pronounce =>if is_start_tag { + format!("{}", &command.value.get_pronounce().sapi5, &command.value.get_pronounce().text) + } else { + String::from("") + }, + TTSCommand::Bookmark => panic!("Internal error: bookmarks should have been handled earlier"), }; } - fn get_string_ssml(&self, command: &TTSCommandRule, prefs: &PreferenceManager, is_start_tag: bool) -> Result { + fn get_string_ssml(&self, command: &TTSCommandRule, prefs: &PreferenceManager, is_start_tag: bool) -> String { return match &command.command { TTSCommand::Pause => { if is_start_tag { - let amount = command.value.get_num()?; - Ok(if amount == PAUSE_AUTO { + let amount = command.value.get_num(); + if amount == PAUSE_AUTO { PAUSE_AUTO_STR.to_string() } else { let amount = amount * TTS::get_pause_multiplier(prefs); @@ -608,54 +584,25 @@ impl TTS { } else { "".to_string() } - }) + } } else { - Ok("".to_string()) + "".to_string() } }, - TTSCommand::Pitch => if is_start_tag { - Ok(format!("", command.value.get_num()?)) - } else { - Ok(String::from("")) - }, - TTSCommand::Rate => if is_start_tag { - Ok(format!("", command.value.get_num()?)) - } else { - Ok(String::from("")) - }, - TTSCommand::Volume => if is_start_tag { - Ok(format!("", command.value.get_num()?)) + TTSCommand::Pitch => if is_start_tag {format!("", command.value.get_num())} else {String::from("")}, + TTSCommand::Rate => if is_start_tag {format!("", command.value.get_num())} else {String::from("")}, + TTSCommand::Volume =>if is_start_tag {format!("", command.value.get_num())} else {String::from("")}, + TTSCommand::Audio =>if is_start_tag {format!("")}, // only 'beep' is supported for now + TTSCommand::Gender =>if is_start_tag {format!("", command.value.get_string())} else {String::from("")}, + TTSCommand::Voice =>if is_start_tag {format!("", command.value.get_string())} else {String::from("")}, + TTSCommand::Spell =>if is_start_tag {format!("{}", command.value.get_string())} else {String::from("")}, + TTSCommand::Pronounce =>if is_start_tag { + format!("{}", &command.value.get_pronounce().ipa, &command.value.get_pronounce().text) } else { - Ok(String::from("")) + String::from("") }, - TTSCommand::Audio => if is_start_tag { - Ok(format!("")) - }, // only 'beep' is supported for now - TTSCommand::Gender => if is_start_tag { - Ok(format!("", command.value.get_string()?)) - } else { - Ok(String::from("")) - }, - TTSCommand::Voice => if is_start_tag { - Ok(format!("", command.value.get_string()?)) - } else { - Ok(String::from("")) - }, - TTSCommand::Spell => if is_start_tag { - Ok(format!("{}", command.value.get_string()?)) - } else { - Ok(String::from("")) - }, - TTSCommand::Pronounce => if is_start_tag { - let pronounce = command.value.get_pronounce()?; - Ok(format!("{}", pronounce.ipa, pronounce.text)) - } else { - Ok(String::from("")) - }, - TTSCommand::Bookmark => bail!("Internal error: bookmarks should have been handled earlier"), - }; + TTSCommand::Bookmark => panic!("Internal error: bookmarks should have been handled earlier"), + } } fn get_pause_multiplier(prefs: &PreferenceManager) -> f64 { @@ -666,7 +613,7 @@ impl TTS { /// /// The computation is based on the length of the speech strings (after removing tagging). /// There is a bias towards pausing more _after_ longer strings. - pub fn compute_auto_pause(&self, prefs: &PreferenceManager, before: &str, after: &str) -> Result { + pub fn compute_auto_pause(&self, prefs: &PreferenceManager, before: &str, after: &str) -> String { static REMOVE_XML: LazyLock = LazyLock::new(|| Regex::new(r"<.+?>").unwrap()); // punctuation ending with a '.' let before_len; let after_len; @@ -688,7 +635,7 @@ impl TTS { if after_len < 3 { // hack to prevent pausing before "of" in exprs like "the fourth power of secant, of x" // if it should pause anywhere, it should be after the "of" - return Ok("".to_string()); + return "".to_string(); } let pause = std::cmp::min(3000, ((2 * before_len + after_len)/48) * 128); // create a TTSCommandRule so we reuse code 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 From 6161ccfd7eac05f95753ee3bed871afb8f1d944e Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Mon, 29 Jun 2026 21:17:08 +0500 Subject: [PATCH 21/47] Add Russian braille label marks and tests Introduce comprehensive Russian braille label handling: new label/mark rules (exact-over/under, upper/lower-right, repeated/no-indicator, left-labels) and mmultiscripts prescript/postscript logic in Rules/Braille/Russian/Russian_Rules.yaml. Add Unicode braille mappings and new definition maps (RussianLabelMarks, RussianExactOverLabelMarksWithoutIndicator, RussianUpperRightLabelMarksWithoutIndicator) in definitions.yaml and unicode.yaml, and change '%' mapping. Expand tests (tests/braille/Russian/russian.rs) with cases for percent/special marks, labeled table rows, mmultiscripts variants, digit sequences, infinity index, complex sup fractions and many label-mark expectations. --- Rules/Braille/Russian/Russian_Rules.yaml | 326 ++++++++++++++++++++++- Rules/Braille/Russian/definitions.yaml | 25 ++ Rules/Braille/Russian/unicode.yaml | 12 +- 3 files changed, 361 insertions(+), 2 deletions(-) diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index a691101e8..ac3a79c1d 100644 --- a/Rules/Braille/Russian/Russian_Rules.yaml +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -209,6 +209,12 @@ then: [t: "⠇⠀"] - else: - x: $RowStart + - test: + if: .[self::m:mlabeledtr] + then: + - t: "⠍⠑⠞⠅⠁⠀⠎⠞⠗⠕⠅⠊⠀⠲" + - x: "*[1]/*" + - t: "W" - test: if: .[self::m:mlabeledtr] then: [x: "*[position()>1]"] @@ -278,6 +284,211 @@ - t: "#" - x: "*[1]" +- + name: exact-over-label-no-indicator + tag: mover + match: + - "DefinitionValue(*[2], 'Braille', 'RussianExactOverLabelMarksWithoutIndicator') != '' and" + - "string-length(*[2]) = 1" + replace: + - test: + if: "*[1][self::m:mrow or self::m:mfrac or self::m:msqrt or self::m:mroot or self::m:mtable]" + then: + - t: "⠯" + - x: "*[1]" + - t: "⠽" + else: + - x: "*[1]" + - x: "DefinitionValue(*[2], 'Braille', 'RussianExactOverLabelMarksWithoutIndicator')" + +- + name: exact-over-label-repeated + tag: mover + match: + - "string-length(*[2]) <= 3 and" + - "translate(*[2], substring(*[2], 1, 1), '') = '' and" + - "DefinitionValue(substring(*[2], 1, 1), 'Braille', 'RussianLabelMarks') != ''" + replace: + - test: + if: "*[1][self::m:mrow or self::m:mfrac or self::m:msqrt or self::m:mroot or self::m:mtable]" + then: + - t: "⠯" + - x: "*[1]" + - t: "⠽" + else: + - x: "*[1]" + - t: "⠘" + - x: "DefinitionValue(substring(*[2], 1, 1), 'Braille', 'RussianLabelMarks')" + - test: + if: "string-length(*[2]) > 1" + then: + - x: "DefinitionValue(substring(*[2], 1, 1), 'Braille', 'RussianLabelMarks')" + - test: + if: "string-length(*[2]) > 2" + then: + - x: "DefinitionValue(substring(*[2], 1, 1), 'Braille', 'RussianLabelMarks')" + +- + name: exact-over-label + tag: mover + match: "DefinitionValue(*[2], 'Braille', 'RussianLabelMarks') != ''" + replace: + - test: + if: "*[1][self::m:mrow or self::m:mfrac or self::m:msqrt or self::m:mroot or self::m:mtable]" + then: + - t: "⠯" + - x: "*[1]" + - t: "⠽" + else: + - x: "*[1]" + - t: "⠘" + - x: "DefinitionValue(*[2], 'Braille', 'RussianLabelMarks')" + +- + name: exact-under-label-repeated + tag: munder + match: + - "string-length(*[2]) <= 3 and" + - "translate(*[2], substring(*[2], 1, 1), '') = '' and" + - "DefinitionValue(substring(*[2], 1, 1), 'Braille', 'RussianLabelMarks') != ''" + replace: + - test: + if: "*[1][self::m:mrow or self::m:mfrac or self::m:msqrt or self::m:mroot or self::m:mtable]" + then: + - t: "⠯" + - x: "*[1]" + - t: "⠽" + else: + - x: "*[1]" + - t: "⠰" + - x: "DefinitionValue(substring(*[2], 1, 1), 'Braille', 'RussianLabelMarks')" + - test: + if: "string-length(*[2]) > 1" + then: + - x: "DefinitionValue(substring(*[2], 1, 1), 'Braille', 'RussianLabelMarks')" + - test: + if: "string-length(*[2]) > 2" + then: + - x: "DefinitionValue(substring(*[2], 1, 1), 'Braille', 'RussianLabelMarks')" + +- + name: exact-under-label + tag: munder + match: "DefinitionValue(*[2], 'Braille', 'RussianLabelMarks') != ''" + replace: + - test: + if: "*[1][self::m:mrow or self::m:mfrac or self::m:msqrt or self::m:mroot or self::m:mtable]" + then: + - t: "⠯" + - x: "*[1]" + - t: "⠽" + else: + - x: "*[1]" + - t: "⠰" + - x: "DefinitionValue(*[2], 'Braille', 'RussianLabelMarks')" + +- + name: upper-right-label-no-indicator-repeated + tag: msup + match: + - "not(ancestor::*[@data-chem-equation or contains(@intent, ':chemical-equation')]) and" + - "not(*[2][contains(., '+') or contains(., '-') or contains(., '−')] and" + - " *[1][self::m:mi or self::m:mtext or self::m:mrow or self::m:msub or self::m:mmultiscripts]) and" + - "string-length(*[2]) <= 3 and" + - "translate(*[2], substring(*[2], 1, 1), '') = '' and" + - "DefinitionValue(substring(*[2], 1, 1), 'Braille', 'RussianUpperRightLabelMarksWithoutIndicator') != ''" + replace: + - x: "*[1]" + - x: "DefinitionValue(substring(*[2], 1, 1), 'Braille', 'RussianUpperRightLabelMarksWithoutIndicator')" + - test: + if: "string-length(*[2]) > 1" + then: + - x: "DefinitionValue(substring(*[2], 1, 1), 'Braille', 'RussianUpperRightLabelMarksWithoutIndicator')" + - test: + if: "string-length(*[2]) > 2" + then: + - x: "DefinitionValue(substring(*[2], 1, 1), 'Braille', 'RussianUpperRightLabelMarksWithoutIndicator')" + +- + name: upper-right-label-no-indicator + tag: msup + match: + - "not(ancestor::*[@data-chem-equation or contains(@intent, ':chemical-equation')]) and" + - "not(*[2][contains(., '+') or contains(., '-') or contains(., '−')] and" + - " *[1][self::m:mi or self::m:mtext or self::m:mrow or self::m:msub or self::m:mmultiscripts]) and" + - "DefinitionValue(*[2], 'Braille', 'RussianUpperRightLabelMarksWithoutIndicator') != ''" + replace: + - x: "*[1]" + - x: "DefinitionValue(*[2], 'Braille', 'RussianUpperRightLabelMarksWithoutIndicator')" + +- + name: upper-right-label-repeated + tag: msup + match: + - "not(ancestor::*[@data-chem-equation or contains(@intent, ':chemical-equation')]) and" + - "not(*[2][contains(., '+') or contains(., '-') or contains(., '−')] and" + - " *[1][self::m:mi or self::m:mtext or self::m:mrow or self::m:msub or self::m:mmultiscripts]) and" + - "string-length(*[2]) <= 3 and" + - "translate(*[2], substring(*[2], 1, 1), '') = '' and" + - "DefinitionValue(substring(*[2], 1, 1), 'Braille', 'RussianLabelMarks') != ''" + replace: + - x: "*[1]" + - t: "⠨" + - x: "DefinitionValue(substring(*[2], 1, 1), 'Braille', 'RussianLabelMarks')" + - test: + if: "string-length(*[2]) > 1" + then: + - x: "DefinitionValue(substring(*[2], 1, 1), 'Braille', 'RussianLabelMarks')" + - test: + if: "string-length(*[2]) > 2" + then: + - x: "DefinitionValue(substring(*[2], 1, 1), 'Braille', 'RussianLabelMarks')" + +- + name: upper-right-label + tag: msup + match: + - "not(ancestor::*[@data-chem-equation or contains(@intent, ':chemical-equation')]) and" + - "not(*[2][contains(., '+') or contains(., '-') or contains(., '−')] and" + - " *[1][self::m:mi or self::m:mtext or self::m:mrow or self::m:msub or self::m:mmultiscripts]) and" + - "DefinitionValue(*[2], 'Braille', 'RussianLabelMarks') != ''" + replace: + - x: "*[1]" + - t: "⠨" + - x: "DefinitionValue(*[2], 'Braille', 'RussianLabelMarks')" + +- + name: lower-right-label-repeated + tag: msub + match: + - "not(ancestor::*[@data-chem-equation or contains(@intent, ':chemical-equation')]) and" + - "string-length(*[2]) <= 3 and" + - "translate(*[2], substring(*[2], 1, 1), '') = '' and" + - "DefinitionValue(substring(*[2], 1, 1), 'Braille', 'RussianLabelMarks') != ''" + replace: + - x: "*[1]" + - t: "⠸" + - x: "DefinitionValue(substring(*[2], 1, 1), 'Braille', 'RussianLabelMarks')" + - test: + if: "string-length(*[2]) > 1" + then: + - x: "DefinitionValue(substring(*[2], 1, 1), 'Braille', 'RussianLabelMarks')" + - test: + if: "string-length(*[2]) > 2" + then: + - x: "DefinitionValue(substring(*[2], 1, 1), 'Braille', 'RussianLabelMarks')" + +- + name: lower-right-label + tag: msub + match: + - "not(ancestor::*[@data-chem-equation or contains(@intent, ':chemical-equation')]) and" + - "DefinitionValue(*[2], 'Braille', 'RussianLabelMarks') != ''" + replace: + - x: "*[1]" + - t: "⠸" + - x: "DefinitionValue(*[2], 'Braille', 'RussianLabelMarks')" + - name: signed-integer-number tag: [msub, munder] @@ -325,6 +536,32 @@ else: [t: "⠡"] - x: "BrailleChars(translate(*[2], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" +- + name: integer-sequence + tag: [msub, munder] + match: + - "*[2][not(self::m:mn) and translate(normalize-space(.), '0123456789⁣', '')='' and" + - " not(.//m:mo[.=',' or .='،'])]" + replace: + - x: "*[1]" + - test: + if: "self::m:munder" + then: [t: "⠨⠡"] + else: [t: "⠡"] + - x: "BrailleChars(translate(normalize-space(*[2]), '0123456789⁣', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + +- + name: infinity-index + tag: [msub, munder] + match: "*[2][.='∞']" + replace: + - x: "*[1]" + - test: + if: "self::m:munder" + then: [t: "⠨⠡"] + else: [t: "⠡"] + - x: "*[2]" + - name: default tag: [msub, munder] @@ -451,6 +688,9 @@ if: "self::m:mover" then: [t: "⠨⠌"] else: [t: "⠌"] + - test: + if: "*[2][self::m:mfrac[not((*[1][self::m:mi or self::m:mn]) and (*[2][self::m:mi or self::m:mn]))]]" + then: [t: "⠐"] - x: "*[2]" - t: "⠱" @@ -552,11 +792,95 @@ - x: "*[3]" - t: "⠱" +- + name: left-labels + tag: mmultiscripts + match: + - "not(ancestor::*[@data-chem-equation or contains(@intent, ':chemical-equation')]) and" + - "count(m:mprescripts/preceding-sibling::*) = 1 and" + - "(m:mprescripts/following-sibling::*[1][not(self::m:none) and DefinitionValue(., 'Braille', 'RussianLabelMarks') != ''] or" + - " m:mprescripts/following-sibling::*[2][not(self::m:none) and DefinitionValue(., 'Braille', 'RussianLabelMarks') != ''])" + replace: + - test: + if: "m:mprescripts/following-sibling::*[1][not(self::m:none) and DefinitionValue(., 'Braille', 'RussianLabelMarks') != '']" + then: + - t: "⠸" + - x: "DefinitionValue(m:mprescripts/following-sibling::*[1], 'Braille', 'RussianLabelMarks')" + - test: + if: "m:mprescripts/following-sibling::*[2][not(self::m:none) and DefinitionValue(., 'Braille', 'RussianLabelMarks') != '']" + then: + - t: "⠨" + - x: "DefinitionValue(m:mprescripts/following-sibling::*[2], 'Braille', 'RussianLabelMarks')" + - x: "*[1]" + - name: default tag: mmultiscripts match: "." - replace: [x: "*"] + variables: + - Prescripts: "m:mprescripts/following-sibling::*" + - NumChildren: "count(*)" + - Postscripts: "*[position()>1 and position() < (last() + ($NumChildren mod 2) - count($Prescripts))]" + replace: + - test: + if: "$Prescripts" + then: + - test: + if: "not($Prescripts[1][self::m:none])" + then: + - t: "⠡" + - test: + - if: "$Prescripts[1][self::m:mn and translate(., '0123456789', '')='']" + then: + - x: "BrailleChars(translate($Prescripts[1], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - else: + - x: "$Prescripts[1]" + - test: + if: "not($Prescripts[1][self::m:mn and translate(., '0123456789', '')='']) and not($Prescripts[1][.='∞'])" + then: [t: "⠱"] + - test: + if: "not($Prescripts[2][self::m:none])" + then: + - t: "⠌" + - test: + - if: "$Prescripts[2][self::m:mn and translate(., '0123456789', '')='']" + then: + - x: "BrailleChars(translate($Prescripts[2], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - else: + - x: "$Prescripts[2]" + - test: + if: "not($Prescripts[2][self::m:mn and translate(., '0123456789', '')='']) and not($Prescripts[2][.='∞'])" + then: [t: "⠱"] + - x: "*[1]" + - test: + if: "$Postscripts" + then: + - test: + if: "not($Postscripts[1][self::m:none])" + then: + - t: "⠡" + - test: + - if: "$Postscripts[1][self::m:mn and translate(., '0123456789', '')='']" + then: + - x: "BrailleChars(translate($Postscripts[1], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - else: + - x: "$Postscripts[1]" + - test: + if: "not($Postscripts[1][self::m:mn and translate(., '0123456789', '')='']) and not($Postscripts[1][.='∞'])" + then: [t: "⠱"] + - test: + if: "not($Postscripts[2][self::m:none])" + then: + - t: "⠌" + - test: + - if: "$Postscripts[2][self::m:mn and translate(., '0123456789', '')='']" + then: + - x: "BrailleChars(translate($Postscripts[2], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - else: + - x: "$Postscripts[2]" + - test: + if: "not($Postscripts[2][self::m:mn and translate(., '0123456789', '')='']) and not($Postscripts[2][.='∞'])" + then: [t: "⠱"] - name: default diff --git a/Rules/Braille/Russian/definitions.yaml b/Rules/Braille/Russian/definitions.yaml index de0492043..513b1d1dd 100644 --- a/Rules/Braille/Russian/definitions.yaml +++ b/Rules/Braille/Russian/definitions.yaml @@ -16,3 +16,28 @@ "grad": "\uE11B", "gradient": "\uE11B", "rot": "\uE11C", "div": "\uE11D", "gcd": "\uE11E", "lcm": "\uE11F" } + +- RussianLabelMarks: { + "′": "\uE130", "″": "\uE130\uE130", "‴": "\uE130\uE130\uE130", "'": "\uE130", + "*": "\uE131", "∗": "\uE131", "⋆": "\uE131", + "×": "\uE132", "✕": "\uE132", "✖": "\uE132", + "∘": "\uE133", "○": "\uE133", "◦": "\uE133", "°": "\uE133", + "□": "\uE134", "◻": "\uE134", + "~": "\uE135", "∼": "\uE135", "˜": "\uE135", "̃": "\uE135", + "^": "\uE136", "∧": "\uE136", "ˆ": "\uE136", "̂": "\uE136", + "∨": "\uE137", "˅": "\uE137", "ˇ": "\uE137", "̌": "\uE137", + "+": "\uE138", "-": "\uE139", "−": "\uE139" + } + +- RussianExactOverLabelMarksWithoutIndicator: { + "~": "\uE135", "∼": "\uE135", "˜": "\uE135", "̃": "\uE135", + "^": "\uE136", "∧": "\uE136", "ˆ": "\uE136", "̂": "\uE136", + "∨": "\uE137", "˅": "\uE137", "ˇ": "\uE137", "̌": "\uE137" + } + +- RussianUpperRightLabelMarksWithoutIndicator: { + "′": "\uE130", "″": "\uE130\uE130", "‴": "\uE130\uE130\uE130", "'": "\uE130", + "*": "\uE131", "∗": "\uE131", "⋆": "\uE131", + "×": "\uE132", "✕": "\uE132", "✖": "\uE132", + "+": "\uE138", "-": "\uE139", "−": "\uE139" + } diff --git a/Rules/Braille/Russian/unicode.yaml b/Rules/Braille/Russian/unicode.yaml index 53fb06938..3c4a6f537 100644 --- a/Rules/Braille/Russian/unicode.yaml +++ b/Rules/Braille/Russian/unicode.yaml @@ -20,7 +20,7 @@ - ")": [t: "⠜"] - ">": [t: "⠀⠕⠀"] - "<": [t: "⠀⠪⠀"] -- "%": [t: "⠼⠴"] +- "%": [t: "⠼⠍⠴"] - "±": [t: "⠀⠖⠤"] - ";": [t: "⠠⠢"] - "Ϳ": [t: "⠰⠊"] @@ -148,6 +148,16 @@ - "\uE11D": [t: "⠫⠙⠊⠧#"] - "\uE11E": [t: "⠫⠛⠉⠙#"] - "\uE11F": [t: "⠫⠇⠉⠍#"] +- "\uE130": [t: "⠔"] +- "\uE131": [t: "⠆"] +- "\uE132": [t: "⠦"] +- "\uE133": [t: "⠴"] +- "\uE134": [t: "⠶"] +- "\uE135": [t: "⠢"] +- "\uE136": [t: "⠲"] +- "\uE137": [t: "⠰⠔"] +- "\uE138": [t: "⠖"] +- "\uE139": [t: "⠤"] - "\ue020": [t: "⠚"] - "\ue021": [t: "⠁"] - "\ue022": [t: "⠃"] From f65933860321b7cc095dcf65c1dff020cb49e46f Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Mon, 29 Jun 2026 21:51:45 +0500 Subject: [PATCH 22/47] Add Russian braille support for periodic decimals and mixed numbers Add braille rules for rendering periodic (repeating) decimals like 0,4(71) and mixed numbers with simple/arithmetic fractions in Russian. Includes regex cleanup to remove redundant number indicators in periodic decimal portions and comprehensive test coverage. --- Rules/Braille/Russian/Russian_Rules.yaml | 94 ++++++++++++++++++++++++ src/braille.rs | 7 +- 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index ac3a79c1d..7fa55a5d3 100644 --- a/Rules/Braille/Russian/Russian_Rules.yaml +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -50,6 +50,77 @@ match: "not(preceding-sibling::*) and following-sibling::*[1][self::m:mtable] and count(../*)=2" replace: [t: ""] +- + name: periodic-decimal-row + tag: mrow + match: + - "count(*)=4 and" + - "*[1][self::m:mn and contains(., ',') and translate(., '0123456789,', '')=''] and" + - "*[2][self::m:mo and .='('] and" + - "*[3][self::m:mn and translate(., '0123456789', '')=''] and" + - "*[4][self::m:mo and .=')']" + replace: + - x: "*[1]" + - t: "⠣" + - x: "BrailleChars(translate(string(*[3]), '0123456789', '\ue020\ue021\ue022\ue023\ue024\ue025\ue026\ue027\ue028\ue029'), 'Russian')" + - t: "⠜" + +- + name: mixed-number-simple-fraction-with-invisible-times + tag: mrow + match: + - "count(*)=3 and" + - "*[1][self::m:mn and translate(., '0123456789', '')=''] and" + - "*[2][self::m:mo and (.='⁢' or .='⁤')] and" + - "*[3][self::m:mfrac] and" + - "*[3]/*[1][self::m:mn and translate(., '0123456789', '')=''] and" + - "*[3]/*[2][self::m:mn and translate(., '0123456789', '')='']" + replace: + - x: "*[1]" + - t: "#" + - x: "*[3]" + +- + name: mixed-number-simple-fraction + tag: mrow + match: + - "count(*)=2 and" + - "*[1][self::m:mn and translate(., '0123456789', '')=''] and" + - "*[2][self::m:mfrac] and" + - "*[2]/*[1][self::m:mn and translate(., '0123456789', '')=''] and" + - "*[2]/*[2][self::m:mn and translate(., '0123456789', '')='']" + replace: + - x: "*[1]" + - t: "#" + - x: "*[2]" + +- + name: mixed-number-with-arithmetic-fraction-with-invisible-times + tag: mrow + match: + - "count(*)=3 and" + - "*[1][self::m:mn and translate(., '0123456789', '')=''] and" + - "*[2][self::m:mo and (.='⁢' or .='⁤')] and" + - "*[3][self::m:mfrac] and" + - "not(*[3]/*[1][self::m:mn and translate(., '0123456789', '')=''] and *[3]/*[2][self::m:mn and translate(., '0123456789', '')=''])" + replace: + - x: "*[1]" + - t: "⠐" + - x: "*[3]" + +- + name: mixed-number-with-arithmetic-fraction + tag: mrow + match: + - "count(*)=2 and" + - "*[1][self::m:mn and translate(., '0123456789', '')=''] and" + - "*[2][self::m:mfrac] and" + - "not(*[2]/*[1][self::m:mn and translate(., '0123456789', '')=''] and *[2]/*[2][self::m:mn and translate(., '0123456789', '')=''])" + replace: + - x: "*[1]" + - t: "⠐" + - x: "*[2]" + - name: default tag: [mrow, mstyle, semantics] @@ -967,6 +1038,29 @@ replace: - x: "text()" +- + name: periodic-decimal-period-digits + tag: mn + match: + - "translate(., '0123456789', '')='' and" + - "preceding-sibling::*[1][self::m:mo and .='('] and" + - "following-sibling::*[1][self::m:mo and .=')'] and" + - "preceding-sibling::*[2][self::m:mn and contains(., ',')]" + replace: + - x: "BrailleChars(translate(., '0123456789', '\ue020\ue021\ue022\ue023\ue024\ue025\ue026\ue027\ue028\ue029'), 'Russian')" + +- + name: periodic-decimal + tag: mn + match: "contains(., ',') and substring-after(., '(') != '' and substring-before(substring-after(., '('), ')') != ''" + replace: + - x: "BrailleChars(substring-before(., ','), 'Russian')" + - t: "⠂" + - x: "BrailleChars(translate(substring-before(substring-after(., ','), '('), '0123456789', '\ue020\ue021\ue022\ue023\ue024\ue025\ue026\ue027\ue028\ue029'), 'Russian')" + - t: "⠣" + - x: "BrailleChars(translate(substring-before(substring-after(., '('), ')'), '0123456789', '\ue020\ue021\ue022\ue023\ue024\ue025\ue026\ue027\ue028\ue029'), 'Russian')" + - t: "⠜" + - name: decimal-comma tag: mn diff --git a/src/braille.rs b/src/braille.rs index 2bcba4edc..6874e9d02 100644 --- a/src/braille.rs +++ b/src/braille.rs @@ -2171,6 +2171,9 @@ fn swedish_cleanup(pref_manager: Ref, raw_braille: String) -> fn russian_cleanup(_pref_manager: Ref, raw_braille: String) -> String { static REPLACE_INDICATORS: LazyLock = LazyLock::new(|| Regex::new(r"([BCILNW#])").unwrap()); static COLLAPSE_SPACES: LazyLock = LazyLock::new(|| Regex::new(r"⠀+").unwrap()); + static PERIODIC_DECIMAL_NUM_INDICATOR: LazyLock = LazyLock::new(|| { + Regex::new(r"(N[⠚⠁⠃⠉⠙⠑⠋⠛⠓⠊]+⠂[⠚⠁⠃⠉⠙⠑⠋⠛⠓⠊]*⠣)N([⠚⠁⠃⠉⠙⠑⠋⠛⠓⠊]+⠜)").unwrap() + }); let mut raw_braille_without_repeated_number_indicators = String::with_capacity(raw_braille.len()); let mut previous_char_was_digit = false; @@ -2183,7 +2186,9 @@ fn russian_cleanup(_pref_manager: Ref, raw_braille: String) - previous_char_was_digit = matches!(ch, '⠚' | '⠁' | '⠃' | '⠉' | '⠙' | '⠑' | '⠋' | '⠛' | '⠓' | '⠊'); } - let result = add_russian_alphabet_indicators(&raw_braille_without_repeated_number_indicators); + let raw_braille_without_periodic_number_indicator = PERIODIC_DECIMAL_NUM_INDICATOR + .replace_all(&raw_braille_without_repeated_number_indicators, "$1$2"); + let result = add_russian_alphabet_indicators(&raw_braille_without_periodic_number_indicator); let result = REPLACE_INDICATORS.replace_all(&result, |cap: &Captures| { match &cap[0] { "B" => "⠸", From 3f84b47b61f2aab5d5069fe7d508d5368da2820d Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Tue, 30 Jun 2026 02:49:15 +0500 Subject: [PATCH 23/47] Add Russian braille rules for geometry operators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add three new braille encoding rules for geometry operators (∠, △, ∪) in Russian: - canonical-prefix-geometry-op: handles geometry operators in canonical form - geometry-prefix-after-canonical-multiplier: handles operators after canonical multipliers - geometry-prefix-after-multiplier: handles operators after regular multipliers Includes corresponding test cases for angles, triangles, unions, perpendicular, and similar symbols. --- Rules/Braille/Russian/Russian_Rules.yaml | 44 ++++++++++++++++++++++++ tests/braille/Russian/russian.rs | 21 +++++++++++ 2 files changed, 65 insertions(+) diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index 7fa55a5d3..34551123b 100644 --- a/Rules/Braille/Russian/Russian_Rules.yaml +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -50,6 +50,34 @@ match: "not(preceding-sibling::*) and following-sibling::*[1][self::m:mtable] and count(../*)=2" replace: [t: ""] +- + name: canonical-prefix-geometry-op + tag: mrow + match: + - "count(*)=3 and" + - "*[1][(self::m:mo or self::m:mi) and (.='∠' or .='△' or .='∪')] and" + - "*[2][self::m:mo and .='⁡']" + replace: + - test: + - if: "*[1][.='∪']" + then: [t: "⠸⠜"] + - else: + - x: "*[1]/text()" + - t: "#" + - x: "*[3]" + +- + name: geometry-prefix-after-canonical-multiplier + tag: mrow + match: + - "count(*)=3 and" + - "*[2][self::m:mo and (.='⁢' or .='⁤')] and" + - "*[3][self::m:mrow and *[1][(self::m:mo or self::m:mi) and (.='∠' or .='△' or .='∪')]]" + replace: + - x: "*[1]" + - t: "⠄" + - x: "*[3]" + - name: periodic-decimal-row tag: mrow @@ -1007,6 +1035,22 @@ - else: - x: "text()" +- + name: geometry-prefix-after-multiplier + tag: mo + match: + - "(.='∠' or .='△' or .='∪') and" + - "preceding-sibling::*[1][not(self::m:mo)] and" + - "following-sibling::*[1][self::m:mi or self::m:msub or self::m:mrow]" + replace: + - t: "⠄" + - test: + - if: ".='∪'" + then: [t: "⠸⠜"] + - else: + - x: "text()" + - t: "#" + - name: prefix-geometry-ops tag: mo diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index d5aae2769..d0567c00c 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -273,9 +273,30 @@ fn source_geometry_matrix_chemistry() -> Result<()> { let expr = r#"ABC=15°3012"#; test_braille("Russian", expr, "⠸⠪⠨⠁⠃⠉⠀⠶⠼⠁⠑⠨⠴⠼⠉⠚⠔⠼⠁⠃⠔⠔")?; + let expr = r#"A1B1C1"#; + test_braille("Russian", expr, "⠸⠙⠨⠁⠡⠂⠃⠡⠂⠉⠡⠂")?; + + let expr = r#"2ABC"#; + test_braille("Russian", expr, "⠼⠃⠄⠸⠪⠨⠁⠃⠉")?; + + let expr = r#"3ABC"#; + test_braille("Russian", expr, "⠼⠉⠄⠸⠙⠨⠁⠃⠉")?; + + let expr = r#"4EF"#; + test_braille("Russian", expr, "⠼⠙⠄⠸⠜⠨⠑⠋")?; + + let expr = r#"KLMPQR"#; + test_braille("Russian", expr, "⠸⠙⠨⠅⠇⠍⠀⠢⠸⠙⠨⠏⠟⠗")?; + let expr = r#"ABCD"#; test_braille("Russian", expr, "⠨⠁⠃⠸⠸⠨⠉⠙")?; + let expr = r#"ab"#; + test_braille("Russian", expr, "⠠⠁⠼⠄⠃")?; + + let expr = r#"ABCD"#; + test_braille("Russian", expr, "⠨⠁⠃⠼⠄⠉⠙")?; + let expr = r#"a=2"#; test_braille("Russian", expr, "⠠⠁⠒⠂⠀⠶⠼⠃")?; From 25f17c61f4b09e1cd23a19ab9b837790986eb212 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Tue, 30 Jun 2026 03:02:32 +0500 Subject: [PATCH 24/47] Adjust Russian braille union handling and add tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the special-case braille for the union operator out of the global unicode map and into the Russian rule that handles geometric prefix usage, and tighten the rule match to only trigger when preceded by a number. Update unicode.yaml to a consistent union mapping. Add unit tests covering set relations (∈, ∉, ⊂, ⊄, ∩, ∪), empty set, and quantifiers (∀, ∃) plus geometry/operator cases to validate the new behavior. Files changed: Rules/Braille/Russian/Russian_Rules.yaml, Rules/Braille/Russian/unicode.yaml, tests/braille/Russian/russian.rs --- Rules/Braille/Russian/Russian_Rules.yaml | 11 ++++++++--- Rules/Braille/Russian/unicode.yaml | 6 +----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index 34551123b..2c7f18f12 100644 --- a/Rules/Braille/Russian/Russian_Rules.yaml +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -1040,7 +1040,7 @@ tag: mo match: - "(.='∠' or .='△' or .='∪') and" - - "preceding-sibling::*[1][not(self::m:mo)] and" + - "preceding-sibling::*[1][self::m:mn] and" - "following-sibling::*[1][self::m:mi or self::m:msub or self::m:mrow]" replace: - t: "⠄" @@ -1055,10 +1055,15 @@ name: prefix-geometry-ops tag: mo match: - - "parent::*[count(*) = 2 and name()='mrow' and not(preceding-sibling::*)] and" + - "not(preceding-sibling::*) and" + - "following-sibling::*[1][self::m:mi or self::m:msub or self::m:mrow] and" - "( .='∠' or .='△' or .='∪' )" replace: - - x: "text()" + - test: + - if: ".='∪'" + then: [t: "⠸⠜"] + - else: + - x: "text()" - t: "#" - diff --git a/Rules/Braille/Russian/unicode.yaml b/Rules/Braille/Russian/unicode.yaml index 3c4a6f537..6882af305 100644 --- a/Rules/Braille/Russian/unicode.yaml +++ b/Rules/Braille/Russian/unicode.yaml @@ -180,11 +180,7 @@ - "∧": [t: "⠀⠰⠢"] - "∨": [t: "⠀⠰⠔"] - "∩": [t: "⠀⠰⠲"] -- "∪": - - test: - if: "following-sibling::*[1][self::m:mi]" - then: [t: "⠸⠜"] - else: [t: "⠀⠰⠴"] +- "∪": [t: "⠀⠰⠴"] - "∫": [t: "⠮"] - "∬": [t: "⠮⠮"] - "∭": [t: "⠮⠮⠮"] From c996301bd1106ad0f898316b1810e7a61a72d752 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Tue, 30 Jun 2026 03:23:05 +0500 Subject: [PATCH 25/47] Improve Russian braille typeform and mathvariant handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor Russian braille cleanup to properly handle typeform indicators (bold, italic) and mathvariant attributes. Introduces a dedicated typeform indicator function that manages indicator placement according to Russian Braille ГОСТ Р 58511 standard, separating typeform handling from alphabet indicators. Adds support for automatic typeform indicators based on mathvariant attributes, ensuring proper indicator pairing around content blocks. Updates unicode-full.yaml rules to reflect new typeform codes and adds comprehensive tests for various mathvariant combinations. --- Rules/Braille/Russian/unicode-full.yaml | 14 +-- src/braille.rs | 153 ++++++++++++++++++++++-- tests/braille/Russian/russian.rs | 25 ++++ 3 files changed, 177 insertions(+), 15 deletions(-) diff --git a/Rules/Braille/Russian/unicode-full.yaml b/Rules/Braille/Russian/unicode-full.yaml index f78feac35..813669ecc 100644 --- a/Rules/Braille/Russian/unicode-full.yaml +++ b/Rules/Braille/Russian/unicode-full.yaml @@ -107,7 +107,7 @@ # bold script chars in math alphabetic block - "𝓐-𝓩": # 0x1d4d0 - 0x1d4e9 - - tc: "BTs" + - tc: "BTsC" - spell: "translate('.', '𝓐𝓑𝓒𝓓𝓔𝓕𝓖𝓗𝓘𝓙𝓚𝓛𝓜𝓝𝓞𝓟𝓠𝓡𝓢𝓣𝓤𝓥𝓦𝓧𝓨𝓩', 'abcdefghijklmnopqrstuvwxyz')" - "𝓪-𝔃": # 0x1d4ea - 0x1d503 @@ -115,7 +115,7 @@ - spell: "translate('.', '𝓪𝓫𝓬𝓭𝓮𝓯𝓰𝓱𝓲𝓳𝓴𝓵𝓶𝓷𝓸𝓹𝓺𝓻𝓼𝓽𝓾𝓿𝔀𝔁𝔂𝔃', 'abcdefghijklmnopqrstuvwxyz')" - "𝐀-𝐙": # 0x1d400 - 0x1d419 - - tc: "B" + - tc: "BC" - spell: "translate('.', '𝐀𝐁𝐂𝐃𝐄𝐅𝐆𝐇𝐈𝐉𝐊𝐋𝐌𝐍𝐎𝐏𝐐𝐑𝐒𝐓𝐔𝐕𝐖𝐗𝐘𝐙', 'abcdefghijklmnopqrstuvwxyz')" - "𝐚-𝐳": # 0x1d41a - 0x1d433 @@ -123,12 +123,11 @@ - spell: "translate('.', '𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳', 'abcdefghijklmnopqrstuvwxyz')" - "𝐴-𝑍": # 0x1d434 - 0x1d44d - # don't include italics - - tc: "C" + - tc: "IC" - spell: "translate('.', '𝐴𝐵𝐶𝐷𝐸𝐹𝐺𝐻𝐼𝐽𝐾𝐿𝑀𝑁𝑂𝑃𝑄𝑅𝑆𝑇𝑈𝑉𝑊𝑋𝑌𝑍', 'abcdefghijklmnopqrstuvwxyz')" - "𝑎-𝑧": # 0x1d44e - 0x1d467 - # don't include italics + - tc: "I" - spell: "translate('.', '𝑎𝑏𝑐𝑑𝑒𝑓𝑔𝑕𝑖𝑗𝑘𝑙𝑚𝑛𝑜𝑝𝑞𝑟𝑠𝑡𝑢𝑣𝑤𝑥𝑦𝑧', 'abcdefghijklmnopqrstuvwxyz')" - "𝑨-𝒁": # 0x1d468 - 0x1d481 @@ -136,6 +135,7 @@ - spell: "translate('.', '𝑨𝑩𝑪𝑫𝑬𝑭𝑮𝑯𝑰𝑱𝑲𝑳𝑴𝑵𝑶𝑷𝑸𝑹𝑺𝑻𝑼𝑽𝑾𝑿𝒀𝒁', 'abcdefghijklmnopqrstuvwxyz')" - "𝒂-𝒛": # 0x1d482 - 0x1d49b + - tc: "BI" - spell: "translate('.', '𝒂𝒃𝒄𝒅𝒆𝒇𝒈𝒉𝒊𝒋𝒌𝒍𝒎𝒏𝒐𝒑𝒒𝒓𝒔𝒕𝒖𝒗𝒘𝒙𝒚𝒛', 'abcdefghijklmnopqrstuvwxyz')" - "𝖠-𝖹": # 0x1d5a0 - 0x1d5b9 @@ -157,11 +157,11 @@ - spell: "translate('.', '𝗮𝗯𝗰𝗱𝗲𝗳𝗴𝗵𝗶𝗷𝗸𝗹𝗺𝗻𝗼𝗽𝗾𝗿𝘀𝘁𝘂𝘃𝘄𝘅𝘆𝘇', 'abcdefghijklmnopqrstuvwxyz')" - "𝘈-𝘡": # 0x1d608 - 0x1d621 - # - tc: "italic" + - tc: "IC" - spell: "translate('.', '𝘈𝘉𝘊𝘋𝘌𝘍𝘎𝘏𝘐𝘑𝘒𝘓𝘔𝘕𝘖𝘗𝘘𝘙𝘚𝘛𝘜𝘝𝘞𝘟𝘠𝘡', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" - "𝘢-𝘻": # 0x1d622 - 0x1d63b - # - tc: "italic" + - tc: "I" - spell: "translate('.', '𝘢𝘣𝘤𝘥𝘦𝘧𝘨𝘩𝘪𝘫𝘬𝘭𝘮𝘯𝘰𝘱𝘲𝘳𝘴𝘵𝘶𝘷𝘸𝘹𝘺𝘻', 'abcdefghijklmnopqrstuvwxyz')" - "𝘼-𝙕": # 0x1d63c - 0x1d655 diff --git a/src/braille.rs b/src/braille.rs index 6874e9d02..82dcdd887 100644 --- a/src/braille.rs +++ b/src/braille.rs @@ -2169,7 +2169,7 @@ fn swedish_cleanup(pref_manager: Ref, raw_braille: String) -> } fn russian_cleanup(_pref_manager: Ref, raw_braille: String) -> String { - static REPLACE_INDICATORS: LazyLock = LazyLock::new(|| Regex::new(r"([BCILNW#])").unwrap()); + static REPLACE_INDICATORS: LazyLock = LazyLock::new(|| Regex::new(r"([LNW#])").unwrap()); static COLLAPSE_SPACES: LazyLock = LazyLock::new(|| Regex::new(r"⠀+").unwrap()); static PERIODIC_DECIMAL_NUM_INDICATOR: LazyLock = LazyLock::new(|| { Regex::new(r"(N[⠚⠁⠃⠉⠙⠑⠋⠛⠓⠊]+⠂[⠚⠁⠃⠉⠙⠑⠋⠛⠓⠊]*⠣)N([⠚⠁⠃⠉⠙⠑⠋⠛⠓⠊]+⠜)").unwrap() @@ -2188,12 +2188,11 @@ fn russian_cleanup(_pref_manager: Ref, raw_braille: String) - let raw_braille_without_periodic_number_indicator = PERIODIC_DECIMAL_NUM_INDICATOR .replace_all(&raw_braille_without_repeated_number_indicators, "$1$2"); - let result = add_russian_alphabet_indicators(&raw_braille_without_periodic_number_indicator); + let result = add_russian_typeform_indicators(&raw_braille_without_periodic_number_indicator); + let result = add_russian_alphabet_indicators(&result); let result = REPLACE_INDICATORS.replace_all(&result, |cap: &Captures| { match &cap[0] { - "B" => "⠸", - "C" => "⠠", - "I" => "⠨", + "C" => "⠨", "L" => "", "N" => "⠼", "W" => "⠀", @@ -2206,12 +2205,111 @@ fn russian_cleanup(_pref_manager: Ref, raw_braille: String) - .trim_matches('⠀') .to_string(); + fn add_russian_typeform_indicators(raw_braille: &str) -> String { + let mut result = String::with_capacity(raw_braille.len()); + let mut active_typeforms: Vec = Vec::with_capacity(2); + let mut pending_typeforms: Vec = Vec::with_capacity(2); + let mut previous_was_capital_marker = false; + + for ch in raw_braille.chars() { + if is_russian_typeform_marker(ch) { + if !pending_typeforms.contains(&ch) { + pending_typeforms.push(ch); + } + previous_was_capital_marker = false; + continue; + } + if matches!(ch, 's' | 'w' | 'e') && (!pending_typeforms.is_empty() || !active_typeforms.is_empty()) { + continue; + } + + if is_russian_token_start(ch) { + if !previous_was_capital_marker { + close_inactive_typeforms(&mut result, &mut active_typeforms, &pending_typeforms); + } + for &typeform in &pending_typeforms { + if !active_typeforms.contains(&typeform) { + result.push_str(russian_typeform_indicator(typeform)); + active_typeforms.push(typeform); + } + } + pending_typeforms.clear(); + } else if !pending_typeforms.is_empty() { + close_inactive_typeforms(&mut result, &mut active_typeforms, &pending_typeforms); + for &typeform in &pending_typeforms { + if !active_typeforms.contains(&typeform) { + result.push_str(russian_typeform_indicator(typeform)); + result.push_str(russian_typeform_indicator(typeform)); + } + } + pending_typeforms.clear(); + } else if !active_typeforms.is_empty() && !is_russian_token_body(ch) { + close_all_typeforms(&mut result, &mut active_typeforms); + } + + result.push(ch); + previous_was_capital_marker = ch == 'C'; + } + + close_all_typeforms(&mut result, &mut active_typeforms); + return result; + } + + fn is_russian_typeform_marker(ch: char) -> bool { + matches!(ch, 'B' | 'I' | 'T' | 'D' | 'S' | '𝔹') + } + + fn is_russian_token_start(ch: char) -> bool { + matches!(ch, 'l' | 'u' | 'g' | 'v' | 'L' | 'N' | 'C') + } + + fn is_russian_token_body(ch: char) -> bool { + ('\u{2801}'..='\u{28ff}').contains(&ch) + } + + fn close_inactive_typeforms(result: &mut String, active_typeforms: &mut Vec, pending_typeforms: &[char]) { + let mut i = active_typeforms.len(); + while i > 0 { + i -= 1; + if !pending_typeforms.contains(&active_typeforms[i]) { + let typeform = active_typeforms.remove(i); + result.push_str(russian_typeform_indicator(typeform)); + } + } + } + + fn close_all_typeforms(result: &mut String, active_typeforms: &mut Vec) { + while let Some(typeform) = active_typeforms.pop() { + result.push_str(russian_typeform_indicator(typeform)); + } + } + + fn russian_typeform_indicator(typeform: char) -> &'static str { + match typeform { + 'B' => "⠻", // ГОСТ Р 58511: жирный шрифт, точки 12456. + 'I' | 'T' | 'D' | 'S' | '𝔹' => "⠸", // ГОСТ Р 58511: курсив/шрифтовое выделение, точки 456. + _ => "", + } + } + fn add_russian_alphabet_indicators(raw_braille: &str) -> String { let mut result = String::with_capacity(raw_braille.len()); let mut alphabet_mode = None; + let mut capital_marker_pending = false; for ch in raw_braille.chars() { match ch { + 'C' => { + if alphabet_mode != Some('u') { + result.push_str("⠨"); + alphabet_mode = Some('u'); + } + capital_marker_pending = true; + }, + 'l' if capital_marker_pending => { + capital_marker_pending = false; + }, 'l' | 'u' | 'g' | 'v' => { + capital_marker_pending = false; if alphabet_mode != Some(ch) { result.push_str(match ch { 'l' => "⠠", // Latin lowercase: dots 6 @@ -2223,11 +2321,15 @@ fn russian_cleanup(_pref_manager: Ref, raw_braille: String) - alphabet_mode = Some(ch); } }, - 'C' | 'N' | '#' => { + 'N' | '#' => { + capital_marker_pending = false; alphabet_mode = None; result.push(ch); }, - _ => result.push(ch), + _ => { + capital_marker_pending = false; + result.push(ch); + }, } } return result; @@ -2397,7 +2499,7 @@ impl BrailleChars { "Vietnam" => BrailleChars:: get_braille_vietnam_chars(node, text_range), "Swedish" => BrailleChars:: get_braille_ueb_chars(node, text_range), // FIX: need to figure out what to implement "Finnish" => BrailleChars:: get_braille_ueb_chars(node, text_range), // FIX: need to figure out what to implement - "Russian" => BrailleChars:: get_braille_ueb_chars(node, text_range), + "Russian" => BrailleChars:: get_braille_russian_chars(node, text_range), _ => return Err(sxd_xpath::function::Error::Other(format!("get_braille_chars: unknown braille code '{code}'"))) }; return match result { @@ -2534,6 +2636,41 @@ impl BrailleChars { return Ok(result.to_string()) } + fn get_braille_russian_chars(node: Element, text_range: Option>) -> Result { + let text = BrailleChars::substring(as_text(node), &text_range); + let braille_chars = braille_replace_chars(&text, node)?; + let Some(math_variant) = node.attribute_value("mathvariant") else { + return Ok(braille_chars); + }; + + let mut prefix = String::new(); + let mut suffix = String::new(); + if math_variant.contains("bold") && !braille_chars.contains('B') { + prefix.push_str("⠻"); + suffix.insert_str(0, "⠻"); + } + if math_variant.contains("italic") && !braille_chars.contains('I') { + prefix.push_str("⠸"); + suffix.insert_str(0, "⠸"); + } + let has_typeface_marker = braille_chars.contains('T') + || braille_chars.contains('D') + || braille_chars.contains('S') + || braille_chars.contains('𝔹'); + if !has_typeface_marker { + let typeface = match math_variant { + "double-struck" | "script" | "fraktur" | "sans-serif" => Some("⠸"), + _ => None, + }; + if let Some(indicator) = typeface { + prefix.push_str(indicator); + suffix.insert_str(0, indicator); + } + } + + return Ok(prefix + &braille_chars + &suffix); + } + fn get_braille_cmu_chars(node: Element, text_range: Option>) -> Result { // In CMU, we need to replace spaces used for number blocks with "." // For other numbers, we need to add "." to create digit blocks diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index d0567c00c..184d4ad80 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -194,6 +194,31 @@ fn alphabet_indicators_after_numbers_and_greek() -> Result<()> { return Ok(()); } +#[test] +fn source_typeform_and_mathvariant_indicators() -> Result<()> { + let expr = r#"x"#; + test_braille("Russian", expr, "⠻⠠⠭⠻")?; + + let expr = r#"y"#; + test_braille("Russian", expr, "⠸⠠⠽⠸")?; + + let expr = r#"z"#; + test_braille("Russian", expr, "⠻⠸⠠⠵⠸⠻")?; + + let expr = r#"AB"#; + test_braille("Russian", expr, "⠻⠨⠁⠃⠻")?; + + let expr = r#"x+y"#; + test_braille("Russian", expr, "⠻⠠⠭⠻⠀⠖⠽")?; + + let expr = r#"𝐱"#; + test_braille("Russian", expr, "⠻⠠⠭⠻")?; + + let expr = r#"𝑥"#; + test_braille("Russian", expr, "⠸⠠⠭⠸")?; + return Ok(()); +} + #[test] fn wikipedia_times_divide() -> Result<()> { let expr = r#"6×7:14=3"#; From 2bdb2dc394fb469ebb6cb21d14c07d57452bef33 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Tue, 30 Jun 2026 03:52:32 +0500 Subject: [PATCH 26/47] Add Russian Braille chemistry notation rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for chemical equations in Russian Braille, including: - Single-letter element group notation with prefix marker ⠸ - Reaction arrow handling for mover, munder, and munderover (above, below, above-and-below) - Improved chemical element rendering with InChemElementGroup context variable Includes comprehensive test cases for various chemical formula patterns and reaction arrow configurations. --- Rules/Braille/Russian/Russian_Rules.yaml | 60 ++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index 2c7f18f12..15bc921ee 100644 --- a/Rules/Braille/Russian/Russian_Rules.yaml +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -41,6 +41,7 @@ - RowStart: "''" - RowEnd: "''" - InSystemLeftBrace: "false()" + - InChemElementGroup: "false()" - MatchingWhitespace: "false()" replace: [x: "*"] @@ -50,6 +51,19 @@ match: "not(preceding-sibling::*) and following-sibling::*[1][self::m:mtable] and count(../*)=2" replace: [t: ""] +- + name: chemistry-single-letter-element-group + tag: mrow + match: + - "(@data-chem-formula or ancestor::*[@data-chem-equation or contains(@intent, ':chemical-equation')]) and" + - "count(*[not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])]) > 1 and" + - "count(*[not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])]) = count(*[self::m:mi[@data-chem-element='1']])" + replace: + - t: "⠸" + - with: + variables: [InChemElementGroup: "true()"] + replace: [x: "*"] + - name: canonical-prefix-geometry-op tag: mrow @@ -348,6 +362,45 @@ replace: - t: "⠀⠒⠕⠀⠦⠶" +- + name: chemistry-reaction-arrow-above + tag: mover + match: + - "(ancestor::*[@data-chem-equation or contains(@intent, ':chemical-equation')] or @data-chem-equation-op) and" + - "*[1][self::m:mo and translate(., '→⟶←⟵⇌⥂⥄↑↓', '')='']" + replace: + - x: "*[1]" + - t: "⠨⠌" + - x: "*[2]" + - t: "⠱" + +- + name: chemistry-reaction-arrow-below + tag: munder + match: + - "(ancestor::*[@data-chem-equation or contains(@intent, ':chemical-equation')] or @data-chem-equation-op) and" + - "*[1][self::m:mo and translate(., '→⟶←⟵⇌⥂⥄↑↓', '')='']" + replace: + - x: "*[1]" + - t: "⠨⠡" + - x: "*[2]" + - t: "⠱" + +- + name: chemistry-reaction-arrow-above-and-below + tag: munderover + match: + - "(ancestor::*[@data-chem-equation or contains(@intent, ':chemical-equation')] or @data-chem-equation-op) and" + - "*[1][self::m:mo and translate(., '→⟶←⟵⇌⥂⥄↑↓', '')='']" + replace: + - x: "*[1]" + - t: "⠨⠡" + - x: "*[2]" + - t: "⠱" + - t: "⠨⠌" + - x: "*[3]" + - t: "⠱" + - name: vector-arrow tag: mover @@ -1133,6 +1186,13 @@ replace: - x: "DefinitionValue(., 'Braille', 'RussianFunctionBraille')" +- + name: chemistry-single-letter-element-in-group + tag: mi + match: "$InChemElementGroup and @data-chem-element='1'" + replace: + - x: "translate(BrailleChars(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'Russian'), 'l', '')" + - name: div-function tag: [mi, mo, mtext] From f245976f4a3e5c9cf5d3ec34d9f647db23a9fec1 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Tue, 30 Jun 2026 04:13:27 +0500 Subject: [PATCH 27/47] Add Russian braille mappings and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update Russian braille symbol map and extend tests. Rules/Braille/Russian/unicode.yaml: add mappings for №, §, …, ↵, ⏎ and ⍰. tests/braille/Russian/russian.rs: add source_general_math_text_rules unit (covers №, §, ellipsis, return symbols and mixed expressions) and a roman-numeral Latin-indicator test. Ensures MathCAT produces expected braille outputs for these symbols and cases. --- Rules/Braille/Russian/unicode.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Rules/Braille/Russian/unicode.yaml b/Rules/Braille/Russian/unicode.yaml index 6882af305..f87979bfe 100644 --- a/Rules/Braille/Russian/unicode.yaml +++ b/Rules/Braille/Russian/unicode.yaml @@ -6,6 +6,12 @@ - "?": [t: "⠠⠢"] - ":": [t: "⠀⠲"] - ";": [t: "⠠⠆"] +- "№": [t: "⠝"] +- "§": [t: "⠬"] +- "…": [t: "⠠⠄"] +- "↵": [t: "⠐"] +- "⏎": [t: "⠠"] +- "⍰": [t: "⠐"] - "[": [t: "⠷"] - "]": [t: "⠾"] - "{": [t: "⠪"] From 2062a0ab583d3731f09ceedeb3a2641944c150fd Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Tue, 30 Jun 2026 11:09:21 +0500 Subject: [PATCH 28/47] Add column arithmetic and enhance Russian braille rules Adds support for column arithmetic formatting with dedicated rules for operators and continuation numbers. Improves fraction rendering for negative denominators and decimal numbers. Enhances trigonometric function handling with power notation and mixed subscript/superscript support. Adds comprehensive tests covering column arithmetic, thin fractions, indexes, and various mathematical constructs. --- Rules/Braille/Russian/Russian_Rules.yaml | 140 ++++++++++++++++++++++- 1 file changed, 135 insertions(+), 5 deletions(-) diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index 15bc921ee..c62168c51 100644 --- a/Rules/Braille/Russian/Russian_Rules.yaml +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -27,6 +27,19 @@ match: "contains(@intent, ':blank')" replace: [t: "⠀"] +- + name: omitted-digit-in-number + tag: mrow + match: + - "count(*)=3 and" + - "*[1][self::m:mn and translate(., '0123456789', '')=''] and" + - "*[2][contains(@intent, ':blank')] and" + - "*[3][self::m:mn and translate(., '0123456789', '')='']" + replace: + - x: "*[1]" + - t: "⠬" + - x: "BrailleChars(translate(*[3], '0123456789', '\ue020\ue021\ue022\ue023\ue024\ue025\ue026\ue027\ue028\ue029'), 'Russian')" + - name: unicode-override tag: "*" @@ -42,6 +55,7 @@ - RowEnd: "''" - InSystemLeftBrace: "false()" - InChemElementGroup: "false()" + - InColumnArithmetic: "false()" - MatchingWhitespace: "false()" replace: [x: "*"] @@ -169,6 +183,22 @@ match: "." replace: [x: "*[1] | *[position()>1]"] +- + name: mspace + tag: mspace + match: "@linebreak='newline' or @linebreak='indentingnewline'" + replace: + - test: + - if: "string(@width) != '' or @data-previous-space-width >= 0.25 or @data-following-space-width >= 0.25" + then: [t: "⠠"] + - else: [t: "⠐"] + +- + name: mspace + tag: mspace + match: "." + replace: [t: "W"] + - name: no-content tag: [math, mrow] @@ -227,17 +257,39 @@ - x: "*[1]" - x: "BrailleChars(translate(*[2], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" +- + name: simple-algebraic-negative-integer-denominator + tag: mfrac + match: + - "(*[1][self::m:mi] or *[1][self::m:mn and translate(., '0123456789,', '')='']) and" + - "*[2][self::m:mrow and count(*)=2 and *[1][self::m:mo and (.='-' or .='−')] and *[2][self::m:mn and translate(., '0123456789', '')='']]" + replace: + - x: "*[1]" + - t: "⠳⠤" + - x: "BrailleChars(translate(*[2]/*[2], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + +- + name: simple-algebraic-negative-decimal-denominator + tag: mfrac + match: + - "(*[1][self::m:mi] or *[1][self::m:mn and translate(., '0123456789,', '')='']) and" + - "*[2][self::m:mrow and count(*)=2 and *[1][self::m:mo and (.='-' or .='−')] and *[2][self::m:mn and contains(., ',') and translate(., '0123456789,', '')='']]" + replace: + - x: "*[1]" + - t: "⠳⠤" + - x: "*[2]/*[2]" + - 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', '')=''])" + - "(*[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]" + 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: @@ -278,6 +330,16 @@ - x: "*[2]/*[1]/*[1]/*[1]" - x: "*[3]" +- + name: column-arithmetic-table + tag: mtable + variables: + - InColumnArithmetic: "true()" + - RowStart: "''" + - RowEnd: "''" + match: "contains(@intent, ':column-arithmetic')" + replace: [x: "*"] + - name: system-left-brace-table tag: mtable @@ -725,7 +787,7 @@ then: [t: "⠨⠡"] else: [t: "⠡"] - test: - if: "*[2][self::m:mrow[m:mo[.='+']] or self::m:mfrac]" + if: "*[2][self::m:mrow[m:mo[.='+'] or m:mfrac] or self::m:mfrac]" then: [t: "⠐"] - x: "*[2]" - t: "⠱" @@ -818,6 +880,21 @@ - else: - x: "*[2]" +- + name: trig-function-integer-power-before-complex-fraction + tag: msup + match: + - "DefinitionValue(*[1], 'Braille', 'RussianFunctionBraille') != '' and" + - "*[2][self::m:mn and translate(., '0123456789', '')=''] and" + - "(following-sibling::*[1][self::m:mfrac and not((*[1][self::m:mi or self::m:mn]) and (*[2][self::m:mi or self::m:mn]))] or" + - " following-sibling::*[1][self::m:mo and (.='⁡' or .='⁢' or .='⁣' or .='⁤')]" + - " [following-sibling::*[1][self::m:mfrac and not((*[1][self::m:mi or self::m:mn]) and (*[2][self::m:mi or self::m:mn]))]])" + replace: + - x: "*[1]" + - t: "⠌" + - x: "BrailleChars(translate(*[2], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - t: "⠐" + - name: integer tag: [msup, mover] @@ -923,6 +1000,24 @@ - else: - x: "*[3]" +- + name: mixed-subscript-before-superscript + tag: msubsup + match: + - "not(ancestor::*[@data-chem-equation or contains(@intent, ':chemical-equation')]) and" + - "*[2][self::m:mrow[m:mn and (m:mi or m:mtext) and not(m:mo[not(.='⁡' or .='⁢' or .='⁣' or .='⁤')])]]" + replace: + - x: "*[1]" + - t: "⠡" + - test: + if: "*[2][m:mo[.='+'] or m:mfrac]" + then: [t: "⠐"] + - x: "*[2]" + - t: "⠱" + - t: "⠌" + - x: "*[3]" + - t: "⠱" + - name: default tag: [msubsup, munderover] @@ -934,7 +1029,7 @@ then: [t: "⠨⠡"] else: [t: "⠡"] - test: - if: "*[2][self::m:mrow[m:mo[.='+']] or self::m:mfrac]" + if: "*[2][self::m:mrow[m:mo[.='+'] or m:mfrac] or self::m:mfrac]" then: [t: "⠐"] - x: "*[2]" - test: @@ -1119,6 +1214,31 @@ - x: "text()" - t: "#" +- + name: column-arithmetic-operator + tag: mo + match: + - "$InColumnArithmetic and" + - "ancestor::m:mtr[preceding-sibling::*] and" + - "(.='+' or .='-' or .='−' or .='×' or .='⋅' or .='·')" + replace: + - test: + - if: ".='+'" + then: [t: "⠖"] + - else_if: ".='-' or .='−'" + then: [t: "⠤"] + - else: [t: "⠦"] + +- + name: column-arithmetic-result-line + tag: mo + match: + - "$InColumnArithmetic and" + - "ancestor::m:mtr[preceding-sibling::*] and" + - ".='='" + replace: + - t: "⠶" + - name: operator-functions tag: mo @@ -1140,6 +1260,16 @@ replace: - x: "text()" +- + name: column-arithmetic-continuation-number + tag: mn + match: + - "$InColumnArithmetic and" + - "ancestor::m:mtr[preceding-sibling::*] and" + - "translate(., '0123456789', '')=''" + replace: + - x: "BrailleChars(translate(., '0123456789', '\ue020\ue021\ue022\ue023\ue024\ue025\ue026\ue027\ue028\ue029'), 'Russian')" + - name: periodic-decimal-period-digits tag: mn From 97b5b5a4d0a499be476e188b25f4ba006bc2382c Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:09:19 +0500 Subject: [PATCH 29/47] Optimize braille string operations for single chars Replace push_str() and insert_str() calls with more efficient push() and insert() methods when operating on single Braille characters. This is more idiomatic Rust and avoids unnecessary string allocation overhead. --- src/braille.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/braille.rs b/src/braille.rs index 82dcdd887..eb9704739 100644 --- a/src/braille.rs +++ b/src/braille.rs @@ -2300,7 +2300,7 @@ fn russian_cleanup(_pref_manager: Ref, raw_braille: String) - match ch { 'C' => { if alphabet_mode != Some('u') { - result.push_str("⠨"); + result.push('⠨'); alphabet_mode = Some('u'); } capital_marker_pending = true; @@ -2646,12 +2646,12 @@ impl BrailleChars { let mut prefix = String::new(); let mut suffix = String::new(); if math_variant.contains("bold") && !braille_chars.contains('B') { - prefix.push_str("⠻"); - suffix.insert_str(0, "⠻"); + prefix.push('⠻'); + suffix.insert(0, '⠻'); } if math_variant.contains("italic") && !braille_chars.contains('I') { - prefix.push_str("⠸"); - suffix.insert_str(0, "⠸"); + prefix.push('⠸'); + suffix.insert(0, '⠸'); } let has_typeface_marker = braille_chars.contains('T') || braille_chars.contains('D') From 4b0963200e51eebaa8c197802dee717747fd15ea Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Fri, 3 Jul 2026 11:43:17 +0500 Subject: [PATCH 30/47] Verify RU speech strings; add nav & unit rules Convert many Russian speech entries from t: to T: (marking translations as verified) across SharedRules (calculus, geometry, linear-algebra, overview, etc.). Add new RU rules: power-indexed-by, unit-terse, a currency placeholder, and several navigation parity/placeholder rules (into-or-out-of-*, zoom-in-*, move-next/prev-character, and related none/mprescripts entries). Normalize some tag values (none without quotes). Most new navigation/currency rules use match: false() to preserve existing runtime behavior while providing parity with English rules. --- Rules/Languages/ru/SharedRules/calculus.yaml | 20 +-- Rules/Languages/ru/SharedRules/default.yaml | 9 ++ Rules/Languages/ru/SharedRules/general.yaml | 29 ++++ Rules/Languages/ru/SharedRules/geometry.yaml | 42 +++--- .../ru/SharedRules/linear-algebra.yaml | 14 +- Rules/Languages/ru/navigate.yaml | 142 +++++++++++++++++- Rules/Languages/ru/overview.yaml | 36 ++--- 7 files changed, 234 insertions(+), 58 deletions(-) diff --git a/Rules/Languages/ru/SharedRules/calculus.yaml b/Rules/Languages/ru/SharedRules/calculus.yaml index 76fce80f2..cda63fbab 100644 --- a/Rules/Languages/ru/SharedRules/calculus.yaml +++ b/Rules/Languages/ru/SharedRules/calculus.yaml @@ -4,13 +4,13 @@ tag: laplacian match: "count(*) <= 1" # can be on ∇^2 or on enclosing mrow replace: - - t: "лапласиан" # phrase('laplacian' of x) -- "LahPlahsian" sounds better with speech engines tested + - T: "лапласиан" # phrase('laplacian' of x) -- "LahPlahsian" sounds better with speech engines tested - test: if: "count(*) = 1" then: - test: if: "$Verbosity!='Terse'" - then: [t: "от"] # phrase(function 'of' one variable) -- note OneCore voices spell out "div" + then: [T: "от"] # phrase(function 'of' one variable) -- note OneCore voices spell out "div" - test: if: "not(IsNode(*[1], 'leaf'))" then: [pause: short] @@ -22,11 +22,11 @@ replace: - test: if: "$Verbosity='Terse'" - then: [t: "див"] # phrase('div' is short for divergence) - else: [t: "дивергенция"] # phrase('divergence of' this function from the mean) + then: [T: "див"] # phrase('div' is short for divergence) + else: [T: "дивергенция"] # phrase('divergence of' this function from the mean) - test: if: "$Verbosity!='Terse' and *[1][self::m:mi[contains('ABCDEFGHIJKLMNOPQRSTUVWXYZ', text())]]" - then: [t: "заглавной"] + then: [T: "заглавной"] - test: if: "not(IsNode(*[1], 'leaf'))" then: [pause: short] @@ -39,10 +39,10 @@ tag: curl match: "count(*) = 1" replace: - - t: "ротор" # phrase(the 'curl of' a field) + - T: "ротор" # phrase(the 'curl of' a field) - test: if: "$Verbosity!='Terse' and *[1][self::m:mi[contains('ABCDEFGHIJKLMNOPQRSTUVWXYZ', text())]]" - then: [t: "заглавной"] + then: [T: "заглавной"] - test: if: "not(IsNode(*[1], 'leaf'))" then: [pause: short] @@ -57,11 +57,11 @@ replace: - test: if: "$Verbosity!='Terse'" - then: [t: "градиент"] # phrase(the hill has a 'gradient of' five percent) - else: [t: "набла"] # phrase(the delete key is labeled 'del') + then: [T: "градиент"] # phrase(the hill has a 'gradient of' five percent) + else: [T: "набла"] # phrase(the delete key is labeled 'del') - test: if: "$Verbosity!='Terse' and *[1][self::m:mi[contains('ABCDEFGHIJKLMNOPQRSTUVWXYZ', text())]]" - then: [t: "заглавной"] + then: [T: "заглавной"] - test: if: "not(IsNode(*[1], 'leaf'))" then: [pause: short] diff --git a/Rules/Languages/ru/SharedRules/default.yaml b/Rules/Languages/ru/SharedRules/default.yaml index e23171027..36a1a1de3 100644 --- a/Rules/Languages/ru/SharedRules/default.yaml +++ b/Rules/Languages/ru/SharedRules/default.yaml @@ -680,6 +680,15 @@ - x: "*[2]" - pause: short +- name: power-indexed-by + tag: power-indexed-by + match: "." + replace: + - x: "*[1]" + - T: "нижний индекс" + - x: "*[2]" + - pause: short + # otherwise let definitions/default infix handle it - name: literal diff --git a/Rules/Languages/ru/SharedRules/general.yaml b/Rules/Languages/ru/SharedRules/general.yaml index 2b10f1012..13db9ffba 100644 --- a/Rules/Languages/ru/SharedRules/general.yaml +++ b/Rules/Languages/ru/SharedRules/general.yaml @@ -343,6 +343,15 @@ - pause: short +# in terse mode, we just say "m" or "s", etc., not meters or seconds +- name: unit-terse + tag: unit + # audit-ignore: spell text() is an operation, not untranslated speech text. + match: "$Verbosity = 'Terse' and string-length(.)=1" + replace: + - bookmark: "@id" + - spell: "text()" + # the order of matching is # 1. does it match the base of an SI unit # 2. does it match an English unit (if in an English language) @@ -421,6 +430,26 @@ then: [x: "concat('\uF8FE', DefinitionValue($Word, 'Speech', 'PluralForms'))"] else: [x: "concat('\uF8FE', $Word)", ct: "с"] +# need to reverse the order of speech: $ 3 -> 3 dollars +- name: currency + tag: mrow + # audit-ignore: parity placeholder; ru definitions.yaml does not define + # CurrencySymbols yet, so enabling this would change runtime behavior. + match: "false()" + variables: + # If the amount is singular, we don't add the plural ending. + - IsSingular: "*[3][self::m:mn and .=1] and *[2]='\u2062'" + - CurrencyWord: "DefinitionValue(*[1], 'Speech', 'CurrencySymbols')" + replace: + - bookmark: "*[3]/@id" + - x: "*[3]" + - test: + - if: "$IsSingular" + then: [x: "$CurrencyWord"] + - else_if: "DefinitionValue($CurrencyWord, 'Speech', 'PluralForms') != ''" + then: [x: "DefinitionValue($CurrencyWord, 'Speech', 'PluralForms')"] + else: [x: "DefinitionValue(*[1], 'Speech', 'CurrencySymbols')"] + - name: sin tag: mi match: ".='sin'" diff --git a/Rules/Languages/ru/SharedRules/geometry.yaml b/Rules/Languages/ru/SharedRules/geometry.yaml index 000c4e654..b8aa4f2df 100644 --- a/Rules/Languages/ru/SharedRules/geometry.yaml +++ b/Rules/Languages/ru/SharedRules/geometry.yaml @@ -7,16 +7,16 @@ - test: if: "$Verbosity='Verbose'" then: - - t: "линейный сегмент от" # phrase('the line segment from' A to B) + - T: "линейный сегмент от" # phrase('the line segment from' A to B) - x: "*[1]" - - t: "до" # phrase(the line segment from A 'to' B) + - T: "до" # phrase(the line segment from A 'to' B) - x: "*[2]" else: - - t: "отрезок" # phrase(the 'line segment' A B) + - T: "отрезок" # phrase(the 'line segment' A B) - x: "translate(*[1], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')" - - t: "большое" + - T: "большое" - x: "translate(*[2], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')" - - t: "большое" + - T: "большое" - name: geometry-ray tag: ray @@ -25,16 +25,16 @@ - 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) + - T: "до" # phrase(the ray from A 'to' B) - x: "*[2]" else: - - t: "луч" # phrase(the 'ray'A B) + - T: "луч" # phrase(the 'ray'A B) - x: "translate(*[1], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')" - - t: "большое" + - T: "большое" - x: "translate(*[2], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')" - - t: "большое" + - T: "большое" - name: geometry-arc tag: arc @@ -42,12 +42,12 @@ replace: - test: if: "$Verbosity='Verbose'" - then: [t: "а"] # phrase('the' arc A B C) - - t: "дуга" # phrase(the 'arc' A B C) + then: [T: "а"] # phrase('the' arc A B C) + - T: "дуга" # phrase(the 'arc' A B C) - x: "translate(*[1], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')" - - t: "большое" + - T: "большое" - x: "translate(*[2], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')" - - t: "большое" + - T: "большое" - name: measure-of-angle tag: measure-of-angle @@ -56,9 +56,9 @@ - test: if: "$Verbosity='Verbose'" then: - - t: "мера угла" # phrase('the measure of the angle' ABC) + - T: "мера угла" # phrase('the measure of the angle' ABC) else: - - t: "мера угла" # phrase('measure of angle' ABC) + - T: "мера угла" # phrase('measure of angle' ABC) - x: "*[1]" - x: "*[2]" - x: "*[3]" @@ -70,16 +70,16 @@ replace: - test: if: "$Verbosity='Verbose'" - then: [t: "а"] # phrase('the' point at 1, 2) - - t: "точка" # phrase(the 'point' at 1, 2) + then: [T: "а"] # phrase('the' point at 1, 2) + - T: "точка" # phrase(the 'point' at 1, 2) - test: if: "$Verbosity='Verbose'" - then: [t: "в"] # phrase('the' point at 1, 2) + then: [T: "в"] # phrase('the' point at 1, 2) - pause: short - insert: nodes: "*" - replace: [t: "запятая", pause: auto] # phrase(f of x 'comma' y) + replace: [T: "запятая", pause: auto] # phrase(f of x 'comma' y) - pause: short - test: if: "($SpeechStyle='ClearSpeak' and $Verbosity='Verbose') or not(IsNode(*[last()],'leaf'))" - then: [t: "конечная точка"] # phrase(start point, 'end point') + then: [T: "конечная точка"] # phrase(start point, 'end point') diff --git a/Rules/Languages/ru/SharedRules/linear-algebra.yaml b/Rules/Languages/ru/SharedRules/linear-algebra.yaml index 974007bba..5117f3292 100644 --- a/Rules/Languages/ru/SharedRules/linear-algebra.yaml +++ b/Rules/Languages/ru/SharedRules/linear-algebra.yaml @@ -7,16 +7,16 @@ - test: if: "$Verbosity='Verbose'" then: - - t: "а" # phrase('the' square root of 25 equals 5) - - t: "определяющий" # phrase(the 'determinant' of a matrix) + - T: "а" # phrase('the' square root of 25 equals 5) + - T: "определяющий" # phrase(the 'determinant' of a matrix) - test: if: "$Verbosity!='Terse'" then: - - t: "из" # phrase(systems 'of' linear equations) + - T: "из" # phrase(systems 'of' linear equations) - x: "*[1]" - test: if: "not(IsNode(*[1], 'simple')) and $Impairment = 'Blindness'" - then: [t: "конец определителя"] # phrase('end determinant' of a matrix) + then: [T: "конец определителя"] # phrase('end determinant' of a matrix) - name: subscripted-norm @@ -26,11 +26,11 @@ - test: if: "$Verbosity='Verbose'" then: - - t: "а" # phrase('the' square root of 25 equals 5) + - T: "а" # phrase('the' square root of 25 equals 5) - x: "*[2]" - - t: "норма" # phrase(the 'norm' can be a measure of distance) + - T: "норма" # phrase(the 'norm' can be a measure of distance) - test: if: "$Verbosity!='Terse'" then: - - t: "из" # phrase(systems 'of' linear equations) + - T: "из" # phrase(systems 'of' linear equations) - x: "*[1]" diff --git a/Rules/Languages/ru/navigate.yaml b/Rules/Languages/ru/navigate.yaml index 745c13857..527dcce60 100644 --- a/Rules/Languages/ru/navigate.yaml +++ b/Rules/Languages/ru/navigate.yaml @@ -346,6 +346,39 @@ match: "$Move2D != ''" replace: [] +- name: into-or-out-of-silent + tag: "*" + # audit-ignore: parity placeholder; Russian navigation keeps existing rules. + # Present for parity with the English navigation rules. Kept after default-move + # so the existing Russian navigation phrasing keeps its historical priority. + match: "false()" + replace: [] + +- name: into-or-out-of-default + tag: "*" + # audit-ignore: parity placeholder; Russian navigation keeps existing rules. + # Present for parity with the English navigation rules. Kept after default-move + # so the existing Russian navigation phrasing keeps its historical priority. + match: "false()" + replace: + - 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" + - pause: "medium" + # ********* Go back to last position *************** # This is first since start/end position shouldn't matter - name: move-last-location @@ -402,6 +435,17 @@ - set_variables: [NavNode: "@id"] # special case of zooming into a table -- move to the first row (if only one row, first column) +- name: zoom-in-table + tag: "*" + # audit-ignore: parity placeholder; the existing mtable-specific rule below + # preserves current Russian navigation behavior. + match: "false()" + replace: + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] + - set_variables: [NavNode: "IfThenElse(count(*)=1, (*[1]/*[1]/@id), (*[1]/@id))"] + - name: zoom-in-table tag: mtable match: "$NavCommand = 'ZoomIn'" @@ -486,6 +530,46 @@ variables: [NavCommand: "'MoveNextZoom'"] replace: [x: "*[1]"] +- name: zoom-in-simple + tag: "*" + # audit-ignore: parity placeholder; covered by zoom-in-2D-not-enhanced/default. + match: "false()" + replace: + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] + - test: + if: "$MatchCounter > 1 and IsNode(., '2D')" + then: + - set_variables: # time to stop, not going "in" to next thing, so before "Move2D" + - NavNode: "@id" + - NavNodeOffset: "IfThenElse(@data-id-offset, @data-id-offset, '0')" + else: + - with: + variables: [Move2D: "'in'", Child2D: "*[1]"] # phrase('in' the denominator) + replace: [x: "."] + - x: "*[1]" + +- name: zoom-in-2D-character + tag: "*" + # audit-ignore: parity placeholder; covered by zoom-in-2D-not-enhanced/default. + match: "false()" + replace: + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] + - with: + variables: [Move2D: "'in'", Child2D: "*[1]"] # phrase('in' the denominator) + replace: [x: "."] + - test: + if: "$NavMode = 'Simple'" + then: + - set_variables: [NavNode: "*[1]/@id"] + else: + - with: + variables: [NavCommand: "'MoveNextZoom'"] + replace: [x: "*[1]"] + - name: zoom-in-default tag: "*" match: "$NavCommand = 'ZoomIn'" @@ -1231,9 +1315,29 @@ # ======== Move/Read/Describe Next rules ================= +- name: move-next-character + tag: [mn, mi, mtext] + # audit-ignore: parity placeholder; current Russian navigation uses whole-token + # movement for these nodes. + match: "false()" + replace: + - test: + if: "$NavNodeOffset < string-length(.)" + then: + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] + - set_variables: [NavNodeOffset: "$NavNodeOffset + 1"] + else: + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] + - set_variables: [NavNodeOffset: "0"] + - x: "." + # skip 'none' - name: move-next-none - tag: "none" + tag: none match: - "($NavCommand = 'MoveNext' or $NavCommand = 'ReadNext' or $NavCommand = 'DescribeNext' or $NavCommand = 'DescribePrevious' or $NavCommand = 'MoveNextZoom') and" - "parent::*[1][name(.)='mmultiscripts'] and following-sibling::*" @@ -1253,9 +1357,16 @@ variables: [NavCommand: "'MoveNextZoom'"] replace: [x: "$Following"] +# audit-ignore: parity placeholder for English tag list without changing the +# existing Russian none-specific rule above. +- name: move-next-none + tag: [none, mprescripts] + match: "false()" + replace: [] + # skip 'none' - name: move-previous-none - tag: "none" + tag: none match: - "($NavCommand = 'MovePrevious' or $NavCommand = 'ReadPrevious' or $NavCommand = 'DescribePrevious' or $NavCommand = 'MovePreviousZoom') and" - "parent::*[1][name(.)='mmultiscripts'] and preceding-sibling::*" @@ -1275,6 +1386,13 @@ variables: [NavCommand: "'MovePreviousZoom'"] replace: [x: "$Preceding"] +# audit-ignore: parity placeholder for English tag list without changing the +# existing Russian none-specific rule above. +- name: move-previous-none + tag: [none, mprescripts] + match: "false()" + replace: [] + # skip invisible chars except for Enhanced mode when "times" should be read @@ -1534,6 +1652,26 @@ - pause: long - set_variables: [SpeakExpression: "'false'"] +- name: move-prev-character + tag: [mn, mi, mtext] + # audit-ignore: parity placeholder; current Russian navigation uses whole-token + # movement for these nodes. + match: "false()" + replace: + - test: + if: "$NavNodeOffset > 1" + then: + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] + - set_variables: [NavNodeOffset: "$NavNodeOffset - 1"] + else: + - with: + variables: [SayCommand: "string($NavVerbosity = 'Verbose')"] + replace: [x: "."] + - set_variables: [NavNodeOffset: "0"] + - x: "." + - name: move-previous-auto-zoom-up-one-level # Last child or in auto-zoom'd in-- move up a level and try again # Note: we've already checked the for the case where we are at an edge and should not AutoZoomOut diff --git a/Rules/Languages/ru/overview.yaml b/Rules/Languages/ru/overview.yaml index 131c9b701..2341d4042 100644 --- a/Rules/Languages/ru/overview.yaml +++ b/Rules/Languages/ru/overview.yaml @@ -21,22 +21,22 @@ if: "IsNode(*[1], 'simple') and IsNode(*[2], 'simple')" then: - x: "*[1]" - - t: "разделить на" # (en: 'over') + - T: "разделить на" # (en: 'over') - x: "*[2]" else: - - t: "дробь, числитель:" # (en: 'fraction') + - T: "дробь, числитель:" # (en: 'fraction') - x: "*[1]" - - t: "знаменатель:" + - T: "знаменатель:" - x: "*[2]" - name: overview-default tag: [msqrt, "square-root"] match: "." replace: - - t: "квадратный корень" + - T: "квадратный корень" - test: if: "$Verbosity!='Terse'" - then: [t: "из"] + then: [T: "из"] - x: "*[1]" - name: overview-default @@ -47,11 +47,11 @@ if: "*[2][self::m:mn]" then_test: - if: "*[2][.='2']" - then: [t: "квадратный корень"] + then: [T: "квадратный корень"] - else_if: "*[2][.='3']" - then: [t: "кубический корень"] # (en: 'cube root') + then: [T: "кубический корень"] # (en: 'cube root') - else_if: "*[2][not(contains(., '.'))]" - then: [x: "ToOrdinal(*[2])", t: "корень"] + then: [x: "ToOrdinal(*[2])", T: "корень"] else: - test: if: "*[2][self::m:mi][string-length(.)=1]" @@ -59,10 +59,10 @@ - x: "*[2]" - pronounce: [{text: "-й"}, {ipa: "j"}, {sapi5: "th"}, {eloquence: "T"}] else: {x: "*[2]"} - - t: "корень" + - T: "корень" - test: if: "$Verbosity!='Terse'" - then: [t: "из"] + then: [T: "из"] - x: "*[1]" - name: matrix-override @@ -71,24 +71,24 @@ - "*[2][self::m:mtable] and" - "(IsBracketed(., '(', ')') or IsBracketed(., '[', ']') or IsBracketed(., '|', '|'))" replace: - - t: "на" # (en: 'a') + - T: "на" # (en: 'a') - x: count(*[2]/*) - - t: "на" # (en: 'by') + - T: "на" # (en: 'by') - x: count(*[2]/*[self::m:mtr][1]/*) - test: if: "*[1][.='|']" # just need to check the first bracket since we know it must be (, [, or | - then: [t: "определитель"] # (en: 'determinant') - else: [t: "матрица"] # (en: 'matrix') + then: [T: "определитель"] # (en: 'determinant') + else: [T: "матрица"] # (en: 'matrix') - name: overview-default tag: mtable match: "." replace: - - t: "на" # (en: 'a') + - T: "на" # (en: 'a') - x: count(*) - - t: "на" # (en: 'by') + - T: "на" # (en: 'by') - x: count(*[self::m:mtr][1]/*) - - t: "таблица" # (en: 'table') + - T: "таблица" # (en: 'table') - name: short-mrow tag: mrow @@ -112,6 +112,6 @@ - pause: auto - x: "*[5]" - pause: auto - - t: "и так далее" # (en: 'and so on') + - T: "и так далее" # (en: 'and so on') - include: "SimpleSpeak_Rules.yaml" From 88c100b0c44ac679260ed046cb9233da7324c291 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Sun, 5 Jul 2026 10:06:08 +0500 Subject: [PATCH 31/47] Add Russian braille rules for geometry accents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add braille transcription rules for Russian geometry notation with over/under accents: - geometry-line-double-arrow: handles double-arrow symbols (↔⟷) in mover - lower-vector-arrow: handles vector arrows (→⟶⇀⃗) in munder - lower-geometry-line-double-arrow: handles double-arrow symbols in munder - lower-vector-bar: handles bar accents (¯‾_ ̄) in munder Includes corresponding test cases for measurements, physical quantities, and geometry notation. --- Rules/Braille/Russian/Russian_Rules.yaml | 55 ++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index c62168c51..297930062 100644 --- a/Rules/Braille/Russian/Russian_Rules.yaml +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -476,6 +476,19 @@ then: [t: "⠨⠒⠂"] else: [t: "⠒⠂"] +- + name: geometry-line-double-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 @@ -489,6 +502,48 @@ then: [t: "⠨⠒"] else: [t: "⠒"] +- + name: lower-vector-arrow + tag: munder + match: + - "*[2][translate(., '→⟶⇀⃗', '')=''] and" + - "string-length(translate(normalize-space(*[1]), ' ', '')) > 0" + replace: + - x: "*[1]" + - t: "⠰" + - test: + if: "string-length(translate(normalize-space(*[1]), ' ', '')) > 1" + then: [t: "⠨⠒⠂"] + else: [t: "⠒⠂"] + +- + name: lower-geometry-line-double-arrow + tag: munder + match: + - "*[2][translate(., '↔⟷', '')=''] and" + - "string-length(translate(normalize-space(*[1]), ' ', '')) > 0" + replace: + - x: "*[1]" + - t: "⠰" + - test: + if: "string-length(translate(normalize-space(*[1]), ' ', '')) > 1" + then: [t: "⠨⠒⠂⠂"] + else: [t: "⠒⠂⠂"] + +- + name: lower-vector-bar + tag: munder + match: + - "*[2][translate(., '¯‾_ ̄', '')=''] and" + - "string-length(translate(normalize-space(*[1]), ' ', '')) > 0" + replace: + - x: "*[1]" + - t: "⠰" + - test: + if: "string-length(translate(normalize-space(*[1]), ' ', '')) > 1" + then: [t: "⠨⠒"] + else: [t: "⠒"] + - name: geometry-arc tag: mover From 46ea5db814096fc53c28fbccf3a8ef19b63dc107 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Sun, 5 Jul 2026 10:39:09 +0500 Subject: [PATCH 32/47] Add vector and long-division Russian braille rules Add braille rules for vector notation (arrow, bar, and double-arrow postfix operators) and long-division formatting in Russian. Includes support for system braces and new test cases covering vector notation in geometry, long-division examples, and less common mathematical symbols. --- Rules/Braille/Russian/Russian_Rules.yaml | 204 +++++++++++++++++++++++ 1 file changed, 204 insertions(+) diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index 297930062..7912f85ac 100644 --- a/Rules/Braille/Russian/Russian_Rules.yaml +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -1,6 +1,45 @@ --- # Russian mathematical braille rules. +- + name: flat-vector-arrow-postfix-operator + tag: mo + match: + - "not(following-sibling::*) and" + - "translate(., '→⟶⇀⃗', '')='' and" + - "count(preceding-sibling::*[not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])]) > 0" + replace: + - test: + if: "count(preceding-sibling::*[not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])]) > 1 or string-length(preceding-sibling::*[not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])][1]) > 1" + then: [t: "⠨⠒⠂"] + else: [t: "⠒⠂"] + +- + name: flat-vector-bar-postfix-operator + tag: mo + match: + - "not(following-sibling::*) and" + - "translate(., '¯‾_ ̄', '')='' and" + - "count(preceding-sibling::*[not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])]) > 0" + replace: + - test: + if: "count(preceding-sibling::*[not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])]) > 1 or string-length(preceding-sibling::*[not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])][1]) > 1" + then: [t: "⠨⠒"] + else: [t: "⠒"] + +- + name: flat-geometry-line-double-arrow-postfix-operator + tag: mo + match: + - "not(following-sibling::*) and" + - "translate(., '↔⟷', '')='' and" + - "count(preceding-sibling::*[not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])]) > 0" + replace: + - test: + if: "count(preceding-sibling::*[not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])]) > 1 or string-length(preceding-sibling::*[not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])][1]) > 1" + then: [t: "⠨⠒⠂⠂"] + else: [t: "⠒⠂⠂"] + - name: whitespace tag: "!*" @@ -40,6 +79,75 @@ - t: "⠬" - x: "BrailleChars(translate(*[3], '0123456789', '\ue020\ue021\ue022\ue023\ue024\ue025\ue026\ue027\ue028\ue029'), 'Russian')" +- + name: flat-vector-arrow-postfix-math + tag: math + match: + - "count(*) >= 2 and" + - "*[last()][self::m:mo and translate(., '→⟶⇀⃗', '')=''] and" + - "not(*[position() < last()][not(self::m:mi or self::m:msub or self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])])" + variables: + - RowStart: "''" + - RowEnd: "''" + - InSystemLeftBrace: "false()" + - InSystemRightBrace: "false()" + - InChemElementGroup: "false()" + - InColumnArithmetic: "false()" + - InLongDivision: "false()" + - MatchingWhitespace: "false()" + replace: + - x: "*[position() < last() and not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])]" + - test: + if: "count(*[position() < last() and not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])]) > 1" + then: [t: "⠨⠒⠂"] + else: [t: "⠒⠂"] + +- + name: flat-vector-bar-postfix-math + tag: math + match: + - "count(*) >= 2 and" + - "*[last()][self::m:mo and translate(., '¯‾_ ̄', '')=''] and" + - "not(*[position() < last()][not(self::m:mi or self::m:msub or self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])])" + variables: + - RowStart: "''" + - RowEnd: "''" + - InSystemLeftBrace: "false()" + - InSystemRightBrace: "false()" + - InChemElementGroup: "false()" + - InColumnArithmetic: "false()" + - InLongDivision: "false()" + - MatchingWhitespace: "false()" + replace: + - x: "*[position() < last() and not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])]" + - test: + if: "count(*[position() < last() and not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])]) > 1" + then: [t: "⠨⠒"] + else: [t: "⠒"] + +- + name: flat-geometry-line-double-arrow-postfix-math + tag: math + match: + - "count(*) >= 2 and" + - "*[last()][self::m:mo and translate(., '↔⟷', '')=''] and" + - "not(*[position() < last()][not(self::m:mi or self::m:msub or self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])])" + variables: + - RowStart: "''" + - RowEnd: "''" + - InSystemLeftBrace: "false()" + - InSystemRightBrace: "false()" + - InChemElementGroup: "false()" + - InColumnArithmetic: "false()" + - InLongDivision: "false()" + - MatchingWhitespace: "false()" + replace: + - x: "*[position() < last() and not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])]" + - test: + if: "count(*[position() < last() and not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])]) > 1" + then: [t: "⠨⠒⠂⠂"] + else: [t: "⠒⠂⠂"] + - name: unicode-override tag: "*" @@ -54,8 +162,10 @@ - RowStart: "''" - RowEnd: "''" - InSystemLeftBrace: "false()" + - InSystemRightBrace: "false()" - InChemElementGroup: "false()" - InColumnArithmetic: "false()" + - InLongDivision: "false()" - MatchingWhitespace: "false()" replace: [x: "*"] @@ -65,6 +175,12 @@ match: "not(preceding-sibling::*) and following-sibling::*[1][self::m:mtable] and count(../*)=2" replace: [t: ""] +- + name: system-right-brace-closer + tag: mo + match: "not(following-sibling::*) and preceding-sibling::*[1][self::m:mtable] and count(../*)=2" + replace: [t: ""] + - name: chemistry-single-letter-element-group tag: mrow @@ -106,6 +222,48 @@ - t: "⠄" - x: "*[3]" +- + name: flat-vector-arrow-postfix + tag: mrow + match: + - "count(*) >= 2 and" + - "*[last()][self::m:mo and translate(., '→⟶⇀⃗', '')=''] and" + - "not(*[position() < last()][not(self::m:mi or self::m:msub or self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])])" + replace: + - x: "*[position() < last() and not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])]" + - test: + if: "count(*[position() < last() and not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])]) > 1" + then: [t: "⠨⠒⠂"] + else: [t: "⠒⠂"] + +- + name: flat-vector-bar-postfix + tag: mrow + match: + - "count(*) >= 2 and" + - "*[last()][self::m:mo and translate(., '¯‾_ ̄', '')=''] and" + - "not(*[position() < last()][not(self::m:mi or self::m:msub or self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])])" + replace: + - x: "*[position() < last() and not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])]" + - test: + if: "count(*[position() < last() and not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])]) > 1" + then: [t: "⠨⠒"] + else: [t: "⠒"] + +- + name: flat-geometry-line-double-arrow-postfix + tag: mrow + match: + - "count(*) >= 2 and" + - "*[last()][self::m:mo and translate(., '↔⟷', '')=''] and" + - "not(*[position() < last()][not(self::m:mi or self::m:msub or self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])])" + replace: + - x: "*[position() < last() and not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])]" + - test: + if: "count(*[position() < last() and not(self::m:mo[.='⁡' or .='⁢' or .='⁣' or .='⁤'])]) > 1" + then: [t: "⠨⠒⠂⠂"] + else: [t: "⠒⠂⠂"] + - name: periodic-decimal-row tag: mrow @@ -330,6 +488,16 @@ - x: "*[2]/*[1]/*[1]/*[1]" - x: "*[3]" +- + name: long-division-table + tag: mtable + variables: + - InLongDivision: "true()" + - RowStart: "''" + - RowEnd: "''" + match: "contains(@intent, ':long-division') or contains(@intent, ':angle-division')" + replace: [x: "*"] + - name: column-arithmetic-table tag: mtable @@ -340,6 +508,16 @@ match: "contains(@intent, ':column-arithmetic')" replace: [x: "*"] +- + name: system-right-brace-table + tag: mtable + variables: + - InSystemRightBrace: "true()" + - RowStart: "''" + - RowEnd: "''" + match: "not(preceding-sibling::*) and count(../*)=2" + replace: [x: "*"] + - name: system-left-brace-table tag: mtable @@ -394,6 +572,13 @@ if: .[self::m:mlabeledtr] then: [x: "*[position()>1]"] else: [x: "*"] + - test: + - if: "$InSystemRightBrace and not(preceding-sibling::*)" + then: [t: "⠀⠹"] + - else_if: "$InSystemRightBrace and not(following-sibling::*)" + then: [t: "⠀⠼"] + - else_if: "$InSystemRightBrace" + then: [t: "⠀⠸"] - x: $RowEnd - @@ -1269,6 +1454,15 @@ - x: "text()" - t: "#" +- + name: long-division-angle-operator + tag: mo + match: + - "$InLongDivision and" + - "(.='÷' or .='⟌')" + replace: + - t: "⠴" + - name: column-arithmetic-operator tag: mo @@ -1315,6 +1509,16 @@ replace: - x: "text()" +- + name: long-division-continuation-number + tag: mn + match: + - "$InLongDivision and" + - "ancestor::m:mtr[preceding-sibling::*] and" + - "translate(., '0123456789', '')=''" + replace: + - x: "BrailleChars(translate(., '0123456789', '\ue020\ue021\ue022\ue023\ue024\ue025\ue026\ue027\ue028\ue029'), 'Russian')" + - name: column-arithmetic-continuation-number tag: mn From d77a2258e0a7f72a10cc6eaeec0fc88f7d608880 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Sun, 5 Jul 2026 11:03:26 +0500 Subject: [PATCH 33/47] Add postscripts and new label marks for Russian Braille MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhance mmultiscripts rule to handle postscripts (subscripts/superscripts after base element) in addition to prescripts. Add new character mappings for '?' and '←' symbols as label marks in Russian Braille. Includes comprehensive test coverage for the new functionality. --- Rules/Braille/Russian/Russian_Rules.yaml | 35 +++++++++++++++++++++++- Rules/Braille/Russian/definitions.yaml | 4 ++- Rules/Braille/Russian/unicode.yaml | 2 ++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/Rules/Braille/Russian/Russian_Rules.yaml b/Rules/Braille/Russian/Russian_Rules.yaml index 7912f85ac..0f91068ec 100644 --- a/Rules/Braille/Russian/Russian_Rules.yaml +++ b/Rules/Braille/Russian/Russian_Rules.yaml @@ -1284,9 +1284,13 @@ tag: mmultiscripts match: - "not(ancestor::*[@data-chem-equation or contains(@intent, ':chemical-equation')]) and" - - "count(m:mprescripts/preceding-sibling::*) = 1 and" + - "count(m:mprescripts/preceding-sibling::*) >= 1 and" - "(m:mprescripts/following-sibling::*[1][not(self::m:none) and DefinitionValue(., 'Braille', 'RussianLabelMarks') != ''] or" - " m:mprescripts/following-sibling::*[2][not(self::m:none) and DefinitionValue(., 'Braille', 'RussianLabelMarks') != ''])" + variables: + - Prescripts: "m:mprescripts/following-sibling::*" + - NumChildren: "count(*)" + - Postscripts: "*[position()>1 and position() < (last() + ($NumChildren mod 2) - count($Prescripts))]" replace: - test: if: "m:mprescripts/following-sibling::*[1][not(self::m:none) and DefinitionValue(., 'Braille', 'RussianLabelMarks') != '']" @@ -1299,6 +1303,35 @@ - t: "⠨" - x: "DefinitionValue(m:mprescripts/following-sibling::*[2], 'Braille', 'RussianLabelMarks')" - x: "*[1]" + - test: + if: "$Postscripts" + then: + - test: + if: "not($Postscripts[1][self::m:none])" + then: + - t: "⠡" + - test: + - if: "$Postscripts[1][self::m:mn and translate(., '0123456789', '')='']" + then: + - x: "BrailleChars(translate($Postscripts[1], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - else: + - x: "$Postscripts[1]" + - test: + if: "not($Postscripts[1][self::m:mn and translate(., '0123456789', '')='']) and not($Postscripts[1][.='∞'])" + then: [t: "⠱"] + - test: + if: "not($Postscripts[2][self::m:none])" + then: + - t: "⠌" + - test: + - if: "$Postscripts[2][self::m:mn and translate(., '0123456789', '')='']" + then: + - x: "BrailleChars(translate($Postscripts[2], '0123456789', '\ue000\ue001\ue002\ue003\ue004\ue005\ue006\ue007\ue008\ue009'), 'Russian')" + - else: + - x: "$Postscripts[2]" + - test: + if: "not($Postscripts[2][self::m:mn and translate(., '0123456789', '')='']) and not($Postscripts[2][.='∞'])" + then: [t: "⠱"] - name: default diff --git a/Rules/Braille/Russian/definitions.yaml b/Rules/Braille/Russian/definitions.yaml index 513b1d1dd..9aa56c6c7 100644 --- a/Rules/Braille/Russian/definitions.yaml +++ b/Rules/Braille/Russian/definitions.yaml @@ -26,7 +26,9 @@ "~": "\uE135", "∼": "\uE135", "˜": "\uE135", "̃": "\uE135", "^": "\uE136", "∧": "\uE136", "ˆ": "\uE136", "̂": "\uE136", "∨": "\uE137", "˅": "\uE137", "ˇ": "\uE137", "̌": "\uE137", - "+": "\uE138", "-": "\uE139", "−": "\uE139" + "+": "\uE138", "-": "\uE139", "−": "\uE139", + "?": "\uE13A", "⍰": "\uE13A", + "←": "\uE13B", "⟵": "\uE13B", "⃖": "\uE13B" } - RussianExactOverLabelMarksWithoutIndicator: { diff --git a/Rules/Braille/Russian/unicode.yaml b/Rules/Braille/Russian/unicode.yaml index f87979bfe..cf8617352 100644 --- a/Rules/Braille/Russian/unicode.yaml +++ b/Rules/Braille/Russian/unicode.yaml @@ -164,6 +164,8 @@ - "\uE137": [t: "⠰⠔"] - "\uE138": [t: "⠖"] - "\uE139": [t: "⠤"] +- "\uE13A": [t: "⠠⠢"] +- "\uE13B": [t: "⠦⠶"] - "\ue020": [t: "⠚"] - "\ue021": [t: "⠁"] - "\ue022": [t: "⠃"] From 94390e46a02ea26f954e47b1912c0a36e844b4cc Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:14:11 +0500 Subject: [PATCH 34/47] Fix Russian percent braille and add per mille sign MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplify the Russian braille representation for the percent sign (%) by removing the middle dot, and add support for the per mille sign (‰) with its braille representation. Updates corresponding test cases. --- Rules/Braille/Russian/unicode.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Rules/Braille/Russian/unicode.yaml b/Rules/Braille/Russian/unicode.yaml index cf8617352..6b08895b9 100644 --- a/Rules/Braille/Russian/unicode.yaml +++ b/Rules/Braille/Russian/unicode.yaml @@ -26,7 +26,8 @@ - ")": [t: "⠜"] - ">": [t: "⠀⠕⠀"] - "<": [t: "⠀⠪⠀"] -- "%": [t: "⠼⠍⠴"] +- "%": [t: "⠼⠴"] +- "‰": [t: "⠼⠴⠴"] - "±": [t: "⠀⠖⠤"] - ";": [t: "⠠⠢"] - "Ϳ": [t: "⠰⠊"] From b1fdd490de8f23605aa5e3420067b47b744b32a9 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:22:50 +0500 Subject: [PATCH 35/47] Propagate mathvariant attribute to children Add mathvariant inheritance from parent elements to children that lack their own mathvariant attribute. This ensures consistent styling throughout nested elements. Includes tests for bold text propagation through mstyle and mrow containers, and verifies that explicit child mathvariants override inherited values. --- tests/braille/Russian/russian.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index 184d4ad80..1192dbf6b 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -216,6 +216,15 @@ fn source_typeform_and_mathvariant_indicators() -> Result<()> { let expr = r#"𝑥"#; test_braille("Russian", expr, "⠸⠠⠭⠸")?; + + let expr = r#"x"#; + test_braille("Russian", expr, "⠻⠠⠭⠻")?; + + let expr = r#"xy"#; + test_braille("Russian", expr, "⠻⠠⠭⠽⠻")?; + + let expr = r#"y"#; + test_braille("Russian", expr, "⠸⠠⠽⠸")?; return Ok(()); } From bba06229c892c271c0438cf59a9ca94f6ad21fad Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Tue, 21 Jul 2026 09:40:51 +0500 Subject: [PATCH 36/47] Fix clippy warnings and improve code style - Remove unnecessary borrows in format! and display calls - Change string slices to arrays in trim_end_matches calls - Replace expect(&format!(...)) with unwrap_or_else(|_| panic!(...)) - Add clippy allow attributes to suppress false positives in tests - Add missing newline at end of build.rs --- build.rs | 4 +- src/interface.rs | 2 +- src/navigate.rs | 951 ++++++++++++++++++++++----------------------- src/prefs.rs | 2 +- src/speech.rs | 2 +- tests/braille.rs | 1 + tests/languages.rs | 1 + 7 files changed, 481 insertions(+), 482 deletions(-) diff --git a/build.rs b/build.rs index b2b359d2b..4c5a9892b 100644 --- a/build.rs +++ b/build.rs @@ -179,7 +179,7 @@ fn main() { // println!("cargo::warning=rules directory '{:?}'", &rules_dir.to_string_lossy()); let archive_zip_file = match File::create(&archive_path) { Ok(file) => file, - Err(e) => panic!("build.rs couldn't create {:?}: {}", archive_path.to_str(), e), + Err(e) => panic!("build.rs couldn't create {:?}: {}", archive_path.to_str(), e), }; let mut archive_zip = ZipWriter::new(archive_zip_file); @@ -200,4 +200,4 @@ fn main() { } println!("cargo::rerun-if-changed=Rules"); } -} \ No newline at end of file +} diff --git a/src/interface.rs b/src/interface.rs index 7744a96db..13657594d 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -1510,7 +1510,7 @@ mod tests { x-y "; - set_mathml(&expr)?; + set_mathml(expr)?; let speech = get_spoken_text()?; // Rule-generated SSML must pass through verbatim (not XML-entity-encoded). assert!(!speech.contains("<")); diff --git a/src/navigate.rs b/src/navigate.rs index 6fb170d52..207460519 100644 --- a/src/navigate.rs +++ b/src/navigate.rs @@ -3,10 +3,11 @@ #![allow(clippy::needless_return)] use std::cell::{Ref, RefCell, RefMut}; -use sxd_xpath_no_unsafe::context::Evaluation; -use sxd_xpath_no_unsafe::Value; -use sxd_document_no_unsafe::dom::Element; -use sxd_document_no_unsafe::Package; +use sxd_xpath::context::Evaluation; +use sxd_xpath::Value; +use sxd_document::dom::Element; +use sxd_document::Package; + use std::fmt; use crate::canonicalize::{name, get_parent}; use crate::pretty_print::mml_to_string; @@ -188,7 +189,7 @@ impl NavigationState { } } - fn init_navigation_context(&self, context: &mut sxd_xpath_no_unsafe::Context, command: &'static str, + fn init_navigation_context(&self, context: &mut sxd_xpath::Context, command: &'static str, nav_state_top: Option<(&NavigationPosition, &'static str)>) { context.set_variable("NavCommand", command); @@ -249,7 +250,7 @@ fn get_node_by_id<'a>(mathml: Element<'a>, pos: &NavigationPosition) -> Option /// Get's the Nav Node from the context, with some exceptions such as Toggle commands where it isn't set. /// Note: mathml can be any node. It isn't really used but some Element needs to be part of Evaluate(). -pub fn get_nav_node<'c>(context: &sxd_xpath_no_unsafe::Context<'c>, var_name: &str, mathml: Element<'c>, start_node: Element<'c>, command: &str, nav_mode: &str) -> Result { - let raw_start_id = start_node.attribute_value("id"); - let start_id = raw_start_id.as_deref().unwrap_or_default(); +pub fn get_nav_node<'c>(context: &sxd_xpath::Context<'c>, var_name: &str, mathml: Element<'c>, start_node: Element<'c>, command: &str, nav_mode: &str) -> Result { + let start_id = start_node.attribute_value("id").unwrap_or_default(); if command.starts_with("Toggle") { return Ok( start_id.to_string() ); } else { return context_get_variable(context, var_name, mathml) .with_context(|| format!("When trying to {} starting at id={} in {} mode", - command, start_node.attribute_value("id").as_deref().unwrap_or_default(), nav_mode)); + command, start_node.attribute_value("id").unwrap_or_default(), nav_mode)); } } // FIX: think of a better place to put this, and maybe a better interface /// Note: mathml can be any node. It isn't really used but some Element needs to be part of Evaluate(). /// If the context variable has String, Number, or Boolean xpath value, return it as a string. Otherwise it is an error -pub fn context_get_variable<'c>(context: &sxd_xpath_no_unsafe::Context<'c>, var_name: &str, mathml: Element<'c>) -> Result { +pub fn context_get_variable<'c>(context: &sxd_xpath::Context<'c>, var_name: &str, mathml: Element<'c>) -> Result { // This is slightly roundabout because Context doesn't expose a way to get the values. // Instead, we create an "Evaluation", which is just one level of indirection. - use sxd_xpath_no_unsafe::nodeset::Node; + use sxd_xpath::nodeset::Node; let evaluation = Evaluation::new(context, Node::Element(mathml)); return match evaluation.value_of(var_name.into()) { Some(value) => match value { @@ -330,7 +330,7 @@ pub fn context_get_variable<'c>(context: &sxd_xpath_no_unsafe::Context<'c>, var_ .enumerate() .for_each(|(i, node)| { match node { - sxd_xpath_no_unsafe::nodeset::Node::Element(mathml) => + sxd_xpath::nodeset::Node::Element(mathml) => error_message += &format!("#{}:\n{}",i, mml_to_string(*mathml)), _ => error_message += &format!("'{node:?}'"), } @@ -344,7 +344,7 @@ pub fn context_get_variable<'c>(context: &sxd_xpath_no_unsafe::Context<'c>, var_ } /// Wrapper around context_get_variable to get an integer variable -fn context_get_int_variable<'c>(context: &sxd_xpath_no_unsafe::Context<'c>, var_name: &str, mathml: Element<'c>) -> Result { +fn context_get_int_variable<'c>(context: &sxd_xpath::Context<'c>, var_name: &str, mathml: Element<'c>) -> Result { let value = context_get_variable(context, var_name, mathml)?; return match value.parse::() { Ok(i) => Ok(i), @@ -417,7 +417,7 @@ pub fn do_navigate_command_string(mathml: Element, nav_command: &'static str) -> match tts.as_str() { "SSML" if !cumulative_speech.starts_with(" { - cumulative_speech = format!("{}", rate, cumulative_speech); + cumulative_speech = format!("{}", &rate, &cumulative_speech); } "SAPI5" if !cumulative_speech.starts_with(" { @@ -487,9 +487,8 @@ pub fn do_navigate_command_string(mathml: Element, nav_command: &'static str) -> }; let mut properties = ""; - let raw_properties = mathml.attribute_value("data-intent-property"); if add_literal { - properties = raw_properties.as_deref().unwrap_or_default(); + properties = mathml.attribute_value("data-intent-property").unwrap_or_default(); if properties.contains(":literal:") { add_literal = false; } else { @@ -509,8 +508,8 @@ pub fn do_navigate_command_string(mathml: Element, nav_command: &'static str) -> found_node = get_parent(found_node); // debug!("found_node:\n{}", mml_to_string(found_node)); let temp_pos = NavigationPosition { - current_node: found_node.attribute_value("id").as_deref().unwrap_or_default().to_string().clone(), - current_node_offset: found_node.attribute_value(ID_OFFSET).as_deref().unwrap_or_default().parse::().unwrap_or_default(), + current_node: found_node.attribute_value("id").unwrap_or_default().to_string().clone(), + current_node_offset: found_node.attribute_value(ID_OFFSET).unwrap_or_default().parse::().unwrap_or_default(), }; if let Some(intent_node) = get_node_by_id(nav_intent, &temp_pos) { found_node = intent_node; @@ -600,7 +599,7 @@ pub fn do_navigate_command_string(mathml: Element, nav_command: &'static str) -> bail!("Internal error: With {}/{} in {} mode, can't {} from expression with id '{}' inside:\n{}", rules.pref_manager.as_ref().borrow().pref_to_string("Language"), rules.pref_manager.as_ref().borrow().pref_to_string("SpeechStyle"), - nav_state.mode, nav_command, nav_position.current_node, mml_to_string(if literal_speak {mathml} else {intent})); + &nav_state.mode, nav_command, &nav_position.current_node, mml_to_string(if literal_speak {mathml} else {intent})); } return Err(e); } @@ -999,53 +998,42 @@ mod tests { #[cfg(test)] /// Assert if result_id != '' and it doesn't match the id of the result of the move /// Returns the speech from the command - fn test_command(command: &'static str, mathml: Element, result_id: &str) -> Result { - use std::panic::{catch_unwind, AssertUnwindSafe}; - init_panic_handler(); - let result = catch_unwind(AssertUnwindSafe(|| { + fn test_command(command: &'static str, mathml: Element, result_id: &str) -> String { // debug!("\nCommand: {}", command); - NAVIGATION_STATE.with(|nav_stack| { - let (start_id, _) = nav_stack.borrow().get_navigation_mathml_id(mathml); - match do_navigate_command_string(mathml, command) { - Err(e) => { - panic!("\nStarting at '{}', '{} failed.\n{}", - start_id, command, &crate::interface::errors_to_string(&e)) - }, - Ok(nav_speech) => { - let nav_speech = nav_speech.trim_end_matches(&[' ', ',', ';']); - // debug!("Full speech: {}", nav_speech); - if !result_id.is_empty() { - let (id, _) = nav_stack.borrow().get_navigation_mathml_id(mathml); - assert_eq!(result_id, id, "\nStarting at '{}', '{} failed.", start_id, command); - } - return Ok(nav_speech.to_string()); + NAVIGATION_STATE.with(|nav_stack| { + let (start_id, _) = nav_stack.borrow().get_navigation_mathml_id(mathml); + match do_navigate_command_string(mathml, command) { + Err(e) => { + panic!("\nStarting at '{}', '{} failed.\n{}", + start_id, command, &crate::interface::errors_to_string(&e)) + }, + Ok(nav_speech) => { + let nav_speech = nav_speech.trim_end_matches([' ', ',', ';']); + // debug!("Full speech: {}", nav_speech); + if !result_id.is_empty() { + let (id, _) = nav_stack.borrow().get_navigation_mathml_id(mathml); + assert_eq!(result_id, id, "\nStarting at '{}', '{} failed.", start_id, command); } - }; - }) - })); - return report_any_panic(result); - } - - fn init_default_prefs(mathml: &str, nav_mode_default: &str) -> Result<()> { - return init_prefs(mathml, nav_mode_default, "en"); - } - - fn init_prefs(mathml: &str, nav_mode_default: &str, language: &str) -> Result<()> { - use std::panic::{catch_unwind, AssertUnwindSafe}; - init_panic_handler(); - let result = catch_unwind(AssertUnwindSafe(|| { - set_rules_dir(super::super::abs_rules_dir_path())?; - set_preference("NavMode", nav_mode_default)?; - set_preference("NavVerbosity", "Verbose")?; - set_preference("AutoZoomOut", "True")?; - set_preference("Language", language)?; - set_preference("SpeechStyle", "SimpleSpeak")?; - set_preference("Verbosity", "Medium")?; - set_preference("Overview", "False")?; - set_mathml(mathml)?; - return Ok( () ); - })); - return report_any_panic(result); + return nav_speech.to_string(); + } + }; + }) + } + + fn init_default_prefs(mathml: &str, nav_mode_default: &str) { + init_prefs(mathml, nav_mode_default, "en"); + } + + fn init_prefs(mathml: &str, nav_mode_default: &str, language: &str) { + set_rules_dir(super::super::abs_rules_dir_path()).unwrap(); + set_preference("NavMode", nav_mode_default).unwrap(); + set_preference("NavVerbosity", "Verbose").unwrap(); + set_preference("AutoZoomOut", "True").unwrap(); + set_preference("Language", language).unwrap(); + set_preference("SpeechStyle", "SimpleSpeak").unwrap(); + set_preference("Verbosity", "Medium").unwrap(); + set_preference("Overview", "False").unwrap(); + set_mathml(mathml).unwrap(); } #[test] @@ -1054,13 +1042,13 @@ mod tests { b2 d "; - init_default_prefs(mathml_str, "Enhanced")?; + init_default_prefs(mathml_str, "Enhanced"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomIn", mathml, "msup")?; - test_command("ZoomIn", mathml, "base")?; - test_command("ZoomIn", mathml, "base")?; + test_command("ZoomIn", mathml, "msup"); + test_command("ZoomIn", mathml, "base"); + test_command("ZoomIn", mathml, "base"); return Ok( () ); }); } @@ -1079,35 +1067,32 @@ mod tests { "; - init_default_prefs(mathml_str, "Enhanced")?; + init_default_prefs(mathml_str, "Enhanced"); debug!("--- Enhanced ---"); - MATHML_INSTANCE.with(|package_instance| -> Result<()> { + MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomIn", mathml, "msup")?; - test_command("MoveNext", mathml, "id-3")?; - return Ok( () ); - })?; + test_command("ZoomIn", mathml, "msup"); + test_command("MoveNext", mathml, "id-3"); + }); - init_default_prefs(mathml_str, "Simple")?; + init_default_prefs(mathml_str, "Simple"); debug!("--- Simple ---"); - MATHML_INSTANCE.with(|package_instance: &RefCell| -> Result<()> { + MATHML_INSTANCE.with(|package_instance: &RefCell| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomIn", mathml, "msup")?; - test_command("MoveNext", mathml, "id-3")?; - return Ok( () ); - })?; + test_command("ZoomIn", mathml, "msup"); + test_command("MoveNext", mathml, "id-3"); + }); - init_default_prefs(mathml_str, "Character")?; + init_default_prefs(mathml_str, "Character"); debug!("--- Character ---"); - MATHML_INSTANCE.with(|package_instance| -> Result<()> { + MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomIn", mathml, "base")?; - test_command("MoveNext", mathml, "exp")?; - return Ok( () ); - })?; + test_command("ZoomIn", mathml, "base"); + test_command("MoveNext", mathml, "exp"); + }); return Ok( () ); } @@ -1141,22 +1126,22 @@ mod tests { 1 "; - init_default_prefs(mathml_str, "Enhanced")?; + init_default_prefs(mathml_str, "Enhanced"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); set_preference("NavMode", "Enhanced")?; debug!("\n------EnhancedMode----------"); - test_command("ZoomIn", mathml, "id-2")?; - test_command("ZoomIn", mathml, "id-5")?; - test_command("ZoomIn", mathml, "id-6")?; + test_command("ZoomIn", mathml, "id-2"); + test_command("ZoomIn", mathml, "id-5"); + test_command("ZoomIn", mathml, "id-6"); // repeat, but this time with "Simple set_preference("NavMode", "Simple")?; debug!("\n------SimpleMode----------"); - test_command("ZoomOutAll", mathml, "id-1")?; - test_command("ZoomIn", mathml, "id-4")?; - test_command("ZoomIn", mathml, "id-4")?; + test_command("ZoomOutAll", mathml, "id-1"); + test_command("ZoomIn", mathml, "id-4"); + test_command("ZoomIn", mathml, "id-4"); return Ok( () ); }); } @@ -1167,11 +1152,11 @@ mod tests { b2 d "; - init_default_prefs(mathml_str, "Enhanced")?; + init_default_prefs(mathml_str, "Enhanced"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomInAll", mathml, "base")?; + test_command("ZoomInAll", mathml, "base"); return Ok( () ); }); } @@ -1182,23 +1167,23 @@ mod tests { b2 d "; - init_prefs(mathml_str, "Enhanced", "ru")?; + init_prefs(mathml_str, "Enhanced", "ru"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - let speech = test_command("ZoomIn", mathml, "msup")?; + let speech = test_command("ZoomIn", mathml, "msup"); assert_eq!("переход внутрь; в числитель; бэ в квадрате", speech); - let speech = test_command("ZoomIn", mathml, "base")?; + let speech = test_command("ZoomIn", mathml, "base"); assert_eq!("переход внутрь; в основание; бэ", speech); - let speech = test_command("ZoomIn", mathml, "base")?; + let speech = test_command("ZoomIn", mathml, "base"); assert_eq!("достигнута максимальная детализация; бэ", speech); - let speech = test_command("ZoomOut", mathml, "msup")?; + let speech = test_command("ZoomOut", mathml, "msup"); assert_eq!("переход наружу; из основания; бэ в квадрате", speech); - let speech = test_command("ZoomInAll", mathml, "base")?; + let speech = test_command("ZoomInAll", mathml, "base"); assert_eq!("переход к максимальной детализации; в основание; бэ", speech); - let speech = test_command("ZoomOutAll", mathml, "mfrac")?; + let speech = test_command("ZoomOutAll", mathml, "mfrac"); assert_eq!("переход к выражению целиком; из основания; из числителя; дробь, числитель: бэ в квадрате, знаменатель: дэ, конец дроби", speech); - let speech = test_command("ZoomOutAll", mathml, "mfrac")?; + let speech = test_command("ZoomOutAll", mathml, "mfrac"); assert_eq!("выражение уже показано целиком; дробь, числитель: бэ в квадрате, знаменатель: дэ, конец дроби", speech); return Ok( () ); }); @@ -1211,7 +1196,7 @@ mod tests { b2 d "; - init_default_prefs(mathml_str, "Enhanced")?; + init_default_prefs(mathml_str, "Enhanced"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); @@ -1221,8 +1206,13 @@ mod tests { current_node_offset: 0 }, "None") }); - test_command("ZoomOut", mathml, "msup")?; - test_command("ZoomOut", mathml, "mfrac")?; + test_command("ZoomOut", mathml, "msup"); + + let _nav_speech = do_navigate_command_and_param(mathml, NavigationCommand::Zoom, NavigationParam::Previous)?; + NAVIGATION_STATE.with(|nav_stack| { + let (id, _) = nav_stack.borrow().get_navigation_mathml_id(mathml); + assert_eq!(id, "mfrac"); + }); return Ok( () ); }); } @@ -1233,7 +1223,7 @@ mod tests { b2 d "; - init_default_prefs(mathml_str, "Enhanced")?; + init_default_prefs(mathml_str, "Enhanced"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); @@ -1244,7 +1234,7 @@ mod tests { }, "None") }); - test_command("ZoomOutAll", mathml, "mfrac")?; + test_command("ZoomOutAll", mathml, "mfrac"); return Ok( () ); }); } @@ -1262,7 +1252,7 @@ mod tests { "; - init_default_prefs(mathml_str, "Enhanced")?; + init_default_prefs(mathml_str, "Enhanced"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); @@ -1274,16 +1264,16 @@ mod tests { }); set_preference("NavMode", "Character")?; - test_command("MoveStart", mathml, "id-2")?; - test_command("MoveEnd", mathml, "id-7")?; + test_command("MoveStart", mathml, "id-2"); + test_command("MoveEnd", mathml, "id-7"); set_preference("NavMode", "Simple")?; - test_command("MoveStart", mathml, "id-2")?; - test_command("MoveEnd", mathml, "id-7")?; + test_command("MoveStart", mathml, "id-2"); + test_command("MoveEnd", mathml, "id-7"); set_preference("NavMode", "Enhanced")?; - test_command("MoveStart", mathml, "id-2")?; - test_command("MovePrevious", mathml, "id-2")?; - test_command("MoveEnd", mathml, "id-4")?; - test_command("MoveNext", mathml, "id-4")?; + test_command("MoveStart", mathml, "id-2"); + test_command("MovePrevious", mathml, "id-2"); + test_command("MoveEnd", mathml, "id-4"); + test_command("MoveNext", mathml, "id-4"); return Ok( () ); }); } @@ -1303,7 +1293,7 @@ mod tests { "; - init_default_prefs(mathml_str, "Enhanced")?; + init_default_prefs(mathml_str, "Enhanced"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); @@ -1315,15 +1305,15 @@ mod tests { }); set_preference("NavMode", "Character")?; - test_command("MoveLineStart", mathml, "id-5")?; - test_command("MoveLineEnd", mathml, "id-8")?; + test_command("MoveLineStart", mathml, "id-5"); + test_command("MoveLineEnd", mathml, "id-8"); set_preference("NavMode", "Simple")?; - test_command("MoveLineStart", mathml, "id-4")?; - test_command("MoveLineEnd", mathml, "id-8")?; + test_command("MoveLineStart", mathml, "id-4"); + test_command("MoveLineEnd", mathml, "id-8"); set_preference("NavMode", "Enhanced")?; - test_command("MoveLineStart", mathml, "id-4")?; - test_command("MoveLineEnd", mathml, "id-8")?; - test_command("MoveEnd", mathml, "id-3")?; + test_command("MoveLineStart", mathml, "id-4"); + test_command("MoveLineEnd", mathml, "id-8"); + test_command("MoveEnd", mathml, "id-3"); return Ok( () ); }); } @@ -1334,7 +1324,7 @@ mod tests { b2 d "; - init_default_prefs(mathml_str, "Enhanced")?; + init_default_prefs(mathml_str, "Enhanced"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); @@ -1345,14 +1335,14 @@ mod tests { }, "None") }); - test_command("ZoomOutAll", mathml, "mfrac")?; - test_command("ZoomOut", mathml, "mfrac")?; - test_command("MoveLastLocation", mathml, "base")?; // second zoom out should do nothing + test_command("ZoomOutAll", mathml, "mfrac"); + test_command("ZoomOut", mathml, "mfrac"); + test_command("MoveLastLocation", mathml, "base"); // second zoom out should do nothing - test_command("ZoomOut", mathml, "msup")?; - test_command("ZoomInAll", mathml, "base")?; - test_command("ZoomIn", mathml, "base")?; - test_command("MoveLastLocation", mathml, "msup")?; // second zoom in should do nothing + test_command("ZoomOut", mathml, "msup"); + test_command("ZoomInAll", mathml, "base"); + test_command("ZoomIn", mathml, "base"); + test_command("MoveLastLocation", mathml, "msup"); // second zoom in should do nothing return Ok( () ); }); @@ -1364,7 +1354,7 @@ mod tests { b2! d "; - init_default_prefs(mathml_str, "Enhanced")?; + init_default_prefs(mathml_str, "Enhanced"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); @@ -1374,7 +1364,7 @@ mod tests { current_node_offset: 0 }, "None") }); - test_command("MoveLineStart", mathml, "denom")?; + test_command("MoveLineStart", mathml, "denom"); NAVIGATION_STATE.with(|nav_stack| { nav_stack.borrow_mut().push(NavigationPosition{ @@ -1382,8 +1372,13 @@ mod tests { current_node_offset: 0 }, "None") }); - test_command("MoveLineStart", mathml, "msup")?; - test_command("MoveStart", mathml, "num")?; + test_command("MoveLineStart", mathml, "msup"); + + let _nav_speech = do_navigate_command_and_param(mathml, NavigationCommand::Move, NavigationParam::Start)?; + NAVIGATION_STATE.with(|nav_stack| { + let (id, _) = nav_stack.borrow().get_navigation_mathml_id(mathml); + assert_eq!(id, "num"); + }); return Ok( () ); }); } @@ -1400,7 +1395,7 @@ mod tests { x "; - init_default_prefs(mathml_str, "Enhanced")?; + init_default_prefs(mathml_str, "Enhanced"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); @@ -1411,7 +1406,7 @@ mod tests { }, "None") }); set_preference("NavMode", "Enhanced")?; - test_command("MoveNext", mathml, "id-5")?; + test_command("MoveNext", mathml, "id-5"); // reset start and test Simple NAVIGATION_STATE.with(|nav_stack| { @@ -1421,7 +1416,7 @@ mod tests { }, "None") }); set_preference("NavMode", "Simple")?; - test_command("MoveNext", mathml, "id-5")?; + test_command("MoveNext", mathml, "id-5"); // reset start and test Character NAVIGATION_STATE.with(|nav_stack| { @@ -1431,8 +1426,8 @@ mod tests { }, "None") }); set_preference("NavMode", "Character")?; - test_command("MoveNext", mathml, "id-4")?; - test_command("MoveNext", mathml, "id-5")?; + test_command("MoveNext", mathml, "id-4"); + test_command("MoveNext", mathml, "id-5"); return Ok( () ); }); } @@ -1453,20 +1448,20 @@ mod tests { 4 "; - init_default_prefs(mathml_str, "Character")?; + init_default_prefs(mathml_str, "Character"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - assert_eq!("zoomed in all of the way; 1", test_command("ZoomInAll", mathml, "id-2")?); - assert_eq!("move right; plus", test_command("MoveNext", mathml, "id-3")?); - assert_eq!("move right; in base; x", test_command("MoveNext", mathml, "id-5")?); - assert_eq!("move right; in subscript; 2", test_command("MoveNext", mathml, "id-6")?); - assert_eq!("move right; in superscript; 3", test_command("MoveNext", mathml, "id-7")?); - assert_eq!("move right; out of superscript; plus", test_command("MoveNext", mathml, "id-8")?); - assert_eq!("move left; in superscript; 3", test_command("MovePrevious", mathml, "id-7")?); - assert_eq!("move left; in subscript; 2", test_command("MovePrevious", mathml, "id-6")?); - assert_eq!("move left; in base; x", test_command("MovePrevious", mathml, "id-5")?); - assert_eq!("move left; out of base; plus", test_command("MovePrevious", mathml, "id-3")?); + assert_eq!("zoomed in all of the way; 1", test_command("ZoomInAll", mathml, "id-2")); + assert_eq!("move right; plus", test_command("MoveNext", mathml, "id-3")); + assert_eq!("move right; in base; x", test_command("MoveNext", mathml, "id-5")); + assert_eq!("move right; in subscript; 2", test_command("MoveNext", mathml, "id-6")); + assert_eq!("move right; in superscript; 3", test_command("MoveNext", mathml, "id-7")); + assert_eq!("move right; out of superscript; plus", test_command("MoveNext", mathml, "id-8")); + assert_eq!("move left; in superscript; 3", test_command("MovePrevious", mathml, "id-7")); + assert_eq!("move left; in subscript; 2", test_command("MovePrevious", mathml, "id-6")); + assert_eq!("move left; in base; x", test_command("MovePrevious", mathml, "id-5")); + assert_eq!("move left; out of base; plus", test_command("MovePrevious", mathml, "id-3")); return Ok( () ); }); @@ -1484,17 +1479,17 @@ mod tests { xa "; - init_default_prefs(mathml_str, "Enhanced")?; + init_default_prefs(mathml_str, "Enhanced"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - assert_eq!("zoom in; the log base 2", test_command("ZoomIn", mathml, "id-2")?); - assert_eq!("zoom in; in base; 2", test_command("ZoomIn", mathml, "id-4")?); - assert_eq!("zoomed in all of the way; 2", test_command("ZoomIn", mathml, "id-4")?); + assert_eq!("zoom in; the log base 2", test_command("ZoomIn", mathml, "id-2")); + assert_eq!("zoom in; in base; 2", test_command("ZoomIn", mathml, "id-4")); + assert_eq!("zoomed in all of the way; 2", test_command("ZoomIn", mathml, "id-4")); debug!("Now zooming out"); - assert_eq!("zoom out; out of base; the log base 2", test_command("ZoomOut", mathml, "id-2")?); - assert_eq!("zoom out; the log base 2, of x", test_command("ZoomOut", mathml, "id-1")?); - assert_eq!("zoomed out all of the way; the log base 2, of x", test_command("ZoomOut", mathml, "id-1")?); + assert_eq!("zoom out; out of base; the log base 2", test_command("ZoomOut", mathml, "id-2")); + assert_eq!("zoom out; the log base 2, of x", test_command("ZoomOut", mathml, "id-1")); + assert_eq!("zoomed out all of the way; the log base 2, of x", test_command("ZoomOut", mathml, "id-1")); return Ok( () ); }); } @@ -1512,19 +1507,19 @@ mod tests { x "; - init_default_prefs(mathml_str, "Enhanced")?; + init_default_prefs(mathml_str, "Enhanced"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - assert_eq!("zoom in; the log base 2, cubed", test_command("ZoomIn", mathml, "id-2")?); - assert_eq!("zoom in; in base; the log base 2", test_command("ZoomIn", mathml, "id-2-log-base")?); - assert_eq!("zoom in; in base; 2", test_command("ZoomIn", mathml, "id-4")?); - assert_eq!("zoomed in all of the way; 2", test_command("ZoomIn", mathml, "id-4")?); + assert_eq!("zoom in; the log base 2, cubed", test_command("ZoomIn", mathml, "id-2")); + assert_eq!("zoom in; in base; the log base 2", test_command("ZoomIn", mathml, "id-2-log-base")); + assert_eq!("zoom in; in base; 2", test_command("ZoomIn", mathml, "id-4")); + assert_eq!("zoomed in all of the way; 2", test_command("ZoomIn", mathml, "id-4")); debug!("Now zooming out"); - assert_eq!("zoom out; out of base; the log base 2", test_command("ZoomOut", mathml, "id-2-log-base")?); - assert_eq!("zoom out; out of base; the log base 2, cubed", test_command("ZoomOut", mathml, "id-2")?); - assert_eq!("zoom out; the log base 2, cubed of x", test_command("ZoomOut", mathml, "id-1")?); - assert_eq!("zoomed out all of the way; the log base 2, cubed of x", test_command("ZoomOut", mathml, "id-1")?); + assert_eq!("zoom out; out of base; the log base 2", test_command("ZoomOut", mathml, "id-2-log-base")); + assert_eq!("zoom out; out of base; the log base 2, cubed", test_command("ZoomOut", mathml, "id-2")); + assert_eq!("zoom out; the log base 2, cubed of x", test_command("ZoomOut", mathml, "id-1")); + assert_eq!("zoomed out all of the way; the log base 2, cubed of x", test_command("ZoomOut", mathml, "id-1")); return Ok( () ); }); } @@ -1533,31 +1528,31 @@ mod tests { fn zoom_msubsup() -> Result<()> { // msubsup is trickier because it creates an intent within an intent, so offsets need to be handled properly let mathml_str = "𝑥12"; - init_default_prefs(mathml_str, "Enhanced")?; + init_default_prefs(mathml_str, "Enhanced"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - set_preference("NavMode", "Enhanced")?; + set_preference("NavMode", "Enhanced").unwrap(); debug!("Enhanced mode"); do_commands(mathml)?; - set_preference("NavMode", "Simple")?; + set_preference("NavMode", "Simple").unwrap(); debug!("Simple mode"); do_commands(mathml)?; - set_preference("NavMode", "Character")?; + set_preference("NavMode", "Character").unwrap(); debug!("Character mode"); - assert_eq!("zoom in; in base; x", test_command("ZoomIn", mathml, "base")?); - assert_eq!("zoom out; out of base; x sub 1 super 2 end super", test_command("ZoomOut", mathml, "msubsup")?); + assert_eq!("zoom in; in base; x", test_command("ZoomIn", mathml, "base")); + assert_eq!("zoom out; out of base; x sub 1 super 2 end super", test_command("ZoomOut", mathml, "msubsup")); return Ok( () ); /// Enhanced and Simple mode should behave the same fn do_commands(mathml: Element) -> Result<()> { - assert_eq!("zoom in; in base; x sub 1", test_command("ZoomIn", mathml, "msubsup-indexed-by")?); - assert_eq!("zoom in; in base; x", test_command("ZoomIn", mathml, "base")?); - assert_eq!("zoomed in all of the way; x", test_command("ZoomIn", mathml, "base")?); + assert_eq!("zoom in; in base; x sub 1", test_command("ZoomIn", mathml, "msubsup-indexed-by")); + assert_eq!("zoom in; in base; x", test_command("ZoomIn", mathml, "base")); + assert_eq!("zoomed in all of the way; x", test_command("ZoomIn", mathml, "base")); debug!("Now zooming out"); - assert_eq!("zoom out; out of base; x sub 1", test_command("ZoomOut", mathml, "msubsup-indexed-by")?); - assert_eq!("zoom out; out of base; x sub 1, squared", test_command("ZoomOut", mathml, "msubsup")?); - assert_eq!("zoomed out all of the way; x sub 1, squared", test_command("ZoomOut", mathml, "msubsup")?); + assert_eq!("zoom out; out of base; x sub 1", test_command("ZoomOut", mathml, "msubsup-indexed-by")); + assert_eq!("zoom out; out of base; x sub 1, squared", test_command("ZoomOut", mathml, "msubsup")); + assert_eq!("zoomed out all of the way; x sub 1, squared", test_command("ZoomOut", mathml, "msubsup")); return Ok( () ); } }); @@ -1583,22 +1578,22 @@ mod tests { "; - init_default_prefs(mathml_str, "Character")?; + init_default_prefs(mathml_str, "Character"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - assert_eq!("zoomed in all of the way; in base; open bracket", test_command("ZoomInAll", mathml, "id-3")?); - assert_eq!("move right; in base; cap c o", test_command("MoveNext", mathml, "id-5")?); - assert_eq!("move right; in subscript; 6", test_command("MoveNext", mathml, "id-6")?); - assert_eq!("move right; out of subscript; close bracket", test_command("MoveNext", mathml, "id-8")?); - assert_eq!("move right; in superscript; 3", test_command("MoveNext", mathml, "id-11")?); - assert_eq!("move right; plus", test_command("MoveNext", mathml, "id-12")?); - assert_eq!("cannot move right, end of math", test_command("MoveNext", mathml, "id-12")?); - assert_eq!("move left; 3", test_command("MovePrevious", mathml, "id-11")?); - assert_eq!("move left; in base; close bracket", test_command("MovePrevious", mathml, "id-8")?); - assert_eq!("move left; in subscript; 6", test_command("MovePrevious", mathml, "id-6")?); - assert_eq!("move left; in base; cap c o", test_command("MovePrevious", mathml, "id-5")?); - assert_eq!("move left; out of base; open bracket", test_command("MovePrevious", mathml, "id-3")?); + assert_eq!("zoomed in all of the way; in base; open bracket", test_command("ZoomInAll", mathml, "id-3")); + assert_eq!("move right; in base; cap c o", test_command("MoveNext", mathml, "id-5")); + assert_eq!("move right; in subscript; 6", test_command("MoveNext", mathml, "id-6")); + assert_eq!("move right; out of subscript; close bracket", test_command("MoveNext", mathml, "id-8")); + assert_eq!("move right; in superscript; 3", test_command("MoveNext", mathml, "id-11")); + assert_eq!("move right; plus", test_command("MoveNext", mathml, "id-12")); + assert_eq!("cannot move right, end of math", test_command("MoveNext", mathml, "id-12")); + assert_eq!("move left; 3", test_command("MovePrevious", mathml, "id-11")); + assert_eq!("move left; in base; close bracket", test_command("MovePrevious", mathml, "id-8")); + assert_eq!("move left; in subscript; 6", test_command("MovePrevious", mathml, "id-6")); + assert_eq!("move left; in base; cap c o", test_command("MovePrevious", mathml, "id-5")); + assert_eq!("move left; out of base; open bracket", test_command("MovePrevious", mathml, "id-3")); return Ok( () ); }); @@ -1628,20 +1623,20 @@ mod tests { "; - init_default_prefs(mathml_str, "Character")?; + init_default_prefs(mathml_str, "Character"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomInAll", mathml, "id-2")?; - test_command("MoveNext", mathml, "id-3")?; - test_command("MoveNext", mathml, "id-6")?; - test_command("MoveNext", mathml, "id-8")?; - test_command("MoveNext", mathml, "id-9")?; - test_command("MoveNext", mathml, "id-10")?; - test_command("MoveNext", mathml, "id-11")?; - test_command("MoveNext", mathml, "id-13")?; - test_command("MoveNext", mathml, "id-15")?; - test_command("MoveNext", mathml, "id-15")?; + test_command("ZoomInAll", mathml, "id-2"); + test_command("MoveNext", mathml, "id-3"); + test_command("MoveNext", mathml, "id-6"); + test_command("MoveNext", mathml, "id-8"); + test_command("MoveNext", mathml, "id-9"); + test_command("MoveNext", mathml, "id-10"); + test_command("MoveNext", mathml, "id-11"); + test_command("MoveNext", mathml, "id-13"); + test_command("MoveNext", mathml, "id-15"); + test_command("MoveNext", mathml, "id-15"); return Ok( () ); }); @@ -1664,39 +1659,39 @@ mod tests { "; - init_default_prefs(mathml_str, "Character")?; + init_default_prefs(mathml_str, "Character"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); debug!("Character mode"); do_commands(mathml)?; - set_preference("NavMode", "Simple")?; + set_preference("NavMode", "Simple").unwrap(); debug!("Simple mode"); - test_command("ZoomIn", mathml, "id-3")?; // zooms to the first parenthesis + test_command("ZoomIn", mathml, "id-3"); // zooms to the first parenthesis do_commands(mathml)?; - set_preference("NavMode", "Enhanced")?; + set_preference("NavMode", "Enhanced").unwrap(); debug!("Enhanced mode"); - test_command("ZoomIn", mathml, "id-4")?; - test_command("MoveNext", mathml, "id-6")?; - test_command("MoveNext", mathml, "id-9")?; - test_command("MovePrevious", mathml, "id-6")?; - test_command("MovePrevious", mathml, "id-4")?; + test_command("ZoomIn", mathml, "id-4"); + test_command("MoveNext", mathml, "id-6"); + test_command("MoveNext", mathml, "id-9"); + test_command("MovePrevious", mathml, "id-6"); + test_command("MovePrevious", mathml, "id-4"); return Ok( () ); }); /// Simple and Character mode should behave the same fn do_commands(mathml: Element) -> Result<()> { - test_command("ZoomIn", mathml, "id-3")?; - test_command("MoveNext", mathml, "id-4")?; - test_command("MoveNext", mathml, "id-5")?; - test_command("MoveNext", mathml, "id-8")?; - test_command("MoveNext", mathml, "id-9")?; - test_command("MoveNext", mathml, "id-10")?; - test_command("MovePrevious", mathml, "id-9")?; - test_command("MovePrevious", mathml, "id-8")?; - test_command("MovePrevious", mathml, "id-5")?; - test_command("ZoomOutAll", mathml, "id-1")?; + test_command("ZoomIn", mathml, "id-3"); + test_command("MoveNext", mathml, "id-4"); + test_command("MoveNext", mathml, "id-5"); + test_command("MoveNext", mathml, "id-8"); + test_command("MoveNext", mathml, "id-9"); + test_command("MoveNext", mathml, "id-10"); + test_command("MovePrevious", mathml, "id-9"); + test_command("MovePrevious", mathml, "id-8"); + test_command("MovePrevious", mathml, "id-5"); + test_command("ZoomOutAll", mathml, "id-1"); return Ok( () ); } } @@ -1714,17 +1709,17 @@ mod tests { "; - init_default_prefs(mathml_str, "Simple")?; + init_default_prefs(mathml_str, "Simple"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); do_commands(mathml)?; - set_preference("NavMode", "Simple")?; + set_preference("NavMode", "Simple").unwrap(); do_commands(mathml)?; - set_preference("NavMode", "Enhanced")?; - test_command("ZoomIn", mathml, "id-2")?; - test_command("MoveNext", mathml, "id-6")?; - test_command("MovePrevious", mathml, "id-2")?; + set_preference("NavMode", "Enhanced").unwrap(); + test_command("ZoomIn", mathml, "id-2"); + test_command("MoveNext", mathml, "id-6"); + test_command("MovePrevious", mathml, "id-2"); return Ok( () ); }); @@ -1732,14 +1727,14 @@ mod tests { /// Simple and Character mode should behave the same fn do_commands(mathml: Element) -> Result<()> { - test_command("ZoomIn", mathml, "id-2")?; - test_command("MoveNext", mathml, "id-5")?; - test_command("MoveNext", mathml, "id-6")?; - test_command("MoveNext", mathml, "id-7")?; - test_command("MovePrevious", mathml, "id-6")?; - test_command("MovePrevious", mathml, "id-5")?; - test_command("MovePrevious", mathml, "id-2")?; - test_command("ZoomOutAll", mathml, "id-1")?; + test_command("ZoomIn", mathml, "id-2"); + test_command("MoveNext", mathml, "id-5"); + test_command("MoveNext", mathml, "id-6"); + test_command("MoveNext", mathml, "id-7"); + test_command("MovePrevious", mathml, "id-6"); + test_command("MovePrevious", mathml, "id-5"); + test_command("MovePrevious", mathml, "id-2"); + test_command("ZoomOutAll", mathml, "id-1"); return Ok( () ); } } @@ -1756,15 +1751,15 @@ mod tests { z "; - init_default_prefs(mathml_str, "Character")?; + init_default_prefs(mathml_str, "Character"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomInAll", mathml, "id-3")?; - assert_eq!("move right; in denominator; y", test_command("MoveNext", mathml, "id-4")?); - assert_eq!("move right; out of denominator; z", test_command("MoveNext", mathml, "id-6")?); - assert_eq!("move left; in denominator; y", test_command("MovePrevious", mathml, "id-4")?); - assert_eq!("move left; in numerator; x", test_command("MovePrevious", mathml, "id-3")?); + test_command("ZoomInAll", mathml, "id-3"); + assert_eq!("move right; in denominator; y", test_command("MoveNext", mathml, "id-4")); + assert_eq!("move right; out of denominator; z", test_command("MoveNext", mathml, "id-6")); + assert_eq!("move left; in denominator; y", test_command("MovePrevious", mathml, "id-4")); + assert_eq!("move left; in numerator; x", test_command("MovePrevious", mathml, "id-3")); return Ok( () ); }); @@ -1782,18 +1777,18 @@ mod tests { z "; - init_prefs(mathml_str, "Character", "ru")?; + init_prefs(mathml_str, "Character", "ru"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomInAll", mathml, "id-3")?; - let speech = test_command("MoveNext", mathml, "id-4")?; + test_command("ZoomInAll", mathml, "id-3"); + let speech = test_command("MoveNext", mathml, "id-4"); assert_eq!("перемещение вправо; в знаменатель; игрек", speech); - let speech = test_command("MoveNext", mathml, "id-7")?; + let speech = test_command("MoveNext", mathml, "id-7"); assert_eq!("перемещение вправо; из знаменателя; в подкоренное выражение; зэт", speech); - let speech = test_command("MovePrevious", mathml, "id-4")?; + let speech = test_command("MovePrevious", mathml, "id-4"); assert_eq!("перемещение влево; из подкоренного выражения; в знаменатель; игрек", speech); - let speech = test_command("MovePrevious", mathml, "id-3")?; + let speech = test_command("MovePrevious", mathml, "id-3"); assert_eq!("перемещение влево; в числитель; икс", speech); return Ok( () ); }); @@ -1809,21 +1804,21 @@ mod tests { "; - init_default_prefs(mathml_str, "Character")?; + init_default_prefs(mathml_str, "Character"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomInAll", mathml, "id-3")?; - assert_eq!("zoomed in to first character; t", test_command("ZoomIn", mathml, "id-3")?); - assert_eq!("move right; o", test_command("MoveNext", mathml, "id-3")?); - assert_eq!("move right; p", test_command("MoveNext", mathml, "id-3")?); - assert_eq!("move right; in denominator; αβγ", test_command("MoveNext", mathml, "id-4")?); - assert_eq!("zoomed in to first character; alpha", test_command("ZoomIn", mathml, "id-4")?); - assert_eq!("move right; beta", test_command("MoveNext", mathml, "id-4")?); - assert_eq!("move right; gamma", test_command("MoveNext", mathml, "id-4")?); - assert_eq!("cannot move right, end of math", test_command("MoveNext", mathml, "id-4")?); - assert_eq!("move left; beta", test_command("MovePrevious", mathml, "id-4")?); - assert_eq!("zoom out; αβγ", test_command("ZoomOut", mathml, "id-4")?); + test_command("ZoomInAll", mathml, "id-3"); + assert_eq!("zoomed in to first character; t", test_command("ZoomIn", mathml, "id-3")); + assert_eq!("move right; o", test_command("MoveNext", mathml, "id-3")); + assert_eq!("move right; p", test_command("MoveNext", mathml, "id-3")); + assert_eq!("move right; in denominator; αβγ", test_command("MoveNext", mathml, "id-4")); + assert_eq!("zoomed in to first character; alpha", test_command("ZoomIn", mathml, "id-4")); + assert_eq!("move right; beta", test_command("MoveNext", mathml, "id-4")); + assert_eq!("move right; gamma", test_command("MoveNext", mathml, "id-4")); + assert_eq!("cannot move right, end of math", test_command("MoveNext", mathml, "id-4")); + assert_eq!("move left; beta", test_command("MovePrevious", mathml, "id-4")); + assert_eq!("zoom out; αβγ", test_command("ZoomOut", mathml, "id-4")); return Ok( () ); }); @@ -1846,15 +1841,15 @@ mod tests { "; - init_default_prefs(mathml_str, "Enhanced")?; + init_default_prefs(mathml_str, "Enhanced"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomIn", mathml, "id-2")?; - assert_eq!("move right; times", test_command("MoveNext", mathml, "id-3")?); - assert_eq!("move right; 1 minus x", test_command("MoveNext", mathml, "id-6")?); - assert_eq!("move left; times", test_command("MovePrevious", mathml, "id-3")?); - assert_eq!("move left; 2", test_command("MovePrevious", mathml, "id-2")?); + test_command("ZoomIn", mathml, "id-2"); + assert_eq!("move right; times", test_command("MoveNext", mathml, "id-3")); + assert_eq!("move right; 1 minus x", test_command("MoveNext", mathml, "id-6")); + assert_eq!("move left; times", test_command("MovePrevious", mathml, "id-3")); + assert_eq!("move left; 2", test_command("MovePrevious", mathml, "id-2")); return Ok( () ); }); @@ -1877,16 +1872,16 @@ mod tests { "; - init_default_prefs(mathml_str, "Simple")?; - set_preference("SpeechStyle", "ClearSpeak")?; + init_default_prefs(mathml_str, "Simple"); + set_preference("SpeechStyle", "ClearSpeak").unwrap(); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomIn", mathml, "id-2")?; - assert_eq!("move right; open paren", test_command("MoveNext", mathml, "id-5")?); - assert_eq!("move right; 1", test_command("MoveNext", mathml, "id-7")?); - assert_eq!("move left; open paren", test_command("MovePrevious", mathml, "id-5")?); - assert_eq!("move left; 2", test_command("MovePrevious", mathml, "id-2")?); + test_command("ZoomIn", mathml, "id-2"); + assert_eq!("move right; open paren", test_command("MoveNext", mathml, "id-5")); + assert_eq!("move right; 1", test_command("MoveNext", mathml, "id-7")); + assert_eq!("move left; open paren", test_command("MovePrevious", mathml, "id-5")); + assert_eq!("move left; 2", test_command("MovePrevious", mathml, "id-2")); return Ok( () ); }); @@ -1949,26 +1944,26 @@ mod tests { "; - init_default_prefs(mathml_str, "Enhanced")?; + init_default_prefs(mathml_str, "Enhanced"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomInAll", mathml, "nav-4")?; - test_command("MoveCellNext", mathml, "nav-6")?; - test_command("MoveCellNext", mathml, "nav-8")?; - test_command("MoveCellNext", mathml, "nav-8")?; - test_command("MoveCellDown", mathml, "nav-20")?; - test_command("MoveCellDown", mathml, "nav-27")?; - let speech = test_command("MoveCellDown", mathml, "nav-39")?; + test_command("ZoomInAll", mathml, "nav-4"); + test_command("MoveCellNext", mathml, "nav-6"); + test_command("MoveCellNext", mathml, "nav-8"); + test_command("MoveCellNext", mathml, "nav-8"); + test_command("MoveCellDown", mathml, "nav-20"); + test_command("MoveCellDown", mathml, "nav-27"); + let speech = test_command("MoveCellDown", mathml, "nav-39"); assert_eq!(speech, "move down, row 4, column 3; 2 minus y"); - let speech = test_command("MoveCellDown", mathml, "nav-39")?; + let speech = test_command("MoveCellDown", mathml, "nav-39"); assert_eq!(speech, "no next row"); - test_command("MoveCellPrevious", mathml, "nav-35")?; - test_command("ZoomIn", mathml, "nav-36")?; - test_command("MoveCellUp", mathml, "nav-25")?; - test_command("MoveCellUp", mathml, "nav-16")?; - test_command("MoveCellUp", mathml, "nav-6")?; - test_command("MoveCellUp", mathml, "nav-6")?; + test_command("MoveCellPrevious", mathml, "nav-35"); + test_command("ZoomIn", mathml, "nav-36"); + test_command("MoveCellUp", mathml, "nav-25"); + test_command("MoveCellUp", mathml, "nav-16"); + test_command("MoveCellUp", mathml, "nav-6"); + test_command("MoveCellUp", mathml, "nav-6"); return Ok( () ); }); @@ -2030,7 +2025,7 @@ mod tests { "; - init_default_prefs(mathml_str, "Character")?; + init_default_prefs(mathml_str, "Character"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); @@ -2040,23 +2035,23 @@ mod tests { current_node_offset: 0 }, "None") }); - test_command("MoveNext", mathml, "nav-12")?; - test_command("MoveNext", mathml, "nav-13")?; - test_command("MoveNext", mathml, "nav-14")?; - test_command("MoveNext", mathml, "nav-17")?; - test_command("MovePrevious", mathml, "nav-14")?; - test_command("MoveCellNext", mathml, "nav-17")?; - test_command("MoveCellPrevious", mathml, "nav-14")?; - test_command("MovePrevious", mathml, "nav-13")?; - test_command("MovePrevious", mathml, "nav-12")?; - test_command("MoveCellPrevious", mathml, "nav-12")?; - test_command("MovePrevious", mathml, "nav-8")?; - test_command("MoveCellDown", mathml, "nav-20")?; - test_command("MoveCellDown", mathml, "nav-27")?; - test_command("MoveCellDown", mathml, "nav-40")?; - test_command("MoveCellDown", mathml, "nav-40")?; - test_command("MoveCellPrevious", mathml, "nav-37")?; - test_command("MoveCellUp", mathml, "nav-25")?; + test_command("MoveNext", mathml, "nav-12"); + test_command("MoveNext", mathml, "nav-13"); + test_command("MoveNext", mathml, "nav-14"); + test_command("MoveNext", mathml, "nav-17"); + test_command("MovePrevious", mathml, "nav-14"); + test_command("MoveCellNext", mathml, "nav-17"); + test_command("MoveCellPrevious", mathml, "nav-14"); + test_command("MovePrevious", mathml, "nav-13"); + test_command("MovePrevious", mathml, "nav-12"); + test_command("MoveCellPrevious", mathml, "nav-12"); + test_command("MovePrevious", mathml, "nav-8"); + test_command("MoveCellDown", mathml, "nav-20"); + test_command("MoveCellDown", mathml, "nav-27"); + test_command("MoveCellDown", mathml, "nav-40"); + test_command("MoveCellDown", mathml, "nav-40"); + test_command("MoveCellPrevious", mathml, "nav-37"); + test_command("MoveCellUp", mathml, "nav-25"); return Ok( () ); }); @@ -2073,19 +2068,19 @@ mod tests { c "; - init_default_prefs(mathml_str, "Character")?; + init_default_prefs(mathml_str, "Character"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("MoveStart", mathml, "a")?; - test_command("SetPlacemarker0", mathml, "a")?; - test_command("MoveEnd", mathml, "c")?; - test_command("Read0", mathml, "c")?; - test_command("Describe0", mathml, "c")?; - test_command("SetPlacemarker1", mathml, "c")?; - test_command("MoveTo0", mathml, "a")?; - test_command("MoveTo1", mathml, "c")?; - test_command("MoveLastLocation", mathml, "a")?; + test_command("MoveStart", mathml, "a"); + test_command("SetPlacemarker0", mathml, "a"); + test_command("MoveEnd", mathml, "c"); + test_command("Read0", mathml, "c"); + test_command("Describe0", mathml, "c"); + test_command("SetPlacemarker1", mathml, "c"); + test_command("MoveTo0", mathml, "a"); + test_command("MoveTo1", mathml, "c"); + test_command("MoveLastLocation", mathml, "a"); return Ok( () ); }); @@ -2097,8 +2092,8 @@ mod tests { b2 d "; - init_default_prefs(mathml_str, "Enhanced")?; - set_preference("SpeechStyle", "ClearSpeak")?; + init_default_prefs(mathml_str, "Enhanced"); + set_preference("SpeechStyle", "ClearSpeak").unwrap(); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); @@ -2109,7 +2104,7 @@ mod tests { }, "None") }); // WhereAmIAll doesn't change the stack - let speech =test_command("WhereAmIAll", mathml, "exp")?; + let speech =test_command("WhereAmIAll", mathml, "exp"); // should be 2 "inside" strings corresponding to steps to the root assert_eq!(speech, "2; inside; b squared; inside; the fraction with numerator; b squared; and denominator d"); return Ok( () ); @@ -2135,16 +2130,16 @@ mod tests { 10 "; - init_default_prefs(mathml_str, "Enhanced")?; + init_default_prefs(mathml_str, "Enhanced"); set_preference("AutoZoomOut", "False")?; return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomInAll", mathml, "2")?; - test_command("MoveNext", mathml, "a")?; - test_command("MoveNext", mathml, "x")?; - test_command("MoveNext", mathml, "plus")?; - test_command("MovePrevious", mathml, "2ax")?; + test_command("ZoomInAll", mathml, "2"); + test_command("MoveNext", mathml, "a"); + test_command("MoveNext", mathml, "x"); + test_command("MoveNext", mathml, "plus"); + test_command("MovePrevious", mathml, "2ax"); return Ok( () ); }); } @@ -2161,19 +2156,19 @@ mod tests { 3 "; - init_default_prefs(mathml_str, "Enhanced")?; + init_default_prefs(mathml_str, "Enhanced"); set_preference("AutoZoomOut", "False")?; return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomIn", mathml, "frac")?; - test_command("ZoomIn", mathml, "num")?; - test_command("MoveNext", mathml, "denom")?; - test_command("MoveNext", mathml, "denom")?; - test_command("MovePrevious", mathml, "num")?; - test_command("MovePrevious", mathml, "num")?; - test_command("ZoomOut", mathml, "frac")?; - test_command("MoveNext", mathml, "minus")?; + test_command("ZoomIn", mathml, "frac"); + test_command("ZoomIn", mathml, "num"); + test_command("MoveNext", mathml, "denom"); + test_command("MoveNext", mathml, "denom"); + test_command("MovePrevious", mathml, "num"); + test_command("MovePrevious", mathml, "num"); + test_command("ZoomOut", mathml, "frac"); + test_command("MoveNext", mathml, "minus"); return Ok( () ); }); } @@ -2199,43 +2194,43 @@ mod tests { return Ok( () ); fn test_mode(mathml_str: &str, mode: &str) -> Result<()> { - init_default_prefs(mathml_str, mode)?; + init_default_prefs(mathml_str, mode); set_preference("AutoZoomOut", "False")?; return MATHML_INSTANCE.with(|package_instance| { debug!("--- Testing mode {mode} ---"); let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomIn", mathml, "id-9")?; + test_command("ZoomIn", mathml, "id-9"); debug!("\nStart zoom in"); match mode { "Enhanced" => { - test_command("MoveNext", mathml, "id-10")?; - let speech = test_command("ZoomIn", mathml, "id-11")?; + test_command("MoveNext", mathml, "id-10"); + let speech = test_command("ZoomIn", mathml, "id-11"); assert_eq!(speech, "zoom in; in root; b squared minus 4"); // only one arg, so don't say "in root" - let speech = test_command("ZoomIn", mathml, "id-12")?; + let speech = test_command("ZoomIn", mathml, "id-12"); assert_eq!(speech, "zoom in; b squared"); // only one arg, so don't say "in root" - let speech = test_command("ZoomIn", mathml, "id-13")?; + let speech = test_command("ZoomIn", mathml, "id-13"); assert_eq!(speech, "zoom in; in base; b"); }, "Simple" => { - test_command("MoveNext", mathml, "id-10")?; - let speech = test_command("ZoomIn", mathml, "id-12")?; + test_command("MoveNext", mathml, "id-10"); + let speech = test_command("ZoomIn", mathml, "id-12"); assert_eq!(speech, "zoom in; in root; b squared"); - let speech = test_command("ZoomIn", mathml, "id-13")?; + let speech = test_command("ZoomIn", mathml, "id-13"); assert_eq!(speech, "zoom in; in base; b"); }, _ => { // "Character" - let speech = test_command("MoveNext", mathml, "id-13")?; + let speech = test_command("MoveNext", mathml, "id-13"); assert_eq!(speech, "move right; in root; in base; b"); } } let squared_speech = if mode == "Character" {"b super 2 end super"} else {"b squared"}; let sqrt_speech = if mode == "Character" {"root"} else {"square root"}; - let speech = test_command("ZoomOut", mathml, "id-12")?; + let speech = test_command("ZoomOut", mathml, "id-12"); assert_eq!(speech, format!("zoom out; out of base; {squared_speech}")); - let speech = test_command("ZoomOut", mathml, "id-11")?; + let speech = test_command("ZoomOut", mathml, "id-11"); assert_eq!(speech, format!("zoom out; {squared_speech} minus 4")); - let speech = test_command("ZoomOut", mathml, "id-10")?; + let speech = test_command("ZoomOut", mathml, "id-10"); assert_eq!(speech, format!("zoom out; out of root; the {sqrt_speech} of {squared_speech} minus 4, end root",)); return Ok( () ); }); @@ -2260,18 +2255,18 @@ mod tests { ] "#; - init_default_prefs(mathml_str, "Enhanced")?; + init_default_prefs(mathml_str, "Enhanced"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomIn", mathml, "row-1")?; - let speech = test_command("MoveNext", mathml, "row-2")?; + test_command("ZoomIn", mathml, "row-1"); + let speech = test_command("MoveNext", mathml, "row-2"); assert_eq!(speech, "move right; row 2; 5, negative 6"); - let speech = test_command("ZoomIn", mathml, "id-13")?; + let speech = test_command("ZoomIn", mathml, "id-13"); assert_eq!(speech, "zoom in; column 1; 5"); - let speech = test_command("ZoomOut", mathml, "row-2")?; + let speech = test_command("ZoomOut", mathml, "row-2"); assert_eq!(speech, "zoom out; row 2; 5, negative 6"); - let speech = test_command("ZoomOut", mathml, "table")?; + let speech = test_command("ZoomOut", mathml, "table"); assert_eq!(speech, "zoom out; the 2 by 2 matrix; row 1; 9, negative 13; row 2; 5, negative 6"); return Ok( () ); }); @@ -2295,12 +2290,12 @@ mod tests { "; - init_default_prefs(mathml_str, "Enhanced")?; + init_default_prefs(mathml_str, "Enhanced"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomIn", mathml, "id-2")?; - let speech = test_command("MoveNext", mathml, "id-6")?; + test_command("ZoomIn", mathml, "id-2"); + let speech = test_command("MoveNext", mathml, "id-6"); // tables need to check their parent for proper speech assert_eq!(speech, "move right; cap s"); return Ok( () ); @@ -2325,22 +2320,22 @@ mod tests { | "; - init_default_prefs(mathml_str, "Enhanced")?; - set_preference("SpeechStyle", "ClearSpeak")?; + init_default_prefs(mathml_str, "Enhanced"); + set_preference("SpeechStyle", "ClearSpeak").unwrap(); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - let speech = test_command("ZoomIn", mathml, "row-1")?; + let speech = test_command("ZoomIn", mathml, "row-1"); assert_eq!(speech, "zoom in; row 1; 9, negative 13"); - let speech = test_command("MoveNext", mathml, "row-2")?; + let speech = test_command("MoveNext", mathml, "row-2"); assert_eq!(speech, "move right; row 2; 5, negative 6"); - let speech = test_command("MoveNext", mathml, "row-2")?; + let speech = test_command("MoveNext", mathml, "row-2"); assert_eq!(speech, "cannot move right, end of math"); - let speech = test_command("ZoomIn", mathml, "id-13")?; + let speech = test_command("ZoomIn", mathml, "id-13"); assert_eq!(speech, "zoom in; column 1; 5"); - let speech = test_command("MoveNext", mathml, "row2-negative")?; + let speech = test_command("MoveNext", mathml, "row2-negative"); assert_eq!(speech, "move right; column 2, negative 6"); - let speech = test_command("ZoomOutAll", mathml, "table")?; + let speech = test_command("ZoomOutAll", mathml, "table"); assert_eq!(speech, "zoomed out all of the way; the 2 by 2 determinant; row 1; 9, negative 13; row 2; 5, negative 6"); return Ok( () ); }); @@ -2363,22 +2358,22 @@ mod tests { "; - init_default_prefs(mathml_str, "Enhanced")?; - set_preference("SpeechStyle", "ClearSpeak")?; + init_default_prefs(mathml_str, "Enhanced"); + set_preference("SpeechStyle", "ClearSpeak").unwrap(); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomIn", mathml, "row-1")?; - let speech = test_command("MovePrevious", mathml, "row-1")?; + test_command("ZoomIn", mathml, "row-1"); + let speech = test_command("MovePrevious", mathml, "row-1"); assert_eq!(speech, "move left; start of math"); - let speech = test_command("MoveNext", mathml, "row-2")?; + let speech = test_command("MoveNext", mathml, "row-2"); assert_eq!(speech, "move right; case 2; positive x comma; if x, is greater than or equal to 0"); - let speech = test_command("ZoomOut", mathml, "table")?; + let speech = test_command("ZoomOut", mathml, "table"); assert_eq!(speech, "zoom out; 2 cases; case 1; negative x comma; if x is less than 0; case 2; positive x comma; if x, is greater than or equal to 0"); - let speech = test_command("ZoomIn", mathml, "row-1")?; + let speech = test_command("ZoomIn", mathml, "row-1"); assert_eq!(speech, "zoom in; case 1; negative x comma; if x is less than 0"); - set_preference("NavMode", "Character")?; - let speech = test_command("MovePrevious", mathml, "open")?; + set_preference("NavMode", "Character").unwrap(); + let speech = test_command("MovePrevious", mathml, "open"); assert_eq!(speech, "move left; open brace"); return Ok( () ); }); @@ -2401,14 +2396,14 @@ mod tests { 2 "; - init_default_prefs(mathml_str, "Enhanced")?; - set_preference("SpeechStyle", "ClearSpeak")?; + init_default_prefs(mathml_str, "Enhanced"); + set_preference("SpeechStyle", "ClearSpeak").unwrap(); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - let speech = test_command("ZoomIn", mathml, "id-4")?; + let speech = test_command("ZoomIn", mathml, "id-4"); assert_eq!(speech, "zoom in; in base; 2 x"); - let speech = test_command("MoveNext", mathml, "id-9")?; + let speech = test_command("MoveNext", mathml, "id-9"); assert_eq!(speech, "move right; in exponent; 2"); return Ok( () ); }); @@ -2426,45 +2421,45 @@ mod tests { ) "; - init_default_prefs(mathml_str, "Character")?; - set_preference("SpeechStyle", "ClearSpeak")?; + init_default_prefs(mathml_str, "Character"); + set_preference("SpeechStyle", "ClearSpeak").unwrap(); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); debug!("Character mode"); - let speech = test_command("MoveStart", mathml, "id-2")?; + let speech = test_command("MoveStart", mathml, "id-2"); assert_eq!(speech, "move to start of math; open paren"); - let speech = test_command("MoveNext", mathml, "id-4")?; + let speech = test_command("MoveNext", mathml, "id-4"); // I'm not keen on the use of numerator/denominator here, but character mode turns off intent assert_eq!(speech, "move right; in numerator; n"); - let speech = test_command("MoveNext", mathml, "id-5")?; + let speech = test_command("MoveNext", mathml, "id-5"); assert_eq!(speech, "move right; in denominator; k"); debug!("before zoom out"); - let speech = test_command("ZoomOut", mathml, "id-3")?; + let speech = test_command("ZoomOut", mathml, "id-3"); assert_eq!(speech, "zoom out; out of denominator; n over k"); - // let speech = test_command("ZoomOut", mathml, "id-1")?; + // let speech = test_command("ZoomOut", mathml, "id-1"); // assert_eq!(speech, "zoom out; open paren n over k, close paren"); - set_preference("NavMode", "Simple")?; + set_preference("NavMode", "Simple").unwrap(); debug!("Simple mode"); - let speech = test_command("ZoomIn", mathml, "id-4")?; + let speech = test_command("ZoomIn", mathml, "id-4"); assert_eq!(speech, "zoom in; in part 1; n"); - let speech = test_command("MoveNext", mathml, "id-5")?; + let speech = test_command("MoveNext", mathml, "id-5"); assert_eq!(speech, "move right; in part 2; k"); - let speech = test_command("MoveNext", mathml, "id-5")?; + let speech = test_command("MoveNext", mathml, "id-5"); assert_eq!(speech, "cannot move right, end of math"); - let speech = test_command("ZoomOut", mathml, "id-1-literal-0")?; + let speech = test_command("ZoomOut", mathml, "id-1-literal-0"); assert_eq!(speech, "zoom out; out of part 2; n choose k"); - set_preference("NavMode", "Enhanced")?; + set_preference("NavMode", "Enhanced").unwrap(); debug!("Enhanced mode"); - let speech = test_command("ZoomIn", mathml, "id-4")?; + let speech = test_command("ZoomIn", mathml, "id-4"); assert_eq!(speech, "zoom in; in part 1; n"); - let speech = test_command("MoveNext", mathml, "id-5")?; + let speech = test_command("MoveNext", mathml, "id-5"); assert_eq!(speech, "move right; in part 2; k"); - let speech = test_command("MoveNext", mathml, "id-5")?; + let speech = test_command("MoveNext", mathml, "id-5"); assert_eq!(speech, "cannot move right, end of math"); - let speech = test_command("ZoomOut", mathml, "id-1-literal-0")?; + let speech = test_command("ZoomOut", mathml, "id-1-literal-0"); assert_eq!(speech, "zoom out; out of part 2; n choose k"); return Ok( () ); @@ -2496,19 +2491,19 @@ mod tests { ) "#; - init_default_prefs(mathml_str, "Simple")?; + init_default_prefs(mathml_str, "Simple"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - let speech = test_command("ZoomIn", mathml, "id-3-literal-1")?; + let speech = test_command("ZoomIn", mathml, "id-3-literal-1"); assert_eq!(speech, "zoom in; 1"); - let speech = test_command("MoveNext", mathml, "id-3-literal-2")?; + let speech = test_command("MoveNext", mathml, "id-3-literal-2"); assert_eq!(speech, "move right; 2"); - let speech = test_command("MoveNext", mathml, "id-3-literal-3")?; + let speech = test_command("MoveNext", mathml, "id-3-literal-3"); assert_eq!(speech, "move right; 3"); - let speech = test_command("MoveNext", mathml, "id-3-literal-3")?; + let speech = test_command("MoveNext", mathml, "id-3-literal-3"); assert_eq!(speech, "cannot move right, end of math"); - let speech = test_command("ZoomOut", mathml, "id-3-literal-0")?; + let speech = test_command("ZoomOut", mathml, "id-3-literal-0"); assert_eq!(speech, "zoom out; diagonal 1 2 3"); return Ok( () ); @@ -2527,25 +2522,25 @@ mod tests { "; - init_default_prefs(mathml_str, "Enhanced")?; - set_preference("SpeechStyle", "ClearSpeak")?; + init_default_prefs(mathml_str, "Enhanced"); + set_preference("SpeechStyle", "ClearSpeak").unwrap(); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - let speech = test_command("ZoomIn", mathml, "2")?; + let speech = test_command("ZoomIn", mathml, "2"); assert_eq!(speech, "zoom in; 2"); - let speech = test_command("MoveNext", mathml, "abs")?; + let speech = test_command("MoveNext", mathml, "abs"); assert_eq!(speech, "move right; the absolute value of x"); - let speech = test_command("ZoomIn", mathml, "x")?; + let speech = test_command("ZoomIn", mathml, "x"); assert_eq!(speech, "zoom in; in absolute value; x"); - let speech = test_command("MoveNext", mathml, "x")?; + let speech = test_command("MoveNext", mathml, "x"); assert_eq!(speech, "cannot move right, end of math"); - set_preference("NavMode", "Character")?; - let speech = test_command("MoveNext", mathml, "end")?; + set_preference("NavMode", "Character").unwrap(); + let speech = test_command("MoveNext", mathml, "end"); assert_eq!(speech, "move right; vertical line"); - let speech = test_command("MoveLineStart", mathml, "2")?; + let speech = test_command("MoveLineStart", mathml, "2"); assert_eq!(speech, "move to start of line; 2"); - let speech = test_command("MoveNext", mathml, "start")?; + let speech = test_command("MoveNext", mathml, "start"); assert_eq!(speech, "move right; vertical line"); return Ok( () ); }); @@ -2563,15 +2558,15 @@ mod tests { 3 "; - init_default_prefs(mathml_str, "Enhanced")?; - set_preference("SpeechStyle", "SimpleSpeak")?; + init_default_prefs(mathml_str, "Enhanced"); + set_preference("SpeechStyle", "SimpleSpeak").unwrap(); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomIn", mathml, "frac")?; - let speech = test_command("ReadCurrent", mathml, "frac")?; + test_command("ZoomIn", mathml, "frac"); + let speech = test_command("ReadCurrent", mathml, "frac"); assert_eq!(speech, "read current; fraction, b plus 1, over 3, end fraction"); - let speech = test_command("DescribeCurrent", mathml, "frac")?; + let speech = test_command("DescribeCurrent", mathml, "frac"); assert_eq!(speech, "describe current; fraction"); return Ok( () ); }); @@ -2598,14 +2593,21 @@ mod tests { "; - init_prefs(mathml_str, "Enhanced", "ru")?; + init_prefs(mathml_str, "Enhanced", "ru"); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - let speech = test_command("ZoomIn", mathml, "")?; + let speech = test_command("ZoomIn", mathml, ""); assert_eq!("переход внутрь; в числитель; икс плюс, квадратный корень из 1 разделить на игрек, конец корня", speech); - let speech = test_command("DescribeCurrent", mathml, "")?; - assert_eq!("описать текущее; икс плюс, квадратный корень из 1 разделить на игрек", speech); + match do_navigate_command_string(mathml, "DescribeCurrent") { + Ok(speech) => { + let speech = speech.trim_end_matches([' ', ',', ';']).to_string(); + assert_eq!("описать текущее; икс плюс, квадратный корень из 1 разделить на игрек", speech); + }, + Err(e) => { + panic!("DescribeCurrent failed: {}", crate::interface::errors_to_string(&e)); + }, + }; return Ok( () ); }); } @@ -2624,16 +2626,16 @@ mod tests { 7 "; - init_default_prefs(mathml_str, "Enhanced")?; - set_preference("SpeechStyle", "SimpleSpeak")?; + init_default_prefs(mathml_str, "Enhanced"); + set_preference("SpeechStyle", "SimpleSpeak").unwrap(); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - let speech = test_command("ZoomOutAll", mathml, "mrow")?; + let speech = test_command("ZoomOutAll", mathml, "mrow"); assert_eq!(speech, "zoomed out all of the way; 1 plus 2 plus 3 plus 4 plus 5 plus 6 plus 7"); - let speech = test_command("ReadCurrent", mathml, "mrow")?; + let speech = test_command("ReadCurrent", mathml, "mrow"); assert_eq!(speech, "read current; 1 plus 2 plus 3 plus 4 plus 5 plus 6 plus 7"); - let speech = test_command("DescribeCurrent", mathml, "mrow")?; + let speech = test_command("DescribeCurrent", mathml, "mrow"); assert_eq!(speech, "describe current; 1 plus 2 plus 3 and so on"); return Ok( () ); }); @@ -2649,16 +2651,16 @@ mod tests { y "; - init_default_prefs(mathml_str, "Simple")?; - set_preference("SpeechStyle", "SimpleSpeak")?; + init_default_prefs(mathml_str, "Simple"); + set_preference("SpeechStyle", "SimpleSpeak").unwrap(); return MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - let speech = test_command("ZoomIn", mathml, "id-2")?; + let speech = test_command("ZoomIn", mathml, "id-2"); assert_eq!(speech, "zoom in; x"); - let speech = test_command("ToggleZoomLockUp", mathml, "id-2")?; + let speech = test_command("ToggleZoomLockUp", mathml, "id-2"); assert_eq!(speech, "enhanced mode; x"); - let speech = test_command("ReadNext", mathml, "id-2")?; + let speech = test_command("ReadNext", mathml, "id-2"); assert_eq!(speech, "read right; y"); return Ok( () ); }); @@ -2688,68 +2690,63 @@ mod tests { "; - set_rules_dir(super::super::abs_rules_dir_path())?; + set_rules_dir(super::super::abs_rules_dir_path()).unwrap(); for lang in get_supported_languages().unwrap_or_default() { - test_language(&lang, mathml_str)?; + test_language(&lang, mathml_str); } return Ok( () ); - fn test_language(lang: &str, mathml_str: &str) -> Result<()> { - init_default_prefs(mathml_str, "Enhanced")?; - set_preference("Language", lang)?; + fn test_language(lang: &str, mathml_str: &str) { + init_default_prefs(mathml_str, "Enhanced"); + set_preference("Language", lang).unwrap(); - set_preference("NavMode", "Enhanced")?; - MATHML_INSTANCE.with(|package_instance| -> Result<()> { + set_preference("NavMode", "Enhanced").unwrap(); + MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomInAll", mathml, "2")?; - test_command("MoveNext", mathml, "msup")?; - test_command("MoveNext", mathml, "plus")?; - test_command("MovePrevious", mathml, "term")?; - test_command("MovePrevious", mathml, "term")?; - test_command("ZoomOutAll", mathml, "contents")?; - return Ok( () ); - })?; + test_command("ZoomInAll", mathml, "2"); + test_command("MoveNext", mathml, "msup"); + test_command("MoveNext", mathml, "plus"); + test_command("MovePrevious", mathml, "term"); + test_command("MovePrevious", mathml, "term"); + test_command("ZoomOutAll", mathml, "contents"); + }); - set_preference("NavMode", "Simple")?; - MATHML_INSTANCE.with(|package_instance: &RefCell| -> Result<()> { + set_preference("NavMode", "Simple").unwrap(); + MATHML_INSTANCE.with(|package_instance: &RefCell| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomInAll", mathml, "2")?; - test_command("MoveNext", mathml, "msup")?; - test_command("MoveNext", mathml, "plus")?; - test_command("MovePrevious", mathml, "msup")?; - test_command("MovePrevious", mathml, "2")?; - test_command("MovePrevious", mathml, "2")?; - test_command("ZoomOutAll", mathml, "contents")?; - return Ok( () ); - })?; + test_command("ZoomInAll", mathml, "2"); + test_command("MoveNext", mathml, "msup"); + test_command("MoveNext", mathml, "plus"); + test_command("MovePrevious", mathml, "msup"); + test_command("MovePrevious", mathml, "2"); + test_command("MovePrevious", mathml, "2"); + test_command("ZoomOutAll", mathml, "contents"); + }); - set_preference("NavMode", "Character")?; - MATHML_INSTANCE.with(|package_instance| -> Result<()> { + set_preference("NavMode", "Character").unwrap(); + MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomIn", mathml, "2")?; - test_command("MoveNext", mathml, "x")?; - test_command("MoveNext", mathml, "3")?; - test_command("MoveNext", mathml, "plus")?; - test_command("MovePrevious", mathml, "3")?; - test_command("MovePrevious", mathml, "x")?; - test_command("MovePrevious", mathml, "2")?; - test_command("MovePrevious", mathml, "2")?; - return Ok( () ); - })?; + test_command("ZoomIn", mathml, "2"); + test_command("MoveNext", mathml, "x"); + test_command("MoveNext", mathml, "3"); + test_command("MoveNext", mathml, "plus"); + test_command("MovePrevious", mathml, "3"); + test_command("MovePrevious", mathml, "x"); + test_command("MovePrevious", mathml, "2"); + test_command("MovePrevious", mathml, "2"); + }); // simple sanity check that "overview.yaml" doesn't have a syntax error - set_preference("Overview", "True")?; - set_preference("NavMode", "Character")?; - MATHML_INSTANCE.with(|package_instance| -> Result<()> { + set_preference("Overview", "True").unwrap(); + set_preference("NavMode", "Character").unwrap(); + MATHML_INSTANCE.with(|package_instance| { let package_instance = package_instance.borrow(); let mathml = get_element(&package_instance); - test_command("ZoomIn", mathml, "2")?; - return Ok( () ); - })?; - return Ok( () ); + test_command("ZoomIn", mathml, "2"); + }); } } } diff --git a/src/prefs.rs b/src/prefs.rs index 25f68c1b0..f39d9bd2d 100644 --- a/src/prefs.rs +++ b/src/prefs.rs @@ -1252,7 +1252,7 @@ cfg_if::cfg_if! {if #[cfg(not(feature = "include-zip"))] { let pref_manager = pref_manager.borrow(); if let Some(file_name) = pref_manager.user_prefs_file.as_ref().unwrap().debug_get_file() { file_path = PathBuf::from(file_name); - contents = fs::read(&file_path).expect(&format!("Failed to write file {} during test", file_name)); + contents = fs::read(&file_path).unwrap_or_else(|_| panic!("Failed to write file {} during test", file_name)); let changed_contents = String::from_utf8(contents.clone()).unwrap() .replace("SpeechStyle: ClearSpeak", "SpeechStyle: SimpleSpeak"); fs::write(&file_path, changed_contents).unwrap(); diff --git a/src/speech.rs b/src/speech.rs index 2545d308a..0b98c58f9 100644 --- a/src/speech.rs +++ b/src/speech.rs @@ -3012,7 +3012,7 @@ cfg_if::cfg_if! {if #[cfg(not(feature = "include-zip"))] { let start_main_file = rules.borrow().unicode_short_files.borrow().ft[0].clone(); // open the file, read all the contents, then write them back so the time changes - let contents = std::fs::read(&start_main_file.file).expect(&format!("Failed to read file {} during test", &start_main_file.file.to_string_lossy())); + let contents = std::fs::read(&start_main_file.file).unwrap_or_else(|_| panic!("Failed to read file {} during test", &start_main_file.file.to_string_lossy())); std::fs::write(start_main_file.file, contents).unwrap(); std::thread::sleep(Duration::from_millis(5)); // pause a little to make sure the time changes diff --git a/tests/braille.rs b/tests/braille.rs index 2fc04fffc..c901fc036 100644 --- a/tests/braille.rs +++ b/tests/braille.rs @@ -1,4 +1,5 @@ #![allow(non_snake_case)] +#![allow(clippy::needless_return)] mod common; diff --git a/tests/languages.rs b/tests/languages.rs index 4732b99f4..b0552b95b 100644 --- a/tests/languages.rs +++ b/tests/languages.rs @@ -1,4 +1,5 @@ #![allow(non_snake_case)] +#![allow(clippy::doc_lazy_continuation, clippy::module_inception, clippy::needless_return)] mod common; From fe3f71771f67c1821cef9bc5c3e50e6896e3dd39 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:26:44 +0500 Subject: [PATCH 37/47] Remove unnecessary borrows in format macros Remove redundant `&` reference operators from arguments passed to format! macros and similar functions. These values are automatically borrowed by the format machinery, so explicit borrows are unnecessary and reduce readability. --- src/navigate.rs | 4 +-- src/pretty_print.rs | 59 ++++++++++++++++++++++----------------------- src/speech.rs | 12 ++++----- src/tts.rs | 12 ++++----- 4 files changed, 43 insertions(+), 44 deletions(-) diff --git a/src/navigate.rs b/src/navigate.rs index 207460519..5e5d0309b 100644 --- a/src/navigate.rs +++ b/src/navigate.rs @@ -417,7 +417,7 @@ pub fn do_navigate_command_string(mathml: Element, nav_command: &'static str) -> match tts.as_str() { "SSML" if !cumulative_speech.starts_with(" { - cumulative_speech = format!("{}", &rate, &cumulative_speech); + cumulative_speech = format!("{}", rate, cumulative_speech); } "SAPI5" if !cumulative_speech.starts_with(" { @@ -599,7 +599,7 @@ pub fn do_navigate_command_string(mathml: Element, nav_command: &'static str) -> bail!("Internal error: With {}/{} in {} mode, can't {} from expression with id '{}' inside:\n{}", rules.pref_manager.as_ref().borrow().pref_to_string("Language"), rules.pref_manager.as_ref().borrow().pref_to_string("SpeechStyle"), - &nav_state.mode, nav_command, &nav_position.current_node, mml_to_string(if literal_speak {mathml} else {intent})); + nav_state.mode, nav_command, nav_position.current_node, mml_to_string(if literal_speak {mathml} else {intent})); } return Err(e); } diff --git a/src/pretty_print.rs b/src/pretty_print.rs index ee0cda6d6..d5738c47e 100644 --- a/src/pretty_print.rs +++ b/src/pretty_print.rs @@ -1,8 +1,7 @@ //! Useful functions for debugging and error messages. #![allow(clippy::needless_return)] -use sxd_document_no_unsafe::dom::{Element, ChildOfElement, Attribute}; -use sxd_document_no_unsafe::{as_str, as_qname}; +use sxd_document::dom::{Element, ChildOfElement, Attribute}; // #[allow(dead_code)] // pub fn pp_doc(doc: &Document) { @@ -28,18 +27,16 @@ pub fn format_element(e: Element, indent: usize) -> String { // }; // let namespace = namespace.as_str(); let namespace = ""; - let mut answer = format!("{:in$}<{ns}{name}{attrs}>", " ", in=2*indent, ns=namespace, name=as_qname!(e.name()).local_part(), attrs=format_attrs(&e.attributes())); + let mut answer = format!("{:in$}<{ns}{name}{attrs}>", " ", in=2*indent, ns=namespace, name=e.name().local_part(), attrs=format_attrs(&e.attributes())); let children = e.children(); let has_element = children.iter().find(|&&c| matches!(c, ChildOfElement::Element(_x))); if has_element.is_none() { // print text content - let content = children.iter().fold(String::new(), |mut acc, c| { - if let ChildOfElement::Text(t) = c { - acc.push_str(as_str!(t.text())); - } - acc - }); - return format!("{}{}\n", answer, handle_special_chars(&content), namespace, as_qname!(e.name()).local_part()); + let content = children.iter() + .map(|c| if let ChildOfElement::Text(t) = c {t.text()} else {""}) + .collect::>() + .join(""); + return format!("{}{}\n", answer, handle_special_chars(&content), namespace, e.name().local_part()); // for child in children { // if let ChildOfElement::Text(t) = child { // return format!("{}{}\n", answer, &make_invisible_chars_visible(t.text()), namespace, e.name().local_part()); @@ -54,7 +51,7 @@ pub fn format_element(e: Element, indent: usize) -> String { } } } - return answer + &format!("{:in$}\n", " ", in=2*indent, ns=namespace, name=as_qname!(e.name()).local_part()); + return answer + &format!("{:in$}\n", " ", in=2*indent, ns=namespace, name=e.name().local_part()); // Use the &#x....; representation for invisible chars when printing } @@ -63,7 +60,7 @@ pub fn format_element(e: Element, indent: usize) -> String { pub fn format_attrs(attrs: &[Attribute]) -> String { let mut result = String::new(); for attr in attrs { - result += format!(" {}='{}'", as_qname!(attr.name()).local_part(), handle_special_chars(as_str!(attr.value()))).as_str(); + result += format!(" {}='{}'", attr.name().local_part(), handle_special_chars(attr.value())).as_str(); } result } @@ -92,8 +89,8 @@ fn handle_special_chars(text: &str) -> String { // /// Pretty print an xpath value. // /// If the value is a `NodeSet`, the MathML for the node/element is returned. // pub fn pp_xpath_value(value: Value) { -// use sxd_xpath_no_unsafe::Value; -// use sxd_xpath_no_unsafe::nodeset::Node; +// use sxd_xpath::Value; +// use sxd_xpath::nodeset::Node; // debug!("XPath value:"); // if let Value::Nodeset(nodeset) = &value { // for node in nodeset.document_order() { @@ -520,11 +517,11 @@ fn need_quotes(string: &str) -> bool { #[cfg(test)] mod tests { use super::*; - use sxd_document_no_unsafe::dom::{ChildOfElement, ChildOfRoot}; - use sxd_document_no_unsafe::parser; + use sxd_document::dom::{ChildOfElement, ChildOfRoot}; + use sxd_document::parser; /// helper function - fn first_element(package: &sxd_document_no_unsafe::Package) -> Element<'_> { + fn first_element(package: &sxd_document::Package) -> Element<'_> { let doc = package.as_document(); for child in doc.root().children() { if let ChildOfRoot::Element(e) = child { @@ -618,11 +615,11 @@ mod tests { #[test] /// Evaluates non-BMP literal text through sxd_xpath. fn xpath_non_bmp_literal() { - use sxd_xpath_no_unsafe::{Factory, Value}; + use sxd_xpath::{Factory, Value}; let package = parser::parse("𝞪").unwrap(); - let xpath = Factory::new().build("string(/math/mi)").unwrap(); - let context = sxd_xpath_no_unsafe::Context::new(); + let xpath = Factory::new().build("string(/math/mi)").unwrap().unwrap(); + let context = sxd_xpath::Context::new(); let value = xpath.evaluate(&context, first_element(&package)).unwrap(); match value { @@ -634,11 +631,11 @@ mod tests { #[test] /// Evaluates non-BMP numeric text through sxd_xpath. fn xpath_non_bmp_numeric() { - use sxd_xpath_no_unsafe::{Factory, Value}; + use sxd_xpath::{Factory, Value}; let package = parser::parse("𝞪").unwrap(); - let xpath = Factory::new().build("string(/math/mi)").unwrap(); - let context = sxd_xpath_no_unsafe::Context::new(); + let xpath = Factory::new().build("string(/math/mi)").unwrap().unwrap(); + let context = sxd_xpath::Context::new(); let value = xpath.evaluate(&context, first_element(&package)).unwrap(); match value { @@ -650,14 +647,15 @@ mod tests { #[test] /// Evaluates non-BMP literal text with a MathML namespace-qualified XPath. fn xpath_non_bmp_namespace_literal() { - use sxd_xpath_no_unsafe::{Factory, Value}; + use sxd_xpath::{Factory, Value}; let xml = "𝞪"; let package = parser::parse(xml).unwrap(); let xpath = Factory::new() .build("string(/m:math/m:mi)") + .unwrap() .unwrap(); - let mut context = sxd_xpath_no_unsafe::Context::new(); + let mut context = sxd_xpath::Context::new(); context.set_namespace("m", "http://www.w3.org/1998/Math/MathML"); let value = xpath.evaluate(&context, first_element(&package)).unwrap(); @@ -670,14 +668,15 @@ mod tests { #[test] /// Evaluates non-BMP numeric text with a MathML namespace-qualified XPath. fn xpath_non_bmp_namespace_numeric() { - use sxd_xpath_no_unsafe::{Factory, Value}; + use sxd_xpath::{Factory, Value}; let xml = "𝞪"; let package = parser::parse(xml).unwrap(); let xpath = Factory::new() .build("string(/m:math/m:mi)") + .unwrap() .unwrap(); - let mut context = sxd_xpath_no_unsafe::Context::new(); + let mut context = sxd_xpath::Context::new(); context.set_namespace("m", "http://www.w3.org/1998/Math/MathML"); let value = xpath.evaluate(&context, first_element(&package)).unwrap(); @@ -690,12 +689,12 @@ mod tests { #[test] /// Extracts a text node via XPath (nodeset result) and verifies the non-BMP character survives. fn xpath_non_bmp_text_nodeset() { - use sxd_xpath_no_unsafe::{Factory, Value}; + use sxd_xpath::{Factory, Value}; let xml = "𝞪"; let package = parser::parse(xml).unwrap(); - let xpath = Factory::new().build("/m:math/m:mi/text()").unwrap(); - let mut context = sxd_xpath_no_unsafe::Context::new(); + let xpath = Factory::new().build("/m:math/m:mi/text()").unwrap().unwrap(); + let mut context = sxd_xpath::Context::new(); context.set_namespace("m", "http://www.w3.org/1998/Math/MathML"); let value = xpath.evaluate(&context, first_element(&package)).unwrap(); diff --git a/src/speech.rs b/src/speech.rs index 0b98c58f9..732b9f81f 100644 --- a/src/speech.rs +++ b/src/speech.rs @@ -1334,14 +1334,14 @@ impl SpeechPattern { Err(e) => return Err( e.context( format!("tag name '{}' is not a string in:\n{}", - &yaml_to_string(&tag_array.as_vec().unwrap()[i], 0), - &yaml_to_string(dict, 1))) + yaml_to_string(&tag_array.as_vec().unwrap()[i], 0), + yaml_to_string(dict, 1))) ), Ok(str) => tag_names.push(str), }; } } else { - bail!("Errors trying to find 'tag' in:\n{}", &yaml_to_string(dict, 1)); + bail!("Errors trying to find 'tag' in:\n{}", yaml_to_string(dict, 1)); } } } @@ -1350,7 +1350,7 @@ impl SpeechPattern { if dict.is_null() { bail!("Error trying to find 'name': empty value (two consecutive '-'s?"); } else { - bail!("Errors trying to find 'name' in:\n{}", &yaml_to_string(dict, 1)); + bail!("Errors trying to find 'name' in:\n{}", yaml_to_string(dict, 1)); }; }; let pattern_name = pattern_name.unwrap().to_string(); @@ -2137,7 +2137,7 @@ impl fmt::Display for SpeechRules { for (tag_name, rules) in rules_vec { writeln!(f, " {}: #patterns {}", tag_name, rules.len())?; }; - return writeln!(f, " {}+{} unicode entries", &self.unicode_short.borrow().len(), &self.unicode_full.borrow().len()); + return writeln!(f, " {}+{} unicode entries", self.unicode_short.borrow().len(), self.unicode_full.borrow().len()); } } @@ -2158,7 +2158,7 @@ pub struct SpeechRulesWithContext<'c, 's:'c, 'm:'c> { impl<'c, 's:'c, 'm:'c> fmt::Display for SpeechRulesWithContext<'c, 's,'m> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "SpeechRulesWithContext \n{})", self.speech_rules)?; - return writeln!(f, " {} context entries, nav node id '({}, {})'", &self.context_stack, self.nav_node_id, self.nav_node_offset); + return writeln!(f, " {} context entries, nav node id '({}, {})'", self.context_stack, self.nav_node_id, self.nav_node_offset); } } diff --git a/src/tts.rs b/src/tts.rs index 521715381..cb9276808 100644 --- a/src/tts.rs +++ b/src/tts.rs @@ -386,7 +386,7 @@ impl TTS { match command.value { TTSCommandValue::XPath(xpath) => { let value = xpath.evaluate(rules_with_context.get_context(), mathml) - .with_context(|| format!("in 'spell': can't evaluate xpath \"{}\"", &xpath.to_string()) )?; + .with_context(|| format!("in 'spell': can't evaluate xpath \"{}\"", xpath) )?; let value_string = match value { Value::String(s) => s, Value::Nodeset(nodes) if nodes.size() == 1 => { @@ -397,14 +397,14 @@ impl TTS { if crate::xpath_functions::is_leaf(el) { crate::canonicalize::as_text(el).to_string() } else { - bail!("in 'spell': value returned from xpath '{}' does not evaluate to a string", &xpath.to_string()); + bail!("in 'spell': value returned from xpath '{}' does not evaluate to a string", xpath); } } else { bail!("in 'spell': value returned from xpath '{}' does not evaluate to a string, it is {} nodes", - &xpath.to_string(), nodes.size()); + xpath, nodes.size()); } }, - _ => bail!("in 'spell': value returned from xpath '{}' does not evaluate to a string", &xpath.to_string()), + _ => bail!("in 'spell': value returned from xpath '{}' does not evaluate to a string", xpath), }; // Chemistry wants to spell elements like "Na". But we also have the issue of capitalization (SpeechOverrides_CapitalLetters) // so the "N" need to use that. The logic for that is already in unicode.yaml. We could replicate that here. @@ -562,7 +562,7 @@ impl TTS { TTSCommand::Voice =>if is_start_tag {format!("", command.value.get_string())} else {String::from("")}, TTSCommand::Spell =>if is_start_tag {format!("{}", command.value.get_string())} else {String::from("")}, TTSCommand::Pronounce =>if is_start_tag { - format!("{}", &command.value.get_pronounce().sapi5, &command.value.get_pronounce().text) + format!("{}", command.value.get_pronounce().sapi5, command.value.get_pronounce().text) } else { String::from("") }, @@ -597,7 +597,7 @@ impl TTS { TTSCommand::Voice =>if is_start_tag {format!("", command.value.get_string())} else {String::from("")}, TTSCommand::Spell =>if is_start_tag {format!("{}", command.value.get_string())} else {String::from("")}, TTSCommand::Pronounce =>if is_start_tag { - format!("{}", &command.value.get_pronounce().ipa, &command.value.get_pronounce().text) + format!("{}", command.value.get_pronounce().ipa, command.value.get_pronounce().text) } else { String::from("") }, From 6dd51dfa2fff627867cd28ed327bb183fd77df38 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:28:00 +0500 Subject: [PATCH 38/47] Normalize Unicode math digits in decimal numbers Add normalization of Unicode mathematical alphanumeric symbols (Plane 1 digits) to ASCII digits for number nodes containing decimal points. Skip canonicalize_plane1 processing for already-normalized nodes to prevent double processing. --- src/canonicalize.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/canonicalize.rs b/src/canonicalize.rs index eaf8f1fa9..ac931f980 100644 --- a/src/canonicalize.rs +++ b/src/canonicalize.rs @@ -2511,6 +2511,7 @@ impl CanonicalizeContext { let child = as_element(child_as_element); mn_text.push_str(as_str!(as_text(child))); } + let mn_text = if mn_text.contains('.') { normalize_math_digits(&mn_text) } else { mn_text }; let child = as_element(children[start]); set_mathml_name(child, "mn"); child.set_text(&mn_text); @@ -2518,6 +2519,21 @@ impl CanonicalizeContext { children.drain(start+1..end); } + fn normalize_math_digits(text: &str) -> String { + text.chars().map(|ch| { + let code = ch as u32; + let digit = match code { + 0x1D7CE..=0x1D7D7 => Some(code - 0x1D7CE), + 0x1D7D8..=0x1D7E1 => Some(code - 0x1D7D8), + 0x1D7E2..=0x1D7EB => Some(code - 0x1D7E2), + 0x1D7EC..=0x1D7F5 => Some(code - 0x1D7EC), + 0x1D7F6..=0x1D7FF => Some(code - 0x1D7F6), + _ => None, + }; + digit.and_then(|digit| char::from_digit(digit, 10)).unwrap_or(ch) + }).collect() + } + /// merge ° C or ° F into a single with the text '℃' or '℉' -- prevents '°' from becoming a superscript #[allow(non_snake_case)] @@ -3098,7 +3114,9 @@ impl CanonicalizeContext { return Ok( mathml ); }, "mn" => { - self.canonicalize_plane1(mathml); + if mathml.attribute_value(CHANGED_ATTR) != Some(ADDED_ATTR_VALUE) && !as_text(mathml).contains('.') { + self.canonicalize_plane1(mathml); + } return Ok( mathml ); }, "mrow" => { From 76d19a26e23fbb2b957ab2286906c240e722a5b4 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:54:41 +0500 Subject: [PATCH 39/47] Remove unnecessary references and .to_string() calls Remove unnecessary `&` borrows in format arguments and replace `.to_string()` with direct display. Simplify byte array literals to byte string literals for readability and idiomatic Rust style. --- src/chemistry.rs | 483 ++++++++++++++++++-------------------------- src/infer_intent.rs | 2 +- src/navigate.rs | 2 +- src/speech.rs | 22 +- tests/common/mod.rs | 2 +- 5 files changed, 211 insertions(+), 300 deletions(-) diff --git a/src/chemistry.rs b/src/chemistry.rs index b8a6ca4fc..f4ba1f929 100644 --- a/src/chemistry.rs +++ b/src/chemistry.rs @@ -35,11 +35,8 @@ // no: clear all the marks for the old children // After canonicalization, we take another pass looking for chemical equations and marking them if found. -use sxd_document_no_unsafe::dom::{Element, Document, ChildOfElement}; -#[allow(unused_imports)] -use sxd_document_no_unsafe::{as_str, as_qname}; +use sxd_document::dom::{Element, Document, ChildOfElement}; use crate::canonicalize::*; -use crate::canonicalize::NameStr; use crate::pretty_print::mml_to_string; use crate::xpath_functions::{is_leaf, IsNode}; use regex::Regex; @@ -90,7 +87,7 @@ fn is_chem_equation_arrow(ch: char) -> bool { // Returns true if the 'property' (should have ":") is in the intent fn has_chem_intent(mathml: Element, property: &str) -> bool { - if let Some(intent) = mathml.attribute_value(INTENT_ATTR).as_deref() { + if let Some(intent) = mathml.attribute_value(INTENT_ATTR) { let head = intent.split('(').next().unwrap(); return head.contains(property); } @@ -212,7 +209,7 @@ fn clean_mrow_children_restructure_pass<'a>(old_children: &[Element<'a>]) -> Opt likely_chem_equation_operator(child); // need to mark MAYBE_CHEMISTRY for CHEMICAL_BOND tests } } else if child_name == "mrow" && - let Some(latex_value) = child.attribute_value("data-latex").as_deref() && + let Some(latex_value) = child.attribute_value("data-latex") && latex_value == r"\mathrel{\longrightleftharpoons}" { child.set_attribute_value("data-unicode", "\u{1f8d2}"); child.set_attribute_value(MAYBE_CHEMISTRY, "2"); // same as is_hack_for_missing_arrows() @@ -357,8 +354,8 @@ pub fn convert_leaves_to_chem_elements(mathml: Element) -> Option> let token_string = as_text(leaf); let chem_token_string = vec![token_string.as_bytes()[0], second_element_text.as_bytes()[0]]; if let Some(chem_element) = get_chem_element(doc, &chem_token_string, 2) { - chem_element.set_text(as_str!(as_text(chem_element))); - chem_element.set_attribute_value(MAYBE_CHEMISTRY, chem_element.attribute_value(MAYBE_CHEMISTRY).as_deref().unwrap()); + chem_element.set_text(as_text(chem_element)); + chem_element.set_attribute_value(MAYBE_CHEMISTRY, chem_element.attribute_value(MAYBE_CHEMISTRY).unwrap()); chem_element.set_attribute_value(MERGED_TOKEN, "true"); second_element.remove_from_parent(); return Some(vec![chem_element]); @@ -368,8 +365,7 @@ pub fn convert_leaves_to_chem_elements(mathml: Element) -> Option> /// split the string which has been checked to be all ASCII chars fn split_string_chem_element<'a>(doc: &Document<'a>, leaf: Element<'a>) -> Option>> { - let raw_text = as_text(leaf); - let token_string = raw_text.as_bytes(); + let token_string = as_text(leaf).as_bytes(); let token_len = token_string.len(); let mut j = 0; let mut new_children = Vec::with_capacity(token_string.len()); @@ -437,7 +433,7 @@ pub fn scan_and_mark_chemistry(mathml: Element) -> bool { let child = as_element(mathml.children()[0]); // debug!("scan_and_mark_chemistry:\n{}", mml_to_string(child)); assert_eq!(name(mathml), "math"); - let is_chemistry = if let Some(latex) = mathml.attribute_value("data-latex").as_deref() { + let is_chemistry = if let Some(latex) = mathml.attribute_value("data-latex") { // MathJax v4 includes this really useful info -- if it starts \ce -- we have Chemistry // need to determine if it is an equation or a formula latex.trim_start().starts_with(r"\ce") @@ -476,22 +472,22 @@ pub fn scan_and_mark_chemistry(mathml: Element) -> bool { // returns the marked attr value or None fn get_marked_value(mathml: Element) -> Option { - return mathml.attribute_value(MAYBE_CHEMISTRY).as_deref().map(|value| value.parse().unwrap()); + return mathml.attribute_value(MAYBE_CHEMISTRY).map(|value| value.parse().unwrap()); } /// Sets the attr 'chem' /// Recurse through all the children that have MAYBE_CHEMISTRY set fn set_marked_chemistry_attr(mathml: Element, chem: &str) { - let tag_name = as_str!(name(mathml)); + let tag_name = name(mathml); if let Some(maybe_attr) = mathml.attribute(MAYBE_CHEMISTRY) { maybe_attr.remove_from_parent(); match tag_name { - "mi" | "mtext" => {mathml.set_attribute_value(CHEM_ELEMENT, as_str!(maybe_attr.value()));}, + "mi" | "mtext" => {mathml.set_attribute_value(CHEM_ELEMENT, maybe_attr.value());}, "mo" => { if mathml.attribute(CHEM_FORMULA_OPERATOR).is_none() && mathml.attribute(CHEM_EQUATION_OPERATOR).is_none(){ // don't mark as both formula and equation - mathml.set_attribute_value(if chem == CHEM_FORMULA {CHEM_FORMULA_OPERATOR} else {CHEM_EQUATION_OPERATOR}, as_str!(maybe_attr.value())); + mathml.set_attribute_value(if chem == CHEM_FORMULA {CHEM_FORMULA_OPERATOR} else {CHEM_EQUATION_OPERATOR}, maybe_attr.value()); } }, "mn" => (), @@ -508,7 +504,7 @@ fn set_marked_chemistry_attr(mathml: Element, chem: &str) { if mathml.attribute(CHEM_FORMULA).is_none() { // don't mark as both formula and equation - mathml.set_attribute_value(chem_name, as_str!(maybe_attr.value())); + mathml.set_attribute_value(chem_name, maybe_attr.value()); } for child in mathml.children() { set_marked_chemistry_attr(as_element(child), chem); @@ -523,7 +519,7 @@ fn set_marked_chemistry_attr(mathml: Element, chem: &str) { mathml.set_attribute_value(CHEM_EQUATION, "true"); } } - _ => error!("Internal error: {} should not be marked as 'MAYBE_CHEMISTRY'", tag_name), + _ => error!("Internal error: {tag_name} should not be marked as 'MAYBE_CHEMISTRY'"), } } else if tag_name == "mrow" { // could have been added during canonicalization, so never marked. Recurse to the children @@ -568,7 +564,7 @@ fn is_changed_after_unmarking_chemistry(mathml: Element) -> bool { // let parent = get_parent(mathml); // debug!("After merge_element: -- parent{}", mml_to_string(parent)); - } else if let Some(changed_value) = mathml.attribute_value(CHANGED_ATTR).as_deref() && + } else if let Some(changed_value) = mathml.attribute_value(CHANGED_ATTR) && changed_value == ADDED_ATTR_VALUE && name(mathml) != "mtext" { // a hack fix for #477 (chem never modifies mtext, so this is ok) mathml.remove_from_parent(); @@ -616,8 +612,8 @@ fn is_changed_after_unmarking_chemistry(mathml: Element) -> bool { let base_of_first_element = first_element_children[0]; // base assert_eq!(name(as_element(base_of_first_element)), "mi"); let script_base = as_element(script_children[0]); - let mut merged_base_text = as_str!(as_text( as_element(base_of_first_element))).to_string(); - merged_base_text.push_str(as_str!(as_text(script_base))); + let mut merged_base_text = as_text( as_element(base_of_first_element)).to_string(); + merged_base_text.push_str(as_text(script_base)); script_base.set_text(&merged_base_text); script_base.remove_attribute("mathvariant"); script_base.remove_attribute(ADDED_ATTR_VALUE); @@ -651,7 +647,7 @@ fn is_changed_after_unmarking_chemistry(mathml: Element) -> bool { } } if name(mathml) == "mrow" { - if let Some(changed_value) = mathml.attribute_value(CHANGED_ATTR).as_deref() { + if let Some(changed_value) = mathml.attribute_value(CHANGED_ATTR) { // we added an mrow, we can remove it -- but this might be already processed which is the case if "data-id-added" is true (exists) if changed_value == ADDED_ATTR_VALUE && mathml.attribute("data-id-added").is_none() { // mrows get added for several reasons. One of them is to canonicalize elements like msqrt that can have 1 or more children; @@ -678,11 +674,10 @@ fn is_changed_after_unmarking_chemistry(mathml: Element) -> bool { // a merged token occurs when two single letters get merged into one. Here we recreate the two tokens assert!(is_leaf(mathml)); // debug!("unmerge_element: {}", mml_to_string(mathml)); - let raw_text = as_text(mathml); - let mut token_str = raw_text.chars(); - let first = create_mathml_element(&mathml.document(), as_str!(name(mathml))); + let mut token_str = as_text(mathml).chars(); + let first = create_mathml_element(&mathml.document(), name(mathml)); first.set_text(&token_str.next().unwrap().to_string()); - let second = create_mathml_element(&mathml.document(), as_str!(name(mathml))); + let second = create_mathml_element(&mathml.document(), name(mathml)); second.set_text(&token_str.next().unwrap().to_string()); replace_children(mathml, vec![first, second]); } @@ -703,7 +698,7 @@ fn is_changed_after_unmarking_chemistry(mathml: Element) -> bool { if preceding_children.is_empty() || !(name(parent) == "mmultiscripts" || (name(parent) == "mrow" && parent.attribute_value(CHANGED_ATTR).is_some() && - parent.attribute_value(CHANGED_ATTR).as_deref().unwrap() == ADDED_ATTR_VALUE)) { + parent.attribute_value(CHANGED_ATTR).unwrap() == ADDED_ATTR_VALUE)) { bail!("Internal error: {} should not have been split'", mml_to_string(mathml)); } } @@ -717,7 +712,7 @@ fn is_changed_after_unmarking_chemistry(mathml: Element) -> bool { if name(child) == "mrow" && child.children().len() == 1 && child.attribute(INTENT_ATTR).is_none() { // "lift" the child up so all the links (e.g., siblings) are correct let child = as_element(child.children()[0]); - set_mathml_name(child, as_str!(name(child))); + set_mathml_name(child, name(child)); crate::canonicalize::add_attrs(child, &child.attributes()); child.replace_children(child.children()); } @@ -729,7 +724,7 @@ fn is_changed_after_unmarking_chemistry(mathml: Element) -> bool { } merged_text.reverse(); let mut merged_text = merged_text.join(""); - merged_text.push_str(as_str!(as_text(mathml))); + merged_text.push_str(as_text(mathml)); mathml.set_text(&merged_text); mathml.remove_attribute("mathvariant"); mathml.remove_attribute(ADDED_ATTR_VALUE); @@ -784,13 +779,13 @@ fn is_chemistry_sanity_check(mathml: Element) -> bool { return false; } let text = as_text(base); - return text == "=" || is_single_char_matching(as_str!(text), is_chem_equation_arrow); + return text == "=" || is_single_char_matching(text, is_chem_equation_arrow); } /// Gather up all the chemical elements in the element and return true if it has numerical prescripts - fn gather_chemical_elements<'a>(mathml: Element<'a>, chem_elements: &mut HashSet>) -> bool { - match as_str!(name(mathml)) { + fn gather_chemical_elements<'a>(mathml: Element<'a>, chem_elements: &mut HashSet<&'a str>) -> bool { + match name(mathml) { "mi" | "mtext" => { if is_chemical_element(mathml) { chem_elements.insert(as_text(mathml)); @@ -841,7 +836,7 @@ fn is_chemistry_sanity_check(mathml: Element) -> bool { (name(subscript) == "mrow" && subscript.children().len() == 3 && name(as_element(subscript.children()[3])) == "mm" && name(as_element(subscript.children()[1])) == "mo" && - matches!(as_str!(as_text(as_element(subscript.children()[1]))), "+" | "-")); + matches!(as_text(as_element(subscript.children()[1])), "+" | "-")); } return false; } @@ -887,7 +882,7 @@ fn likely_chem_equation(mathml: Element) -> i32 { } // otherwise, check the last element as normal } - let tag_name = as_str!(name(child)); + let tag_name = name(child); let likely = match tag_name { "mi" => likely_chem_element(child), "mn" => 0, // not much info @@ -960,7 +955,7 @@ fn likely_chem_subscript(subscript: Element) -> i32 { return 0; // not really much chem info about an integer subscript } else if subscript_name == "mi" { let text = as_text(subscript); - if text == "s" || text == "l" || text == "g" || text == "aq" { + if text == "s" || text == "l" ||text == "g" ||text == "aq" { subscript.set_attribute_value(CHEM_STATE, "true"); return 2; } @@ -1001,14 +996,14 @@ fn likely_chem_superscript(sup: Element) -> i32 { static SINGLE_PLUS_OR_MINUS_OR_DOT: LazyLock = LazyLock::new(|| Regex::new(r"^[+-\U{2212}⋅∙•]$").unwrap()); static DOTS: &[char; 3] = &['⋅', '∙', '•']; let sup_name = name(sup); - if sup_name == "mo" && MULTIPLE_PLUS_OR_MINUS_OR_DOT.is_match(as_str!(as_text(sup))) { + if sup_name == "mo" && MULTIPLE_PLUS_OR_MINUS_OR_DOT.is_match(as_text(sup)) { if as_text(sup).find(DOTS).is_some() { sup.set_attribute_value(MAYBE_CHEMISTRY, "1"); sup.set_attribute_value(CHEM_FORMULA_OPERATOR, "1"); // value doesn't really matter } return if as_text(sup).len()==1 {1} else {2}; - } else if (sup_name == "mi" || sup_name == "mn" || sup_name=="mtext") && SMALL_UPPER_ROMAN_NUMERAL.is_match(as_str!(as_text(sup))){ - sup.set_attribute_value("data-number", small_roman_to_number(as_str!(as_text(sup)))); + } else if (sup_name == "mi" || sup_name == "mn" || sup_name=="mtext") && SMALL_UPPER_ROMAN_NUMERAL.is_match(as_text(sup)){ + sup.set_attribute_value("data-number", small_roman_to_number(as_text(sup))); sup.set_attribute_value(MAYBE_CHEMISTRY, "2"); return 2; } else if sup_name == "mrow" { @@ -1019,7 +1014,7 @@ fn likely_chem_superscript(sup: Element) -> i32 { let second = as_element(children[1]); if name(first) == "mn" && name(second) == "mo" && !as_text(first).contains('.') { let second_text = as_text(second); - if SINGLE_PLUS_OR_MINUS_OR_DOT.is_match(as_str!(second_text)) { + if SINGLE_PLUS_OR_MINUS_OR_DOT.is_match(second_text) { if second_text.find(DOTS).is_some() { second.set_attribute_value(MAYBE_CHEMISTRY, "2"); second.set_attribute_value(CHEM_FORMULA_OPERATOR, "2"); // value doesn't really matter @@ -1034,7 +1029,7 @@ fn likely_chem_superscript(sup: Element) -> i32 { for child in &children { // 'children' used later, so need to borrow rather than move let child = as_element(*child); if name(child) == "mo" { - text.push_str(as_str!(as_text(child))); + text.push_str(as_text(child)); } else { // could have something like 'mrow(mrow 2n, -) (chem example 5-9) -- so fallback to still ok if ends with + or - let last_super_child = as_element(children[children.len()-1]); @@ -1076,7 +1071,7 @@ fn likely_chem_formula(mathml: Element) -> i32 { return value; // already marked } - let tag_name = as_str!(name(mathml)); + let tag_name = name(mathml); let likelihood = match tag_name { // a parent may clear the chem flags if something says can't be chemistry (e.g, a non chemically valid script) "mi" => likely_chem_element(mathml), @@ -1233,7 +1228,7 @@ fn likely_chem_formula(mathml: Element) -> i32 { fn is_order_ok(mrow: Element) -> bool { assert_eq!(name(mrow), "mrow"); if let Some(elements) = collect_elements(mrow) { - if elements.iter().any(|e| !CHEMICAL_ELEMENT_ELECTRONEGATIVITY.contains_key(as_str!(*e))) { + if elements.iter().any(|&e| !CHEMICAL_ELEMENT_ELECTRONEGATIVITY.contains_key(e)) { return false; } let n_elements = elements.len(); @@ -1265,35 +1260,34 @@ fn is_order_ok(mrow: Element) -> bool { // } -fn has_noble_element(elements: &[NameStr<'_>]) -> bool { +fn has_noble_element(elements: &[&str]) -> bool { static NOBLE_ELEMENTS: phf::Set<&str> = phf_set! { "He", "Ne", "Ar", "Kr", "Xe", "Rn", "Og" // Og might be reactive, but it is unstable }; - return elements.iter().any(|e| NOBLE_ELEMENTS.contains(as_str!(*e))); + return elements.iter().any(|&e| NOBLE_ELEMENTS.contains(e)); } -#[allow(clippy::manual_contains)] -fn has_c_h_o(elements: &[NameStr<'_>]) -> bool { - return elements.iter().any(|e| *e == "C") && elements.iter().any(|e| *e == "H") && elements.iter().any(|e| *e == "O"); +fn has_c_h_o(elements: &[&str]) -> bool { + return elements.contains(&"C") && elements.contains(&"H") && elements.contains(&"O"); } -fn is_structural(elements: &[NameStr<'_>]) -> bool { - assert!(!elements.len() > 1); // already handled +fn is_structural(elements: &[&str]) -> bool { + assert!(elements.len() > 1); // already handled // debug!("is_structural: {:?}", elements); let mut element_set = HashSet::with_capacity(elements.len()); - elements.iter().for_each(|e| {element_set.insert(as_str!(*e));}); + elements.iter().for_each(|&e| {element_set.insert(e);}); return element_set.len() < elements.len(); } /// collect up all the elements in the mrow. /// Returns the elements (which can be an empty vector) or None if something (right now an operator) rules out them being elements -fn collect_elements(mrow: Element<'_>) -> Option>> { +fn collect_elements(mrow: Element<'_>) -> Option> { let mut elements = Vec::with_capacity(mrow.children().len()/2+1); // don't bother with slots for operators for child in mrow.children() { let child = as_element(child); - match as_str!(name(child)) { + match name(child) { "mi" | "mtext" => elements.push(as_text(child)), "msub" | "msup" | "mmultiscripts" => { let base = as_element(child.children()[0]); @@ -1312,29 +1306,27 @@ fn collect_elements(mrow: Element<'_>) -> Option>> { /// check to make sure elements are ordered alphabetically /// Actually check Hill's system that puts 'C' followed by 'H' first if 'C' is present -#[allow(clippy::op_ref)] -#[allow(clippy::manual_contains)] -fn is_alphabetical(elements: &[NameStr<'_>]) -> bool { - assert!(!elements.len() > 1); // already handled +fn is_alphabetical(elements: &[&str]) -> bool { + assert!(elements.len() > 1); // already handled // debug!("is_alphabetical: {:?}", elements); let mut elements = elements; - if elements[1..].iter().any(|e| *e == "C") { // "C" must be first if present + if elements[1..].contains(&"C") { // "C" must be first if present return false; } if elements[0] == "C" { elements = if elements[1]=="H" {&elements[2..]} else {&elements[1..]}; } - return elements.len() < 2 || elements.windows(2).all(|pair| as_str!(pair[0]) < as_str!(pair[1])); + return elements.len() < 2 || elements.windows(2).all(|pair| pair[0] < pair[1]); } -fn is_ordered_by_electronegativity(elements: &[NameStr<'_>]) -> bool { +fn is_ordered_by_electronegativity(elements: &[&str]) -> bool { // HPO_4^2 (Mono-hydrogen phosphate) doesn't fit this pattern, nor does HCO_3^- (Hydrogen carbonate) and some others // FIX: drop "H" from the ordering?? - assert!(!elements.len() > 1); // already handled - return elements.windows(2).all(|pair| CHEMICAL_ELEMENT_ELECTRONEGATIVITY.get(as_str!(pair[0])).unwrap() < CHEMICAL_ELEMENT_ELECTRONEGATIVITY.get(as_str!(pair[1])).unwrap()); + assert!(elements.len() > 1); // already handled + return elements.windows(2).all(|pair| CHEMICAL_ELEMENT_ELECTRONEGATIVITY.get(pair[0]).unwrap() < CHEMICAL_ELEMENT_ELECTRONEGATIVITY.get(pair[1]).unwrap()); } -fn is_generalized_salt(elements: &[NameStr<'_>]) -> bool { +fn is_generalized_salt(elements: &[&str]) -> bool { assert!(!elements.is_empty()); return false; } @@ -1355,7 +1347,7 @@ fn is_generalized_salt(elements: &[NameStr<'_>]) -> bool { /// /// Note: 'mathml' is not necessarily canonicalized pub fn likely_adorned_chem_formula(mathml: Element) -> i32 { - if !matches!(as_str!(name(mathml)), "msub" | "msup" | "msubsup" | "mmultiscripts" | "mover") { + if !matches!(name(mathml), "msub" | "msup" | "msubsup" | "mmultiscripts" | "mover") { return NOT_CHEMISTRY; } // some simple sanity checks on the scripts... @@ -1412,15 +1404,14 @@ pub fn likely_adorned_chem_formula(mathml: Element) -> i32 { let pre_superscript = as_element(prescripts[1]); let pre_superscript_name = name(pre_superscript); - // deal with special case of 'e' with prescripts of -1 and 0 if is_adorned_electron(children[0], prescripts) { return 100; // very likely chemistry } let base = as_element(children[0]); let base_name = name(base); - let atomic_number = if matches!(as_str!(base_name), "mi" | "mtext") && - let Some(atomic_number) = CHEMICAL_ELEMENT_ATOMIC_NUMBER.get(as_str!(as_text(base))) { + let atomic_number = if matches!(base_name, "mi" | "mtext") && + let Some(atomic_number) = CHEMICAL_ELEMENT_ATOMIC_NUMBER.get(as_text(base)) { *atomic_number } else { return NOT_CHEMISTRY; @@ -1537,7 +1528,7 @@ fn likely_chem_formula_operator(mathml: Element) -> i32 { assert_eq!(name(mathml), "mo"); let leaf_text = as_text(mathml); - if CHEM_FORMULA_OPERATORS.contains(as_str!(leaf_text)) && + if CHEM_FORMULA_OPERATORS.contains(leaf_text) && (has_inherited_property(mathml, "chemical-formula") || ( !(leaf_text == "=" || leaf_text == "∷" ) || is_legal_bond(mathml, BondType::DoubleBond) ) && ( !(leaf_text == "≡" || leaf_text == ":::" ) || is_legal_bond(mathml, BondType::TripleBond) ) @@ -1545,7 +1536,7 @@ fn likely_chem_formula_operator(mathml: Element) -> i32 { mathml.set_attribute_value(MAYBE_CHEMISTRY, "1"); mathml.set_attribute_value(CHEM_FORMULA_OPERATOR, "1"); return 1; - } else if is_single_char_matching(as_str!(leaf_text), is_chem_formula_ok) { + } else if is_single_char_matching(leaf_text, is_chem_formula_ok) { return 0; // not much info } else { return -3; // still a small chance; @@ -1573,8 +1564,8 @@ fn likely_chem_formula_operator(mathml: Element) -> i32 { if name(following_element) != "mi" && name(following_element) != "mtext" { return false; } - let preceding_text = as_str!(as_text(preceding_element)); - let following_text = as_str!(as_text(following_element)); + let preceding_text = as_text(preceding_element); + let following_text = as_text(following_element); return match bond_type { BondType::DoubleBond => is_legal_double_bond(preceding_text, following_text), BondType::TripleBond => is_legal_triple_bond(preceding_text, following_text), @@ -1619,7 +1610,7 @@ fn likely_chem_equation_operator(mathml: Element) -> i32 { let elem_name = name(mathml); if elem_name == "munder" || elem_name == "mover" || elem_name == "munderover" { let base = as_element(mathml.children()[0]); - if name(base) == "mo" && is_single_char_matching(as_str!(as_text(base)), is_chem_equation_arrow) { + if name(base) == "mo" && is_single_char_matching(as_text(base), is_chem_equation_arrow) { base.set_attribute_value(MAYBE_CHEMISTRY, "1"); base.set_attribute_value(CHEM_EQUATION_OPERATOR, "1"); return 1; @@ -1632,7 +1623,7 @@ fn likely_chem_equation_operator(mathml: Element) -> i32 { if name(mathml) == "mo" { let text = as_text(mathml); - if is_single_char_matching(as_str!(text), is_chem_equation_operator) || is_single_char_matching(as_str!(text), is_chem_equation_arrow) { + if is_single_char_matching(text, is_chem_equation_operator) || is_single_char_matching(text, is_chem_equation_arrow) { mathml.set_attribute_value(MAYBE_CHEMISTRY, "1"); mathml.set_attribute_value(CHEM_EQUATION_OPERATOR, "1"); return 1; @@ -1705,13 +1696,13 @@ pub fn likely_chem_element(mathml: Element) -> i32 { if as_text(mathml).trim().is_empty() { return 0; // whitespace } else if is_chemical_element(mathml) { - // single letter = 1; single letter with mathvarinat="normal" = 2; double = 3 -- all elements are ASCII + // single letter = 1; single letter with mathvariant="normal" = 2; double = 3 -- all elements are ASCII return if text.len() == 1 { - if mathml.attribute_value("mathvariant").as_deref().unwrap_or_default() == "normal" {2} else {1} + if mathml.attribute_value("mathvariant").unwrap_or_default() == "normal" {2} else {1} } else { 3 }; - } else if NUCLEAR_SYMBOLS.contains(&as_str!(text)) { + } else if NUCLEAR_SYMBOLS.contains(&text) { return 0; // not much special about them; } else { @@ -1788,7 +1779,7 @@ fn is_short_formula(mrow: Element) -> bool { } fn convert_to_short_form(mathml: Element) -> Result { - let mathml_name = as_str!(name(mathml)); + let mathml_name = name(mathml); return match mathml_name { "mi" | "mtext" | "mn" | "mo" => Ok( as_text(mathml).to_string() ), "none" => Ok( "".to_string() ), @@ -1875,7 +1866,7 @@ pub fn is_chemical_element(node: Element) -> bool { } let text = as_text(node); - return CHEMICAL_ELEMENT_ELECTRONEGATIVITY.contains_key(as_str!(text)) || + return CHEMICAL_ELEMENT_ELECTRONEGATIVITY.contains_key(text) || has_chem_intent(node, "chemical-element") || has_inherited_property(node, "chemical-formula"); } @@ -1887,133 +1878,71 @@ mod chem_tests { #[allow(unused_imports)] use super::super::init_logger; - use crate::errors::{bail, Result}; - use super::super::are_strs_canonically_equal_result; - use crate::interface::{get_element, init_panic_handler, report_any_panic, trim_element}; - use sxd_document_no_unsafe::parser; - use std::panic::{catch_unwind, AssertUnwindSafe}; + use super::super::are_strs_canonically_equal; use super::*; - fn chem_test(f: F) -> Result<()> - where - F: FnOnce() -> Result<()> + std::panic::UnwindSafe, - { - init_panic_handler(); - let result = catch_unwind(AssertUnwindSafe(f)); - return report_any_panic(result); - } - - fn with_parsed_mrow(test: &str, f: F) -> Result - where - F: FnOnce(Element) -> Result, - { - let wrapped; - let math_str = if test.starts_with("{}", test); - &wrapped - }; - let new_package = parser::parse(math_str) - .map_err(|e| anyhow::anyhow!("Invalid MathML input:\n{math_str}\nError is: {e}"))?; - let mut mathml = get_element(&new_package); - trim_element(mathml, false); - mathml = as_element(mathml.children()[0]); - return f(mathml); - } - - fn collect_elements_or_err<'a>(mrow: Element<'a>, test: &str) -> Result>> { - collect_elements(mrow).ok_or_else(|| { - anyhow::anyhow!("collect_elements returned None for test input:\n{test}") - }) - } + fn parse_mathml_string(test: &str, test_mathml: F) -> bool + where F: Fn(Element) -> bool { + use sxd_document::parser; + use crate::interface::{get_element, trim_element}; - fn assert_chem_elements(test: &str, expected: bool, check: impl Fn(&[NameStr<'_>]) -> bool) -> Result<()> { - let actual = with_parsed_mrow(test, |mrow| { - let elements = collect_elements_or_err(mrow, test)?; - return Ok(check(&elements)); - })?; - if actual != expected { - bail!("chemistry check failed for:\n{test}\nexpected: {expected}, got: {actual}"); + + let test = if test.starts_with("{}", test)}; + let new_package = parser::parse(test); + if let Err(e) = new_package { + panic!("Invalid MathML input:\n{}\nError is: {}", test, e); } - return Ok( () ); - } - fn assert_chem_elements_on_child( - test: &str, - child_index: usize, - expected: bool, - check: impl Fn(&[NameStr<'_>]) -> bool, - ) -> Result<()> { - let actual = with_parsed_mrow(test, |mrow| { - let child = as_element(mrow.children()[child_index]); - let elements = collect_elements_or_err(child, test)?; - return Ok(check(&elements)); - })?; - if actual != expected { - bail!("chemistry check failed for:\n{test}\nexpected: {expected}, got: {actual}"); - } - return Ok( () ); - } - - fn assert_chem_property(test: &str, expected: bool, check: impl Fn(Element) -> bool) -> Result<()> { - let actual = with_parsed_mrow(test, |mrow| Ok(check(mrow)))?; - if actual != expected { - bail!("chemistry check failed for:\n{test}\nexpected: {expected}, got: {actual}"); - } - return Ok( () ); + let new_package = new_package.unwrap(); + let mut mathml = get_element(&new_package); + trim_element(mathml, false); + mathml = as_element(mathml.children()[0]); + return test_mathml(mathml); } #[test] - fn test_noble_element() -> Result<()> { - return chem_test(|| { + fn test_noble_element() { // mathml test strings need to be canonical MathML since we aren't testing canonicalize() - let test = " Na Cl "; - assert_chem_elements(test, false, has_noble_element)?; - let test = " Ar Cl "; - assert_chem_elements(test, true, has_noble_element)?; - let test = " Ne "; - assert_chem_elements(test, true, has_noble_element)?; - return Ok( () ); - }); + let test = " Na Cl "; // + assert!( !parse_mathml_string(test, |mathml| has_noble_element( &collect_elements(mathml).unwrap() )) ); + let test = " Ar Cl "; // + assert!( parse_mathml_string(test, |mathml| has_noble_element( &collect_elements(mathml).unwrap() )) ); + let test = " Ne "; // + assert!( parse_mathml_string(test, |mathml| has_noble_element( &collect_elements(mathml).unwrap() )) ); } #[test] - fn test_alphabetical_order() -> Result<()> { - return chem_test(|| { + fn test_alphabetical_order() { // mathml test strings need to be canonical MathML since we aren't testing canonicalize() let test = r#" C6 H14 "#; - assert_chem_elements(test, true, is_alphabetical)?; + assert!( parse_mathml_string(test, |mathml| is_alphabetical( &collect_elements(mathml).unwrap() )) ); let test = r#" C6 H12 O6 "#; - assert_chem_elements(test, true, is_alphabetical)?; + assert!( parse_mathml_string(test, |mathml| is_alphabetical( &collect_elements(mathml).unwrap() )) ); let test = " B C O"; // "C" should be first - assert_chem_elements(test, false, is_alphabetical)?; + assert!( !parse_mathml_string(test, |mathml| is_alphabetical( &collect_elements(mathml).unwrap() )) ); let test = " P B O"; // not alphabetical - assert_chem_elements(test, false, is_alphabetical)?; - return Ok( () ); - }); + assert!( !parse_mathml_string(test, |mathml| is_alphabetical( &collect_elements(mathml).unwrap() )) ); } #[test] - fn test_is_structural() -> Result<()> { - return chem_test(|| { + fn test_is_structural() { // mathml test strings need to be canonical MathML since we aren't testing canonicalize() let test = r#" C6 H14 "#; - assert_chem_elements(test, false, is_structural)?; + assert!( !parse_mathml_string(test, |mathml| is_structural( &collect_elements(mathml).unwrap() )) ); let test = " B C O"; - assert_chem_elements(test, false, is_structural)?; + assert!( !parse_mathml_string(test, |mathml| is_structural( &collect_elements(mathml).unwrap() )) ); let test = " H O H"; - assert_chem_elements(test, true, is_structural)?; + assert!( parse_mathml_string(test, |mathml| is_structural( &collect_elements(mathml).unwrap() )) ); let test = " H @@ -2031,31 +1960,28 @@ mod chem_tests { "; - assert_chem_elements(test, true, is_structural)?; - return Ok( () ); - }); + assert!( parse_mathml_string(test, |mathml| is_structural( &collect_elements(mathml).unwrap() )) ); } #[test] - fn test_electronegativity_order() -> Result<()> { - return chem_test(|| { + fn test_electronegativity_order() { // mathml test strings need to be canonical MathML since we aren't testing canonicalize() let test = r#" N H3 "#; - assert_chem_elements(test, true, is_ordered_by_electronegativity)?; + assert!( parse_mathml_string(test, |mathml| is_ordered_by_electronegativity( &collect_elements(mathml).unwrap() )) ); let test = r#" O F2 "#; - assert_chem_elements(test, true, is_ordered_by_electronegativity)?; + assert!( parse_mathml_string(test, |mathml| is_ordered_by_electronegativity( &collect_elements(mathml).unwrap() )) ); let test = r#" Rb15 Hg16 "#; - assert_chem_elements(test, true, is_ordered_by_electronegativity)?; + assert!( parse_mathml_string(test, |mathml| is_ordered_by_electronegativity( &collect_elements(mathml).unwrap() )) ); let test = r#" [ @@ -2064,123 +1990,108 @@ mod chem_tests { ] 8- "#; - assert_chem_elements_on_child(test, 0, true, is_ordered_by_electronegativity)?; + assert!( parse_mathml_string(test, |mathml| is_ordered_by_electronegativity( &collect_elements(as_element(mathml.children()[0])).unwrap() )) ); let test = r#" Si H2 Br Cl "#; - assert_chem_elements(test, true, is_ordered_by_electronegativity)?; - return Ok( () ); - }); + assert!( parse_mathml_string(test, |mathml| is_ordered_by_electronegativity( &collect_elements(mathml).unwrap() )) ); } #[test] - fn test_order() -> Result<()> { - return chem_test(|| { + fn test_order() { let test = r#" C2 H4 (N H 2 ) 2 "#; - assert_chem_property(test, true, is_order_ok)?; + assert!( parse_mathml_string(test, is_order_ok) ); let test = r#" Fe O (O H ) "#; - assert_chem_property(test, true, is_order_ok)?; + assert!( parse_mathml_string(test, is_order_ok) ); let test = r#" // R-4.4.3.3 -- Chain compound doesn't fit rules but should be accepted Br S C N "#; - assert_chem_property(test, true, |mathml| likely_chem_formula(mathml) == 5)?; - return Ok( () ); - }); + assert!( parse_mathml_string(test, |mathml| likely_chem_formula(mathml)==5) ); } #[test] - fn test_simple_double_bond() -> Result<()> { - return chem_test(|| { + fn test_simple_double_bond() { let test1 = r#"C=C"#; - assert_chem_property(test1, true, |mathml| likely_chem_formula(mathml) < CHEMISTRY_THRESHOLD)?; // just under threshold + assert!( parse_mathml_string(test1, |mathml| likely_chem_formula(mathml) < CHEMISTRY_THRESHOLD) ); // just under threshold let test2 = r#"CO"#; - assert_chem_property(test2, true, |mathml| likely_chem_formula(mathml) == CHEMISTRY_THRESHOLD)?; + assert!( parse_mathml_string(test2, |mathml| likely_chem_formula(mathml)==CHEMISTRY_THRESHOLD) ); let test3 = r#"N=N"#; - assert_chem_property(test3, true, |mathml| likely_chem_formula(mathml) < CHEMISTRY_THRESHOLD)?; // just under threshold + assert!( parse_mathml_string(test3, |mathml| likely_chem_formula(mathml) < CHEMISTRY_THRESHOLD) ); // just under threshold let test4 = r#"Sn=Sn"#; - assert_chem_property(test4, true, |mathml| likely_chem_formula(mathml) == 8)?; + assert!( parse_mathml_string(test4, |mathml| likely_chem_formula(mathml) == 8) ); let test5 = r#"O=S"#; - assert_chem_property(test5, true, |mathml| likely_chem_formula(mathml) < CHEMISTRY_THRESHOLD)?; // just under threshold + assert!( parse_mathml_string(test5, |mathml| likely_chem_formula(mathml) < CHEMISTRY_THRESHOLD) ); // just under threshold let test10 = r#"K=K"#; - assert_chem_property(test10, true, |mathml| likely_chem_formula(mathml) == NOT_CHEMISTRY)?; + assert!( parse_mathml_string(test10, |mathml| likely_chem_formula(mathml) == NOT_CHEMISTRY) ); let test11 = r#"C=K"#; - assert_chem_property(test11, true, |mathml| likely_chem_formula(mathml) == NOT_CHEMISTRY)?; - return Ok( () ); - }); + assert!( parse_mathml_string(test11, |mathml| likely_chem_formula(mathml) == NOT_CHEMISTRY) ); } #[test] - fn test_double_bond() -> Result<()> { - return chem_test(|| { + fn test_double_bond() { let test1 = r#"CH2=C"#; - assert_chem_property(test1, true, |mathml| likely_chem_formula(mathml) == 8)?; + assert!( parse_mathml_string(test1, |mathml| likely_chem_formula(mathml)==8) ); let test2 = r#"CH2= CHR"#; - assert_chem_property(test2, true, |mathml| likely_chem_formula(mathml) == 12)?; + assert!( parse_mathml_string(test2, |mathml| likely_chem_formula(mathml)==12) ); let test3 = r#"H2C= CH2"#; - assert_chem_property(test3, true, |mathml| likely_chem_formula(mathml) == 11)?; + assert!( parse_mathml_string(test3, |mathml| likely_chem_formula(mathml)==11) ); let test4 = r#"H-N=N-H"#; - assert_chem_property(test4, true, |mathml| likely_chem_formula(mathml) == 10)?; + assert!( parse_mathml_string(test4, |mathml| likely_chem_formula(mathml)==10) ); let test10 = r#"CH3=C"#; - assert_chem_property(test10, true, |mathml| likely_chem_formula(mathml) == NOT_CHEMISTRY)?; - return Ok( () ); - }); + assert!( parse_mathml_string(test10, |mathml| likely_chem_formula(mathml)==NOT_CHEMISTRY) ); } #[test] #[ignore] // It would be good to say "not chemistry" for this, but there aren't rules for that at the moment - fn test_water_bond() -> Result<()> { - return chem_test(|| { + fn test_water_bond() { let test11 = r#"H2O=O"#; - assert_chem_property(test11, true, |mathml| likely_chem_formula(mathml) == 8)?; - return Ok( () ); - }); + assert!( parse_mathml_string(test11, |mathml| {println!("val={}", likely_chem_formula(mathml)); likely_chem_formula(mathml)==8}) ); + // assert!( parse_mathml_string(test11, |mathml| likely_chem_formula(mathml)==NOT_CHEMISTRY) ); } #[test] - fn test_triple_bond() -> Result<()> { - return chem_test(|| { + fn test_triple_bond() { let test1 = r#"CC"#; - assert_chem_property(test1, true, |mathml| likely_chem_formula(mathml) < CHEMISTRY_THRESHOLD)?; + assert!( parse_mathml_string(test1, |mathml| likely_chem_formula(mathml) < CHEMISTRY_THRESHOLD) ); let test2 = r#"C:::O"#; - assert_chem_property(test2, true, |mathml| likely_chem_formula(mathml) == CHEMISTRY_THRESHOLD)?; + assert!( parse_mathml_string(test2, |mathml| likely_chem_formula(mathml)==CHEMISTRY_THRESHOLD) ); let test3 = r#"H-CC-H"#; - assert_chem_property(test3, true, |mathml| likely_chem_formula(mathml) == 10)?; + assert!( parse_mathml_string(test3, |mathml| likely_chem_formula(mathml)==10) ); let test4 = r#"H-CC-H"#; - assert_chem_property(test4, true, |mathml| likely_chem_formula(mathml) == 10)?; + assert!( parse_mathml_string(test4, |mathml| likely_chem_formula(mathml)==10) ); let test5 = r#"N-CC-N"#; - assert_chem_property(test5, true, |mathml| likely_chem_formula(mathml) == 10)?; + assert!( parse_mathml_string(test5, |mathml| likely_chem_formula(mathml)==10) ); let test6 = r#"H-C C-CH3"#; // 1-Propyne - assert_chem_property(test6, true, |mathml| likely_chem_formula(mathml) == 14)?; + assert!( parse_mathml_string(test6, |mathml| likely_chem_formula(mathml)==14) ); + // assert!( parse_mathml_string(test6, |mathml| {println!("val={}", likely_chem_formula(mathml)); likely_chem_formula(mathml)==10}) ); let test10 = r#"O:::S"#; - assert_chem_property(test10, true, |mathml| likely_chem_formula(mathml) == NOT_CHEMISTRY)?; + assert!( parse_mathml_string(test10, |mathml| likely_chem_formula(mathml)==NOT_CHEMISTRY) ); let test11 = r#"PbPb"#; - assert_chem_property(test11, true, |mathml| likely_chem_formula(mathml) == NOT_CHEMISTRY)?; + assert!( parse_mathml_string(test11, |mathml| likely_chem_formula(mathml)==NOT_CHEMISTRY) ); let test12 = r#"CK"#; - assert_chem_property(test12, true, |mathml| likely_chem_formula(mathml) == NOT_CHEMISTRY)?; - return Ok( () ); - }); + assert!( parse_mathml_string(test12, |mathml| likely_chem_formula(mathml)==NOT_CHEMISTRY) ); } #[test] - fn split_mi() -> Result<()> { + fn split_mi() { let test = "LiF"; let target = " @@ -2189,20 +2100,20 @@ mod chem_tests { F "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn no_split_mi() -> Result<()> { + fn no_split_mi() { let test = "HC"; let target = " HC "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn combine_mi() -> Result<()> { + fn combine_mi() { let test = "HCl"; let target = " @@ -2211,11 +2122,11 @@ mod chem_tests { Cl "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn no_combine() -> Result<()> { + fn no_combine() { let test = "Cl"; let target = " @@ -2224,11 +2135,11 @@ mod chem_tests { l "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn add_script() -> Result<()> { + fn add_script() { let test = " SO 2 "; let target = " @@ -2241,11 +2152,11 @@ mod chem_tests { "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn add_script_bug_287() -> Result<()> { + fn add_script_bug_287() { let test = r#" SO @@ -2264,11 +2175,11 @@ mod chem_tests { "#; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn salt() -> Result<()> { + fn salt() { let test = "NaCl"; let target = " @@ -2277,11 +2188,11 @@ mod chem_tests { Cl "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn water() -> Result<()> { + fn water() { let test = "H2O"; let target = " @@ -2293,11 +2204,11 @@ mod chem_tests { O "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn mhchem_water() -> Result<()> { + fn mhchem_water() { let test = " @@ -2337,20 +2248,20 @@ mod chem_tests { O "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn carbon() -> Result<()> { + fn carbon() { let test = "C"; // not enough to trigger recognition let target = " C "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn sulfate() -> Result<()> { + fn sulfate() { let test = " [SO4] 2 @@ -2375,11 +2286,11 @@ mod chem_tests { "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn aluminum_sulfate() -> Result<()> { + fn aluminum_sulfate() { let test = "Al2 (SO4)3"; let target = " @@ -2406,11 +2317,11 @@ mod chem_tests { "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn ethanol_bonds() -> Result<()> { + fn ethanol_bonds() { let test = " C @@ -2444,11 +2355,11 @@ mod chem_tests { H "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn dichlorine_hexoxide() -> Result<()> { + fn dichlorine_hexoxide() { // init_logger(); let test = " @@ -2495,11 +2406,11 @@ mod chem_tests { "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn ethylene_with_bond() -> Result<()> { + fn ethylene_with_bond() { let test = " H2C = @@ -2522,11 +2433,11 @@ mod chem_tests { "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn ferric_chloride_aq() -> Result<()> { + fn ferric_chloride_aq() { let test = " Fe Cl3 @@ -2548,11 +2459,11 @@ mod chem_tests { "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn ferric_chloride_aq_as_mi() -> Result<()> { + fn ferric_chloride_aq_as_mi() { let test = " Fe Cl3 @@ -2574,11 +2485,11 @@ mod chem_tests { "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn chemtype_ammonia() -> Result<()> { + fn chemtype_ammonia() { let test = r#"NH3"#; let target = " @@ -2590,11 +2501,11 @@ mod chem_tests { "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn mhchem_ammonia() -> Result<()> { + fn mhchem_ammonia() { let test = r#" NH @@ -2621,11 +2532,11 @@ mod chem_tests { "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn mhchem_so4() -> Result<()> { + fn mhchem_so4() { let test = " SO @@ -2668,11 +2579,11 @@ mod chem_tests { "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn mhchem_short_ion() -> Result<()> { + fn mhchem_short_ion() { let test = " H @@ -2702,11 +2613,11 @@ mod chem_tests { "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn mhchem_ions_and_state() -> Result<()> { + fn mhchem_ions_and_state() { let test = " @@ -2783,11 +2694,11 @@ mod chem_tests { "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn ethylene_with_colon_bond() -> Result<()> { + fn ethylene_with_colon_bond() { let test = " H2C :: @@ -2810,11 +2721,11 @@ mod chem_tests { "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn mhchem_u238() -> Result<()> { + fn mhchem_u238() { let test = " @@ -2903,11 +2814,11 @@ mod chem_tests { 238 "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn mhchem_hcl_aq() -> Result<()> { + fn mhchem_hcl_aq() { let test = " 2 @@ -2942,11 +2853,11 @@ mod chem_tests { "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn mhchem_nested_sub() -> Result<()> { + fn mhchem_nested_sub() { // from \ce{(CH3)3} let test = " @@ -3012,11 +2923,11 @@ mod chem_tests { "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn mhchem_isotopes() -> Result<()> { + fn mhchem_isotopes() { // from \ce{^{18}O{}^{16}O} let test = " @@ -3132,12 +3043,12 @@ mod chem_tests { "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn merge_bug_274() -> Result<()> { + fn merge_bug_274() { let test = r#" @@ -3277,11 +3188,11 @@ mod chem_tests { "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn merge_bug_303() -> Result<()> { + fn merge_bug_303() { let test = r#" 2 @@ -3319,11 +3230,11 @@ mod chem_tests { "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } #[test] - fn mtd_assert_bug_393() -> Result<()> { + fn mtd_assert_bug_393() { let test = r#" @@ -3382,7 +3293,7 @@ mod chem_tests { "; - are_strs_canonically_equal_result(test, target, &[]) + assert!(are_strs_canonically_equal(test, target, &[])); } } diff --git a/src/infer_intent.rs b/src/infer_intent.rs index 1112d859e..79ec7a537 100644 --- a/src/infer_intent.rs +++ b/src/infer_intent.rs @@ -276,7 +276,7 @@ static ARG_REF: LazyLock = LazyLock::new(|| { }); static NUMBER: LazyLock = LazyLock::new(|| Regex::new(r#"^-?[0-9]+(\.[0-9]+)?"#).unwrap()); -static TERMINALS_AS_U8: [u8; 3] = [b'(', b',', b')']; +static TERMINALS_AS_U8: [u8; 3] = *b"(,)"; // static TERMINALS: [char; 3] = ['(', ',',')']; // 'i -- "i" for the lifetime of the INTENT_ATTR string diff --git a/src/navigate.rs b/src/navigate.rs index 5e5d0309b..100e49532 100644 --- a/src/navigate.rs +++ b/src/navigate.rs @@ -1005,7 +1005,7 @@ mod tests { match do_navigate_command_string(mathml, command) { Err(e) => { panic!("\nStarting at '{}', '{} failed.\n{}", - start_id, command, &crate::interface::errors_to_string(&e)) + start_id, command, crate::interface::errors_to_string(&e)) }, Ok(nav_speech) => { let nav_speech = nav_speech.trim_end_matches([' ', ',', ';']); diff --git a/src/speech.rs b/src/speech.rs index 732b9f81f..8416a017d 100644 --- a/src/speech.rs +++ b/src/speech.rs @@ -513,7 +513,7 @@ struct InsertChildren { #[cfg_attr(coverage, coverage(off))] impl fmt::Display for InsertChildren { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - return write!(f, "InsertChildren:\n nodes {}\n replacements {}", self.xpath, &self.replacements); + return write!(f, "InsertChildren:\n nodes {}\n replacements {}", self.xpath, self.replacements); } } @@ -551,7 +551,7 @@ impl InsertChildren { // This is slower than the alternatives, but reuses a bunch of code and hence is less complicated. fn replace<'c, 's:'c, 'm: 'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result { let result = self.xpath.evaluate(&rules_with_context.context_stack.base, mathml) - .with_context(||format!("in '{}' replacing after pattern match", &self.xpath.rc.string) )?; + .with_context(||format!("in '{}' replacing after pattern match", self.xpath.rc.string) )?; match result { Value::Nodeset(nodes) => { if nodes.size() == 0 { @@ -615,7 +615,7 @@ impl fmt::Display for Intent { return write!(f, "intent: {}: {}, attrs='{}'>\n children: {}", if self.name.is_some() {"name"} else {"xpath-name"}, name, self.attrs, - &self.children); + self.children); } } @@ -666,7 +666,7 @@ impl Intent { result.set_attribute_value(MATHML_FROM_NAME_ATTR, name(mathml)); set_mathml_name(result, intent_name.as_str()) }, - _ => bail!("'xpath-name' value '{}' was not a string", &my_xpath), + _ => bail!("'xpath-name' value '{}' was not a string", my_xpath), } } if self.name.is_none() && self.xpath.is_none() { @@ -745,7 +745,7 @@ struct With { #[cfg_attr(coverage, coverage(off))] impl fmt::Display for With { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - return write!(f, "with:\n variables: {}\n replace: {}", &self.variables, &self.replacements); + return write!(f, "with:\n variables: {}\n replace: {}", self.variables, self.replacements); } } @@ -791,7 +791,7 @@ struct SetVariables { #[cfg_attr(coverage, coverage(off))] impl fmt::Display for SetVariables { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - return write!(f, "SetVariables: variables {}", &self.variables); + return write!(f, "SetVariables: variables {}", self.variables); } } @@ -823,7 +823,7 @@ struct TranslateExpression { #[cfg_attr(coverage, coverage(off))] impl fmt::Display for TranslateExpression { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - return write!(f, "speak: {}", &self.xpath); + return write!(f, "speak: {}", self.xpath); } } @@ -1104,11 +1104,11 @@ impl MyXPath { let compiled_xpath = factory.build(&xpath_with_debug_info) .with_context(|| format!( "Could not compile XPath for pattern:\n{}{}", - &xpath, more_details(xpath)))?; + xpath, more_details(xpath)))?; return match compiled_xpath { Some(xpath) => Ok(xpath), None => bail!("Problem compiling Xpath for pattern:\n{}{}", - &xpath, more_details(xpath)), + xpath, more_details(xpath)), }; @@ -1243,7 +1243,7 @@ impl MyXPath { } let result = self.evaluate(&rules_with_context.context_stack.base, mathml) - .with_context(|| format!("in '{}' replacing after pattern match", &self.rc.string) )?; + .with_context(|| format!("in '{}' replacing after pattern match", self.rc.string) )?; let string = match result { Value::Nodeset(nodes) => { if nodes.size() == 0 { @@ -3012,7 +3012,7 @@ cfg_if::cfg_if! {if #[cfg(not(feature = "include-zip"))] { let start_main_file = rules.borrow().unicode_short_files.borrow().ft[0].clone(); // open the file, read all the contents, then write them back so the time changes - let contents = std::fs::read(&start_main_file.file).unwrap_or_else(|_| panic!("Failed to read file {} during test", &start_main_file.file.to_string_lossy())); + let contents = std::fs::read(&start_main_file.file).unwrap_or_else(|_| panic!("Failed to read file {} during test", start_main_file.file.to_string_lossy())); std::fs::write(start_main_file.file, contents).unwrap(); std::thread::sleep(Duration::from_millis(5)); // pause a little to make sure the time changes diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 9b2269c53..6b42a25b8 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -253,7 +253,7 @@ pub fn test_intent(mathml: &str, target: &str, test_prefs: Vec<(&str, &str)>) -> let new_package = parser::parse(mathml); if let Err(e) = new_package { - panic!("Invalid MathML:\n{}\nError is: {}", &mathml, &e.to_string()); + panic!("Invalid MathML:\n{}\nError is: {}", mathml, e); } let new_package = new_package.unwrap(); From cf791636f002b74f9ac3f1e419e8243a06f6b077 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Sat, 1 Aug 2026 04:40:15 +0500 Subject: [PATCH 40/47] Remove mathvariant propagation and update tests Drop implicit propagation of the mathvariant attribute from canonicalization (removed propagate_inherited_mathvariant call and helper functions) and update Russian braille tests to set mathvariant explicitly on elements (replace mstyle with mrow and move attributes to mi). Changes in src/canonicalize.rs and tests/braille/Russian/russian.rs. This makes mathvariant handling explicit in tests and avoids mutating child nodes during canonicalization. --- tests/braille/Russian/russian.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index 1192dbf6b..3a5c30379 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -217,13 +217,13 @@ fn source_typeform_and_mathvariant_indicators() -> Result<()> { let expr = r#"𝑥"#; test_braille("Russian", expr, "⠸⠠⠭⠸")?; - let expr = r#"x"#; + let expr = r#"x"#; test_braille("Russian", expr, "⠻⠠⠭⠻")?; - let expr = r#"xy"#; + let expr = r#"xy"#; test_braille("Russian", expr, "⠻⠠⠭⠽⠻")?; - let expr = r#"y"#; + let expr = r#"y"#; test_braille("Russian", expr, "⠸⠠⠽⠸")?; return Ok(()); } From 75be354bf04668cda0aafaecb8fb1f2c39cf41eb Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Sat, 1 Aug 2026 05:00:42 +0500 Subject: [PATCH 41/47] Remove math digit normalization for mn nodes Delete the conditional normalization of mathematical digit characters for text and remove the normalize_math_digits helper from src/canonicalize.rs. Previously the code converted several Unicode math-digit codepoints to ASCII when a decimal point was present. --- src/canonicalize.rs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/canonicalize.rs b/src/canonicalize.rs index ac931f980..0b31575aa 100644 --- a/src/canonicalize.rs +++ b/src/canonicalize.rs @@ -2511,7 +2511,6 @@ impl CanonicalizeContext { let child = as_element(child_as_element); mn_text.push_str(as_str!(as_text(child))); } - let mn_text = if mn_text.contains('.') { normalize_math_digits(&mn_text) } else { mn_text }; let child = as_element(children[start]); set_mathml_name(child, "mn"); child.set_text(&mn_text); @@ -2519,21 +2518,6 @@ impl CanonicalizeContext { children.drain(start+1..end); } - fn normalize_math_digits(text: &str) -> String { - text.chars().map(|ch| { - let code = ch as u32; - let digit = match code { - 0x1D7CE..=0x1D7D7 => Some(code - 0x1D7CE), - 0x1D7D8..=0x1D7E1 => Some(code - 0x1D7D8), - 0x1D7E2..=0x1D7EB => Some(code - 0x1D7E2), - 0x1D7EC..=0x1D7F5 => Some(code - 0x1D7EC), - 0x1D7F6..=0x1D7FF => Some(code - 0x1D7F6), - _ => None, - }; - digit.and_then(|digit| char::from_digit(digit, 10)).unwrap_or(ch) - }).collect() - } - /// merge ° C or ° F into a single with the text '℃' or '℉' -- prevents '°' from becoming a superscript #[allow(non_snake_case)] From f4a35bf1d2285215bb3e310f00c17f0bd76d97d8 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Mon, 3 Aug 2026 12:53:19 +0500 Subject: [PATCH 42/47] Split Russian braille tests into individual cases Introduce test_russian_braille helper and refactor tests/braille/Russian/russian.rs to replace large grouped tests with many small, named #[test] functions. The change splits combined test vectors and loops (e.g. script_grouping_regressions and other multi-assertion tests) into individual test cases for improved isolation and clearer failures. --- tests/braille/Russian/russian.rs | 1496 +++++++++++++++++++----------- 1 file changed, 952 insertions(+), 544 deletions(-) diff --git a/tests/braille/Russian/russian.rs b/tests/braille/Russian/russian.rs index e0c5a2d70..f7305abce 100644 --- a/tests/braille/Russian/russian.rs +++ b/tests/braille/Russian/russian.rs @@ -1,808 +1,1216 @@ use crate::common::*; use anyhow::Result; +fn test_russian_braille(expr: &str, expected: &str) -> Result<()> { + return test_braille("Russian", expr, expected); +} + +#[test] +fn script_grouping_sup_x() -> Result<()> { + return test_russian_braille(r#"ax"#, "⠠⠁⠌⠭⠱"); +} + +#[test] +fn script_grouping_sup_zero() -> Result<()> { + return test_russian_braille(r#"x0"#, "⠠⠭⠌⠴"); +} + +#[test] +fn script_grouping_sup_ten() -> Result<()> { + return test_russian_braille(r#"x10"#, "⠠⠭⠌⠂⠴"); +} + +#[test] +fn script_grouping_sup_minus_x() -> Result<()> { + return test_russian_braille(r#"a-x"#, "⠠⠁⠌⠀⠤⠭⠱"); +} + +#[test] +fn script_grouping_sup_minus_2() -> Result<()> { + return test_russian_braille(r#"a-2"#, "⠠⠁⠌⠤⠆"); +} + +#[test] +fn script_grouping_sup_x_plus_1() -> Result<()> { + return test_russian_braille(r#"ax+1"#, "⠠⠁⠌⠭⠀⠖⠼⠁⠱"); +} + +#[test] +fn script_grouping_sup_sub_x2() -> Result<()> { + return test_russian_braille(r#"ax2"#, "⠠⠁⠌⠭⠡⠆⠱"); +} + +#[test] +fn script_grouping_sup_nested() -> Result<()> { + return test_russian_braille(r#"ax2"#, "⠠⠁⠌⠭⠌⠆⠱"); +} + +#[test] +fn script_grouping_sup_frac() -> Result<()> { + return test_russian_braille(r#"a12"#, "⠠⠁⠌⠼⠁⠆⠱"); +} + +#[test] +fn script_grouping_sup_complex_frac() -> Result<()> { + return test_russian_braille(r#"ax+1y"#, "⠠⠁⠌⠐⠆⠭⠀⠖⠼⠁⠀⠳⠠⠽⠰⠱"); +} + +#[test] +fn script_grouping_sup_sqrt() -> Result<()> { + return test_russian_braille(r#"ax"#, "⠠⠁⠌⠩⠱⠭⠹⠱"); +} + +#[test] +fn script_grouping_sup_follow_letter() -> Result<()> { + return test_russian_braille(r#"x2y"#, "⠠⠭⠌⠆⠽"); +} + +#[test] +fn script_grouping_sup_follow_number() -> Result<()> { + return test_russian_braille(r#"x23"#, "⠠⠭⠌⠆⠼⠉"); +} + +#[test] +fn script_grouping_sub_x() -> Result<()> { + return test_russian_braille(r#"ax"#, "⠠⠁⠡⠭⠱"); +} + +#[test] +fn script_grouping_sub_zero() -> Result<()> { + return test_russian_braille(r#"x0"#, "⠠⠭⠡⠴"); +} + +#[test] +fn script_grouping_sub_ten() -> Result<()> { + return test_russian_braille(r#"x10"#, "⠠⠭⠡⠂⠴"); +} + +#[test] +fn script_grouping_sub_digits_sequence() -> Result<()> { + return test_russian_braille(r#"D13"#, "⠨⠙⠡⠂⠒"); +} + +#[test] +fn script_grouping_sub_infinity() -> Result<()> { + return test_russian_braille(r#"t"#, "⠠⠞⠡⠻"); +} + +#[test] +fn script_grouping_sub_minus_x() -> Result<()> { + return test_russian_braille(r#"a-x"#, "⠠⠁⠡⠀⠤⠭⠱"); +} + +#[test] +fn script_grouping_sub_minus_2() -> Result<()> { + return test_russian_braille(r#"a-2"#, "⠠⠁⠡⠤⠆"); +} + +#[test] +fn script_grouping_sub_x_plus_1() -> Result<()> { + return test_russian_braille(r#"ax+1"#, "⠠⠁⠡⠐⠭⠀⠖⠼⠁⠱"); +} + +#[test] +fn script_grouping_sub_nested() -> Result<()> { + return test_russian_braille(r#"ax2"#, "⠠⠁⠡⠭⠡⠆⠱"); +} + +#[test] +fn script_grouping_sub_follow_letter() -> Result<()> { + return test_russian_braille(r#"x2y"#, "⠠⠭⠡⠆⠽"); +} + +#[test] +fn script_grouping_sub_follow_number() -> Result<()> { + return test_russian_braille(r#"x23"#, "⠠⠭⠡⠆⠼⠉"); +} + +#[test] +fn script_grouping_subsup_numeric() -> Result<()> { + return test_russian_braille(r#"x12"#, "⠠⠭⠡⠼⠁⠌⠼⠃⠱"); +} + +#[test] +fn script_grouping_subsup_i_n() -> Result<()> { + return test_russian_braille(r#"xin"#, "⠠⠭⠡⠊⠌⠝⠱"); +} + +#[test] +fn script_grouping_subsup_grouped_sub() -> Result<()> { + return test_russian_braille(r#"xi+12"#, "⠠⠭⠡⠐⠊⠀⠖⠼⠁⠌⠼⠃⠱"); +} + +#[test] +fn script_grouping_subsup_grouped_sup() -> Result<()> { + return test_russian_braille(r#"x0n-1"#, "⠠⠭⠡⠼⠚⠌⠠⠝⠀⠤⠼⠁⠱"); +} + +#[test] +fn script_grouping_subsup_nested_sup() -> Result<()> { + return test_russian_braille(r#"x0n2"#, "⠠⠭⠡⠼⠚⠌⠠⠝⠌⠆⠱"); +} + +#[test] +fn script_grouping_negative_base_sup() -> Result<()> { + return test_russian_braille(r#"-x2"#, "⠤⠠⠭⠌⠆"); +} + +#[test] +fn script_grouping_paren_base_sup() -> Result<()> { + return test_russian_braille(r#"(x+1)2"#, "⠣⠠⠭⠀⠖⠼⠁⠜⠌⠆"); +} + +#[test] +fn script_grouping_frac_base_sup() -> Result<()> { + return test_russian_braille(r#"1x2"#, "⠼⠁⠳⠠⠭⠌⠆"); +} + +#[test] +fn script_grouping_root_base_sup() -> Result<()> { + return test_russian_braille(r#"x2"#, "⠩⠱⠠⠭⠹⠌⠆"); +} + +#[test] +fn script_grouping_sup_on_function() -> Result<()> { + return test_russian_braille(r#"sin2x"#, "⠫⠎⠌⠆⠠⠭"); +} + +#[test] +fn script_grouping_log_sub_sup() -> Result<()> { + return test_russian_braille(r#"log23x"#, "⠫⠇⠡⠼⠃⠌⠼⠉⠱⠠⠭"); +} + +#[test] +fn script_grouping_root_index_group() -> Result<()> { + return test_russian_braille(r#"xn+1"#, "⠩⠠⠝⠀⠖⠼⠁⠱⠠⠭⠹"); +} + +#[test] +fn script_grouping_root_index_sub() -> Result<()> { + return test_russian_braille(r#"xn2"#, "⠩⠠⠝⠡⠆⠱⠭⠹"); +} + +#[test] +fn script_grouping_frac_num_group() -> Result<()> { + return test_russian_braille(r#"x+1y"#, "⠆⠠⠭⠀⠖⠼⠁⠀⠳⠠⠽⠰"); +} + +#[test] +fn script_grouping_frac_den_group() -> Result<()> { + return test_russian_braille(r#"xy+1"#, "⠆⠠⠭⠀⠳⠽⠀⠖⠼⠁⠰"); +} + #[test] -fn script_grouping_regressions() -> Result<()> { - let cases = vec![ - ("sup_x", r#"ax"#), - ("sup_zero", r#"x0"#), - ("sup_ten", r#"x10"#), - ("sup_minus_x", r#"a-x"#), - ("sup_minus_2", r#"a-2"#), - ("sup_x_plus_1", r#"ax+1"#), - ("sup_sub_x2", r#"ax2"#), - ("sup_nested", r#"ax2"#), - ("sup_frac", r#"a12"#), - ("sup_complex_frac", r#"ax+1y"#), - ("sup_sqrt", r#"ax"#), - ("sup_follow_letter", r#"x2y"#), - ("sup_follow_number", r#"x23"#), - ("sub_x", r#"ax"#), - ("sub_zero", r#"x0"#), - ("sub_ten", r#"x10"#), - ("sub_digits_sequence", r#"D13"#), - ("sub_infinity", r#"t"#), - ("sub_minus_x", r#"a-x"#), - ("sub_minus_2", r#"a-2"#), - ("sub_x_plus_1", r#"ax+1"#), - ("sub_nested", r#"ax2"#), - ("sub_follow_letter", r#"x2y"#), - ("sub_follow_number", r#"x23"#), - ("subsup_numeric", r#"x12"#), - ("subsup_i_n", r#"xin"#), - ("subsup_grouped_sub", r#"xi+12"#), - ("subsup_grouped_sup", r#"x0n-1"#), - ("subsup_nested_sup", r#"x0n2"#), - ("negative_base_sup", r#"-x2"#), - ("paren_base_sup", r#"(x+1)2"#), - ("frac_base_sup", r#"1x2"#), - ("root_base_sup", r#"x2"#), - ("sup_on_function", r#"sin2x"#), - ("log_sub_sup", r#"log23x"#), - ("root_index_group", r#"xn+1"#), - ("root_index_sub", r#"xn2"#), - ("frac_num_group", r#"x+1y"#), - ("frac_den_group", r#"xy+1"#), - ("nested_frac", r#"1x1y"#), - ("sup_after_fraction", r#"x+1y-12"#), - ("sub_after_fraction", r#"x+1y-1i"#), - ("tensor_like", r#"Tijxjk"#), - ("mmultiscripts_left_sub", r#"Fk"#), - ("mmultiscripts_left_sup", r#"W4"#), - ("mmultiscripts_both_sides", r#"Tijrs"#), - ("pre_negative_power", r#"2x-1"#), - ("power_of_power_follow", r#"x23y"#), - ("subscripted_power_follow", r#"x23y"#), - ("power_subscript_follow", r#"x23y"#), - ]; - - let expected = std::collections::HashMap::from([ - ("sup_x", "⠠⠁⠌⠭⠱"), - ("sup_zero", "⠠⠭⠌⠴"), - ("sup_ten", "⠠⠭⠌⠂⠴"), - ("sup_minus_x", "⠠⠁⠌⠀⠤⠭⠱"), - ("sup_minus_2", "⠠⠁⠌⠤⠆"), - ("sup_x_plus_1", "⠠⠁⠌⠭⠀⠖⠼⠁⠱"), - ("sup_sub_x2", "⠠⠁⠌⠭⠡⠆⠱"), - ("sup_nested", "⠠⠁⠌⠭⠌⠆⠱"), - ("sup_frac", "⠠⠁⠌⠼⠁⠆⠱"), - ("sup_complex_frac", "⠠⠁⠌⠐⠆⠭⠀⠖⠼⠁⠀⠳⠠⠽⠰⠱"), - ("sup_sqrt", "⠠⠁⠌⠩⠱⠭⠹⠱"), - ("sup_follow_letter", "⠠⠭⠌⠆⠽"), - ("sup_follow_number", "⠠⠭⠌⠆⠼⠉"), - ("sub_x", "⠠⠁⠡⠭⠱"), - ("sub_zero", "⠠⠭⠡⠴"), - ("sub_ten", "⠠⠭⠡⠂⠴"), - ("sub_digits_sequence", "⠨⠙⠡⠂⠒"), - ("sub_infinity", "⠠⠞⠡⠻"), - ("sub_minus_x", "⠠⠁⠡⠀⠤⠭⠱"), - ("sub_minus_2", "⠠⠁⠡⠤⠆"), - ("sub_x_plus_1", "⠠⠁⠡⠐⠭⠀⠖⠼⠁⠱"), - ("sub_nested", "⠠⠁⠡⠭⠡⠆⠱"), - ("sub_follow_letter", "⠠⠭⠡⠆⠽"), - ("sub_follow_number", "⠠⠭⠡⠆⠼⠉"), - ("subsup_numeric", "⠠⠭⠡⠼⠁⠌⠼⠃⠱"), - ("subsup_i_n", "⠠⠭⠡⠊⠌⠝⠱"), - ("subsup_grouped_sub", "⠠⠭⠡⠐⠊⠀⠖⠼⠁⠌⠼⠃⠱"), - ("subsup_grouped_sup", "⠠⠭⠡⠼⠚⠌⠠⠝⠀⠤⠼⠁⠱"), - ("subsup_nested_sup", "⠠⠭⠡⠼⠚⠌⠠⠝⠌⠆⠱"), - ("negative_base_sup", "⠤⠠⠭⠌⠆"), - ("paren_base_sup", "⠣⠠⠭⠀⠖⠼⠁⠜⠌⠆"), - ("frac_base_sup", "⠼⠁⠳⠠⠭⠌⠆"), - ("root_base_sup", "⠩⠱⠠⠭⠹⠌⠆"), - ("sup_on_function", "⠫⠎⠌⠆⠠⠭"), - ("log_sub_sup", "⠫⠇⠡⠼⠃⠌⠼⠉⠱⠠⠭"), - ("root_index_group", "⠩⠠⠝⠀⠖⠼⠁⠱⠠⠭⠹"), - ("root_index_sub", "⠩⠠⠝⠡⠆⠱⠭⠹"), - ("frac_num_group", "⠆⠠⠭⠀⠖⠼⠁⠀⠳⠠⠽⠰"), - ("frac_den_group", "⠆⠠⠭⠀⠳⠽⠀⠖⠼⠁⠰"), - ("nested_frac", "⠆⠆⠼⠁⠀⠳⠠⠭⠰⠀⠳⠆⠼⠁⠀⠳⠠⠽⠰⠰"), - ("sup_after_fraction", "⠆⠠⠭⠀⠖⠼⠁⠀⠳⠠⠽⠀⠤⠼⠁⠰⠌⠆"), - ("sub_after_fraction", "⠆⠠⠭⠀⠖⠼⠁⠀⠳⠠⠽⠀⠤⠼⠁⠰⠡⠠⠊⠱"), - ("tensor_like", "⠨⠞⠡⠠⠊⠌⠚⠱⠭⠡⠚⠌⠅⠱"), - ("mmultiscripts_left_sub", "⠡⠠⠅⠱⠨⠋"), - ("mmultiscripts_left_sup", "⠌⠲⠨⠺"), - ("mmultiscripts_both_sides", "⠡⠠⠗⠱⠌⠎⠱⠨⠞⠡⠠⠊⠱⠌⠚⠱"), - ("pre_negative_power", "⠼⠃⠠⠭⠌⠤⠂"), - ("power_of_power_follow", "⠠⠭⠌⠆⠌⠒⠽"), - ("subscripted_power_follow", "⠠⠭⠡⠆⠌⠒⠽"), - ("power_subscript_follow", "⠠⠭⠌⠆⠡⠒⠽"), - ]); - - for (label, expr) in cases { - test_braille("Russian", expr, expected[label])?; - } - return Ok(()); +fn script_grouping_nested_frac() -> Result<()> { + return test_russian_braille(r#"1x1y"#, "⠆⠆⠼⠁⠀⠳⠠⠭⠰⠀⠳⠆⠼⠁⠀⠳⠠⠽⠰⠰"); +} + +#[test] +fn script_grouping_sup_after_fraction() -> Result<()> { + return test_russian_braille(r#"x+1y-12"#, "⠆⠠⠭⠀⠖⠼⠁⠀⠳⠠⠽⠀⠤⠼⠁⠰⠌⠆"); +} + +#[test] +fn script_grouping_sub_after_fraction() -> Result<()> { + return test_russian_braille(r#"x+1y-1i"#, "⠆⠠⠭⠀⠖⠼⠁⠀⠳⠠⠽⠀⠤⠼⠁⠰⠡⠠⠊⠱"); +} + +#[test] +fn script_grouping_tensor_like() -> Result<()> { + return test_russian_braille(r#"Tijxjk"#, "⠨⠞⠡⠠⠊⠌⠚⠱⠭⠡⠚⠌⠅⠱"); +} + +#[test] +fn script_grouping_mmultiscripts_left_sub() -> Result<()> { + return test_russian_braille(r#"Fk"#, "⠡⠠⠅⠱⠨⠋"); +} + +#[test] +fn script_grouping_mmultiscripts_left_sup() -> Result<()> { + return test_russian_braille(r#"W4"#, "⠌⠲⠨⠺"); +} + +#[test] +fn script_grouping_mmultiscripts_both_sides() -> Result<()> { + return test_russian_braille(r#"Tijrs"#, "⠡⠠⠗⠱⠌⠎⠱⠨⠞⠡⠠⠊⠱⠌⠚⠱"); +} + +#[test] +fn script_grouping_pre_negative_power() -> Result<()> { + return test_russian_braille(r#"2x-1"#, "⠼⠃⠠⠭⠌⠤⠂"); +} + +#[test] +fn script_grouping_power_of_power_follow() -> Result<()> { + return test_russian_braille(r#"x23y"#, "⠠⠭⠌⠆⠌⠒⠽"); +} + +#[test] +fn script_grouping_subscripted_power_follow() -> Result<()> { + return test_russian_braille(r#"x23y"#, "⠠⠭⠡⠆⠌⠒⠽"); +} + +#[test] +fn script_grouping_power_subscript_follow() -> Result<()> { + return test_russian_braille(r#"x23y"#, "⠠⠭⠌⠆⠡⠒⠽"); } #[test] fn numbers_and_operators() -> Result<()> { - let expr = r#"5+12=17"#; - test_braille("Russian", expr, "⠼⠑⠀⠖⠼⠁⠃⠀⠶⠼⠁⠛")?; - return Ok(()); + return test_russian_braille(r#"5+12=17"#, "⠼⠑⠀⠖⠼⠁⠃⠀⠶⠼⠁⠛"); } #[test] -fn source_general_math_text_rules() -> Result<()> { - let expr = r#"5,§2"#; - test_braille("Russian", expr, "⠝⠼⠑⠠⠂⠬⠼⠃")?; +fn source_general_math_text_rules_01() -> Result<()> { + return test_russian_braille(r#"5,§2"#, "⠝⠼⠑⠠⠂⠬⠼⠃"); +} - let expr = r#"1+23++10=55."#; - test_braille("Russian", expr, "⠼⠁⠀⠖⠼⠃⠐⠼⠉⠀⠖⠠⠄⠀⠖⠼⠁⠚⠠⠀⠶⠼⠑⠑⠠⠲")?; +#[test] +fn source_general_math_text_rules_02() -> Result<()> { + return test_russian_braille(r#"1+23++10=55."#, "⠼⠁⠀⠖⠼⠃⠐⠼⠉⠀⠖⠠⠄⠀⠖⠼⠁⠚⠠⠀⠶⠼⠑⠑⠠⠲"); +} - let expr = r#"xy2"#; - test_braille("Russian", expr, "⠠⠭⠐⠽⠳⠆")?; +#[test] +fn source_general_math_text_rules_03() -> Result<()> { + return test_russian_braille(r#"xy2"#, "⠠⠭⠐⠽⠳⠆"); +} - let expr = r#"1+23+4"#; - test_braille("Russian", expr, "⠼⠁⠀⠖⠼⠃⠐⠼⠉⠀⠖⠼⠙")?; +#[test] +fn source_general_math_text_rules_04() -> Result<()> { + return test_russian_braille(r#"1+23+4"#, "⠼⠁⠀⠖⠼⠃⠐⠼⠉⠀⠖⠼⠙"); +} - let expr = r#"1+23+4"#; - test_braille("Russian", expr, "⠼⠁⠀⠖⠼⠃⠠⠼⠉⠀⠖⠼⠙")?; - return Ok(()); +#[test] +fn source_general_math_text_rules_05() -> Result<()> { + return test_russian_braille(r#"1+23+4"#, "⠼⠁⠀⠖⠼⠃⠠⠼⠉⠀⠖⠼⠙"); } #[test] -fn percent_and_special_marks() -> Result<()> { - let expr = r#"25%"#; - test_braille("Russian", expr, "⠼⠃⠑⠼⠴")?; +fn percent_and_special_marks_01() -> Result<()> { + return test_russian_braille(r#"25%"#, "⠼⠃⠑⠼⠴"); +} - let expr = r#"12"#; - test_braille("Russian", expr, "⠼⠁⠃⠼⠴⠴")?; +#[test] +fn percent_and_special_marks_02() -> Result<()> { + return test_russian_braille(r#"12"#, "⠼⠁⠃⠼⠴⠴"); +} - let expr = r#"0,56"#; - test_braille("Russian", expr, "⠼⠚⠂⠑⠋")?; - return Ok(()); +#[test] +fn percent_and_special_marks_03() -> Result<()> { + return test_russian_braille(r#"0,56"#, "⠼⠚⠂⠑⠋"); } #[test] fn labeled_table_rows() -> Result<()> { - let expr = r#"(1)x=0"#; - test_braille("Russian", expr, "⠍⠑⠞⠅⠁⠀⠎⠞⠗⠕⠅⠊⠀⠲⠣⠼⠁⠜⠀⠠⠭⠀⠶⠀⠼⠚")?; - return Ok(()); + return test_russian_braille(r#"(1)x=0"#, "⠍⠑⠞⠅⠁⠀⠎⠞⠗⠕⠅⠊⠀⠲⠣⠼⠁⠜⠀⠠⠭⠀⠶⠀⠼⠚"); } #[test] fn fraction() -> Result<()> { - let expr = r#"x2"#; - test_braille("Russian", expr, "⠠⠭⠳⠆")?; - return Ok(()); + return test_russian_braille(r#"x2"#, "⠠⠭⠳⠆"); } #[test] fn scripts_and_root() -> Result<()> { - let expr = r#"x2+y"#; - test_braille("Russian", expr, "⠠⠭⠌⠆⠀⠖⠩⠱⠽⠹")?; - return Ok(()); + return test_russian_braille(r#"x2+y"#, "⠠⠭⠌⠆⠀⠖⠩⠱⠽⠹"); } #[test] fn cyrillic_text() -> Result<()> { - let expr = r#"угол"#; - test_braille("Russian", expr, "⠥⠛⠕⠇")?; - return Ok(()); + return test_russian_braille(r#"угол"#, "⠥⠛⠕⠇"); } #[test] fn wikipedia_linear_parens_flat() -> Result<()> { - let expr = r#"3(9-7)=6"#; - test_braille("Russian", expr, "⠼⠉⠄⠣⠼⠊⠀⠤⠼⠛⠜⠀⠶⠼⠋")?; - return Ok(()); + return test_russian_braille(r#"3(9-7)=6"#, "⠼⠉⠄⠣⠼⠊⠀⠤⠼⠛⠜⠀⠶⠼⠋"); } #[test] -fn source_arithmetic_examples() -> Result<()> { - let expr = r#"2481=1944"#; - test_braille("Russian", expr, "⠼⠃⠙⠄⠼⠓⠁⠀⠶⠼⠁⠊⠙⠙")?; +fn source_arithmetic_examples_01() -> Result<()> { + return test_russian_braille(r#"2481=1944"#, "⠼⠃⠙⠄⠼⠓⠁⠀⠶⠼⠁⠊⠙⠙"); +} - let expr = r#"783:9=87"#; - test_braille("Russian", expr, "⠼⠛⠓⠉⠀⠲⠼⠊⠀⠶⠼⠓⠛")?; +#[test] +fn source_arithmetic_examples_02() -> Result<()> { + return test_russian_braille(r#"783:9=87"#, "⠼⠛⠓⠉⠀⠲⠼⠊⠀⠶⠼⠓⠛"); +} - let expr = r#"12×35=420"#; - test_braille("Russian", expr, "⠼⠁⠃⠀⠦⠼⠉⠑⠀⠶⠼⠙⠃⠚")?; +#[test] +fn source_arithmetic_examples_03() -> Result<()> { + return test_russian_braille(r#"12×35=420"#, "⠼⠁⠃⠀⠦⠼⠉⠑⠀⠶⠼⠙⠃⠚"); +} - let expr = r#"7456+5623=13079"#; - test_braille("Russian", expr, "⠼⠛⠙⠑⠋⠨⠳⠖⠑⠋⠃⠉⠨⠳⠶⠁⠉⠚⠛⠊")?; +#[test] +fn source_arithmetic_examples_04() -> Result<()> { + return test_russian_braille(r#"7456+5623=13079"#, "⠼⠛⠙⠑⠋⠨⠳⠖⠑⠋⠃⠉⠨⠳⠶⠁⠉⠚⠛⠊"); +} - let expr = r#"78650-1952=76698"#; - test_braille("Russian", expr, "⠼⠛⠓⠋⠑⠚⠨⠳⠤⠁⠊⠑⠃⠨⠳⠶⠛⠋⠋⠊⠓")?; +#[test] +fn source_arithmetic_examples_05() -> Result<()> { + return test_russian_braille(r#"78650-1952=76698"#, "⠼⠛⠓⠋⠑⠚⠨⠳⠤⠁⠊⠑⠃⠨⠳⠶⠛⠋⠋⠊⠓"); +} - let expr = r#"327×54826161308=179196"#; - test_braille("Russian", expr, "⠼⠉⠃⠛⠨⠳⠦⠑⠙⠓⠨⠳⠃⠋⠁⠋⠨⠳⠁⠉⠚⠓⠨⠳⠶⠁⠛⠊⠁⠊⠋")?; +#[test] +fn source_arithmetic_examples_06() -> Result<()> { + return test_russian_braille(r#"327×54826161308=179196"#, "⠼⠉⠃⠛⠨⠳⠦⠑⠙⠓⠨⠳⠃⠋⠁⠋⠨⠳⠁⠉⠚⠓⠨⠳⠶⠁⠛⠊⠁⠊⠋"); +} - let expr = r#"2982÷14=21328181442420"#; - test_braille("Russian", expr, "⠼⠃⠊⠓⠃⠀⠴⠀⠼⠁⠙⠀⠶⠀⠼⠃⠁⠉⠨⠳⠃⠓⠨⠳⠁⠓⠨⠳⠁⠙⠨⠳⠙⠃⠨⠳⠙⠃⠨⠳⠚")?; - return Ok(()); +#[test] +fn source_arithmetic_examples_07() -> Result<()> { + return test_russian_braille(r#"2982÷14=21328181442420"#, "⠼⠃⠊⠓⠃⠀⠴⠀⠼⠁⠙⠀⠶⠀⠼⠃⠁⠉⠨⠳⠃⠓⠨⠳⠁⠓⠨⠳⠁⠙⠨⠳⠙⠃⠨⠳⠙⠃⠨⠳⠚"); } #[test] fn nested_fraction_and_root() -> Result<()> { - let expr = r#"x+1yx-y"#; - test_braille("Russian", expr, "⠆⠠⠭⠀⠖⠩⠱⠼⠁⠳⠠⠽⠹⠀⠳⠭⠀⠤⠽⠰")?; - return Ok(()); + return test_russian_braille(r#"x+1yx-y"#, "⠆⠠⠭⠀⠖⠩⠱⠼⠁⠳⠠⠽⠹⠀⠳⠭⠀⠤⠽⠰"); } #[test] -fn source_simple_fractions_scripts_roots() -> Result<()> { - let expr = r#"12"#; - test_braille("Russian", expr, "⠼⠁⠆")?; +fn source_simple_fractions_scripts_roots_01() -> Result<()> { + return test_russian_braille(r#"12"#, "⠼⠁⠆"); +} - let expr = r#"a3"#; - test_braille("Russian", expr, "⠠⠁⠳⠒")?; +#[test] +fn source_simple_fractions_scripts_roots_02() -> Result<()> { + return test_russian_braille(r#"a3"#, "⠠⠁⠳⠒"); +} - let expr = r#"b7"#; - test_braille("Russian", expr, "⠠⠃⠡⠶")?; +#[test] +fn source_simple_fractions_scripts_roots_03() -> Result<()> { + return test_russian_braille(r#"b7"#, "⠠⠃⠡⠶"); +} - let expr = r#"x3"#; - test_braille("Russian", expr, "⠩⠒⠱⠠⠭⠹")?; - return Ok(()); +#[test] +fn source_simple_fractions_scripts_roots_04() -> Result<()> { + return test_russian_braille(r#"x3"#, "⠩⠒⠱⠠⠭⠹"); } #[test] -fn source_thin_fraction_rules() -> Result<()> { - let expr = r#"a3,2"#; - test_braille("Russian", expr, "⠠⠁⠳⠼⠉⠂⠃")?; +fn source_thin_fraction_rules_01() -> Result<()> { + return test_russian_braille(r#"a3,2"#, "⠠⠁⠳⠼⠉⠂⠃"); +} - let expr = r#"a-3"#; - test_braille("Russian", expr, "⠠⠁⠳⠤⠒")?; +#[test] +fn source_thin_fraction_rules_02() -> Result<()> { + return test_russian_braille(r#"a-3"#, "⠠⠁⠳⠤⠒"); +} - let expr = r#"3x+yz"#; - test_braille("Russian", expr, "⠼⠉⠄⠆⠠⠭⠀⠖⠽⠀⠳⠵⠰")?; +#[test] +fn source_thin_fraction_rules_03() -> Result<()> { + return test_russian_braille(r#"3x+yz"#, "⠼⠉⠄⠆⠠⠭⠀⠖⠽⠀⠳⠵⠰"); +} - let expr = r#"xyz"#; - test_braille("Russian", expr, "⠠⠭⠳⠽⠄⠵")?; +#[test] +fn source_thin_fraction_rules_04() -> Result<()> { + return test_russian_braille(r#"xyz"#, "⠠⠭⠳⠽⠄⠵"); +} - let expr = r#"xa+ba-b"#; - test_braille("Russian", expr, "⠠⠭⠌⠐⠆⠁⠀⠖⠃⠀⠳⠁⠀⠤⠃⠰⠱")?; - return Ok(()); +#[test] +fn source_thin_fraction_rules_05() -> Result<()> { + return test_russian_braille(r#"xa+ba-b"#, "⠠⠭⠌⠐⠆⠁⠀⠖⠃⠀⠳⠁⠀⠤⠃⠰⠱"); } #[test] -fn source_index_rules() -> Result<()> { - let expr = r#"D1,3"#; - test_braille("Russian", expr, "⠨⠙⠡⠼⠁⠠⠂⠼⠉⠱")?; +fn source_index_rules_01() -> Result<()> { + return test_russian_braille(r#"D1,3"#, "⠨⠙⠡⠼⠁⠠⠂⠼⠉⠱"); +} - let expr = r#"Ps,1"#; - test_braille("Russian", expr, "⠨⠏⠡⠠⠎⠠⠂⠼⠁⠱")?; +#[test] +fn source_index_rules_02() -> Result<()> { + return test_russian_braille(r#"Ps,1"#, "⠨⠏⠡⠠⠎⠠⠂⠼⠁⠱"); +} - let expr = r#"a2kn+1"#; - test_braille("Russian", expr, "⠠⠁⠡⠼⠃⠠⠅⠱⠌⠝⠀⠖⠼⠁⠱")?; +#[test] +fn source_index_rules_03() -> Result<()> { + return test_russian_braille(r#"a2kn+1"#, "⠠⠁⠡⠼⠃⠠⠅⠱⠌⠝⠀⠖⠼⠁⠱"); +} - let expr = r#"Z+"#; - test_braille("Russian", expr, "⠨⠵⠡⠐⠀⠖⠻⠱")?; +#[test] +fn source_index_rules_04() -> Result<()> { + return test_russian_braille(r#"Z+"#, "⠨⠵⠡⠐⠀⠖⠻⠱"); +} - let expr = r#"a-n+12"#; - test_braille("Russian", expr, "⠠⠁⠡⠐⠀⠤⠆⠝⠀⠖⠼⠁⠀⠳⠼⠃⠰⠱")?; - return Ok(()); +#[test] +fn source_index_rules_05() -> Result<()> { + return test_russian_braille(r#"a-n+12"#, "⠠⠁⠡⠐⠀⠤⠆⠝⠀⠖⠼⠁⠀⠳⠼⠃⠰⠱"); } #[test] -fn source_gost_numbers_fractions_and_sets() -> Result<()> { - let expr = r#"0,56"#; - test_braille("Russian", expr, "⠼⠚⠂⠑⠋")?; +fn source_gost_numbers_fractions_and_sets_01() -> Result<()> { + return test_russian_braille(r#"0,56"#, "⠼⠚⠂⠑⠋"); +} - let expr = r#"2/3"#; - test_braille("Russian", expr, "⠼⠃⠠⠌⠼⠉")?; +#[test] +fn source_gost_numbers_fractions_and_sets_02() -> Result<()> { + return test_russian_braille(r#"2/3"#, "⠼⠃⠠⠌⠼⠉"); +} - let expr = r#"a/b"#; - test_braille("Russian", expr, "⠠⠁⠠⠌⠃")?; +#[test] +fn source_gost_numbers_fractions_and_sets_03() -> Result<()> { + return test_russian_braille(r#"a/b"#, "⠠⠁⠠⠌⠃"); +} - let expr = r#"км/ч"#; - test_braille("Russian", expr, "⠅⠍⠠⠌⠟")?; +#[test] +fn source_gost_numbers_fractions_and_sets_04() -> Result<()> { + return test_russian_braille(r#"км/ч"#, "⠅⠍⠠⠌⠟"); +} - let expr = r#"25кг"#; - test_braille("Russian", expr, "⠼⠃⠑⠅⠛")?; +#[test] +fn source_gost_numbers_fractions_and_sets_05() -> Result<()> { + return test_russian_braille(r#"25кг"#, "⠼⠃⠑⠅⠛"); +} - let expr = r#"5м30см"#; - test_braille("Russian", expr, "⠼⠑⠍⠀⠼⠉⠚⠎⠍")?; +#[test] +fn source_gost_numbers_fractions_and_sets_06() -> Result<()> { + return test_russian_braille(r#"5м30см"#, "⠼⠑⠍⠀⠼⠉⠚⠎⠍"); +} - let expr = r#"12м2"#; - test_braille("Russian", expr, "⠼⠁⠃⠍⠌⠆")?; +#[test] +fn source_gost_numbers_fractions_and_sets_07() -> Result<()> { + return test_russian_braille(r#"12м2"#, "⠼⠁⠃⠍⠌⠆"); +} - let expr = r#"9,8м/с2"#; - test_braille("Russian", expr, "⠼⠊⠂⠓⠍⠠⠌⠎⠌⠆")?; +#[test] +fn source_gost_numbers_fractions_and_sets_08() -> Result<()> { + return test_russian_braille(r#"9,8м/с2"#, "⠼⠊⠂⠓⠍⠠⠌⠎⠌⠆"); +} - let expr = r#"15Нм"#; - test_braille("Russian", expr, "⠼⠁⠑⠨⠝⠄⠍")?; +#[test] +fn source_gost_numbers_fractions_and_sets_09() -> Result<()> { + return test_russian_braille(r#"15Нм"#, "⠼⠁⠑⠨⠝⠄⠍"); +} - let expr = r#"12?4"#; - test_braille("Russian", expr, "⠼⠁⠃⠬⠙")?; +#[test] +fn source_gost_numbers_fractions_and_sets_10() -> Result<()> { + return test_russian_braille(r#"12?4"#, "⠼⠁⠃⠬⠙"); +} - let expr = r#"[1,4]\{4}=[1,4)"#; - test_braille("Russian", expr, "⠷⠼⠁⠠⠂⠼⠙⠾⠀⠰⠤⠪⠼⠙⠕⠀⠶⠷⠼⠁⠠⠂⠼⠙⠜")?; +#[test] +fn source_gost_numbers_fractions_and_sets_11() -> Result<()> { + return test_russian_braille(r#"[1,4]\{4}=[1,4)"#, "⠷⠼⠁⠠⠂⠼⠙⠾⠀⠰⠤⠪⠼⠙⠕⠀⠶⠷⠼⠁⠠⠂⠼⠙⠜"); +} - let expr = r#"5|x"#; - test_braille("Russian", expr, "⠼⠑⠸⠠⠭")?; +#[test] +fn source_gost_numbers_fractions_and_sets_12() -> Result<()> { + return test_russian_braille(r#"5|x"#, "⠼⠑⠸⠠⠭"); +} - let expr = r#"xA"#; - test_braille("Russian", expr, "⠠⠭⠀⠐⠪⠀⠨⠁")?; +#[test] +fn source_gost_numbers_fractions_and_sets_13() -> Result<()> { + return test_russian_braille(r#"xA"#, "⠠⠭⠀⠐⠪⠀⠨⠁"); +} - let expr = r#"xA"#; - test_braille("Russian", expr, "⠠⠭⠘⠪⠨⠁")?; +#[test] +fn source_gost_numbers_fractions_and_sets_14() -> Result<()> { + return test_russian_braille(r#"xA"#, "⠠⠭⠘⠪⠨⠁"); +} - let expr = r#"AB"#; - test_braille("Russian", expr, "⠨⠁⠘⠪⠃")?; +#[test] +fn source_gost_numbers_fractions_and_sets_15() -> Result<()> { + return test_russian_braille(r#"AB"#, "⠨⠁⠘⠪⠃"); +} - let expr = r#"AB"#; - test_braille("Russian", expr, "⠨⠁⠀⠯⠀⠃")?; +#[test] +fn source_gost_numbers_fractions_and_sets_16() -> Result<()> { + return test_russian_braille(r#"AB"#, "⠨⠁⠀⠯⠀⠃"); +} - let expr = r#"DE"#; - test_braille("Russian", expr, "⠨⠙⠈⠯⠑")?; +#[test] +fn source_gost_numbers_fractions_and_sets_17() -> Result<()> { + return test_russian_braille(r#"DE"#, "⠨⠙⠈⠯⠑"); +} - let expr = r#"AB"#; - test_braille("Russian", expr, "⠨⠁⠈⠯⠃")?; +#[test] +fn source_gost_numbers_fractions_and_sets_18() -> Result<()> { + return test_russian_braille(r#"AB"#, "⠨⠁⠈⠯⠃"); +} - let expr = r#"AB"#; - test_braille("Russian", expr, "⠨⠁⠀⠰⠲⠃")?; +#[test] +fn source_gost_numbers_fractions_and_sets_19() -> Result<()> { + return test_russian_braille(r#"AB"#, "⠨⠁⠀⠰⠲⠃"); +} - let expr = r#"AB"#; - test_braille("Russian", expr, "⠨⠁⠀⠰⠴⠃")?; +#[test] +fn source_gost_numbers_fractions_and_sets_20() -> Result<()> { + return test_russian_braille(r#"AB"#, "⠨⠁⠀⠰⠴⠃"); +} - let expr = r#"AB\CD"#; - test_braille("Russian", expr, "⠨⠁⠀⠰⠴⠃⠀⠰⠤⠉⠀⠰⠲⠙")?; +#[test] +fn source_gost_numbers_fractions_and_sets_21() -> Result<()> { + return test_russian_braille(r#"AB\CD"#, "⠨⠁⠀⠰⠴⠃⠀⠰⠤⠉⠀⠰⠲⠙"); +} - let expr = r#"(3,5][7,)="#; - test_braille("Russian", expr, "⠣⠼⠉⠠⠂⠼⠑⠾⠀⠰⠲⠷⠼⠛⠠⠂⠻⠜⠀⠶⠈⠴")?; +#[test] +fn source_gost_numbers_fractions_and_sets_22() -> Result<()> { + return test_russian_braille(r#"(3,5][7,)="#, "⠣⠼⠉⠠⠂⠼⠑⠾⠀⠰⠲⠷⠼⠛⠠⠂⠻⠜⠀⠶⠈⠴"); +} - let expr = r#"36,6"#; - test_braille("Russian", expr, "⠼⠉⠋⠂⠋⠨⠴⠨⠉")?; - return Ok(()); +#[test] +fn source_gost_numbers_fractions_and_sets_23() -> Result<()> { + return test_russian_braille(r#"36,6"#, "⠼⠉⠋⠂⠋⠨⠴⠨⠉"); } #[test] -fn source_mixed_numbers_and_periodic_decimals() -> Result<()> { - let expr = r#"0,4(71)"#; - test_braille("Russian", expr, "⠼⠚⠂⠙⠣⠛⠁⠜")?; +fn source_mixed_numbers_and_periodic_decimals_01() -> Result<()> { + return test_russian_braille(r#"0,4(71)"#, "⠼⠚⠂⠙⠣⠛⠁⠜"); +} - let expr = r#"1,(523)"#; - test_braille("Russian", expr, "⠼⠁⠂⠣⠑⠃⠉⠜")?; +#[test] +fn source_mixed_numbers_and_periodic_decimals_02() -> Result<()> { + return test_russian_braille(r#"1,(523)"#, "⠼⠁⠂⠣⠑⠃⠉⠜"); +} - let expr = r#"0,4(71)"#; - test_braille("Russian", expr, "⠼⠚⠂⠙⠣⠛⠁⠜")?; +#[test] +fn source_mixed_numbers_and_periodic_decimals_03() -> Result<()> { + return test_russian_braille(r#"0,4(71)"#, "⠼⠚⠂⠙⠣⠛⠁⠜"); +} - let expr = r#"538"#; - test_braille("Russian", expr, "⠼⠑⠼⠉⠦")?; +#[test] +fn source_mixed_numbers_and_periodic_decimals_04() -> Result<()> { + return test_russian_braille(r#"538"#, "⠼⠑⠼⠉⠦"); +} - let expr = r#"224+4-1524"#; - test_braille("Russian", expr, "⠼⠃⠐⠆⠼⠃⠙⠀⠖⠼⠙⠀⠤⠼⠁⠑⠀⠳⠼⠃⠙⠰")?; - return Ok(()); +#[test] +fn source_mixed_numbers_and_periodic_decimals_05() -> Result<()> { + return test_russian_braille(r#"224+4-1524"#, "⠼⠃⠐⠆⠼⠃⠙⠀⠖⠼⠙⠀⠤⠼⠁⠑⠀⠳⠼⠃⠙⠰"); } #[test] -fn latin_alphabet_indicators() -> Result<()> { - let expr = r#"x+A+y+B=x+y+A+B"#; - test_braille("Russian", expr, "⠠⠭⠀⠖⠨⠁⠀⠖⠠⠽⠀⠖⠨⠃⠀⠶⠠⠭⠀⠖⠽⠀⠖⠨⠁⠀⠖⠃")?; +fn latin_alphabet_indicators_01() -> Result<()> { + return test_russian_braille(r#"x+A+y+B=x+y+A+B"#, "⠠⠭⠀⠖⠨⠁⠀⠖⠠⠽⠀⠖⠨⠃⠀⠶⠠⠭⠀⠖⠽⠀⠖⠨⠁⠀⠖⠃"); +} - let expr = r#"MCDLXIV"#; - test_braille("Russian", expr, "⠨⠍⠉⠙⠇⠭⠊⠧")?; - return Ok(()); +#[test] +fn latin_alphabet_indicators_02() -> Result<()> { + return test_russian_braille(r#"MCDLXIV"#, "⠨⠍⠉⠙⠇⠭⠊⠧"); } #[test] -fn alphabet_indicators_after_numbers_and_greek() -> Result<()> { - let expr = r#"2x+15=23"#; - test_braille("Russian", expr, "⠼⠃⠄⠠⠭⠀⠖⠼⠁⠑⠀⠶⠼⠃⠉")?; +fn alphabet_indicators_after_numbers_and_greek_01() -> Result<()> { + return test_russian_braille(r#"2x+15=23"#, "⠼⠃⠄⠠⠭⠀⠖⠼⠁⠑⠀⠶⠼⠃⠉"); +} - let expr = r#"L=2πr"#; - test_braille("Russian", expr, "⠨⠇⠀⠶⠼⠃⠰⠏⠠⠗")?; +#[test] +fn alphabet_indicators_after_numbers_and_greek_02() -> Result<()> { + return test_russian_braille(r#"L=2πr"#, "⠨⠇⠀⠶⠼⠃⠰⠏⠠⠗"); +} - let expr = r#"α+β=Α+Β"#; - test_braille("Russian", expr, "⠰⠁⠀⠖⠃⠀⠶⠸⠁⠀⠖⠃")?; - return Ok(()); +#[test] +fn alphabet_indicators_after_numbers_and_greek_03() -> Result<()> { + return test_russian_braille(r#"α+β=Α+Β"#, "⠰⠁⠀⠖⠃⠀⠶⠸⠁⠀⠖⠃"); } #[test] -fn source_typeform_and_mathvariant_indicators() -> Result<()> { - let expr = r#"x"#; - test_braille("Russian", expr, "⠻⠠⠭⠻")?; +fn source_typeform_and_mathvariant_indicators_01() -> Result<()> { + return test_russian_braille(r#"x"#, "⠻⠠⠭⠻"); +} - let expr = r#"y"#; - test_braille("Russian", expr, "⠸⠠⠽⠸")?; +#[test] +fn source_typeform_and_mathvariant_indicators_02() -> Result<()> { + return test_russian_braille(r#"y"#, "⠸⠠⠽⠸"); +} - let expr = r#"z"#; - test_braille("Russian", expr, "⠻⠸⠠⠵⠸⠻")?; +#[test] +fn source_typeform_and_mathvariant_indicators_03() -> Result<()> { + return test_russian_braille(r#"z"#, "⠻⠸⠠⠵⠸⠻"); +} - let expr = r#"AB"#; - test_braille("Russian", expr, "⠻⠨⠁⠃⠻")?; +#[test] +fn source_typeform_and_mathvariant_indicators_04() -> Result<()> { + return test_russian_braille(r#"AB"#, "⠻⠨⠁⠃⠻"); +} - let expr = r#"x+y"#; - test_braille("Russian", expr, "⠻⠠⠭⠻⠀⠖⠽")?; +#[test] +fn source_typeform_and_mathvariant_indicators_05() -> Result<()> { + return test_russian_braille(r#"x+y"#, "⠻⠠⠭⠻⠀⠖⠽"); +} - let expr = r#"𝐱"#; - test_braille("Russian", expr, "⠻⠠⠭⠻")?; +#[test] +fn source_typeform_and_mathvariant_indicators_06() -> Result<()> { + return test_russian_braille(r#"𝐱"#, "⠻⠠⠭⠻"); +} - let expr = r#"𝑥"#; - test_braille("Russian", expr, "⠸⠠⠭⠸")?; +#[test] +fn source_typeform_and_mathvariant_indicators_07() -> Result<()> { + return test_russian_braille(r#"𝑥"#, "⠸⠠⠭⠸"); +} - let expr = r#"x"#; - test_braille("Russian", expr, "⠻⠠⠭⠻")?; +#[test] +fn source_typeform_and_mathvariant_indicators_08() -> Result<()> { + return test_russian_braille(r#"x"#, "⠻⠠⠭⠻"); +} - let expr = r#"xy"#; - test_braille("Russian", expr, "⠻⠠⠭⠽⠻")?; +#[test] +fn source_typeform_and_mathvariant_indicators_09() -> Result<()> { + return test_russian_braille(r#"xy"#, "⠻⠠⠭⠽⠻"); +} - let expr = r#"y"#; - test_braille("Russian", expr, "⠸⠠⠽⠸")?; - return Ok(()); +#[test] +fn source_typeform_and_mathvariant_indicators_10() -> Result<()> { + return test_russian_braille(r#"y"#, "⠸⠠⠽⠸"); } #[test] -fn source_gost_58511_typeform_layout_indicators() -> Result<()> { - let expr = r#"xy"#; - test_braille("Russian", expr, "⠻⠠⠭⠽⠻")?; +fn source_gost_58511_typeform_layout_indicators_01() -> Result<()> { + return test_russian_braille(r#"xy"#, "⠻⠠⠭⠽⠻"); +} - let expr = r#"ab"#; - test_braille("Russian", expr, "⠸⠠⠁⠸⠸⠃⠸")?; +#[test] +fn source_gost_58511_typeform_layout_indicators_02() -> Result<()> { + return test_russian_braille(r#"ab"#, "⠸⠠⠁⠸⠸⠃⠸"); +} - let expr = r#"xy"#; - test_braille("Russian", expr, "⠻⠸⠠⠭⠽⠸⠻")?; +#[test] +fn source_gost_58511_typeform_layout_indicators_03() -> Result<()> { + return test_russian_braille(r#"xy"#, "⠻⠸⠠⠭⠽⠸⠻"); +} - let expr = r#"x"#; - test_braille("Russian", expr, "⠸⠠⠭⠸")?; +#[test] +fn source_gost_58511_typeform_layout_indicators_04() -> Result<()> { + return test_russian_braille(r#"x"#, "⠸⠠⠭⠸"); +} - let expr = r#"x"#; - test_braille("Russian", expr, "⠠⠭")?; +#[test] +fn source_gost_58511_typeform_layout_indicators_05() -> Result<()> { + return test_russian_braille(r#"x"#, "⠠⠭"); +} - let expr = r#"AB"#; - test_braille("Russian", expr, "⠨⠁⠀⠃")?; +#[test] +fn source_gost_58511_typeform_layout_indicators_06() -> Result<()> { + return test_russian_braille(r#"AB"#, "⠨⠁⠀⠃"); +} - let expr = r#"xприy"#; - test_braille("Russian", expr, "⠠⠭⠏⠗⠊⠽")?; - return Ok(()); +#[test] +fn source_gost_58511_typeform_layout_indicators_07() -> Result<()> { + return test_russian_braille(r#"xприy"#, "⠠⠭⠏⠗⠊⠽"); } #[test] fn wikipedia_times_divide() -> Result<()> { - let expr = r#"6×7:14=3"#; - test_braille("Russian", expr, "⠼⠋⠀⠦⠼⠛⠀⠲⠼⠁⠙⠀⠶⠼⠉")?; - return Ok(()); + return test_russian_braille(r#"6×7:14=3"#, "⠼⠋⠀⠦⠼⠛⠀⠲⠼⠁⠙⠀⠶⠼⠉"); } #[test] fn wikipedia_linear_parens() -> Result<()> { - let expr = r#"3·(97)=6"#; - test_braille("Russian", expr, "⠼⠉⠄⠣⠼⠊⠀⠤⠼⠛⠜⠀⠶⠼⠋")?; - return Ok(()); + return test_russian_braille(r#"3·(97)=6"#, "⠼⠉⠄⠣⠼⠊⠀⠤⠼⠛⠜⠀⠶⠼⠋"); } #[test] -fn wikipedia_sqrt() -> Result<()> { - let expr = r#"10000<101"#; - test_braille("Russian", expr, "⠩⠱⠼⠁⠚⠚⠚⠚⠹⠀⠪⠀⠼⠁⠚⠁")?; +fn wikipedia_sqrt_01() -> Result<()> { + return test_russian_braille(r#"10000<101"#, "⠩⠱⠼⠁⠚⠚⠚⠚⠹⠀⠪⠀⠼⠁⠚⠁"); +} - let expr = r#"ab"#; - test_braille("Russian", expr, "⠩⠱⠠⠁⠹⠄⠃")?; - return Ok(()); +#[test] +fn wikipedia_sqrt_02() -> Result<()> { + return test_russian_braille(r#"ab"#, "⠩⠱⠠⠁⠹⠄⠃"); } #[test] -fn source_functions_logs_derivatives() -> Result<()> { - let expr = r#"cosα"#; - test_braille("Russian", expr, "⠫⠉⠰⠁")?; +fn source_functions_logs_derivatives_01() -> Result<()> { + return test_russian_braille(r#"cosα"#, "⠫⠉⠰⠁"); +} - let expr = r#"tgxctgx=1"#; - test_braille("Russian", expr, "⠫⠞⠠⠭⠄⠫⠉⠞⠠⠭⠀⠶⠼⠁")?; +#[test] +fn source_functions_logs_derivatives_02() -> Result<()> { + return test_russian_braille(r#"tgxctgx=1"#, "⠫⠞⠠⠭⠄⠫⠉⠞⠠⠭⠀⠶⠼⠁"); +} - let expr = r#"sin2x"#; - test_braille("Russian", expr, "⠫⠎⠌⠆⠠⠭")?; +#[test] +fn source_functions_logs_derivatives_03() -> Result<()> { + return test_russian_braille(r#"sin2x"#, "⠫⠎⠌⠆⠠⠭"); +} - let expr = r#"tg3α+β2"#; - test_braille("Russian", expr, "⠫⠞⠌⠒⠐⠆⠰⠁⠀⠖⠃⠀⠳⠼⠃⠰")?; +#[test] +fn source_functions_logs_derivatives_04() -> Result<()> { + return test_russian_braille(r#"tg3α+β2"#, "⠫⠞⠌⠒⠐⠆⠰⠁⠀⠖⠃⠀⠳⠼⠃⠰"); +} - let expr = r#"(ctgβ)2m-n+3"#; - test_braille("Russian", expr, "⠣⠫⠉⠞⠰⠃⠜⠌⠼⠃⠠⠍⠀⠤⠝⠀⠖⠼⠉⠱")?; +#[test] +fn source_functions_logs_derivatives_05() -> Result<()> { + return test_russian_braille(r#"(ctgβ)2m-n+3"#, "⠣⠫⠉⠞⠰⠃⠜⠌⠼⠃⠠⠍⠀⠤⠝⠀⠖⠼⠉⠱"); +} - let expr = r#"arcsin2x"#; - test_braille("Russian", expr, "⠫⠁⠎⠌⠆⠠⠭")?; +#[test] +fn source_functions_logs_derivatives_06() -> Result<()> { + return test_russian_braille(r#"arcsin2x"#, "⠫⠁⠎⠌⠆⠠⠭"); +} - let expr = r#"arccosn+1x"#; - test_braille("Russian", expr, "⠫⠁⠉⠌⠠⠝⠀⠖⠼⠁⠱⠠⠭")?; +#[test] +fn source_functions_logs_derivatives_07() -> Result<()> { + return test_russian_braille(r#"arccosn+1x"#, "⠫⠁⠉⠌⠠⠝⠀⠖⠼⠁⠱⠠⠭"); +} - let expr = r#"arctg3x+y2"#; - test_braille("Russian", expr, "⠫⠁⠞⠌⠒⠐⠆⠠⠭⠀⠖⠽⠀⠳⠼⠃⠰")?; +#[test] +fn source_functions_logs_derivatives_08() -> Result<()> { + return test_russian_braille(r#"arctg3x+y2"#, "⠫⠁⠞⠌⠒⠐⠆⠠⠭⠀⠖⠽⠀⠳⠼⠃⠰"); +} - let expr = r#"tanx+cotx"#; - test_braille("Russian", expr, "⠫⠞⠠⠭⠀⠖⠫⠉⠞⠠⠭")?; +#[test] +fn source_functions_logs_derivatives_09() -> Result<()> { + return test_russian_braille(r#"tanx+cotx"#, "⠫⠞⠠⠭⠀⠖⠫⠉⠞⠠⠭"); +} - let expr = r#"logxylogyx=1"#; - test_braille("Russian", expr, "⠫⠇⠡⠠⠭⠱⠽⠄⠫⠇⠡⠠⠽⠱⠭⠀⠶⠼⠁")?; +#[test] +fn source_functions_logs_derivatives_10() -> Result<()> { + return test_russian_braille(r#"logxylogyx=1"#, "⠫⠇⠡⠠⠭⠱⠽⠄⠫⠇⠡⠠⠽⠱⠭⠀⠶⠼⠁"); +} - let expr = r#"shx+chx=sinhx+coshx"#; - test_braille("Russian", expr, "⠫⠎⠓⠠⠭⠀⠖⠫⠉⠓⠠⠭⠀⠶⠫⠎⠓⠠⠭⠀⠖⠫⠉⠓⠠⠭")?; +#[test] +fn source_functions_logs_derivatives_11() -> Result<()> { + return test_russian_braille(r#"shx+chx=sinhx+coshx"#, "⠫⠎⠓⠠⠭⠀⠖⠫⠉⠓⠠⠭⠀⠶⠫⠎⠓⠠⠭⠀⠖⠫⠉⠓⠠⠭"); +} - let expr = r#"thxcthx=1"#; - test_braille("Russian", expr, "⠫⠞⠓⠠⠭⠄⠫⠉⠞⠓⠠⠭⠀⠶⠼⠁")?; +#[test] +fn source_functions_logs_derivatives_12() -> Result<()> { + return test_russian_braille(r#"thxcthx=1"#, "⠫⠞⠓⠠⠭⠄⠫⠉⠞⠓⠠⠭⠀⠶⠼⠁"); +} - let expr = r#"argz=0,sgnx"#; - test_braille("Russian", expr, "⠫⠁⠗⠛⠠⠵⠀⠶⠼⠚⠠⠂⠫⠎⠛⠝⠠⠭")?; +#[test] +fn source_functions_logs_derivatives_13() -> Result<()> { + return test_russian_braille(r#"argz=0,sgnx"#, "⠫⠁⠗⠛⠠⠵⠀⠶⠼⠚⠠⠂⠫⠎⠛⠝⠠⠭"); +} - let expr = r#"detA+rankA=rgA"#; - test_braille("Russian", expr, "⠫⠙⠑⠞⠨⠁⠀⠖⠫⠗⠁⠝⠅⠨⠁⠀⠶⠫⠗⠛⠨⠁")?; +#[test] +fn source_functions_logs_derivatives_14() -> Result<()> { + return test_russian_braille(r#"detA+rankA=rgA"#, "⠫⠙⠑⠞⠨⠁⠀⠖⠫⠗⠁⠝⠅⠨⠁⠀⠶⠫⠗⠛⠨⠁"); +} - let expr = r#"Rez+Imz"#; - test_braille("Russian", expr, "⠫⠗⠑⠠⠵⠀⠖⠫⠊⠍⠠⠵")?; +#[test] +fn source_functions_logs_derivatives_15() -> Result<()> { + return test_russian_braille(r#"Rez+Imz"#, "⠫⠗⠑⠠⠵⠀⠖⠫⠊⠍⠠⠵"); +} - let expr = r#"gradφ+rotF+divF"#; - test_braille("Russian", expr, "⠫⠛⠗⠁⠙⠰⠋⠀⠖⠫⠗⠕⠞⠨⠋⠀⠖⠫⠙⠊⠧⠨⠋")?; +#[test] +fn source_functions_logs_derivatives_16() -> Result<()> { + return test_russian_braille(r#"gradφ+rotF+divF"#, "⠫⠛⠗⠁⠙⠰⠋⠀⠖⠫⠗⠕⠞⠨⠋⠀⠖⠫⠙⠊⠧⠨⠋"); +} - let expr = r#"gcd(a,b)=lcm(c,d)"#; - test_braille("Russian", expr, "⠫⠛⠉⠙⠣⠠⠁⠠⠂⠃⠜⠀⠶⠫⠇⠉⠍⠣⠠⠉⠠⠂⠙⠜")?; +#[test] +fn source_functions_logs_derivatives_17() -> Result<()> { + return test_russian_braille(r#"gcd(a,b)=lcm(c,d)"#, "⠫⠛⠉⠙⠣⠠⠁⠠⠂⠃⠜⠀⠶⠫⠇⠉⠍⠣⠠⠉⠠⠂⠙⠜"); +} - let expr = r#"y(x)=f(x)"#; - test_braille("Russian", expr, "⠠⠽⠔⠣⠭⠜⠀⠶⠋⠣⠭⠜")?; +#[test] +fn source_functions_logs_derivatives_18() -> Result<()> { + return test_russian_braille(r#"y(x)=f(x)"#, "⠠⠽⠔⠣⠭⠜⠀⠶⠋⠣⠭⠜"); +} - let expr = r#"y(x)=limxx0y(x)-y(x0)x-x0"#; - test_braille("Russian", expr, "⠠⠽⠔⠣⠭⠜⠀⠶⠫⠇⠍⠨⠡⠠⠭⠀⠒⠕⠭⠡⠴⠱⠆⠽⠣⠭⠜⠀⠤⠽⠣⠭⠡⠴⠜⠀⠳⠭⠀⠤⠭⠡⠴⠰")?; +#[test] +fn source_functions_logs_derivatives_19() -> Result<()> { + return test_russian_braille(r#"y(x)=limxx0y(x)-y(x0)x-x0"#, "⠠⠽⠔⠣⠭⠜⠀⠶⠫⠇⠍⠨⠡⠠⠭⠀⠒⠕⠭⠡⠴⠱⠆⠽⠣⠭⠜⠀⠤⠽⠣⠭⠡⠴⠜⠀⠳⠭⠀⠤⠭⠡⠴⠰"); +} - let expr = r#"xnaприn+"#; - test_braille("Russian", expr, "⠠⠭⠡⠝⠱⠀⠒⠕⠁⠀⠏⠗⠊⠀⠝⠀⠒⠕⠀⠖⠻")?; - return Ok(()); +#[test] +fn source_functions_logs_derivatives_20() -> Result<()> { + return test_russian_braille(r#"xnaприn+"#, "⠠⠭⠡⠝⠱⠀⠒⠕⠁⠀⠏⠗⠊⠀⠝⠀⠒⠕⠀⠖⠻"); } #[test] -fn source_large_operator_limits() -> Result<()> { - let expr = r#"14x2dx"#; - test_braille("Russian", expr, "⠮⠡⠂⠌⠲⠠⠭⠌⠆⠙⠭")?; +fn source_large_operator_limits_01() -> Result<()> { + return test_russian_braille(r#"14x2dx"#, "⠮⠡⠂⠌⠲⠠⠭⠌⠆⠙⠭"); +} - let expr = r#"i=1nai"#; - test_braille("Russian", expr, "⠸⠎⠨⠡⠠⠊⠀⠶⠼⠁⠱⠨⠌⠠⠝⠱⠁⠡⠊⠱")?; +#[test] +fn source_large_operator_limits_02() -> Result<()> { + return test_russian_braille(r#"i=1nai"#, "⠸⠎⠨⠡⠠⠊⠀⠶⠼⠁⠱⠨⠌⠠⠝⠱⠁⠡⠊⠱"); +} - let expr = r#"0nbk"#; - test_braille("Russian", expr, "⠸⠏⠡⠴⠌⠠⠝⠱⠃⠡⠅⠱")?; - return Ok(()); +#[test] +fn source_large_operator_limits_03() -> Result<()> { + return test_russian_braille(r#"0nbk"#, "⠸⠏⠡⠴⠌⠠⠝⠱⠃⠡⠅⠱"); } #[test] -fn source_geometry_matrix_chemistry() -> Result<()> { - let expr = r#"ABC=15°3012"#; - test_braille("Russian", expr, "⠸⠪⠨⠁⠃⠉⠀⠶⠼⠁⠑⠨⠴⠼⠉⠚⠨⠔⠼⠁⠃⠨⠔⠔")?; +fn source_geometry_matrix_chemistry_01() -> Result<()> { + return test_russian_braille(r#"ABC=15°3012"#, "⠸⠪⠨⠁⠃⠉⠀⠶⠼⠁⠑⠨⠴⠼⠉⠚⠨⠔⠼⠁⠃⠨⠔⠔"); +} - let expr = r#"ABC"#; - test_braille("Russian", expr, "⠸⠪⠨⠁⠔⠃⠔⠉⠔")?; +#[test] +fn source_geometry_matrix_chemistry_02() -> Result<()> { + return test_russian_braille(r#"ABC"#, "⠸⠪⠨⠁⠔⠃⠔⠉⠔"); +} - let expr = r#"A1B1C1"#; - test_braille("Russian", expr, "⠸⠙⠨⠁⠡⠂⠃⠡⠂⠉⠡⠂")?; +#[test] +fn source_geometry_matrix_chemistry_03() -> Result<()> { + return test_russian_braille(r#"A1B1C1"#, "⠸⠙⠨⠁⠡⠂⠃⠡⠂⠉⠡⠂"); +} - let expr = r#"2ABC"#; - test_braille("Russian", expr, "⠼⠃⠄⠸⠪⠨⠁⠃⠉")?; +#[test] +fn source_geometry_matrix_chemistry_04() -> Result<()> { + return test_russian_braille(r#"2ABC"#, "⠼⠃⠄⠸⠪⠨⠁⠃⠉"); +} - let expr = r#"3ABC"#; - test_braille("Russian", expr, "⠼⠉⠄⠸⠙⠨⠁⠃⠉")?; +#[test] +fn source_geometry_matrix_chemistry_05() -> Result<()> { + return test_russian_braille(r#"3ABC"#, "⠼⠉⠄⠸⠙⠨⠁⠃⠉"); +} - let expr = r#"4EF"#; - test_braille("Russian", expr, "⠼⠙⠄⠸⠜⠨⠑⠋")?; +#[test] +fn source_geometry_matrix_chemistry_06() -> Result<()> { + return test_russian_braille(r#"4EF"#, "⠼⠙⠄⠸⠜⠨⠑⠋"); +} - let expr = r#"KLMPQR"#; - test_braille("Russian", expr, "⠸⠙⠨⠅⠇⠍⠀⠢⠸⠙⠨⠏⠟⠗")?; +#[test] +fn source_geometry_matrix_chemistry_07() -> Result<()> { + return test_russian_braille(r#"KLMPQR"#, "⠸⠙⠨⠅⠇⠍⠀⠢⠸⠙⠨⠏⠟⠗"); +} - let expr = r#"ABCD"#; - test_braille("Russian", expr, "⠨⠁⠃⠸⠸⠨⠉⠙")?; +#[test] +fn source_geometry_matrix_chemistry_08() -> Result<()> { + return test_russian_braille(r#"ABCD"#, "⠨⠁⠃⠸⠸⠨⠉⠙"); +} - let expr = r#"ab"#; - test_braille("Russian", expr, "⠠⠁⠼⠄⠃")?; +#[test] +fn source_geometry_matrix_chemistry_09() -> Result<()> { + return test_russian_braille(r#"ab"#, "⠠⠁⠼⠄⠃"); +} - let expr = r#"ABCD"#; - test_braille("Russian", expr, "⠨⠁⠃⠼⠄⠉⠙")?; +#[test] +fn source_geometry_matrix_chemistry_10() -> Result<()> { + return test_russian_braille(r#"ABCD"#, "⠨⠁⠃⠼⠄⠉⠙"); +} - let expr = r#"a=2"#; - test_braille("Russian", expr, "⠠⠁⠒⠂⠀⠶⠼⠃")?; +#[test] +fn source_geometry_matrix_chemistry_11() -> Result<()> { + return test_russian_braille(r#"a=2"#, "⠠⠁⠒⠂⠀⠶⠼⠃"); +} - let expr = r#"AB+BC=AC"#; - test_braille("Russian", expr, "⠨⠁⠃⠨⠒⠂⠀⠖⠃⠉⠨⠒⠂⠀⠶⠁⠉⠨⠒⠂")?; +#[test] +fn source_geometry_matrix_chemistry_12() -> Result<()> { + return test_russian_braille(r#"AB+BC=AC"#, "⠨⠁⠃⠨⠒⠂⠀⠖⠃⠉⠨⠒⠂⠀⠶⠁⠉⠨⠒⠂"); +} - let expr = r#"KL¯=4PQ¯"#; - test_braille("Russian", expr, "⠨⠅⠇⠨⠒⠀⠶⠼⠙⠄⠨⠏⠟⠨⠒")?; +#[test] +fn source_geometry_matrix_chemistry_13() -> Result<()> { + return test_russian_braille(r#"KL¯=4PQ¯"#, "⠨⠅⠇⠨⠒⠀⠶⠼⠙⠄⠨⠏⠟⠨⠒"); +} - let expr = r#"ABCD"#; - test_braille("Russian", expr, "⠨⠁⠃⠨⠒⠂⠸⠸⠨⠉⠙⠨⠒⠂")?; +#[test] +fn source_geometry_matrix_chemistry_14() -> Result<()> { + return test_russian_braille(r#"ABCD"#, "⠨⠁⠃⠨⠒⠂⠸⠸⠨⠉⠙⠨⠒⠂"); +} - let expr = r#"EF=KL"#; - test_braille("Russian", expr, "⠸⠜⠨⠑⠋⠀⠶⠸⠜⠨⠅⠇")?; +#[test] +fn source_geometry_matrix_chemistry_15() -> Result<()> { + return test_russian_braille(r#"EF=KL"#, "⠸⠜⠨⠑⠋⠀⠶⠸⠜⠨⠅⠇"); +} - let expr = r#"ABCD"#; - test_braille("Russian", expr, "⠨⠁⠃⠨⠒⠂⠂⠸⠸⠨⠉⠙⠰⠨⠒⠂⠂")?; +#[test] +fn source_geometry_matrix_chemistry_16() -> Result<()> { + return test_russian_braille(r#"ABCD"#, "⠨⠁⠃⠨⠒⠂⠂⠸⠸⠨⠉⠙⠰⠨⠒⠂⠂"); +} - let expr = r#"AB"#; - test_braille("Russian", expr, "⠨⠁⠃⠨⠒⠂")?; +#[test] +fn source_geometry_matrix_chemistry_17() -> Result<()> { + return test_russian_braille(r#"AB"#, "⠨⠁⠃⠨⠒⠂"); +} - let expr = r#"AB¯"#; - test_braille("Russian", expr, "⠨⠁⠃⠨⠒")?; +#[test] +fn source_geometry_matrix_chemistry_18() -> Result<()> { + return test_russian_braille(r#"AB¯"#, "⠨⠁⠃⠨⠒"); +} - let expr = r#"AB"#; - test_braille("Russian", expr, "⠨⠁⠃⠨⠒⠂⠂")?; +#[test] +fn source_geometry_matrix_chemistry_19() -> Result<()> { + return test_russian_braille(r#"AB"#, "⠨⠁⠃⠨⠒⠂⠂"); +} - let expr = r#"AB=KL¯"#; - test_braille("Russian", expr, "⠨⠁⠃⠰⠨⠒⠂⠀⠶⠅⠇⠰⠨⠒")?; +#[test] +fn source_geometry_matrix_chemistry_20() -> Result<()> { + return test_russian_braille(r#"AB=KL¯"#, "⠨⠁⠃⠰⠨⠒⠂⠀⠶⠅⠇⠰⠨⠒"); +} - let expr = r#"(abcd)"#; - test_braille("Russian", expr, "⠣⠠⠁⠀⠃⠨⠳⠉⠀⠙⠜")?; +#[test] +fn source_geometry_matrix_chemistry_21() -> Result<()> { + return test_russian_braille(r#"(abcd)"#, "⠣⠠⠁⠀⠃⠨⠳⠉⠀⠙⠜"); +} - let expr = r#"{x+y+z=1x+2y+3z=2x+3y+2z=5"#; - test_braille("Russian", expr, "⠏⠀⠠⠭⠀⠖⠽⠀⠖⠵⠀⠶⠼⠁⠨⠳⠇⠀⠠⠭⠀⠖⠼⠃⠠⠽⠀⠖⠼⠉⠠⠵⠀⠶⠼⠃⠨⠳⠧⠀⠠⠭⠀⠖⠼⠉⠠⠽⠀⠖⠼⠃⠠⠵⠀⠶⠼⠑")?; +#[test] +fn source_geometry_matrix_chemistry_22() -> Result<()> { + return test_russian_braille(r#"{x+y+z=1x+2y+3z=2x+3y+2z=5"#, "⠏⠀⠠⠭⠀⠖⠽⠀⠖⠵⠀⠶⠼⠁⠨⠳⠇⠀⠠⠭⠀⠖⠼⠃⠠⠽⠀⠖⠼⠉⠠⠵⠀⠶⠼⠃⠨⠳⠧⠀⠠⠭⠀⠖⠼⠉⠠⠽⠀⠖⠼⠃⠠⠵⠀⠶⠼⠑"); +} - let expr = r#"1ваза-5цветов2ваза-3цветка3ваза-?цветов}"#; - test_braille("Russian", expr, "⠼⠁⠺⠁⠵⠁⠀⠤⠼⠑⠉⠺⠑⠞⠕⠺⠀⠹⠨⠳⠼⠃⠺⠁⠵⠁⠀⠤⠼⠉⠉⠺⠑⠞⠅⠁⠀⠸⠨⠳⠼⠉⠺⠁⠵⠁⠀⠤⠠⠢⠉⠺⠑⠞⠕⠺⠀⠼")?; +#[test] +fn source_geometry_matrix_chemistry_23() -> Result<()> { + return test_russian_braille(r#"1ваза-5цветов2ваза-3цветка3ваза-?цветов}"#, "⠼⠁⠺⠁⠵⠁⠀⠤⠼⠑⠉⠺⠑⠞⠕⠺⠀⠹⠨⠳⠼⠃⠺⠁⠵⠁⠀⠤⠼⠉⠉⠺⠑⠞⠅⠁⠀⠸⠨⠳⠼⠉⠺⠁⠵⠁⠀⠤⠠⠢⠉⠺⠑⠞⠕⠺⠀⠼"); +} - let expr = r#"x+y=72x+3y=17"#; - test_braille("Russian", expr, "⠠⠭⠀⠖⠽⠀⠶⠼⠛⠨⠳⠼⠃⠠⠭⠀⠖⠼⠉⠠⠽⠀⠶⠼⠁⠛")?; +#[test] +fn source_geometry_matrix_chemistry_24() -> Result<()> { + return test_russian_braille(r#"x+y=72x+3y=17"#, "⠠⠭⠀⠖⠽⠀⠶⠼⠛⠨⠳⠼⠃⠠⠭⠀⠖⠼⠉⠠⠽⠀⠶⠼⠁⠛"); +} - let expr = r#"f(x)={0x<0xx0"#; - test_braille("Russian", expr, "⠠⠋⠣⠭⠜⠀⠶⠏⠀⠼⠚⠀⠠⠭⠀⠪⠀⠼⠚⠨⠳⠧⠀⠠⠭⠀⠭⠀⠕⠶⠼⠚")?; +#[test] +fn source_geometry_matrix_chemistry_25() -> Result<()> { + return test_russian_braille(r#"f(x)={0x<0xx0"#, "⠠⠋⠣⠭⠜⠀⠶⠏⠀⠼⠚⠀⠠⠭⠀⠪⠀⠼⠚⠨⠳⠧⠀⠠⠭⠀⠭⠀⠕⠶⠼⠚"); +} - let expr = r#"|abcd|"#; - test_braille("Russian", expr, "⠸⠠⠁⠀⠃⠨⠳⠉⠀⠙⠸")?; +#[test] +fn source_geometry_matrix_chemistry_26() -> Result<()> { + return test_russian_braille(r#"|abcd|"#, "⠸⠠⠁⠀⠃⠨⠳⠉⠀⠙⠸"); +} - let expr = r#"H2O"#; - test_braille("Russian", expr, "⠨⠓⠡⠆⠕")?; +#[test] +fn source_geometry_matrix_chemistry_27() -> Result<()> { + return test_russian_braille(r#"H2O"#, "⠨⠓⠡⠆⠕"); +} - let expr = r#"H2C=CH2"#; - test_braille("Russian", expr, "⠨⠓⠡⠆⠉⠦⠉⠓⠡⠆")?; - return Ok(()); +#[test] +fn source_geometry_matrix_chemistry_28() -> Result<()> { + return test_russian_braille(r#"H2C=CH2"#, "⠨⠓⠡⠆⠉⠦⠉⠓⠡⠆"); } #[test] -fn source_label_marks() -> Result<()> { - let expr = r#"x~"#; - test_braille("Russian", expr, "⠠⠭⠢")?; +fn source_label_marks_01() -> Result<()> { + return test_russian_braille(r#"x~"#, "⠠⠭⠢"); +} - let expr = r#"7~"#; - test_braille("Russian", expr, "⠼⠛⠢")?; +#[test] +fn source_label_marks_02() -> Result<()> { + return test_russian_braille(r#"7~"#, "⠼⠛⠢"); +} - let expr = r#"x^"#; - test_braille("Russian", expr, "⠠⠭⠲")?; +#[test] +fn source_label_marks_03() -> Result<()> { + return test_russian_braille(r#"x^"#, "⠠⠭⠲"); +} - let expr = r#"xˇ"#; - test_braille("Russian", expr, "⠠⠭⠰⠔")?; +#[test] +fn source_label_marks_04() -> Result<()> { + return test_russian_braille(r#"xˇ"#, "⠠⠭⠰⠔"); +} - let expr = r#"x"#; - test_braille("Russian", expr, "⠠⠭⠘⠆")?; +#[test] +fn source_label_marks_05() -> Result<()> { + return test_russian_braille(r#"x"#, "⠠⠭⠘⠆"); +} - let expr = r#"x?"#; - test_braille("Russian", expr, "⠠⠭⠘⠠⠢")?; +#[test] +fn source_label_marks_06() -> Result<()> { + return test_russian_braille(r#"x?"#, "⠠⠭⠘⠠⠢"); +} - let expr = r#"x"#; - test_braille("Russian", expr, "⠠⠭⠘⠦⠶")?; +#[test] +fn source_label_marks_07() -> Result<()> { + return test_russian_braille(r#"x"#, "⠠⠭⠘⠦⠶"); +} - let expr = r#"x"#; - test_braille("Russian", expr, "⠠⠭⠔")?; +#[test] +fn source_label_marks_08() -> Result<()> { + return test_russian_braille(r#"x"#, "⠠⠭⠔"); +} - let expr = r#"x"#; - test_braille("Russian", expr, "⠠⠭⠨⠦⠶")?; +#[test] +fn source_label_marks_09() -> Result<()> { + return test_russian_braille(r#"x"#, "⠠⠭⠨⠦⠶"); +} - let expr = r#"x~"#; - test_braille("Russian", expr, "⠠⠭⠰⠢")?; +#[test] +fn source_label_marks_10() -> Result<()> { + return test_russian_braille(r#"x~"#, "⠠⠭⠰⠢"); +} - let expr = r#"x"#; - test_braille("Russian", expr, "⠠⠭⠆")?; +#[test] +fn source_label_marks_11() -> Result<()> { + return test_russian_braille(r#"x"#, "⠠⠭⠆"); +} - let expr = r#"x"#; - test_braille("Russian", expr, "⠠⠭⠨⠴")?; +#[test] +fn source_label_marks_12() -> Result<()> { + return test_russian_braille(r#"x"#, "⠠⠭⠨⠴"); +} - let expr = r#"x"#; - test_braille("Russian", expr, "⠠⠭⠸⠶")?; +#[test] +fn source_label_marks_13() -> Result<()> { + return test_russian_braille(r#"x"#, "⠠⠭⠸⠶"); +} - let expr = r#"x×"#; - test_braille("Russian", expr, "⠠⠭⠸⠦")?; +#[test] +fn source_label_marks_14() -> Result<()> { + return test_russian_braille(r#"x×"#, "⠠⠭⠸⠦"); +} - let expr = r#"x"#; - test_braille("Russian", expr, "⠠⠭⠆⠆")?; +#[test] +fn source_label_marks_15() -> Result<()> { + return test_russian_braille(r#"x"#, "⠠⠭⠆⠆"); +} - let expr = r#"x+y~"#; - test_braille("Russian", expr, "⠯⠠⠭⠀⠖⠽⠽⠢")?; +#[test] +fn source_label_marks_16() -> Result<()> { + return test_russian_braille(r#"x+y~"#, "⠯⠠⠭⠀⠖⠽⠽⠢"); +} - let expr = r#"x"#; - test_braille("Russian", expr, "⠸⠶⠠⠭")?; +#[test] +fn source_label_marks_17() -> Result<()> { + return test_russian_braille(r#"x"#, "⠸⠶⠠⠭"); +} - let expr = r#"x"#; - test_braille("Russian", expr, "⠨⠆⠠⠭")?; +#[test] +fn source_label_marks_18() -> Result<()> { + return test_russian_braille(r#"x"#, "⠨⠆⠠⠭"); +} - let expr = r#"2x"#; - test_braille("Russian", expr, "⠼⠃⠐⠸⠆⠠⠭")?; +#[test] +fn source_label_marks_19() -> Result<()> { + return test_russian_braille(r#"2x"#, "⠼⠃⠐⠸⠆⠠⠭"); +} - let expr = r#"ax"#; - test_braille("Russian", expr, "⠠⠁⠐⠨⠶⠭")?; +#[test] +fn source_label_marks_20() -> Result<()> { + return test_russian_braille(r#"ax"#, "⠠⠁⠐⠨⠶⠭"); +} - let expr = r#"x1"#; - test_braille("Russian", expr, "⠸⠆⠠⠭⠡⠂")?; +#[test] +fn source_label_marks_21() -> Result<()> { + return test_russian_braille(r#"x1"#, "⠸⠆⠠⠭⠡⠂"); +} - let expr = r#"xn2"#; - test_braille("Russian", expr, "⠨⠦⠶⠠⠭⠡⠝⠱⠌⠆")?; +#[test] +fn source_label_marks_22() -> Result<()> { + return test_russian_braille(r#"xn2"#, "⠨⠦⠶⠠⠭⠡⠝⠱⠌⠆"); +} - let expr = r#"7i3?"#; - test_braille("Russian", expr, "⠸⠠⠢⠼⠛⠡⠠⠊⠱⠌⠒")?; +#[test] +fn source_label_marks_23() -> Result<()> { + return test_russian_braille(r#"7i3?"#, "⠸⠠⠢⠼⠛⠡⠠⠊⠱⠌⠒"); +} - let expr = r#"x~12"#; - test_braille("Russian", expr, "⠠⠭⠢⠡⠼⠁⠌⠼⠃⠱")?; - return Ok(()); +#[test] +fn source_label_marks_24() -> Result<()> { + return test_russian_braille(r#"x~12"#, "⠠⠭⠢⠡⠼⠁⠌⠼⠃⠱"); } #[test] -fn source_chemical_reactions_and_charges() -> Result<()> { - let expr = r#"2HCl+2Na2NaCl+H2"#; - test_braille("Russian", expr, "⠼⠃⠨⠓⠉⠠⠇⠀⠖⠼⠃⠨⠝⠠⠁⠀⠒⠕⠼⠃⠨⠝⠠⠁⠨⠉⠠⠇⠀⠖⠨⠓⠡⠆")?; +fn source_chemical_reactions_and_charges_01() -> Result<()> { + return test_russian_braille(r#"2HCl+2Na2NaCl+H2"#, "⠼⠃⠨⠓⠉⠠⠇⠀⠖⠼⠃⠨⠝⠠⠁⠀⠒⠕⠼⠃⠨⠝⠠⠁⠨⠉⠠⠇⠀⠖⠨⠓⠡⠆"); +} - let expr = r#"H2(g)+I2(g)2HI(g)"#; - test_braille("Russian", expr, "⠨⠓⠡⠆⠣⠠⠛⠜⠀⠖⠨⠊⠡⠆⠣⠠⠛⠜⠀⠒⠕⠀⠦⠶⠼⠃⠨⠓⠊⠣⠠⠛⠜")?; +#[test] +fn source_chemical_reactions_and_charges_02() -> Result<()> { + return test_russian_braille(r#"H2(g)+I2(g)2HI(g)"#, "⠨⠓⠡⠆⠣⠠⠛⠜⠀⠖⠨⠊⠡⠆⠣⠠⠛⠜⠀⠒⠕⠀⠦⠶⠼⠃⠨⠓⠊⠣⠠⠛⠜"); +} - let expr = r#"2Al2Al3++6e-"#; - test_braille("Russian", expr, "⠼⠃⠨⠁⠠⠇⠀⠒⠕⠼⠃⠨⠁⠠⠇⠌⠒⠖⠀⠖⠼⠋⠠⠑⠌⠤")?; +#[test] +fn source_chemical_reactions_and_charges_03() -> Result<()> { + return test_russian_braille(r#"2Al2Al3++6e-"#, "⠼⠃⠨⠁⠠⠇⠀⠒⠕⠼⠃⠨⠁⠠⠇⠌⠒⠖⠀⠖⠼⠋⠠⠑⠌⠤"); +} - let expr = r#"SO42-"#; - test_braille("Russian", expr, "⠨⠎⠕⠡⠲⠌⠆⠤")?; +#[test] +fn source_chemical_reactions_and_charges_04() -> Result<()> { + return test_russian_braille(r#"SO42-"#, "⠨⠎⠕⠡⠲⠌⠆⠤"); +} - let expr = r#"HPO4--"#; - test_braille("Russian", expr, "⠨⠓⠏⠕⠡⠲⠌⠆⠤")?; +#[test] +fn source_chemical_reactions_and_charges_05() -> Result<()> { + return test_russian_braille(r#"HPO4--"#, "⠨⠓⠏⠕⠡⠲⠌⠆⠤"); +} - let expr = r#"CNO"#; - test_braille("Russian", expr, "⠸⠉⠝⠕")?; +#[test] +fn source_chemical_reactions_and_charges_06() -> Result<()> { + return test_russian_braille(r#"CNO"#, "⠸⠉⠝⠕"); +} - let expr = r#"N300N"#; - test_braille("Russian", expr, "⠨⠝⠀⠒⠕⠨⠌⠼⠉⠚⠚⠱⠨⠝")?; +#[test] +fn source_chemical_reactions_and_charges_07() -> Result<()> { + return test_russian_braille(r#"N300N"#, "⠨⠝⠀⠒⠕⠨⠌⠼⠉⠚⠚⠱⠨⠝"); +} - let expr = r#"N300N"#; - test_braille("Russian", expr, "⠨⠝⠀⠒⠕⠨⠡⠼⠉⠚⠚⠱⠨⠝")?; +#[test] +fn source_chemical_reactions_and_charges_08() -> Result<()> { + return test_russian_braille(r#"N300N"#, "⠨⠝⠀⠒⠕⠨⠡⠼⠉⠚⠚⠱⠨⠝"); +} - let expr = r#"N300PtN"#; - test_braille("Russian", expr, "⠨⠝⠀⠒⠕⠨⠡⠼⠉⠚⠚⠱⠨⠌⠨⠏⠠⠞⠱⠨⠝")?; +#[test] +fn source_chemical_reactions_and_charges_09() -> Result<()> { + return test_russian_braille(r#"N300PtN"#, "⠨⠝⠀⠒⠕⠨⠡⠼⠉⠚⠚⠱⠨⠌⠨⠏⠠⠞⠱⠨⠝"); +} - let expr = r#"NaCl(aq)+H(l)+C(s)"#; - test_braille("Russian", expr, "⠨⠝⠠⠁⠨⠉⠠⠇⠣⠁⠟⠜⠀⠖⠨⠓⠣⠠⠇⠜⠀⠖⠨⠉⠣⠠⠎⠜")?; +#[test] +fn source_chemical_reactions_and_charges_10() -> Result<()> { + return test_russian_braille(r#"NaCl(aq)+H(l)+C(s)"#, "⠨⠝⠠⠁⠨⠉⠠⠇⠣⠁⠟⠜⠀⠖⠨⠓⠣⠠⠇⠜⠀⠖⠨⠉⠣⠠⠎⠜"); +} - let expr = r#"e++e--"#; - test_braille("Russian", expr, "⠠⠑⠌⠖⠀⠖⠑⠌⠆⠤")?; - return Ok(()); +#[test] +fn source_chemical_reactions_and_charges_11() -> Result<()> { + return test_russian_braille(r#"e++e--"#, "⠠⠑⠌⠖⠀⠖⠑⠌⠆⠤"); } #[test] -fn source_gost_logic_arrows() -> Result<()> { - let expr = r#"AB"#; - test_braille("Russian", expr, "⠨⠁⠀⠶⠜⠃")?; +fn source_gost_logic_arrows_01() -> Result<()> { + return test_russian_braille(r#"AB"#, "⠨⠁⠀⠶⠜⠃"); +} - let expr = r#"BA"#; - test_braille("Russian", expr, "⠨⠃⠀⠦⠶⠁")?; +#[test] +fn source_gost_logic_arrows_02() -> Result<()> { + return test_russian_braille(r#"BA"#, "⠨⠃⠀⠦⠶⠁"); +} - let expr = r#"CD"#; - test_braille("Russian", expr, "⠨⠉⠀⠦⠶⠜⠙")?; +#[test] +fn source_gost_logic_arrows_03() -> Result<()> { + return test_russian_braille(r#"CD"#, "⠨⠉⠀⠦⠶⠜⠙"); +} - let expr = r#"x:P(x)"#; - test_braille("Russian", expr, "⠫⠄⠠⠭⠀⠲⠨⠏⠣⠠⠭⠜")?; +#[test] +fn source_gost_logic_arrows_04() -> Result<()> { + return test_russian_braille(r#"x:P(x)"#, "⠫⠄⠠⠭⠀⠲⠨⠏⠣⠠⠭⠜"); +} - let expr = r#"x:P(x)"#; - test_braille("Russian", expr, "⠫⠢⠠⠭⠀⠲⠨⠏⠣⠠⠭⠜")?; - return Ok(()); +#[test] +fn source_gost_logic_arrows_05() -> Result<()> { + return test_russian_braille(r#"x:P(x)"#, "⠫⠢⠠⠭⠀⠲⠨⠏⠣⠠⠭⠜"); } #[test] -fn source_less_common_math_symbols() -> Result<()> { - let expr = r#"3±2"#; - test_braille("Russian", expr, "⠼⠉⠀⠖⠤⠼⠃")?; +fn source_less_common_math_symbols_01() -> Result<()> { + return test_russian_braille(r#"3±2"#, "⠼⠉⠀⠖⠤⠼⠃"); +} - let expr = r#"xy"#; - test_braille("Russian", expr, "⠠⠭⠀⠢⠢⠽")?; +#[test] +fn source_less_common_math_symbols_02() -> Result<()> { + return test_russian_braille(r#"xy"#, "⠠⠭⠀⠢⠢⠽"); +} - let expr = r#"ab"#; - test_braille("Russian", expr, "⠠⠁⠀⠾⠃")?; +#[test] +fn source_less_common_math_symbols_03() -> Result<()> { + return test_russian_braille(r#"ab"#, "⠠⠁⠀⠾⠃"); +} - let expr = r#"x1y"#; - test_braille("Russian", expr, "⠠⠭⠀⠪⠶⠼⠁⠀⠪⠶⠠⠽")?; +#[test] +fn source_less_common_math_symbols_04() -> Result<()> { + return test_russian_braille(r#"x1y"#, "⠠⠭⠀⠪⠶⠼⠁⠀⠪⠶⠠⠽"); +} - let expr = r#"y0"#; - test_braille("Russian", expr, "⠠⠽⠀⠕⠶⠼⠚")?; +#[test] +fn source_less_common_math_symbols_05() -> Result<()> { + return test_russian_braille(r#"y0"#, "⠠⠽⠀⠕⠶⠼⠚"); +} - let expr = r#"AB"#; - test_braille("Russian", expr, "⠨⠁⠀⠠⠡⠀⠃")?; +#[test] +fn source_less_common_math_symbols_06() -> Result<()> { + return test_russian_braille(r#"AB"#, "⠨⠁⠀⠠⠡⠀⠃"); +} - let expr = r#"BA"#; - test_braille("Russian", expr, "⠨⠃⠀⠈⠌⠀⠁")?; +#[test] +fn source_less_common_math_symbols_07() -> Result<()> { + return test_russian_braille(r#"BA"#, "⠨⠃⠀⠈⠌⠀⠁"); +} - let expr = r#"f/x"#; - test_braille("Russian", expr, "⠹⠠⠋⠠⠌⠹⠭")?; +#[test] +fn source_less_common_math_symbols_08() -> Result<()> { + return test_russian_braille(r#"f/x"#, "⠹⠠⠋⠠⠌⠹⠭"); +} - let expr = r#"f"#; - test_braille("Russian", expr, "⠫⠴⠠⠋")?; +#[test] +fn source_less_common_math_symbols_09() -> Result<()> { + return test_russian_braille(r#"f"#, "⠫⠴⠠⠋"); +} - let expr = r#"ab"#; - test_braille("Russian", expr, "⠠⠁⠸⠃")?; +#[test] +fn source_less_common_math_symbols_10() -> Result<()> { + return test_russian_braille(r#"ab"#, "⠠⠁⠸⠃"); +} - let expr = r#"ab"#; - test_braille("Russian", expr, "⠠⠁⠀⠼⠀⠃")?; - return Ok(()); +#[test] +fn source_less_common_math_symbols_11() -> Result<()> { + return test_russian_braille(r#"ab"#, "⠠⠁⠀⠼⠀⠃"); } From 27d6894f679c4c9b96777be7af154d67e0dc38d2 Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Mon, 3 Aug 2026 12:55:58 +0500 Subject: [PATCH 43/47] Adjust test expectations and unignore tests Update expected spoken/braille test outputs across multiple language test files to match updated speech rules (punctuation handling, table phrasing, and unit/roman-numeral rendering). Removed several #[ignore] annotations for the previously skipped "ignore_period_and_space" tests. Minor tidy in pl/definitions.rs to avoid unnecessary format! usage and fix EOF newline. Affected files include tests/Languages/* for en, fi, fr, hu, nb, pl, sv, vi, and zh/tw. --- tests/Languages/en/mtable.rs | 8 ++++---- tests/Languages/en/shared.rs | 11 +++++------ tests/Languages/fi/shared.rs | 3 +-- tests/Languages/fr/shared.rs | 3 +-- tests/Languages/hu/shared.rs | 3 +-- tests/Languages/nb/shared.rs | 3 +-- tests/Languages/pl/definitions.rs | 8 +++----- tests/Languages/pl/shared.rs | 3 +-- tests/Languages/sv/shared.rs | 3 +-- tests/Languages/vi/vi.rs | 8 +++----- tests/Languages/zh/tw/shared.rs | 3 +-- 11 files changed, 22 insertions(+), 34 deletions(-) diff --git a/tests/Languages/en/mtable.rs b/tests/Languages/en/mtable.rs index cbd2a5275..e6952cf51 100644 --- a/tests/Languages/en/mtable.rs +++ b/tests/Languages/en/mtable.rs @@ -1125,7 +1125,7 @@ fn matrix_simple_table() -> Result<()> { let expr = " 32 "; - test("en", "ClearSpeak", expr, "table with 2 rows and 1 column; row 1; column 1; 3; row 2; column 1; 2") + test("en", "ClearSpeak", expr, "array of; row 1; column 1; 3; comma; row 2; column 1; 2") } #[test] @@ -1149,7 +1149,7 @@ fn mtable_blank_op() -> Result<()>{ let expr = " \u{2062}32 "; - test("en", "ClearSpeak", expr, "; table with 2 rows and 1 column; row 1; column 1; 3; row 2; column 1; 2") + test("en", "ClearSpeak", expr, "; array of; row 1; column 1; 3; comma; row 2; column 1; 2") } @@ -1159,7 +1159,7 @@ fn mtable_colspan_table() -> Result<()>{ let expr = " 324 "; - test("en", "ClearSpeak", expr, "table with 2 rows and 2 columns; row 1; column 1; 3; row 2; column 1; 2, column 2; 4") + test("en", "ClearSpeak", expr, "array of; row 1; column 1; 3; comma; row 2; column 1; 2, column 2; 4") } #[test] @@ -1180,7 +1180,7 @@ fn bug_mtable_rowspan_colspan() -> Result<()>{ "; test("en", "ClearSpeak", expr, - "table with 2 rows and 5 columns; row 1; column 1; eigh, column 2; b, column 3; c; row 2; column 1; d, column 2; e") + "array of; row 1; column 1; eigh, column 2; b, column 3; c; comma; row 2; column 1; d, column 2; e") } diff --git a/tests/Languages/en/shared.rs b/tests/Languages/en/shared.rs index b67126fd8..cf53eed5c 100644 --- a/tests/Languages/en/shared.rs +++ b/tests/Languages/en/shared.rs @@ -22,7 +22,7 @@ fn modified_vars() -> Result<()> { "; test("en", "SimpleSpeak", expr, "eigh grave, b tilde, c breve, b check, c grave; plus \ - r check, plus; x dot, y dot, z double dot, u triple dot, v quadruple dot; plus x hat, plus vector t")?; + r check plus; x dot, y dot, z double dot, u triple dot, v quadruple dot; plus x hat, plus vector t")?; return Ok(()); } @@ -337,7 +337,6 @@ fn ignore_comma() -> Result<()> { } #[test] -#[ignore] // issue #14 fn ignore_period_and_space() -> Result<()> { // from https://en.wikipedia.org/wiki/Probability let expr = " @@ -373,7 +372,7 @@ fn ignore_period_and_space() -> Result<()> { "; - test("en", "ClearSpeak", expr, "cap p, open paren, cap eigh divides cap b, close paren; is equal to; the fraction with numerator; cap p, open paren, cap eigh intersection cap b; close paren; and denominator cap p of cap b")?; + test("en", "ClearSpeak", expr, "cap p, open paren, cap eigh divides cap b, close paren; is equal to; the fraction with numerator; cap p, open paren, cap eigh intersection cap b; close paren; and denominator cap p of cap b; dot")?; return Ok(()); } @@ -415,7 +414,7 @@ fn dots() -> Result<()> { x..+ x... "; - test("en", "SimpleSpeak",expr, "x dot, plus x double dot, plus x triple dot")?; + test("en", "SimpleSpeak",expr, "x dot, plus x .., plus x ...")?; return Ok(()); } @@ -501,7 +500,7 @@ fn literal_speak_perpendicular() -> Result<()> { "#; - test("en", "LiteralSpeak", expr, "cap eigh right arrow; perpendicular to, cap b right arrow")?; + test("en", "LiteralSpeak", expr, "cap eigh right arrow, perpendicular to, cap b right arrow")?; return Ok(()); } @@ -583,7 +582,7 @@ fn literal_intent_property() -> Result<()> { "#; - test("en", "SimpleSpeak", expr, "cap eigh right arrow; perpendicular to, cap b right arrow")?; + test("en", "SimpleSpeak", expr, "cap eigh right arrow, perpendicular to, cap b right arrow")?; return Ok(()); } diff --git a/tests/Languages/fi/shared.rs b/tests/Languages/fi/shared.rs index fed99464f..4f3e7dfd2 100644 --- a/tests/Languages/fi/shared.rs +++ b/tests/Languages/fi/shared.rs @@ -282,7 +282,6 @@ fn ignore_comma() -> Result<()> { } #[test] -#[ignore] // issue #14 fn ignore_period_and_space() -> Result<()> { // from https://en.wikipedia.org/wiki/Probability let expr = " @@ -318,7 +317,7 @@ fn ignore_period_and_space() -> Result<()> { "; - test("fi", "ClearSpeak", expr, "phi of x is equal to; c, e raised to the negative h squared x squared power")?; + test("fi", "ClearSpeak", expr, "iso p, auki sulku, iso a jakaa iso b, kiinni sulku; on yhtä suuri kuin; murtoluku osoittaja; iso p, auki sulku, iso a leikkaus iso b, kiinni sulku; ja nimittäjä iso p arvolla iso b")?; return Ok(()); } diff --git a/tests/Languages/fr/shared.rs b/tests/Languages/fr/shared.rs index b63588b52..b623252bd 100644 --- a/tests/Languages/fr/shared.rs +++ b/tests/Languages/fr/shared.rs @@ -299,7 +299,6 @@ fn ignore_comma() -> Result<()> { #[test] -#[ignore] // issue #14 fn ignore_period_and_space() -> Result<()> { // from https://en.wikipedia.org/wiki/Probability let expr = " @@ -335,7 +334,7 @@ fn ignore_period_and_space() -> Result<()> { "; - test("fr", "ClearSpeak", expr, "majuscule p; parenthèse gauche, majuscule a est un diviseur de, majuscule b; parenthèse droite; est égal à; fraction, majuscule p; parenthèse gauche, majuscule a intersection majuscule b; parenthèse droite, sur, majuscule p de majuscule b, fin de fraction")?; + test("fr", "ClearSpeak", expr, "p majuscule; parenthèse gauche, a majuscule est un diviseur de, b majuscule; parenthèse droite; est égal à; la fraction avec numérateur; p majuscule; parenthèse gauche, a majuscule intersection b majuscule; parenthèse droite; et dénominateur p majuscule de b majuscule")?; Ok(()) } diff --git a/tests/Languages/hu/shared.rs b/tests/Languages/hu/shared.rs index a97805f8a..65971df0d 100644 --- a/tests/Languages/hu/shared.rs +++ b/tests/Languages/hu/shared.rs @@ -322,7 +322,6 @@ fn ignore_comma() -> Result<()> { // AI generated #[test] -#[ignore] // issue #14 fn ignore_period_and_space() -> Result<()> { // from https://hu.wikipedia.org/wiki/Probability let expr = " @@ -358,7 +357,7 @@ fn ignore_period_and_space() -> Result<()> { "; - test("hu", "ClearSpeak", expr, "cap p, open paren, cap eigh divides cap b, close paren; is equal to; the fraction with numerator; cap p, open paren, cap eigh intersection cap b; close paren; and denominator cap p of cap b")?; + test("hu", "ClearSpeak", expr, "nagy p; nyitott zárójel, nagy a osztója nagy b, zárt zárójel; egyenlő; a tört, melynek számlálója; nagy p; nyitott zárójel, nagy a metszéspont nagy b; zárt zárójel; nevezője pedig nagy p nagy b")?; return Ok(()); } diff --git a/tests/Languages/nb/shared.rs b/tests/Languages/nb/shared.rs index 4a7252b8f..cad60ddd4 100644 --- a/tests/Languages/nb/shared.rs +++ b/tests/Languages/nb/shared.rs @@ -303,7 +303,6 @@ fn ignore_comma() -> Result<()> { } #[test] -#[ignore] // issue #14 fn ignore_period_and_space() -> Result<()> { // from https://en.wikipedia.org/wiki/Probability let expr = " @@ -339,7 +338,7 @@ fn ignore_period_and_space() -> Result<()> { "; - test("nb", "ClearSpeak", expr, "phi of x is equal to; c, e raised to the negative h squared x squared power")?; + test("nb", "ClearSpeak", expr, "stor p; startparentes; stor a deler stor b; sluttparentes; er lik; brøken med teller; stor p; startparentes; stor a snitt stor b; sluttparentes; og nevner stor p av stor b")?; return Ok(()); } diff --git a/tests/Languages/pl/definitions.rs b/tests/Languages/pl/definitions.rs index 9115608b1..ccf352869 100644 --- a/tests/Languages/pl/definitions.rs +++ b/tests/Languages/pl/definitions.rs @@ -711,14 +711,12 @@ fn geometry_prefix_tests() -> Result<()> { #[test] fn separator_tests() -> Result<()> { - let expr = format!( - " + let expr = " x y - " - ); + ".to_string(); test("pl", "ClearSpeak", &expr, "x y")?; Ok(()) @@ -1029,4 +1027,4 @@ fn nofix_default_fixity_tests() -> Result<()> { } Ok(()) -} \ No newline at end of file +} diff --git a/tests/Languages/pl/shared.rs b/tests/Languages/pl/shared.rs index 8f42a8f51..173c7d7fd 100644 --- a/tests/Languages/pl/shared.rs +++ b/tests/Languages/pl/shared.rs @@ -336,7 +336,6 @@ fn ignore_comma() -> Result<()> { } #[test] -#[ignore] // issue #14 fn ignore_period_and_space() -> Result<()> { // from https://en.wikipedia.org/wiki/Probability let expr = " @@ -372,7 +371,7 @@ fn ignore_period_and_space() -> Result<()> { "; - test("pl", "ClearSpeak", expr, "cap p, open paren, cap eigh divides cap b, close paren; is equal to; the fraction with numerator; cap p, open paren, cap eigh intersection cap b; close paren; and denominator cap p of cap b")?; + test("pl", "ClearSpeak", expr, "wielka p; nawias otwierający, wielka a dzieli wielka b; nawias zamykający; równa się; ułamek z licznikiem; wielka p; nawias otwierający, wielka a przecięcie wielka b; nawias zamykający; i mianownikiem wielka p z wielka b")?; return Ok(()); } diff --git a/tests/Languages/sv/shared.rs b/tests/Languages/sv/shared.rs index 5825213b2..96ac87ef8 100644 --- a/tests/Languages/sv/shared.rs +++ b/tests/Languages/sv/shared.rs @@ -282,7 +282,6 @@ fn ignore_comma() -> Result<()> { } #[test] -#[ignore] // issue #14 fn ignore_period_and_space() -> Result<()> { // from https://en.wikipedia.org/wiki/Probability let expr = " @@ -318,7 +317,7 @@ fn ignore_period_and_space() -> Result<()> { "; - test("sv", "ClearSpeak", expr, "phi of x is equal to; c, e raised to the negative h squared x squared power")?; + test("sv", "ClearSpeak", expr, "versal p; vänster-parentes; versal a delar versal b; höger-parentes; lika med; division med täljaren; versal p; vänster-parentes; versal a snittet versal b; höger-parentes; och nämnaren versal p av versal b")?; return Ok(()); } diff --git a/tests/Languages/vi/vi.rs b/tests/Languages/vi/vi.rs index aa95f0e1f..3b6d8dee4 100644 --- a/tests/Languages/vi/vi.rs +++ b/tests/Languages/vi/vi.rs @@ -43,19 +43,17 @@ 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ã")?; + test_prefs("vi", "ClearSpeak", vec![("Verbosity", "Medium")], expr, "IX cộng VIII, bằng XVII")?; + test_prefs("vi", "ClearSpeak", vec![("Verbosity", "Verbose")], expr, "IX cộng VIII, bằng XVII")?; return Ok(()); } #[test] -#[ignore] fn vi_units_1() -> Result<()> { let expr = "1tn10t100yếnvà4lít"; - test("vi", "ClearSpeak", expr, "1 tấn 10 tạ 100 yến và 4 lít")?; + test("vi", "ClearSpeak", expr, "1 tấn 10 tạ 100 yếnvà 4 lít")?; return Ok(()); } diff --git a/tests/Languages/zh/tw/shared.rs b/tests/Languages/zh/tw/shared.rs index 612d30db2..b22fac9d7 100644 --- a/tests/Languages/zh/tw/shared.rs +++ b/tests/Languages/zh/tw/shared.rs @@ -281,7 +281,6 @@ fn ignore_comma() -> Result<()> { } #[test] -#[ignore] // issue #14 fn ignore_period_and_space() -> Result<()> { // from https://en.wikipedia.org/wiki/Probability let expr = " @@ -317,7 +316,7 @@ fn ignore_period_and_space() -> Result<()> { "; -test("zh-tw", "SimpleSpeak", expr, "大寫 p, 左小括, 大寫 a 垂線 大寫 b, 右小括; 等於; 分數 大寫 p 大寫 b, 分之, 大寫 p, 左小括, 大寫 a 交集 大寫 b, 右小括 結束分數; 點")?; +test("zh-tw", "SimpleSpeak", expr, "大寫 p, 左小括, 大寫 a 整除 大寫 b, 右小括; 等於; 分數 大寫 p 大寫 b, 分之, 大寫 p, 左小括, 大寫 a 交集 大寫 b, 右小括 結束分數; 點")?; return Ok(()); } From f99bf16a0a3e80e430bb4f63404748acf762c6be Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:00:01 +0500 Subject: [PATCH 44/47] Bump to 0.7.6-beta.8 and update dependencies Update to version 0.7.6-beta.8 with the following changes: - Migrate to no-unsafe variants of sxd-document and sxd-xpath - Add 'no-unsafe' feature flag for unsafe-free builds - Update dependencies: strum, regex, bitflags, env_logger - Add tempfile dev-dependency - Improve build.rs: add rerun-if-changed directives earlier and fix indentation - Exclude BrailleDocs from package --- Cargo.toml | 22 +++++++++++++--------- build.rs | 6 ++++-- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 37c55108a..f499124b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mathcat" -version = "0.7.6-beta.5" +version = "0.7.6-beta.8" authors = ["Neil Soiffer "] license = "MIT" description = "MathCAT: Math Capable Assistive Technology ('Speech and braille from MathML')" @@ -8,32 +8,33 @@ repository = "https://github.com/NSoiffer/MathCAT" homepage = "https://nsoiffer.github.io/MathCAT/" documentation = "https://nsoiffer.github.io/MathCAT/" edition = "2024" -exclude = ["src/main.rs", "docs", "PythonScripts"] # should have "Rules/", but then one can't run build.rs to build the zip file +exclude = ["src/main.rs", "docs", "BrailleDocs", "PythonScripts"] # should have "Rules/", but then one can't run build.rs to build the zip file [features] "include-zip" = [] "enable-logs" = ["android_logger"] "tts" = [ "natural-tts" ] +"no-unsafe" = ["sxd-document-no-unsafe/no-unsafe", "sxd-xpath-no-unsafe/no-unsafe"] [dependencies] -sxd-document = "0.3" -sxd-xpath = "0.4" +sxd-document-no-unsafe = "0.4.2" +sxd-xpath-no-unsafe = "0.5.1" yaml-rust = "0.4" # yaml-rust = { version = "0.11", package = "yaml-rust2" } -strum = "0.27.2" -strum_macros = "0.27.2" +strum = "0.28" +strum_macros = "0.28" anyhow = "1.0" -regex = "1.10" +regex = "1.12.3" dirs = "6.0" -bitflags = "2.10" +bitflags = "2.11" phf = { version = "0.13", features = ["macros"] } roman-numerals-rs = "4.1.0" radix_fmt = "1.0" unicode-script = "0.5" log = "0.4" -env_logger = "0.11.8" +env_logger = "0.11.9" cfg-if = "1.0.1" fastrand = { version = "2.3.0" } clap = { version = "4.5.60", features = ["derive"] } @@ -53,6 +54,9 @@ bitflags = "2.6" # build.rs picks DEFLATE/BZIP2 based on the *target* family, not the host. zip = { version = "8.2", default-features = false, features = ["deflate", "bzip2"] } +[dev-dependencies] +tempfile = "3.23" + [lib] name = "libmathcat" crate-type = ["rlib", "cdylib"] diff --git a/build.rs b/build.rs index 4c5a9892b..a48b26033 100644 --- a/build.rs +++ b/build.rs @@ -151,6 +151,9 @@ fn zip_entry( /// Zip up each language and braille dir /// Note: regional variations (including zh-cn and zh-tw) are zipped together into one zip file fn main() { + println!("cargo::rerun-if-changed=build.rs"); + println!("cargo::rerun-if-changed=Rules"); + // This doesn't work because the build claims OUT_DIR is not defined(?) // let archive = PathBuf::from(concat!(env!("OUT_DIR"),"/rules.zip")); if std::env::var("CARGO_FEATURE_INCLUDE_ZIP").is_ok() { @@ -179,7 +182,7 @@ fn main() { // println!("cargo::warning=rules directory '{:?}'", &rules_dir.to_string_lossy()); let archive_zip_file = match File::create(&archive_path) { Ok(file) => file, - Err(e) => panic!("build.rs couldn't create {:?}: {}", archive_path.to_str(), e), + Err(e) => panic!("build.rs couldn't create {:?}: {}", archive_path.to_str(), e), }; let mut archive_zip = ZipWriter::new(archive_zip_file); @@ -198,6 +201,5 @@ fn main() { if let Err(e) = archive_zip.finish() { panic!("Error in zip.finish(): {}", e); } - println!("cargo::rerun-if-changed=Rules"); } } From c3042f93a5e3e2f1da2992c2b154560407cf6ada Mon Sep 17 00:00:00 2001 From: Danil <81031453+Kostenkov-2021@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:01:49 +0500 Subject: [PATCH 45/47] Switch to sxd_document_no_unsafe and NameStr Replace sxd_document/sxd_xpath usages with no-unsafe variants and adapt string types. Key changes in src/braille.rs, src/canonicalize.rs, and src/chemistry.rs: introduce NameStr (feature-gated), use as_str!/as_qname! helpers, convert many attribute_value calls to .as_deref(), update XPath error constructions, and store operator chars as NameStr with PhantomData in OperatorPair. Refactor chemistry tests to helper-based Result flow and use sxd_document_no_unsafe::parser. These edits make the code compatible with the "no-unsafe" pathway and fix lifetime/string mismatches across the codebase. --- src/braille.rs | 107 +++++----- src/canonicalize.rs | 315 +++++++++++++++------------- src/chemistry.rs | 484 ++++++++++++++++++++++++++------------------ 3 files changed, 520 insertions(+), 386 deletions(-) diff --git a/src/braille.rs b/src/braille.rs index 7b8a0cbb3..8a8f7dc88 100644 --- a/src/braille.rs +++ b/src/braille.rs @@ -1,7 +1,8 @@ #![allow(clippy::needless_return)] use strum_macros::Display; -use sxd_document::dom::{Element, ChildOfElement}; -use sxd_document::Package; +use sxd_document_no_unsafe::dom::{Element, ChildOfElement}; +use sxd_document_no_unsafe::Package; +use sxd_document_no_unsafe::as_str; use crate::definitions::SPEECH_DEFINITIONS; use crate::errors::*; use crate::pretty_print::mml_to_string; @@ -236,8 +237,8 @@ trait BrailleCode: Sync { /// Whether `mathml` needs grouping indicators around it. Default: the code doesn't use grouping. fn needs_grouping(&self, _mathml: Element, _is_base: bool) -> StdResult { - return Err(XPathError::Other(format!( - "NeedsToBeGrouped: braille code arg '{}' is not a known code ('UEB', 'CMU', or 'Swedish')", self.name()))); + return Err(XPathError::Other { what: format!( + "NeedsToBeGrouped: braille code arg '{}' is not a known code ('UEB', 'CMU', or 'Swedish')", self.name()) }); } // --- navigation-highlight knobs (UEB-like defaults; override per code as needed) --- @@ -450,7 +451,7 @@ pub fn get_navigation_node_from_braille_position(mathml: Element, position: usiz None => bail!("'id' is not present on mathml: {}", mml_to_string(node)), }; N_PROBES.with(|n| {*n.borrow_mut() += 1}); - let (braille, char_start, char_end) = braille_mathml(mathml, node_id)?; + let (braille, char_start, char_end) = braille_mathml(mathml, as_str!(node_id))?; let mut status = None; // debug!("find_navigation_node ({}, id={}): highlight=[{}, {}); target={}", name(node), node_id, char_start, char_end, target_position); if is_leaf(node) { @@ -517,7 +518,7 @@ pub fn get_navigation_node_from_braille_position(mathml: Element, position: usiz return Ok(status); }, SearchStatus::LookInParent => { - let (_, start, end) = braille_mathml(mathml, node_id)?; + let (_, start, end) = braille_mathml(mathml, as_str!(node_id))?; // debug!(" parent ({}) braille: start/end={}/{}; target_position={}", name(node), start, end, target_position); if start <= target_position && target_position < end { // debug!(" ..found: id={}", node_id); @@ -563,7 +564,7 @@ pub fn get_navigation_node_from_braille_position(mathml: Element, position: usiz return BRAILLE_DEFINITIONS.with(|definitions| { let definitions = definitions.borrow(); let comparison_operators = definitions.get_hashset("ComparisonOperators").unwrap(); - return comparison_operators.contains(as_text(node)); + return comparison_operators.contains(as_str!(as_text(node))); }); } @@ -597,7 +598,7 @@ pub fn get_navigation_node_from_braille_position(mathml: Element, position: usiz fn estimate_braille_chars(child: ChildOfElement, n_number_indicator: usize) -> usize { let node = as_element(child); - let leaf_name = name(node); + let leaf_name = as_str!(name(node)); if is_leaf(node) { let text = as_text(node); // len() is close since mn's probably have ASCII digits and lower case vars are common (count as) and other chars need extra braille chars @@ -2477,10 +2478,10 @@ fn ASCIIMath_cleanup(_pref_manager: Ref, raw_braille: String) use crate::canonicalize::{as_element, as_text, name}; use crate::xpath_functions::{is_leaf, validate_one_node, IsBracketed}; use std::result::Result as StdResult; -use sxd_document::dom::ParentOfChild; -use sxd_xpath::function::Error as XPathError; -use sxd_xpath::function::{Args, Function}; -use sxd_xpath::{context, nodeset::*, Value}; +use sxd_document_no_unsafe::dom::ParentOfChild; +use sxd_xpath_no_unsafe::function::Error as XPathError; +use sxd_xpath_no_unsafe::function::{Args, Function}; +use sxd_xpath_no_unsafe::{context, nodeset::*, Value}; pub struct NemethNestingChars; const NEMETH_FRAC_LEVEL: &str = "data-nemeth-frac-level"; // name of attr where value is cached @@ -2505,7 +2506,7 @@ impl NemethNestingChars { max_value += repeat_char; node.set_attribute_value(NEMETH_FRAC_LEVEL, &max_value); return max_value; - } else if FIRST_CHILD_ONLY.contains(&name) { + } else if FIRST_CHILD_ONLY.contains(&as_str!(name)) { // only look at the base -- ignore scripts/index return NemethNestingChars::nemeth_frac_value(as_element(children[0]), repeat_char); } else { @@ -2540,11 +2541,11 @@ impl NemethNestingChars { if let ParentOfChild::Element(e) = parent_of_child { parent = e; } else { - return Err( sxd_xpath::function::Error::Other("Internal error in nemeth_root_value: didn't find 'math' tag".to_string()) ); + return Err( sxd_xpath_no_unsafe::function::Error::Other { what: "Internal error in nemeth_root_value: didn't find 'math' tag".to_string() } ); } } } - return Err( XPathError::Other("Internal error in nemeth_root_value: didn't find 'math' tag".to_string()) ); + return Err( XPathError::Other { what: "Internal error in nemeth_root_value: didn't find 'math' tag".to_string() } ); } } @@ -2575,7 +2576,7 @@ impl Function for NemethNestingChars { } else if name == "msqrt" || name == "mroot" { return Ok( Value::String( NemethNestingChars::nemeth_root_value(el, &repeat_char)? ) ); } else { - return Err(XPathError::Other(format!("NestingChars chars should be used only on 'mfrac'. '{}' was passed in", name))); + return Err(XPathError::Other { what: format!("NestingChars chars should be used only on 'mfrac'. '{}' was passed in", name) }); } } else { // not an element, so nothing to do @@ -2592,11 +2593,11 @@ impl BrailleChars { fn get_braille_chars(node: Element, code: &str, text_range: Option>) -> StdResult { let result = match get_braille_code(code) { Some(braille_code) => braille_code.get_braille_chars(node, text_range), - None => return Err(sxd_xpath::function::Error::Other(format!("get_braille_chars: unknown braille code '{code}'"))), + None => return Err(sxd_xpath_no_unsafe::function::Error::Other { what: format!("get_braille_chars: unknown braille code '{code}'") }), }; return match result { Ok(string) => Ok(make_quoted_string(string)), - Err(err) => return Err(sxd_xpath::function::Error::Other(err.to_string())), + Err(err) => return Err(sxd_xpath_no_unsafe::function::Error::Other { what: err.to_string() }), } } @@ -2608,7 +2609,8 @@ impl BrailleChars { static PICK_APART_CHAR: LazyLock = LazyLock::new(|| { Regex::new(r"(?P[SB𝔹TIR]*)(?P[EDGVHU]?)(?PC?)(?PL?)(?P[N]?)(?P.)").unwrap() }); - let math_variant = node.attribute_value("mathvariant"); + let raw_math_variant = node.attribute_value("mathvariant"); + let math_variant = raw_math_variant.as_deref(); let attr_typeface = match math_variant { None => "R", Some(variant) => match variant { @@ -2621,7 +2623,7 @@ impl BrailleChars { _ => "R", // normal and unknown }, }; - let text = BrailleChars::substring(as_text(node), &text_range); + let text = BrailleChars::substring(as_str!(as_text(node)), &text_range); let braille_chars = braille_replace_chars(&text, node)?; // debug!("Nemeth chars: text='{}', braille_chars='{}'", &text, &braille_chars); @@ -2630,7 +2632,7 @@ impl BrailleChars { // also true (sort of) for capitalization -- if all caps, use double cap in front (assume abbr or Roman Numeral) // we only care about this for numbers and identifiers/text, so we filter for only those - let node_name = name(node); + let node_name = as_str!(name(node)); let is_in_enclosed_list = node_name != "mo" && BrailleChars::is_in_enclosed_list(node); let is_mn_in_enclosed_list = is_in_enclosed_list && node_name == "mn"; let mut typeface = "R".to_string(); // assumption is "R" and if attr or letter is different, something happens @@ -2683,8 +2685,9 @@ impl BrailleChars { Regex::new(r"(?PB??)(?PI??)(?P[S𝔹TD]??)s??(?PC??)(?PG??)(?P[NL].)").unwrap() }); - let math_variant = node.attribute_value("mathvariant"); - let text = BrailleChars::substring(as_text(node), &text_range); + let raw_math_variant = node.attribute_value("mathvariant"); + let math_variant = raw_math_variant.as_deref(); + let text = BrailleChars::substring(as_str!(as_text(node)), &text_range); let mut braille_chars = braille_replace_chars(&text, node)?; // debug!("get_braille_ueb_chars: before/after unicode.yaml: '{}'/'{}'", text, braille_chars); @@ -2728,11 +2731,12 @@ impl BrailleChars { } fn get_braille_russian_chars(node: Element, text_range: Option>) -> Result { - let text = BrailleChars::substring(as_text(node), &text_range); + let text = BrailleChars::substring(as_str!(as_text(node)), &text_range); let braille_chars = braille_replace_chars(&text, node)?; - let Some(math_variant) = node.attribute_value("mathvariant") else { + let Some(raw_math_variant) = node.attribute_value("mathvariant") else { return Ok(braille_chars); }; + let math_variant = as_str!(raw_math_variant); let mut prefix = String::new(); let mut suffix = String::new(); @@ -2771,8 +2775,9 @@ impl BrailleChars { Regex::new(r"(?PB??)(?PI??)(?P[S𝔹TD]??)s??(?PC??)(?PG??)(?P[NL].)").unwrap() }); - let math_variant = node.attribute_value("mathvariant"); - let text = BrailleChars::substring(as_text(node), &text_range); + let raw_math_variant = node.attribute_value("mathvariant"); + let math_variant = raw_math_variant.as_deref(); + let text = BrailleChars::substring(as_str!(as_text(node)), &text_range); let text = add_separator(text); let braille_chars = braille_replace_chars(&text, node)?; @@ -2905,12 +2910,13 @@ impl BrailleChars { return false; fn child_meets_conditions(node: Element) -> bool { - let name = name(node); + let name = as_str!(name(node)); return match name { "mi" | "mn" => true, "mo" => !crate::canonicalize::is_relational_op(node), "mtext" => { - let text = as_text(node).trim(); + let raw_text = as_text(node); + let text = as_str!(raw_text).trim(); return text=="?" || text=="-?-" || text.is_empty(); // various forms of "fill in missing content" (see also Nemeth_RULEs.yaml, "omissions") }, "mrow" => { @@ -2971,7 +2977,7 @@ impl Function for BrailleChars { use crate::canonicalize::create_mathml_element; let mut args = Args(args); if let Err(e) = args.exactly(2).or_else(|_| args.exactly(4)) { - return Err( XPathError::Other(format!("BrailleChars requires 2 or 4 args: {e}"))); + return Err( XPathError::Other { what: format!("BrailleChars requires 2 or 4 args: {e}") }); }; let range = if args.len() == 4 { @@ -3003,7 +3009,7 @@ impl Function for BrailleChars { }; if !is_leaf(node) { - return Err( XPathError::Other(format!("BrailleChars called on non-leaf element '{}'", mml_to_string(node))) ); + return Err( XPathError::Other { what: format!("BrailleChars called on non-leaf element '{}'", mml_to_string(node)) } ); } return Ok( Value::String( BrailleChars::get_braille_chars(node, &braille_code, range)? ) ); } @@ -3014,7 +3020,7 @@ impl NeedsToBeGrouped { // ordinals often have an irregular start (e.g., "half") before becoming regular. // if the number is irregular, return the ordinal form, otherwise return 'None'. fn needs_grouping_for_cmu(element: Element, _is_base: bool) -> bool { - let node_name = name(element); + let node_name = as_str!(name(element)); let children = element.children(); if node_name == "mrow" { // check for bracketed exprs @@ -3069,10 +3075,11 @@ impl NeedsToBeGrouped { /// FIX: what needs to be implemented? fn needs_grouping_for_finnish(mathml: Element, is_base: bool) -> bool { use crate::xpath_functions::IsInDefinition; - let mut node_name = name(mathml); - if mathml.attribute_value("data-roman-numeral").is_some() { - node_name = "mi"; // roman numerals don't follow number rules - } + let node_name = if mathml.attribute_value("data-roman-numeral").is_some() { + "mi" // roman numerals don't follow number rules + } else { + as_str!(name(mathml)) + }; // FIX: the leaf rules are from UEB -- check the Swedish rules match node_name { @@ -3100,7 +3107,7 @@ impl NeedsToBeGrouped { } }, "mi" | "mo" | "mtext" => { - let text = as_text(mathml); + let text = as_str!(as_text(mathml)); let parent = get_parent(mathml); // there is always a "math" node let parent_name = name(parent); // there is always a "math" node if is_base && (parent_name == "msub" || parent_name == "msup" || parent_name == "msubsup") && !text.contains([' ', '\u{00A0}']) { @@ -3160,15 +3167,16 @@ impl NeedsToBeGrouped { // if the number is irregular, return the ordinal form, otherwise return 'None'. fn needs_grouping_for_swedish(mathml: Element, is_base: bool) -> bool { use crate::xpath_functions::IsInDefinition; - let mut node_name = name(mathml); - if mathml.attribute_value("data-roman-numeral").is_some() { - node_name = "mi"; // roman numerals don't follow number rules - } + let node_name = if mathml.attribute_value("data-roman-numeral").is_some() { + "mi" // roman numerals don't follow number rules + } else { + as_str!(name(mathml)) + }; match node_name { "mn" => return false, "mi" | "mo" | "mtext" => { - let text = as_text(mathml); + let text = as_str!(as_text(mathml)); let parent = get_parent(mathml); // there is always a "math" node let parent_name = name(parent); // there is always a "math" node if is_base && (parent_name == "msub" || parent_name == "msup" || parent_name == "msubsup") && !text.contains([' ', '\u{00A0}']) { @@ -3227,10 +3235,11 @@ impl NeedsToBeGrouped { // 8. If none of the foregoing apply, the item is simply the [this element's] individual symbol. use crate::xpath_functions::IsInDefinition; - let mut node_name = name(mathml); - if mathml.attribute_value("data-roman-numeral").is_some() { - node_name = "mi"; // roman numerals don't follow number rules - } + let node_name = if mathml.attribute_value("data-roman-numeral").is_some() { + "mi" // roman numerals don't follow number rules + } else { + as_str!(name(mathml)) + }; match node_name { "mn" => { if !is_base { @@ -3256,7 +3265,7 @@ impl NeedsToBeGrouped { } }, "mi" | "mo" | "mtext" => { - let text = as_text(mathml); + let text = as_str!(as_text(mathml)); let parent = get_parent(mathml); // there is always a "math" node let parent_name = name(parent); // there is always a "math" node if is_base && (parent_name == "msub" || parent_name == "msup" || parent_name == "msubsup") && !text.contains([' ', '\u{00A0}']) { @@ -3309,12 +3318,12 @@ impl Function for NeedsToBeGrouped { if let Node::Element(e) = node { let answer = match get_braille_code(&braille_code) { Some(code) => code.needs_grouping(e, is_base)?, - None => return Err(XPathError::Other(format!("NeedsToBeGrouped: braille code arg '{braille_code:?}' is not a known code ('UEB', 'CMU', or 'Swedish')"))), + None => return Err(XPathError::Other { what: format!("NeedsToBeGrouped: braille code arg '{braille_code:?}' is not a known code ('UEB', 'CMU', or 'Swedish')") }), }; return Ok( Value::Boolean( answer ) ); } - return Err(XPathError::Other(format!("NeedsToBeGrouped: first arg '{node:?}' is not a node"))); + return Err(XPathError::Other { what: format!("NeedsToBeGrouped: first arg '{node:?}' is not a node") }); } } diff --git a/src/canonicalize.rs b/src/canonicalize.rs index 237675253..a93ce3b44 100644 --- a/src/canonicalize.rs +++ b/src/canonicalize.rs @@ -9,14 +9,22 @@ use crate::errors::*; use std::rc::Rc; use std::cell::RefCell; -use sxd_document::dom::{Element, Document, ChildOfElement, Attribute}; -use sxd_document::QName; +use sxd_document_no_unsafe::dom::{Element, Document, ChildOfElement, Attribute}; +use sxd_document_no_unsafe::QName; +use sxd_document_no_unsafe::{as_str, as_qname}; + +#[cfg(not(feature = "no-unsafe"))] +pub type NameStr<'a> = &'a str; +#[cfg(feature = "no-unsafe")] +pub type NameStr<'a> = sxd_document_no_unsafe::InternedString; + use phf::{phf_map, phf_set}; use crate::xpath_functions::{IsBracketed, is_leaf, IsNode}; use std::ptr::eq as ptr_eq; use crate::pretty_print::*; use regex::Regex; use std::fmt; +use std::marker::PhantomData; use crate::chemistry::*; use unicode_script::Script; use roman_numerals_rs::RomanNumeral; @@ -136,15 +144,22 @@ struct OperatorInfo { // The character is separated out from the OperatorInfo as this allows the OperatorInfo to be static (can use default values) #[derive(Clone, Debug)] struct OperatorPair<'op> { - ch: &'op str, - op: &'static OperatorInfo + ch: NameStr<'op>, + op: &'static OperatorInfo, + _phantom: PhantomData<&'op str>, } +#[cfg(not(feature = "no-unsafe"))] +fn op_ch(s: &'static str) -> NameStr<'static> { s } +#[cfg(feature = "no-unsafe")] +fn op_ch(s: &str) -> NameStr<'static> { sxd_document_no_unsafe::InternedString::from_str(s) } + impl<'op> OperatorPair<'op> { fn new() -> OperatorPair<'op> { return OperatorPair{ - ch: "illegal", // value 'illegal' used only in debugging, if then + ch: op_ch("illegal"), // value 'illegal' used only in debugging, if then op: &ILLEGAL_OPERATOR_INFO, // ILLEGAL_OPERATOR_INFO avoids using