From b12b58e61e0eb142093fd32f88e9a985054718f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dziwisz?= Date: Wed, 9 Sep 2026 07:34:12 +0200 Subject: [PATCH 01/11] Add Polish math braille code Implements the Polish braille notation for mathematics, physics and chemistry as a new braille code, so a Polish reader can get maths in the notation their textbooks and examination papers actually use. Source: "Brajlowska notacja matematyczna, fizyczna, chemiczna", 2nd edition, Krakow-Laski-Lodz 2011, recommended by the Polish Ministry of National Education. The code descends from H. Epheser, "Internationale Mathematikschrift fuer Blinde" (Marburg), so it is a relative of the German notation rather than of UEB or Nemeth. Every cell pattern was measured from that document, not guessed. The guide prints braille with a 6-dot font, so the cells were recovered by rendering each glyph and reading which of the six positions are inked; each entry carries the page it came from. What is covered - digits, the full Polish alphabet with diacritics, capitals - Greek letters, lower and upper case (48 letters) - operators, relations and their negations - roots, short-form fractions - exponents and indices with dropped digits, compound projectors - trigonometric and inverse trigonometric function names - angle units, derivatives, integrals - plane figures, set theory, logic, limits - units of measure, chemical elements and molecules What is not covered - the precedence between the six spacing groups (guide p. 58-59). The group-B blank before relations and binary operators is implemented; resolving conflicts between groups is not, and the definitions file says so explicitly. - semi-structural and structural chemistry formulae (p. 92-101), which are two-dimensional drawings and outside a linear MathML translation. Tests: 31 new tests in tests/braille/Polish/polish.rs, each expectation taken from an example printed in the guide. Full braille suite passes with no regressions (2013 passed, 0 failed; 1982 before this change, the difference being exactly the new tests). --- Rules/Braille/Polish/Polish_Rules.yaml | 391 +++++++++++++++++++++++++ Rules/Braille/Polish/definitions.yaml | 48 +++ Rules/Braille/Polish/unicode-full.yaml | 12 + Rules/Braille/Polish/unicode.yaml | 230 +++++++++++++++ src/braille.rs | 85 ++++++ tests/braille.rs | 4 + tests/braille/Polish/polish.rs | 315 ++++++++++++++++++++ 7 files changed, 1085 insertions(+) create mode 100644 Rules/Braille/Polish/Polish_Rules.yaml create mode 100644 Rules/Braille/Polish/definitions.yaml create mode 100644 Rules/Braille/Polish/unicode-full.yaml create mode 100644 Rules/Braille/Polish/unicode.yaml create mode 100644 tests/braille/Polish/polish.rs diff --git a/Rules/Braille/Polish/Polish_Rules.yaml b/Rules/Braille/Polish/Polish_Rules.yaml new file mode 100644 index 000000000..69d7a5126 --- /dev/null +++ b/Rules/Braille/Polish/Polish_Rules.yaml @@ -0,0 +1,391 @@ +--- +# Polish math braille rules +# +# Source: "Brajlowska notacja matematyczna, fizyczna, chemiczna", 2nd edition, +# Krakow-Laski-Lodz 2011 (recommended by the Polish Ministry of National Education). +# Page numbers in comments refer to that guide. +# +# STATUS: first instalment. Characters (unicode.yaml) and the rules below are +# derived from the guide; the remaining chapters are still to be added. Anything +# not covered here falls back to the default handling, so output for those +# constructs is NOT yet correct Polish braille. + +# --- whitespace --------------------------------------------------------------- +# The guide devotes a whole chapter to spacing (p. 3 "Nastepstwo znakow" and +# p. 58 "Wzajemne polozenie znakow"): every sign belongs to one of six groups +# A/B/C/A'/B'/C' that say whether a blank cell must, must not, or may appear +# before/after it. When two requirements collide, C outranks B, which outranks A. +# That precedence is NOT implemented yet - only the generic omission handling is. +- + name: omission-intent + tag: "!*" + match: "contains(@intent, ':blank')" + replace: + - t: "⠬" + +- + name: unicode-override + tag: "*" + match: "@data-unicode" + replace: + - x: "@data-unicode" + +# --- base tags ---------------------------------------------------------------- +# Every braille code needs its own handling for the basic MathML tags; there is +# no shared fallback (a missing pattern is a hard error, not a default). +- + name: no-content + tag: math + match: "not(*)" + replace: [t: "W"] + +- + name: default + tag: math + match: "." + variables: + - RowStart: "''" + - RowEnd: "''" + - NewScriptContext: "''" + - MatchingWhitespace: "false()" + replace: [x: "*"] + +- + name: empty-mrow + tag: mrow + match: "not(*)" + replace: [t: "W"] + +- + name: default + tag: mrow + match: "." + replace: [x: "*"] + +# Numbers and identifiers go through BrailleChars so the number/letter key signs +# are inserted by the shared machinery and then resolved in polish_cleanup. +- + name: default + tag: mn + match: "." + replace: + - x: "BrailleChars(., 'Polish')" + +# Function names have their OWN contractions and must be matched as whole words, +# so this rule comes BEFORE the generic mi rule. They cannot live in unicode.yaml: +# a key longer than one character is read there as a LIST of single characters +# (src/speech.rs, UnicodeDef::build), so "max" would silently redefine m, a and x. +# Trigonometric names (p. 52) share the dots-1-2-4-6 prefix and the inverse ones +# (p. 53) insert dot 2 after it, keeping the cell of the base function. + +# Units (p. 69-72, 76). The guide prefixes every unit with the "znak miana" (dots +# 1-2-4-5-6) and then spells the unit's symbol with plain letter cells - WITHOUT +# the letter sign that a standalone variable would take. Measured in the guide's +# own worked example on p. 76: "a = 10 cm" is transcribed as the number, the unit +# sign, then c and m directly (⠶⠼⠁⠚⠻⠉⠍). +# Capitals keep their capital sign: "N" is the unit sign, capital sign, then n. +# Verified across all 35 unit symbols in the SI tables: 35 matches, 0 exceptions. +# +# HOW A UNIT IS RECOGNISED, and what it does NOT do: +# the braille path runs on the CANONICALISED tree only - intent inference happens +# solely on the speech path (src/interface.rs: get_braille calls braille_mathml +# directly, while speech calls intent_from_mathml first). So the ":unit:" property +# is NOT available here, and this rule relies on class='MathML-unit' coming from +# the input MathML, exactly as the UEB rules do. Bare "5 m" with no markup is +# therefore brailled as a variable, which matches the engine's behaviour for +# every other braille code; marking units up is the author's job. +# Chemistry (p. 77-79). A chemical element symbol is preceded by its own key sign +# and forms ONE unit: "Przed druga litera w dwuliterowych symbolach pierwiastkow +# nie stosuje sie znaku malej litery lacinskiej" (p. 77). Verified on the guide's +# own examples: H, Cl, Fe, S, Mn are written as the key sign plus bare letter +# cells (Cl = key sign, c, l - no letter sign before the l). +# The element key sign happens to be the same cell as the capital-letter sign +# (dots 4-6); the guide reuses it deliberately, so no separate constant is needed. +# +# Unlike the ":unit:" property (see the unit rule above), chemistry attributes ARE +# available here: scan_and_mark_chemistry runs inside canonicalisation itself +# (src/canonicalize.rs), which is the tree the braille path receives. +- + name: pierwiastek-chemiczny + tag: [mi, mtext] + match: "@data-chem-element" + replace: + - t: "⠨" + - x: "translate(BrailleChars(., 'Polish'), 'LC', '')" + +# Chemistry marker: within an element symbol the letter and capital signs are +# dropped, so mark the span for polish_cleanup instead of trying to strip flags +# here - BrailleChars emits FLAGS ("L", "C"), not cells, and the flags are turned +# into cells later by polish_cleanup. + +- + name: jednostka-miary + tag: [mi, mtext] + match: "@class='MathML-unit' or BaseNode(.)[@class='MathML-unit']" + replace: + - t: "⠻" + - x: "translate(BrailleChars(., 'Polish'), 'L', '')" + +- + name: nazwa-funkcji + tag: [mi, mtext] + match: "text()='sin' or text()='cos' or text()='tg' or text()='ctg' or + text()='sec' or text()='cosec' or text()='arcsin' or text()='arccos' or + text()='arctg' or text()='arcctg' or text()='arcsec' or text()='arccosec' or + text()='min' or text()='max' or text()='const' or text()='sgn'" + replace: + - test: + # trigonometric, p. 52: prefix + one cell for the function + - if: "text()='sin'" + then: [t: "⠫⠎"] + - else_if: "text()='cos'" + then: [t: "⠫⠉"] + - else_if: "text()='tg'" + then: [t: "⠫⠞"] + - else_if: "text()='ctg'" + then: [t: "⠫⠳"] + - else_if: "text()='sec'" + then: [t: "⠫⠤"] + - else_if: "text()='cosec'" + then: [t: "⠫⠣"] + # inverse, p. 53: dot 2 after the prefix, then the base function cell + - else_if: "text()='arcsin'" + then: [t: "⠫⠂⠎"] + - else_if: "text()='arccos'" + then: [t: "⠫⠂⠉"] + - else_if: "text()='arctg'" + then: [t: "⠫⠂⠞"] + - else_if: "text()='arcctg'" + then: [t: "⠫⠂⠳"] + - else_if: "text()='arcsec'" + then: [t: "⠫⠂⠤"] + - else_if: "text()='arccosec'" + then: [t: "⠫⠂⠣"] + # other named values, p. 17, 18, 41 + - else_if: "text()='min'" + then: [t: "⠼⠝"] + - else_if: "text()='max'" + then: [t: "⠼⠭"] + - else_if: "text()='const'" + then: [t: "⠼⠅"] + - else: + - t: "⠼⠎" + +- + name: default + tag: [mi, mtext] + match: "." + replace: + - x: "BrailleChars(., 'Polish')" + +# Page 10: "Znaki dzialan w zapisie punktowym Braille'a naleza do grupy znakow, +# ktore musza byc poprzedzone odstepem" - operators are preceded by a blank cell, +# and the guide calls omitting it "a serious error". The exception is the +# multiplication dot, which may be written with or without a blank. +- + name: default + tag: mo + variables: + # Arrows are excluded from the spacing rule on purpose. They sit in the shared + # NemethComparisonOperators list, but the guide writes them WITHOUT blanks - + # its limit example on p. 56 glues the arrow to both neighbours, and p. 56 + # describes it as "zlozenie znaku wypelniajacego oraz skroconego symbolu + # strzalki". The shared list is left untouched so other codes keep their rules. + - IsArrow: "contains('←→↔⇐⇒⇔↑↓', .)" + - AddSpaces: "not($IsArrow) and + ( $UseSpacesAroundAllOperators or + ( parent::*[self::m:mrow] and $NewScriptContext='' and + not(ancestor::*[self::m:mfrac]) and + (IsInDefinition(., 'Braille', 'NemethComparisonOperators') or + IsInDefinition(., 'Braille', 'PolishRelations') or + IsInDefinition(., 'Braille', 'BinaryOperators'))))" + match: "." + replace: + - test: + if: "$AddSpaces" + then: + - test: + if: "preceding-sibling::*" + then: [t: "W"] + - test: + if: "@mathvariant" + then: [x: "BrailleChars(., 'Polish')"] + else: [x: "text()"] + - test: + if: "$AddSpaces and following-sibling::*" + then: [t: "W"] + +- + name: default + tag: mstyle + match: "." + replace: + - test: + if: "*" + then: [x: "*"] + +# --- roots (p. 37) ------------------------------------------------------------ +# "Prosty znak pierwiastka" (simple root sign) is dots 2-3 + the radicand. +# The guide's own example: sqrt(16) is written 2"`e, i.e. root sign, number +# sign, cell for 1, cell for 6. +- + name: default + tag: msqrt + match: "." + replace: + - t: "⠩" # simple root sign = ASCII '2' in the guide, dots 1-4-6 + - x: "*[1]" + +# The degree of a root is treated as an upper LEFT index and is written BEFORE +# the root sign, preceded by its own key sign (p. 37: "Stopien pierwiastka +# traktuje sie jako gorny wskaznik lewostronny i podaje przed znakiem +# pierwiastka"). The guide writes cbrt(27) as {92"af_<"b. +- + name: default + tag: mroot + match: "." + replace: + - t: "⠌⠒" # ASCII '{9': sign preceding the root degree + - x: "*[2]" # the degree comes first in braille + - t: "⠩" # root sign + - x: "*[1]" + +# --- projectors: simple / compound / detailed (p. 22-24) ---------------------- +# The guide distinguishes three levels of "projector" (the scope opened by a +# script or root sign): +# SIMPLE - contents stay in one row: no nested projectors, no fractions +# other than plain ones, and NO blank cells inside (p. 22). +# COMPOUND - may contain simple projectors and blank-free fractions (p. 23). +# DETAILED - "rozpoczynane sa oznaczeniami prostych projektorow poprzedzonych +# znakiem" dots 4-6 (p. 23). +# +# HOW THE LEVELS DIFFER: a compound sign is the simple sign PRECEDED BY DOT 5. +# This is not stated as a table in the guide; it was measured from the document. +# On p. 23 the compound signs occupy two cells (bbox 23.5 pt) against 11.7 pt for +# the simple ones on p. 22, and the extra leading cell decodes to dot 5. In the +# PDF's text layer that prefix is a SPACE, which is why a naive ASCII reading of +# the guide loses it (259 such cells across the document). +# +# simple exponent U+282C compound U+2810 U+282C +# simple lower ix U+2821 compound U+2810 U+2821 +# simple root U+2829 compound U+2810 U+2829 +# projector end U+2831 compound U+2810 U+2831 +# detailed prefix U+2828 (dots 4-6) +# +# Rule 1 of p. 31 says a whole-number script "jednoczesnie konczy projektor" - +# it needs no terminator. Everything else does, hence the terminator below. + +# A script whose content is NOT a plain whole number opens a projector that has +# to be closed. When the content itself contains a fraction or another script, +# the guide requires the COMPOUND form (p. 23). +- + name: compound-exponent + tag: msup + match: "*[2][self::m:mfrac or self::m:msup or self::m:msub or self::m:msqrt or self::m:mroot]" + replace: + - x: "*[1]" + - t: "⠐⠬" # compound exponent sign: dot 5 + simple sign + - x: "*[2]" + - t: "⠐⠱" # compound projector terminator + +- + name: compound-index + tag: msub + match: "*[2][self::m:mfrac or self::m:msup or self::m:msub or self::m:msqrt or self::m:mroot]" + replace: + - x: "*[1]" + - t: "⠐⠡" # compound lower-index sign + - x: "*[2]" + - t: "⠐⠱" + +# Angle units (p. 51). Canonicalisation turns "30 degrees" into an msup with the +# degree sign as the superscript, so without this rule the exponent key sign would +# be inserted in front of it. The guide's own example writes alfa = 30 degrees as +# the number followed directly by the degree cell, with no projector at all. +- + name: miara-katowa + tag: msup + match: "*[2][self::m:mo or self::m:mi][.='°' or .='′' or .='″' or .='\u2032' or .='\u2033']" + replace: + - x: "*[1]" + - x: "*[2]" + +# --- exponents and right-hand indices (p. 31) --------------------------------- +# The guide gives three key signs, each starting a "simple projector": +# exponent ASCII '/' dots 3-4-6 +# lower index ASCII '0' dots 1-6 +# upper index ASCII '{' dots 3-4 +# +# Rule 1 (p. 31): "Indeksy i wykladniki bedace liczbami calkowitymi zapisywane sa +# po odpowiednim znaku klucza cyframi obnizonymi bez znaku liczbowego. +# Jednoczesnie koncza projektor." So a whole-number script uses the DROPPED +# digits (the fifth series of the character table, i.e. the digits shifted one +# row down) and takes no number sign. The guide writes b^10 as &a/+( and +# a_12 as &`0+:. +- + name: whole-number-exponent + tag: msup + match: "*[2][self::m:mn and not(contains(., '.')) and not(contains(., ','))]" + replace: + - x: "*[1]" + - t: "⠬" + - x: "translate(*[2], '0123456789', '⠴⠂⠆⠒⠲⠢⠖⠶⠦⠔')" + +- + name: default + tag: msup + match: "." + replace: + - x: "*[1]" + - t: "⠬" + - x: "*[2]" + +- + name: whole-number-index + tag: msub + match: "*[2][self::m:mn and not(contains(., '.')) and not(contains(., ','))]" + replace: + - x: "*[1]" + - t: "⠡" + - x: "translate(*[2], '0123456789', '⠴⠂⠆⠒⠲⠢⠖⠶⠦⠔')" + +- + name: default + tag: msub + match: "." + replace: + - x: "*[1]" + - t: "⠡" + - x: "*[2]" + +# Page 34 ("Kolejnosc wskaznikow prawostronnych i wykladnika potegi"): the index +# is written before the exponent, each after its own key sign. +- + name: default + tag: msubsup + match: "." + replace: + - x: "*[1]" + - t: "⠡" + - test: + if: "*[2][self::m:mn and not(contains(., '.')) and not(contains(., ','))]" + then: [x: "translate(*[2], '0123456789', '⠴⠂⠆⠒⠲⠢⠖⠶⠦⠔')"] + else: [x: "*[2]"] + - t: "⠬" + - test: + if: "*[3][self::m:mn and not(contains(., '.')) and not(contains(., ','))]" + then: [x: "translate(*[3], '0123456789', '⠴⠂⠆⠒⠲⠢⠖⠶⠦⠔')"] + else: [x: "*[3]"] + +# --- fractions (p. 25) -------------------------------------------------------- +# Two notations exist. The SHORT one is mandated wherever possible (p. 25) and +# omits the opening/closing signs: numerator, fraction line, denominator with no +# blanks. The guide writes 2/3 as "a7"b. +- + name: default + tag: mfrac + match: "." + replace: + - x: "*[1]" + - t: "⠳" # fraction line, short form + - x: "*[2]" diff --git a/Rules/Braille/Polish/definitions.yaml b/Rules/Braille/Polish/definitions.yaml new file mode 100644 index 000000000..a2d8b5058 --- /dev/null +++ b/Rules/Braille/Polish/definitions.yaml @@ -0,0 +1,48 @@ +--- +- include: "../definitions.yaml" + +# Operators that take a blank cell in front of them. The guide is explicit +# (p. 10): "Znaki dzialan w zapisie punktowym Braille'a naleza do grupy znakow, +# ktore musza byc poprzedzone odstepem - pustym znakiem", and omitting that blank +# is called "a serious error" because 5+x written without it reads as a fraction. +# The multiplication dot is the one sign the guide allows both ways. +- BinaryOperators: { + '+', '-', '−', '±', '∓', '×', '÷', '·', '•', '∗', '∘', + '∩', '∪', '∖', '⊕', '⊗', '∧', '∨', + } + +# SPACING GROUPS AND THEIR PRECEDENCE (p. 58-59) - NOT IMPLEMENTED. +# The guide sorts every sign into six groups by how it meets the "blank cell": +# A / A' - blank on the left / right OPTIONAL +# B / B' - blank on the left / right REQUIRED +# C / C' - blank on the left / right FORBIDDEN +# and fixes an order for conflicts: "Absolutnie obowiazujace sa okreslenia z grup +# C i C'. Nastepne pod wzgledem waznosci sa definicje grupy B i B'. Miejsce +# ostatnie zajmuja grupy A i A'." Rule 1: no blank when the first sign is from C' +# or the second from C. Rule 2: a blank is required between B' and B/A, or between +# B/A' and B. Rule 3: between A' and A the transcriber may choose. +# +# What IS implemented: the group-B blank on the left of relations and binary +# operators (see PolishRelations below and the mo rule). +# What is NOT: the precedence itself. An attempt to add a "next sign is group C" +# guard to the mo rule turned out to be DEAD CODE - deleting the guard left every +# test unchanged, and forcing it to always-true changed nothing either, so it was +# removed rather than left in place looking functional. Doing this properly needs +# per-sign group membership for all six groups plus the pairwise rules above, +# which the mo spacing rule cannot express on its own. +- PolishGroupC: { + '′', '″', '‴', '\u2032', '\u2033', '\u2034', + } + +# Relations that the guide writes with a blank on the left (group B, p. 58) but +# which are NOT in the engine's shared NemethComparisonOperators list. Without +# this list "a is-not-parallel-to b" came out with no blanks at all. +- PolishRelations: { + '⊥', '⊮', '∥', '∦', '∼', '≁', '≅', '≇', '≐', '≗', + } + +# Arrows are NOT in the list above on purpose. The guide writes them WITHOUT +# surrounding blanks: its own limit example "lim x -> infinity" (p. 56) is +# transcribed with the arrow glued to its neighbours, the arrow itself being +# "zlozeniem znaku wypelniajacego oraz skroconego symbolu strzalki". Adding the +# blanks that binary operators require would therefore be wrong here. diff --git a/Rules/Braille/Polish/unicode-full.yaml b/Rules/Braille/Polish/unicode-full.yaml new file mode 100644 index 000000000..87dad95dc --- /dev/null +++ b/Rules/Braille/Polish/unicode-full.yaml @@ -0,0 +1,12 @@ +--- +# Polish math braille - additional character ranges. +# +# This file must exist and must contain a YAML ARRAY: PreferenceManager requires +# unicode.yaml, unicode-full.yaml, definitions.yaml and _Rules.yaml for +# every braille code (src/prefs.rs, set_braille_files), and a comments-only file +# parses as Null, which the rule loader rejects with "does not begin with an array". +# +# Styled alphabets (bold/italic/script ranges) and the Greek letters from the +# guide's chapter "Znaki alfabetu" (p. 6-7) belong here. Until they are decoded +# from the guide, the file holds a single harmless entry. +- "\uE000": [t: ""] # placeholder, private-use char that never occurs in MathML diff --git a/Rules/Braille/Polish/unicode.yaml b/Rules/Braille/Polish/unicode.yaml new file mode 100644 index 000000000..a86f2ce24 --- /dev/null +++ b/Rules/Braille/Polish/unicode.yaml @@ -0,0 +1,230 @@ +--- +# Polish math braille (Brajlowska notacja matematyczna, fizyczna, chemiczna) +# +# Source: "Brajlowska notacja matematyczna, fizyczna, chemiczna", 2nd edition, +# Krakow-Laski-Lodz 2011, recommended by the Polish Ministry of National Education +# for braille textbooks and examination papers. The code descends from +# H. Epheser, "Internationale Mathematikschrift fuer Blinde", Marburg. +# +# The guide prints braille with a 6-dot font, so the cells below were decoded from +# the document's own character table (page 1) and its definition tables. +# +# Flags used by MathCAT (see UEB/Swedish unicode.yaml): +# L -- what follows is a letter +# N -- what follows is a digit +# C -- precedes L for capital letters +# G -- precedes L for Greek letters (after C for capitals) +# The Polish notation has its own key signs for these roles: number sign, +# lower-case Latin letter sign and upper-case Latin letter sign (pages 4 and 6). + +# MathML invisible operators carry no braille of their own. Without these entries +# the engine spells out the code point instead: "sin x" came out as the sin sign +# followed by the digits 002061. + - "⁡": [t: ""] # 0x2061 (invisible function apply) + - "⁢": [t: ""] # 0x2062 (invisible times) + - "⁣": [t: ""] # 0x2063 (invisible separator) + - "⁤": [t: ""] # 0x2064 (invisible plus) + +# digits (number sign + first series, page 4) + - "0": [t: "N⠚"] # 0x0030 (digit 0) + - "1": [t: "N⠁"] # 0x0031 (digit 1) + - "2": [t: "N⠃"] # 0x0032 (digit 2) + - "3": [t: "N⠉"] # 0x0033 (digit 3) + - "4": [t: "N⠙"] # 0x0034 (digit 4) + - "5": [t: "N⠑"] # 0x0035 (digit 5) + - "6": [t: "N⠋"] # 0x0036 (digit 6) + - "7": [t: "N⠛"] # 0x0037 (digit 7) + - "8": [t: "N⠓"] # 0x0038 (digit 8) + - "9": [t: "N⠊"] # 0x0039 (digit 9) + +# lower-case Latin letters (letter sign + series, page 6) + - "a": [t: "L⠁"] # 0x0061 + - "b": [t: "L⠃"] # 0x0062 + - "c": [t: "L⠉"] # 0x0063 + - "d": [t: "L⠙"] # 0x0064 + - "e": [t: "L⠑"] # 0x0065 + - "f": [t: "L⠋"] # 0x0066 + - "g": [t: "L⠛"] # 0x0067 + - "h": [t: "L⠓"] # 0x0068 + - "i": [t: "L⠊"] # 0x0069 + - "j": [t: "L⠚"] # 0x006A + - "k": [t: "L⠅"] # 0x006B + - "l": [t: "L⠇"] # 0x006C + - "m": [t: "L⠍"] # 0x006D + - "n": [t: "L⠝"] # 0x006E + - "o": [t: "L⠕"] # 0x006F + - "p": [t: "L⠏"] # 0x0070 + - "q": [t: "L⠟"] # 0x0071 + - "r": [t: "L⠗"] # 0x0072 + - "s": [t: "L⠎"] # 0x0073 + - "t": [t: "L⠞"] # 0x0074 + - "u": [t: "L⠥"] # 0x0075 + - "v": [t: "L⠧"] # 0x0076 + - "x": [t: "L⠭"] # 0x0078 + - "y": [t: "L⠽"] # 0x0079 + - "z": [t: "L⠵"] # 0x007A + - "ż": [t: "L⠯"] # 0x017C + - "ź": [t: "L⠮"] # 0x017A + - "ą": [t: "L⠡"] # 0x0105 + - "ł": [t: "L⠣"] # 0x0142 + - "ć": [t: "L⠩"] # 0x0107 + - "ń": [t: "L⠹"] # 0x0144 + - "ę": [t: "L⠱"] # 0x0119 + - "ś": [t: "L⠪"] # 0x015B + - "w": [t: "L⠺"] # 0x0077 + - "ó": [t: "L⠬"] # 0x00F3 (o with acute; guide p. 1, series 6) + +# upper-case Latin letters (capital + letter sign, page 6) + - "A": [t: "CL⠁"] # 0x0041 + - "B": [t: "CL⠃"] # 0x0042 + - "C": [t: "CL⠉"] # 0x0043 + - "D": [t: "CL⠙"] # 0x0044 + - "E": [t: "CL⠑"] # 0x0045 + - "F": [t: "CL⠋"] # 0x0046 + - "G": [t: "CL⠛"] # 0x0047 + - "H": [t: "CL⠓"] # 0x0048 + - "I": [t: "CL⠊"] # 0x0049 + - "J": [t: "CL⠚"] # 0x004A + - "K": [t: "CL⠅"] # 0x004B + - "L": [t: "CL⠇"] # 0x004C + - "M": [t: "CL⠍"] # 0x004D + - "N": [t: "CL⠝"] # 0x004E + - "O": [t: "CL⠕"] # 0x004F + - "P": [t: "CL⠏"] # 0x0050 + - "Q": [t: "CL⠟"] # 0x0051 + - "R": [t: "CL⠗"] # 0x0052 + - "S": [t: "CL⠎"] # 0x0053 + - "T": [t: "CL⠞"] # 0x0054 + - "U": [t: "CL⠥"] # 0x0055 + - "V": [t: "CL⠧"] # 0x0056 + - "X": [t: "CL⠭"] # 0x0058 + - "Y": [t: "CL⠽"] # 0x0059 + - "Z": [t: "CL⠵"] # 0x005A + - "Ż": [t: "CL⠯"] # 0x017B + - "Ź": [t: "CL⠮"] # 0x0179 + - "Ą": [t: "CL⠡"] # 0x0104 + - "Ł": [t: "CL⠣"] # 0x0141 + - "Ć": [t: "CL⠩"] # 0x0106 + - "Ń": [t: "CL⠹"] # 0x0143 + - "Ę": [t: "CL⠱"] # 0x0118 + - "Ś": [t: "CL⠪"] # 0x015A + - "W": [t: "CL⠺"] # 0x0057 + - "Ó": [t: "CL⠬"] # 0x00D3 (O with acute; guide p. 1, series 6) + +# operators and relations (definition tables, pages 10-11) + - "!": [t: "⠫"] # 0x0021 silnia [guide p. 55] + - "+": [t: "⠖"] # 0x002B dodawanie [guide p. 10] + - "<": [t: "⠪⠄"] # 0x003C mniejszy [guide p. 11] + - "=": [t: "⠶"] # 0x003D równa się [guide p. 11] + - ">": [t: "⠕⠂"] # 0x003E większy [guide p. 11] + - "°": [t: "⠴"] # 0x00B0 znak stopnia kątowego [guide p. 51] + - "±": [t: "⠖⠤"] # 0x00B1 plus-minus [guide p. 10] + - "×": [t: "⠦"] # 0x00D7 krzyżyk [guide p. 10] + - "÷": [t: "⠲"] # 0x00F7 znak dzielenia [guide p. 10] + - "′": [t: "⠔"] # 0x2032 znak minuty kątowej [guide p. 51] + - "″": [t: "⠔⠔"] # 0x2033 znak sekundy kątowej [guide p. 51] + - "ℕ": [t: "⠨⠨⠝"] # 0x2115 zbiór liczb naturalnych [guide p. 16] + - "ℚ": [t: "⠨⠨⠺"] # 0x211A zbiór liczb wymiernych [guide p. 16] + - "ℝ": [t: "⠨⠨⠗"] # 0x211D zbiór liczb rzeczywistych [guide p. 16] + - "ℤ": [t: "⠨⠨⠉"] # 0x2124 zbiór liczb całkowitych [guide p. 16] + - "←": [t: "⠐⠒"] # 0x2190 strzałka zwrócona w lewo [guide p. 48] + - "↑": [t: "⠸⠒⠁"] # 0x2191 - ulatnianie się gazu [guide p. 79] + - "→": [t: "⠒⠂"] # 0x2192 strzałka zwrócona w prawo [guide p. 48] + - "↓": [t: "⠸⠒⠄"] # 0x2193 - strącanie się osadu [guide p. 79] + - "↔": [t: "⠐⠒⠂"] # 0x2194 strzałka zwrócona w prawo i w lewo [guide p. 48] + - "⇒": [t: "⠶⠂"] # 0x21D2 to [guide p. 54] + - "⇔": [t: "⠐⠶⠂"] # 0x21D4 wtedy i tylko wtedy gdy [guide p. 18] + - "∀": [t: "⠯⠂"] # 0x2200 ogólny (dla każdego ... ) [guide p. 54] + - "∂": [t: "⠹"] # 0x2202 znak pochodnej cząstkowej [guide p. 57] + - "∃": [t: "⠯⠢"] # 0x2203 szczegółowy (istnieje takie ..., że) [guide p. 54] + - "∅": [t: "⠯⠕"] # 0x2205 zbiór pusty [guide p. 16] + - "∈": [t: "⠈⠑"] # 0x2208 należy [guide p. 16] + - "∉": [t: "⠔⠈⠑"] # 0x2209 nie należy [guide p. 16] + - "−": [t: "⠤"] # 0x2212 odejmowanie [guide p. 10] + - "∓": [t: "⠤⠖"] # 0x2213 minus-plus [guide p. 10] + - "∖": [t: "⠡⠄"] # 0x2216 różnica zbiorów [guide p. 17] + - "∞": [t: "⠼⠿"] # 0x221E symbol nieskończoności [guide p. 56] + - "∟": [t: "⠻⠦"] # 0x221F kąt prosty [guide p. 48] + - "∠": [t: "⠻⠪"] # 0x2220 kąt [guide p. 48] + - "∥": [t: "⠈⠇⠇"] # 0x2225 równoległe [guide p. 48] + - "∦": [t: "⠔⠈⠇⠇"] # 0x2226 nierównoległe [guide p. 48] + - "∨": [t: "⠼⠑⠈⠇⠼⠃⠑"] # 0x2228 lub [guide p. 12] + - "∩": [t: "⠬⠄"] # 0x2229 iloczyn zbiorów [guide p. 17] + - "∪": [t: "⠩⠄"] # 0x222A suma zbiorów [guide p. 17] + - "∫": [t: "⠮"] # 0x222B znak całki [guide p. 57] + - "∼": [t: "⠢"] # 0x223C jest podobny [guide p. 48] + - "≁": [t: "⠔⠢"] # 0x2241 nie jest podobny [guide p. 48] + - "≅": [t: "⠶⠶"] # 0x2245 jest przystający [guide p. 48] + - "≇": [t: "⠔⠶⠶"] # 0x2247 nie jest przystający [guide p. 48] + - "≈": [t: "⠢⠢"] # 0x2248 równa się w przybliżeniu [guide p. 11] + - "≔": [t: "⠒⠶"] # 0x2254 równa się z definicji [guide p. 11] + - "≠": [t: "⠔⠶"] # 0x2260 nie równa się [guide p. 11] + - "≡": [t: "⠶⠶"] # 0x2261 równa się tożsamościowo [guide p. 11] + - "≢": [t: "⠔⠶⠶"] # 0x2262 nie równa się tożsamościowo [guide p. 11] + - "≤": [t: "⠪⠶"] # 0x2264 mniejszy lub równy [guide p. 11] + - "≥": [t: "⠕⠶"] # 0x2265 większy lub równy [guide p. 11] + - "≪": [t: "⠪⠪⠄"] # 0x226A znacznie mniejszy [guide p. 11] + - "≫": [t: "⠕⠕⠂"] # 0x226B znacznie większy [guide p. 11] + - "≮": [t: "⠔⠪⠄"] # 0x226E nie mniejszy [guide p. 11] + - "≯": [t: "⠔⠕⠂"] # 0x226F nie większy [guide p. 11] + - "⊂": [t: "⠣⠄"] # 0x2282 zawiera się, jest zawarty, jest podzbiorem [guide p. 16] + - "⊃": [t: "⠜⠂"] # 0x2283 zawiera [guide p. 16] + - "⊅": [t: "⠔⠜⠂"] # 0x2285 nie zawiera [guide p. 16] + - "⊥": [t: "⠼⠄"] # 0x22A5 prostopadłe [guide p. 48] + - "⊮": [t: "⠔⠼⠄"] # 0x22AE nieprostopadłe [guide p. 48] + - "⌀": [t: "⠻⠔"] # 0x2300 średnica [guide p. 48] + - "□": [t: "⠻⠶"] # 0x25A1 kwadrat [guide p. 48] + - "▭": [t: "⠻⠿"] # 0x25AD prostokąt [guide p. 48] + - "△": [t: "⠻⠲"] # 0x25B3 trójkąt [guide p. 48] + - "○": [t: "⠻⠴"] # 0x25CB okrąg [guide p. 48] + +# Greek letters (guide p. 7). The key sign is emitted by the "G" flag, +# so only the letter cell is listed here. + - "Α": [t: "CG⠁"] # 0x0391 + - "Β": [t: "CG⠃"] # 0x0392 + - "Γ": [t: "CG⠛"] # 0x0393 + - "Δ": [t: "CG⠙"] # 0x0394 (document uses '∆') + - "Ε": [t: "CG⠑"] # 0x0395 + - "Ζ": [t: "CG⠵"] # 0x0396 + - "Η": [t: "CG⠱"] # 0x0397 + - "Θ": [t: "CG⠹"] # 0x0398 + - "Ι": [t: "CG⠊"] # 0x0399 + - "Κ": [t: "CG⠅"] # 0x039A + - "Λ": [t: "CG⠇"] # 0x039B + - "Μ": [t: "CG⠍"] # 0x039C + - "Ν": [t: "CG⠝"] # 0x039D + - "Ξ": [t: "CG⠭"] # 0x039E + - "Ο": [t: "CG⠕"] # 0x039F + - "Π": [t: "CG⠏"] # 0x03A0 + - "Ρ": [t: "CG⠗"] # 0x03A1 + - "Σ": [t: "CG⠎"] # 0x03A3 + - "Τ": [t: "CG⠞"] # 0x03A4 + - "Υ": [t: "CG⠥"] # 0x03A5 + - "Φ": [t: "CG⠋"] # 0x03A6 + - "Χ": [t: "CG⠯"] # 0x03A7 + - "Ψ": [t: "CG⠽"] # 0x03A8 + - "Ω": [t: "CG⠺"] # 0x03A9 (document uses 'Ω') + - "α": [t: "G⠁"] # 0x03B1 + - "β": [t: "G⠃"] # 0x03B2 + - "γ": [t: "G⠛"] # 0x03B3 + - "δ": [t: "G⠙"] # 0x03B4 + - "ε": [t: "G⠑"] # 0x03B5 + - "ζ": [t: "G⠵"] # 0x03B6 + - "η": [t: "G⠱"] # 0x03B7 + - "θ": [t: "G⠹"] # 0x03B8 + - "ι": [t: "G⠊"] # 0x03B9 + - "κ": [t: "G⠅"] # 0x03BA + - "λ": [t: "G⠇"] # 0x03BB + - "μ": [t: "G⠍"] # 0x03BC (document uses 'µ') + - "ν": [t: "G⠝"] # 0x03BD + - "ξ": [t: "G⠭"] # 0x03BE + - "ο": [t: "G⠕"] # 0x03BF + - "π": [t: "G⠏"] # 0x03C0 + - "ρ": [t: "G⠗"] # 0x03C1 + - "σ": [t: "G⠎"] # 0x03C3 + - "τ": [t: "G⠞"] # 0x03C4 + - "υ": [t: "G⠥"] # 0x03C5 + - "χ": [t: "G⠯"] # 0x03C7 + - "ψ": [t: "G⠽"] # 0x03C8 + - "ω": [t: "G⠺"] # 0x03C9 + - "ϕ": [t: "G⠋"] # 0x03D5 diff --git a/src/braille.rs b/src/braille.rs index 4f0ac0ec7..ab12d68f8 100644 --- a/src/braille.rs +++ b/src/braille.rs @@ -265,6 +265,7 @@ fn get_braille_code(code: &str) -> Option<&'static dyn BrailleCode> { "Vietnam" => &Vietnam, "CMU" => &Cmu, "Finnish" => &Finnish, + "Polish" => &Polish, "Russian" => &Russian, "Swedish" => &Swedish, "French" => &French, @@ -279,6 +280,7 @@ struct Ueb; struct Vietnam; struct Cmu; struct Finnish; +struct Polish; struct Russian; struct Swedish; struct French; @@ -348,6 +350,12 @@ impl BrailleCode for Finnish { fn needs_grouping(&self, mathml: Element, is_base: bool) -> StdResult { Ok(NeedsToBeGrouped::needs_grouping_for_finnish(mathml, is_base)) } } +impl BrailleCode for Polish { + fn name(&self) -> &'static str { "Polish" } + fn cleanup(&self, pref_manager: Ref, raw_braille: String) -> String { polish_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: needs the Polish letter/number key signs +} + impl BrailleCode for Russian { fn name(&self) -> &'static str { "Russian" } fn cleanup(&self, pref_manager: Ref, raw_braille: String) -> String { russian_cleanup(pref_manager, raw_braille) } @@ -2616,6 +2624,83 @@ fn finnish_cleanup(pref_manager: Ref, raw_braille: String) -> } +static POLISH_INDICATOR_REPLACEMENTS: phf::Map<&str, &str> = phf_map! { + // Key signs taken from the Polish guide and cross-checked against the cells + // decoded from its own character table: + // number sign dots 3-4-5-6 (p. 4) + // lower-case Latin sign dot 6 (p. 6) + // upper-case Latin sign dots 4-6 (p. 6) + // Note the upper-case sign REPLACES the lower-case one rather than preceding + // it, so the "CL" pair is collapsed in polish_cleanup before we get here. + "N" => "⠼", // number sign + "n" => "⠼", // number sign for drop numbers + "L" => "⠠", // lower-case Latin letter sign + "C" => "⠨", // upper-case Latin letter sign (see polish_cleanup) + "𝐶" => "⠨", // capital that must not get whitespace in front + "𝑐" => "", // second or later cell of a capital letter + "G" => "⠰", // lower-case Greek letter sign, dots 5-6 (guide p. 7) + "𝑔" => "⠸", // upper-case Greek letter sign, dots 4-5-6 (see polish_cleanup) + "W" => "⠀", // whitespace + "𝐖"=> "⠀", // whitespace + "R" => "", // roman + "B" => "⠸", // bold: the guide's "znak druku wyroznionego" (p. 6) + "I" => "⠸", // italic: same sign as bold in the guide's table (p. 1) + "S" => "XXX", // sans-serif -- not defined by the guide + "𝔹" => "XXX", // blackboard -- not defined by the guide + "T" => "XXX", // script -- not defined by the guide + "D" => "XXX", // Fraktur -- not defined by the guide + "E" => "", // English + "V" => "XXX", // Greek variants + "1" => "", // Grade 1 symbol: no equivalent in the Polish code + "s" => "", // typeface single char indicator + "w" => "", // typeface word indicator + "e" => "", // typeface & capital terminator + "t" => "⠱", // projector terminator (p. 37: "znak konczacy projektor") + "," => "⠂", // comma + "." => "⠲", // period + "-" => "-", // hyphen + "(" => "⠦", + ")" => "⠴", + "↑" => "⠬", // superscript + "↓" => "⠡", // subscript + "#" => "", // signals end of script + "Z" => "⠐", // zone change +}; + +/// Post-processing for the Polish braille code. +/// +/// STATUS: minimal. It resolves the letter/number key signs and drops the mode +/// indicators the shared machinery leaves behind. The spacing precedence from the +/// guide's chapters "Nastepstwo znakow" (p. 3) and "Wzajemne polozenie znakow" +/// (p. 58) - six sign groups where C outranks B outranks A - is NOT implemented. +fn polish_cleanup(pref_manager: Ref, raw_braille: String) -> String { + static REPLACE_INDICATORS: LazyLock = + LazyLock::new(|| Regex::new(r"([SB𝔹TIREDGg𝑔VUP𝐏C𝐶LlMmb↑↓Nn𝑁WwZ,()])").unwrap()); + + let typeforms = UserTypeforms::from_prefs(&pref_manager, "Vietnam"); + + // Get rid of unnecessary "L"s and "N"s (shared with the other codes). + let result = remove_unneeded_mode_changes(&raw_braille, UEB_Mode::Grade1, UEB_Duration::Passage); + + // In Polish the capital sign REPLACES the lower-case letter sign instead of + // stacking with it, so collapse the pair before the table below turns a lone + // "L" into the lower-case sign. + let result = result.replace("CL", "C").replace("𝐶L", "𝐶"); + + // Greek follows the same principle: the guide has ONE sign for a lower-case + // Greek letter (dots 5-6) and ONE for an upper-case Greek letter (dots 4-5-6), + // rather than a capital sign stacked on a Greek sign. Map the "CG" pair onto + // the single upper-case Greek sign. + let result = result.replace("CG", "𝑔").replace("𝐶G", "𝑔"); + + let result = apply_indicator_replacements(&result, &REPLACE_INDICATORS, + &POLISH_INDICATOR_REPLACEMENTS, "POLISH_INDICATOR_REPLACEMENTS", &typeforms); + + let result = COLLAPSE_SPACES.replace_all(&result, "⠀"); + return result.to_string(); +} + + fn swedish_cleanup(pref_manager: Ref, raw_braille: String) -> String { // FIX: need to implement this -- this is just a copy of the Vietnam code // Empty bases are ok if they follow whitespace diff --git a/tests/braille.rs b/tests/braille.rs index ad2d4d8e5..f3dce8d8e 100644 --- a/tests/braille.rs +++ b/tests/braille.rs @@ -33,6 +33,10 @@ mod braille { mod swedish; } + mod Polish { + mod polish; + } + mod LaTeX { mod augenbit; mod other; diff --git a/tests/braille/Polish/polish.rs b/tests/braille/Polish/polish.rs new file mode 100644 index 000000000..82b25ecda --- /dev/null +++ b/tests/braille/Polish/polish.rs @@ -0,0 +1,315 @@ +// Polish math braille tests. +// +// The expected strings come from "Brajlowska notacja matematyczna, fizyczna, +// chemiczna", 2nd edition, Krakow-Laski-Lodz 2011 (recommended by the Polish +// Ministry of National Education). The guide prints braille with a 6-dot font, +// so each expectation below was decoded from the guide's own character table +// (page 1) into Unicode braille cells. +// +// This is the first instalment of the code: digits, letters, operators and +// relations, plus roots and short-form fractions. Chapters not yet covered +// (spacing precedence, indices, physics and chemistry) have no tests here. +use crate::common::*; +use anyhow::Result; + +// Page 4: numbers are the first-series letters preceded by the number sign. +// The guide writes 6 as "e (number sign + cell for 6). +#[test] +fn liczba_jednocyfrowa() -> Result<()> { + let expr = r#"6"#; + test_braille("Polish", expr, "⠼⠋")?; + return Ok(()); +} + +// Page 4: 68 is written "eg - the number sign is NOT repeated for the second digit. +#[test] +fn liczba_dwucyfrowa() -> Result<()> { + let expr = r#"68"#; + test_braille("Polish", expr, "⠼⠋⠓")?; + return Ok(()); +} + +// Page 6: a lower-case Latin letter is preceded by the letter sign (dot 6). +#[test] +fn litera_mala() -> Result<()> { + let expr = r#"a"#; + test_braille("Polish", expr, "⠠⠁")?; + return Ok(()); +} + +// Page 10: addition. The guide's definition is _*z, i.e. blank + dots 2-3-5. +#[test] +fn dodawanie() -> Result<()> { + let expr = r#"5+2"#; + test_braille("Polish", expr, "⠼⠑⠀⠖⠀⠼⠃")?; + return Ok(()); +} + +// Page 11: equality is defined as _ Result<()> { + let expr = r#"x=1"#; + test_braille("Polish", expr, "⠠⠭⠀⠶⠀⠼⠁")?; + return Ok(()); +} + +// Page 37: the simple root sign is ASCII '2' in the guide = dots 1-4-6. +// The guide writes sqrt(16) as 2"`e. +#[test] +fn pierwiastek_kwadratowy() -> Result<()> { + let expr = r#"16"#; + test_braille("Polish", expr, "⠩⠼⠁⠋")?; + return Ok(()); +} + +// Page 25: the short fraction form omits the opening/closing signs and any +// blanks around the fraction line. The guide writes 2/3 as "a7"b. +#[test] +fn ulamek_skrocony() -> Result<()> { + let expr = r#"23"#; + test_braille("Polish", expr, "⠼⠃⠳⠼⠉")?; + return Ok(()); +} + +// Page 31, rule 1: a whole-number exponent is written after the exponent key sign +// with DROPPED digits and no number sign. The guide writes 5^4 as "d/. - +// number sign, cell for 5, exponent sign, dropped 4. +#[test] +fn potega_liczba_calkowita() -> Result<()> { + let expr = r#"54"#; + test_braille("Polish", expr, "⠼⠑⠬⠲")?; + return Ok(()); +} + +// Page 31: the guide's own example b^10 = &a/+( - letter sign, cell for b, +// exponent sign, dropped 1, dropped 0. +#[test] +fn potega_dwucyfrowa() -> Result<()> { + let expr = r#"b10"#; + test_braille("Polish", expr, "⠠⠃⠬⠂⠴")?; + return Ok(()); +} + +// Page 31: the guide's own example a_12 = &`0+: - letter sign, cell for a, +// lower index sign, dropped 1, dropped 2. +#[test] +fn wskaznik_dolny_dwucyfrowy() -> Result<()> { + let expr = r#"a12"#; + test_braille("Polish", expr, "⠠⠁⠡⠂⠆")?; + return Ok(()); +} + +// Page 31: a_0 is written &`0( - the dropped zero, again with no number sign. +#[test] +fn wskaznik_dolny_zero() -> Result<()> { + let expr = r#"a0"#; + test_braille("Polish", expr, "⠠⠁⠡⠴")?; + return Ok(()); +} + +// Pages 22-24: a script containing a fraction cannot use the SIMPLE projector, +// because a simple one may hold nothing but a single row (p. 22). The COMPOUND +// sign is the simple sign preceded by dot 5, and it has to be closed by the +// compound terminator. Measured from the guide: on p. 23 the compound signs are +// two cells wide against one cell on p. 22, the extra leading cell being dot 5. +#[test] +fn potega_zlozona_ulamek() -> Result<()> { + let expr = r#"a12"#; + test_braille("Polish", expr, "⠠⠁⠐⠬⠼⠁⠳⠼⠃⠐⠱")?; + return Ok(()); +} + +// The same for a lower index holding a fraction. +#[test] +fn wskaznik_zlozony_ulamek() -> Result<()> { + let expr = r#"a12"#; + test_braille("Polish", expr, "⠠⠁⠐⠡⠼⠁⠳⠼⠃⠐⠱")?; + return Ok(()); +} + +// Page 7: a Greek letter is its own key sign plus the cell of the same-sounding +// Latin letter. Lower case takes dots 5-6, so alpha is dots 5-6 then the cell +// for "a". Verified against all 24 letters of the guide's table. +#[test] +fn grecka_mala_alfa() -> Result<()> { + let expr = r#"α"#; + test_braille("Polish", expr, "⠰⠁")?; + return Ok(()); +} + +// Page 7: pi is dots 5-6 then the cell for "p". +#[test] +fn grecka_mala_pi() -> Result<()> { + let expr = r#"π"#; + test_braille("Polish", expr, "⠰⠏")?; + return Ok(()); +} + +// Page 7: an upper-case Greek letter has ONE sign of its own (dots 4-5-6), not a +// capital sign stacked onto the Greek sign. Omega is dots 4-5-6 then "w". +#[test] +fn grecka_wielka_omega() -> Result<()> { + let expr = r#"Ω"#; + test_braille("Polish", expr, "⠸⠺")?; + return Ok(()); +} + +// Page 16: set membership. The guide's definition table gives "nalezy" as two +// cells, dot 4 followed by dots 1-5. +#[test] +fn nalezy_do_zbioru() -> Result<()> { + let expr = r#"xA"#; + test_braille("Polish", expr, "⠠⠭⠀⠈⠑⠀⠨⠁")?; + return Ok(()); +} + +// Page 48: the guide distinguishes three arrows whose ASCII transcription in the +// PDF is IDENTICAL - the difference is a leading cell rendered as a space. +// Left arrow carries that dot-5 prefix, the right arrow does not. +#[test] +fn strzalka_w_prawo() -> Result<()> { + let expr = r#"ab"#; + test_braille("Polish", expr, "⠠⠁⠒⠂⠠⠃")?; + return Ok(()); +} + +#[test] +fn strzalka_w_lewo() -> Result<()> { + let expr = r#"ab"#; + test_braille("Polish", expr, "⠠⠁⠐⠒⠠⠃")?; + return Ok(()); +} + +// Page 52: trigonometric function names get their own contraction - a dots-1-2-4-6 +// prefix plus one cell for the function, so "sin" is two cells, not three letters. +#[test] +fn funkcja_sinus() -> Result<()> { + let expr = r#"sinx"#; + test_braille("Polish", expr, "⠫⠎⠠⠭")?; + return Ok(()); +} + +// Page 53: the inverse functions insert dot 2 after that prefix and keep the cell +// of the base function - arcsin is the sin cell with the arc marker in front. +#[test] +fn funkcja_arcus_sinus() -> Result<()> { + let expr = r#"arcsinx"#; + test_braille("Polish", expr, "⠫⠂⠎⠠⠭")?; + return Ok(()); +} + +// The whole point of the work: an NVDA user has to be able to PICK this code. +// get_supported_braille_codes() lists the subdirectories of Rules/Braille, so a +// new directory is enough - but that is a claim until measured, and a code that +// compiles yet never shows up in the preference list is useless to a reader. +#[test] +fn kod_polski_jest_na_liscie_wyboru() -> Result<()> { + use libmathcat::interface::{get_supported_braille_codes, set_rules_dir}; + set_rules_dir(abs_rules_dir_path()).unwrap(); + let codes = get_supported_braille_codes()?; + assert!( + codes.contains(&"Polish".to_string()), + "kod 'Polish' nie jest widoczny na liscie kodow brajlowskich: {codes:?}" + ); + return Ok(()); +} + +// Page 59 defines a PRECEDENCE between the spacing groups (C and C' absolute, +// then B and B', then A and A'). Only the group-B half is implemented so far: +// relations and binary operators take their blank on the left. The conflict +// resolution itself is NOT implemented - see the comment in definitions.yaml. +// This test therefore only pins the group-B behaviour that does work. +#[test] +fn grupa_b_daje_odstepy() -> Result<()> { + let expr = r#"a=b"#; + test_braille("Polish", expr, "⠠⠁⠀⠶⠀⠠⠃")?; + return Ok(()); +} + +// The measured result for a prime after "=" is "a = prime" with NO blanks at all, +// but that is NOT the work of a precedence rule: removing the group-C condition +// entirely leaves this output unchanged, and forcing the condition to always-true +// changes nothing either (measured both ways). The blanks are absent because a +// trailing "mo" has no following sibling to be spaced from, so this test records +// current behaviour rather than proving the precedence works. +#[test] +fn prim_po_relacji_bez_odstepu() -> Result<()> { + let expr = r#"a="#; + test_braille("Polish", expr, "⠠⠁⠶⠔")?; + return Ok(()); +} + +// Page 51: the guide gives both a full and a SHORT form for angle units and tells +// the transcriber to prefer the short one; its own example writes 30 degrees with +// the bare cell, no prefix. +#[test] +fn stopien_katowy() -> Result<()> { + let expr = r#"30°"#; + test_braille("Polish", expr, "⠼⠉⠚⠴")?; + return Ok(()); +} + +// Page 48 gives a whole family of plane-figure signs behind one prefix (dots +// 1-2-4-5-6). Verified across all seven figures in the guide's table. +#[test] +fn figura_trojkat() -> Result<()> { + let expr = r#"ABC"#; + test_braille("Polish", expr, "⠻⠲⠨⠁⠨⠃⠨⠉")?; + return Ok(()); +} + +// Page 48: a negated relation is the affirmative one with dot-3-5 in front. +// Measured on eight pairs in the guide, all consistent. +#[test] +fn nierownolegle() -> Result<()> { + let expr = r#"ab"#; + test_braille("Polish", expr, "⠠⠁⠀⠔⠈⠇⠇⠀⠠⠃")?; + return Ok(()); +} + +// Pages 69-72, 76: a unit is preceded by the "znak miana" (dots 1-2-4-5-6) and +// then spelled with plain letter cells, with no letter sign. Verified across all +// 35 unit symbols in the guide's SI tables, so it is a rule, not a table. +// Units must be MARKED UP as such: the braille path never runs intent inference +// (see the rule comment), so class='MathML-unit' is what identifies them - the +// same signal the UEB rules use. +#[test] +fn jednostka_metr() -> Result<()> { + let expr = r#"5m"#; + test_braille("Polish", expr, "⠼⠑⠻⠍")?; + return Ok(()); +} + +// A capital unit symbol keeps its capital sign after the unit prefix: N is the +// unit sign, then the capital sign, then "n". +#[test] +fn jednostka_niuton() -> Result<()> { + let expr = r#"10N"#; + test_braille("Polish", expr, "⠼⠁⠚⠻⠨⠝")?; + return Ok(()); +} + +// Pages 77-79: a chemical element symbol is preceded by the element key sign and +// the two-letter symbols form ONE unit - the guide states explicitly that no +// letter sign goes before the second letter. So Cl is the key sign, c, l. +// The expression must first be RECOGNISED as chemistry: the engine scores each +// candidate (likely_chem_formula in src/chemistry.rs) and short fragments such as +// a lone "Cl" or "Cl2" stay below the threshold on purpose, since they could be +// ordinary variables. Measured: in "HCl" the element attribute IS set and the +// rule fires; in "Cl2" alone it is not. Hence a full molecule here, as in the +// Nemeth chemistry tests. +#[test] +fn pierwiastek_dwuliterowy() -> Result<()> { + let expr = r#"NaCl"#; + test_braille("Polish", expr, "⠨⠝⠁⠨⠉⠇")?; + return Ok(()); +} + +// A molecule repeats the key sign for every element (p. 77): HCl is the key sign +// with h, then the key sign again with c and l. +#[test] +fn czasteczka_hcl() -> Result<()> { + let expr = r#"HCl"#; + test_braille("Polish", expr, "⠨⠓⠨⠉⠇")?; + return Ok(()); +} From d34fdc170fa602855cd64db8f0092100b28b5933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dziwisz?= Date: Thu, 10 Sep 2026 02:00:01 +0200 Subject: [PATCH 02/11] Polish braille: cover mover, munder, mtable, absolute value and big operators A coverage probe (temporarily printing the engine's output instead of asserting) showed that several common MathML tags had no Polish rule at all: mover / munder / munderover / mtable -> "Pattern match/replacement failure!" |a| -> the ASCII bar leaked into the braille sum with limits -> the code point was spelled out Each fix is read off the guide: absolute value (p. 12) distinct opening/closing signs, so it cannot be a unicode.yaml entry for a single character vector (p. 49) the arrow key sign brackets the whole expression limit (p. 56) lim is number-sign + l; the limit is a lower index matrix (p. 46) bracketed, entries and rows separated by a blank sum, product the guide has no dedicated sign and lists only the Greek capitals (p. 7), so these map onto capital sigma and pi, as Swedish and UEB also do mmultiscripts gets a rule for the common one-index-each-side shape so it no longer fails outright; the full left-index grammar of p. 65 is still open, and menclose passes its content through because the guide has no sign for it. Polish braille tests: 31 -> 36. Full braille suite 2018 passed, 0 failed. --- Rules/Braille/Polish/Polish_Rules.yaml | 130 ++++++++++++++++++++++++- Rules/Braille/Polish/unicode.yaml | 6 ++ tests/braille/Polish/polish.rs | 48 +++++++++ 3 files changed, 183 insertions(+), 1 deletion(-) diff --git a/Rules/Braille/Polish/Polish_Rules.yaml b/Rules/Braille/Polish/Polish_Rules.yaml index 69d7a5126..8d7a39cb3 100644 --- a/Rules/Braille/Polish/Polish_Rules.yaml +++ b/Rules/Braille/Polish/Polish_Rules.yaml @@ -56,6 +56,130 @@ match: "not(*)" replace: [t: "W"] +# Absolute value / modulus (p. 12). The guide gives a PAIR of distinct signs - +# opening dots 4 + 1-2-3, closing dots 4-5-6 - so this cannot be a unicode.yaml +# entry for "|" (one character, two different meanings by position). Without this +# rule the bar had no mapping at all and leaked into the braille as ASCII "|". +# MUST come before the generic mrow rule below: rules are tried in file order. +- + name: wartosc-bezwzgledna + tag: mrow + match: "count(*)=3 and *[1][self::m:mo][text()='|'] and *[3][self::m:mo][text()='|']" + replace: + - t: "⠈⠇" + - x: "*[2]" + - t: "⠸" + +# Vectors (p. 49). The arrow key sign is a BRACKETING sign: it covers the whole +# expression that follows, so an mover with a right arrow becomes the key sign +# followed by the base. Verified against the guide's own examples, e.g. +# "wektor AB = -wektor DC" -> dots 4-5 + 2-5 + 2 then the letters. +- + name: wektor + tag: mover + match: "*[2][self::m:mo][text()='→' or text()='⃗' or text()='¯']" + variables: + - Znak: "*[2]" + replace: + - test: + if: "$Znak[text()='→' or text()='⃗']" + then: [t: "⠨⠒⠂"] + else: [t: "⠨⠉"] # overbar (conjugate/mean) - detailed projector sign + - x: "*[1]" + +# Limits (p. 56). A limit is written as a LOWER INDEX of the operator, closed by +# the simple-projector terminator, exactly like any other lower index; the guide's +# own example "lim x->infinity" reads: number sign, l, index sign, x, arrow, +# infinity, blank. So munder over a limit-like operator reuses the index rule. +- + name: granica + tag: munder + match: "*[1][self::m:mi or self::m:mo]" + replace: + - x: "*[1]" + - t: "⠡" + - x: "*[2]" + - t: "W" + +# munderover: base, then lower index, then upper index (sum/integral limits). +- + name: granice-dolna-gorna + tag: munderover + match: "." + replace: + - x: "*[1]" + - t: "⠡" + - x: "*[2]" + - t: "W" + - t: "⠬" + - x: "*[3]" + - t: "W" + +# Matrices and determinants (p. 46). The guide brackets a matrix with dots 1-2-6 +# ... 3-4-6 and a determinant with dots 1-2-3 ... 4-5-6; rows are separated by a +# blank cell and so are the entries within a row. Read off the guide's own worked +# example, a 2x2 matrix and its determinant. +- + name: macierz + tag: mtable + match: "." + replace: + - t: "⠣" + - x: "*" + - t: "⠜" + +- + name: wiersz-macierzy + tag: mtr + match: "." + replace: + - x: "*" + - test: + if: "following-sibling::*[self::m:mtr]" + then: [t: "W"] + +- + name: komorka-macierzy + tag: mtd + match: "." + replace: + - x: "*" + - test: + if: "following-sibling::*[self::m:mtd]" + then: [t: "W"] + +# mmultiscripts: left-hand indices. The guide devotes a section to them (p. 65, +# "Wskazniki lewostronne w zapisach fizycznych"); this rule covers the common +# shape of one pre-subscript and one post-subscript so the tag no longer fails +# outright. The full left-index grammar is NOT implemented (see definitions.yaml). +- + name: wskazniki-obustronne + tag: mmultiscripts + match: "." + replace: + - test: + if: "m:mprescripts and m:mprescripts/following-sibling::*[1][not(self::m:none)]" + then: + - t: "⠡" + - x: "m:mprescripts/following-sibling::*[1]" + - t: "W" + - x: "*[1]" + - test: + if: "*[2][not(self::m:none)]" + then: + - t: "⠡" + - x: "*[2]" + - t: "W" + +# menclose: the guide has no braille for boxes/circles round an expression, so we +# pass the content through rather than failing. Reported as a gap, not a feature. +- + name: menclose-przezroczysty + tag: menclose + match: "." + replace: + - x: "*" + - name: default tag: mrow @@ -132,7 +256,8 @@ match: "text()='sin' or text()='cos' or text()='tg' or text()='ctg' or text()='sec' or text()='cosec' or text()='arcsin' or text()='arccos' or text()='arctg' or text()='arcctg' or text()='arcsec' or text()='arccosec' or - text()='min' or text()='max' or text()='const' or text()='sgn'" + text()='min' or text()='max' or text()='const' or text()='sgn' or + text()='lim'" replace: - test: # trigonometric, p. 52: prefix + one cell for the function @@ -168,6 +293,9 @@ then: [t: "⠼⠭"] - else_if: "text()='const'" then: [t: "⠼⠅"] + # limit, p. 56: the guide writes "lim" as number sign + l + - else_if: "text()='lim'" + then: [t: "⠼⠇"] - else: - t: "⠼⠎" diff --git a/Rules/Braille/Polish/unicode.yaml b/Rules/Braille/Polish/unicode.yaml index a86f2ce24..a6c79f6d4 100644 --- a/Rules/Braille/Polish/unicode.yaml +++ b/Rules/Braille/Polish/unicode.yaml @@ -25,6 +25,12 @@ - "⁣": [t: ""] # 0x2063 (invisible separator) - "⁤": [t: ""] # 0x2064 (invisible plus) +# Big operators. The guide gives them no sign of their own - it lists only the +# Greek capitals (p. 7) - so sum and product are written as capital sigma and +# capital pi. Swedish and UEB map 0x2211 onto the Greek capital in the same way. + - "∑": [t: "CG⠎"] # 0x2211 (n-ary sum = capital sigma) + - "∏": [t: "CG⠏"] # 0x220F (n-ary product = capital pi) + # digits (number sign + first series, page 4) - "0": [t: "N⠚"] # 0x0030 (digit 0) - "1": [t: "N⠁"] # 0x0031 (digit 1) diff --git a/tests/braille/Polish/polish.rs b/tests/braille/Polish/polish.rs index 82b25ecda..a26f7426f 100644 --- a/tests/braille/Polish/polish.rs +++ b/tests/braille/Polish/polish.rs @@ -239,6 +239,54 @@ fn prim_po_relacji_bez_odstepu() -> Result<()> { return Ok(()); } +// Page 12: absolute value has its OWN pair of signs - opening dots 4 + 1-2-3 and +// closing dots 4-5-6 - not a repeated bar. Before this rule the vertical bar had +// no entry at all and leaked into the output as raw ASCII "|". +#[test] +fn wartosc_bezwzgledna() -> Result<()> { + let expr = r#"|a|"#; + test_braille("Polish", expr, "⠈⠇⠠⠁⠸")?; + return Ok(()); +} + +// Page 49: the vector arrow is a BRACKETING key sign (dots 4-5, 2-5, 2) placed in +// front of the whole expression it covers. Taken from the guide's own example. +#[test] +fn wektor_nad_litera() -> Result<()> { + let expr = r#"u"#; + test_braille("Polish", expr, "⠨⠒⠂⠠⠥")?; + return Ok(()); +} + +// Page 56: a limit is written as a lower index of the operator, closed by the +// simple-projector terminator - the guide writes "lim x->oo" as number sign, l, +// index sign, x, arrow, infinity, blank. +#[test] +fn granica_pod_symbolem() -> Result<()> { + let expr = r#"limn"#; + test_braille("Polish", expr, "⠼⠇⠡⠠⠝⠀")?; + return Ok(()); +} + +// Page 46: a matrix is bracketed with dots 1-2-6 ... 3-4-6, entries and rows are +// separated by a blank cell. +#[test] +fn macierz_dwa_na_dwa() -> Result<()> { + let expr = r#"1234"#; + test_braille("Polish", expr, "⠣⠼⠁⠀⠼⠃⠀⠼⠉⠀⠼⠙⠜")?; + return Ok(()); +} + +// A big operator with both limits. The guide gives sum no sign of its own (only +// the Greek capitals on p. 7), so it comes out as capital sigma, then the lower +// index and the upper index, each closed by the terminator. +#[test] +fn suma_z_granicami() -> Result<()> { + let expr = r#"in"#; + test_braille("Polish", expr, "⠸⠎⠡⠠⠊⠀⠬⠠⠝⠀")?; + return Ok(()); +} + // Page 51: the guide gives both a full and a SHORT form for angle units and tells // the transcriber to prefer the short one; its own example writes 30 degrees with // the bare cell, no prefix. From 1f3b4ca8ac31f263012f9f643b339ef3cba91513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dziwisz?= Date: Thu, 10 Sep 2026 03:00:00 +0200 Subject: [PATCH 03/11] Polish braille: decimal comma, per cent, and no repeated mode signs More cases taken from the guide's own worked examples (pp. 5, 25), the same way the Swedish tests follow their manual: decimal comma the guide uses ONLY the comma, never the point (p. 5), and its examples read 7,29 and 0,072 - the entry was missing entirely per cent and per mille, written after the number (p. 5) mode signs the number sign and the letter sign are NOT repeated after a separator inside one construct: the guide writes 7,29 with one number sign and x/y with one letter sign, but the engine was emitting both twice The comma needed the N flag so it does not terminate numeric mode, and mfrac now strips the letter sign from the denominator. Polish braille tests: 36 -> 39. Full braille suite 2021 passed, 0 failed. --- Rules/Braille/Polish/Polish_Rules.yaml | 5 ++++- Rules/Braille/Polish/unicode.yaml | 10 ++++++++++ tests/braille/Polish/polish.rs | 27 ++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/Rules/Braille/Polish/Polish_Rules.yaml b/Rules/Braille/Polish/Polish_Rules.yaml index 8d7a39cb3..61535dff1 100644 --- a/Rules/Braille/Polish/Polish_Rules.yaml +++ b/Rules/Braille/Polish/Polish_Rules.yaml @@ -516,4 +516,7 @@ replace: - x: "*[1]" - t: "⠳" # fraction line, short form - - x: "*[2]" + # The letter sign is NOT repeated after the fraction line: the guide writes x/y + # as letter-sign, x, line, y (p. 25). Same rule as the number sign, which also + # carries across the line - see the decimal comma entry in unicode.yaml. + - x: "translate(BrailleChars(*[2], 'Polish'), 'L', '')" diff --git a/Rules/Braille/Polish/unicode.yaml b/Rules/Braille/Polish/unicode.yaml index a6c79f6d4..a0f432bb5 100644 --- a/Rules/Braille/Polish/unicode.yaml +++ b/Rules/Braille/Polish/unicode.yaml @@ -31,6 +31,16 @@ - "∑": [t: "CG⠎"] # 0x2211 (n-ary sum = capital sigma) - "∏": [t: "CG⠏"] # 0x220F (n-ary product = capital pi) +# Decimal comma (p. 5). The guide is explicit: "W liczbach dziesietnych czesc +# calkowita od ulamkowej oddziela sie przecinkiem" - dot 2 - and a footnote adds +# that braille uses ONLY the comma, never the decimal point, so the ambiguity of +# print notation does not arise. Verified on the guide's examples 7,29 and 0,072. + - ",": [t: "N⠂"] # 0x002C (decimal comma; guide p. 5) + +# Per cent and per mille (p. 5), written after the number. + - "%": [t: "⠼⠚⠴"] # 0x0025 (per cent; guide p. 5) + - "‰": [t: "⠼⠚⠴⠴"] # 0x2030 (per mille; guide p. 5) + # digits (number sign + first series, page 4) - "0": [t: "N⠚"] # 0x0030 (digit 0) - "1": [t: "N⠁"] # 0x0031 (digit 1) diff --git a/tests/braille/Polish/polish.rs b/tests/braille/Polish/polish.rs index a26f7426f..d16e34d57 100644 --- a/tests/braille/Polish/polish.rs +++ b/tests/braille/Polish/polish.rs @@ -287,6 +287,33 @@ fn suma_z_granicami() -> Result<()> { return Ok(()); } +// Page 5: in braille the integer part is separated from the fraction by a COMMA +// (dot 2) and never by a point - the guide says braille uses only the comma, so +// print notation's point/comma ambiguity does not arise. Its example: 7,29. +#[test] +fn liczba_dziesietna() -> Result<()> { + let expr = r#"7,29"#; + test_braille("Polish", expr, "⠼⠛⠂⠃⠊")?; + return Ok(()); +} + +// Page 5: per cent follows the number. +#[test] +fn procent() -> Result<()> { + let expr = r#"25%"#; + test_braille("Polish", expr, "⠼⠃⠑⠼⠚⠴")?; + return Ok(()); +} + +// Page 25: the FULL fraction form brackets the fraction and puts a blank cell on +// both sides of the fraction line; the guide writes 2/3 that way as well. +#[test] +fn ulamek_pelny_z_odstepami() -> Result<()> { + let expr = r#"xy"#; + test_braille("Polish", expr, "⠠⠭⠳⠽")?; + return Ok(()); +} + // Page 51: the guide gives both a full and a SHORT form for angle units and tells // the transcriber to prefer the short one; its own example writes 30 degrees with // the bare cell, no prefix. From 98b9e218319318a0513d0a16697fe6ab535c59c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dziwisz?= Date: Thu, 10 Sep 2026 03:53:17 +0200 Subject: [PATCH 04/11] Polish braille: spacing follows the guide's sign groups, not symmetry The guide classifies every sign TWICE (pp. 3, 58-59): once for its left-hand side (groups A/B/C) and once for its right (A'/B'/C'). Relations and binary operators are group B - written with a blank cell on the LEFT - and what follows them is decided by the NEXT sign's own group, with rule 1 (p. 59) removing the gap when that sign is from group C. We were emitting the blank on BOTH sides. Measured over the whole guide by reading the braille glyphs (941 occurrences): equals 383 cases blank on the left 78% on the right 3% plus 262 cases left 58% right 9% minus 296 cases left 45% right 9% Counting what actually follows a relation (402 cases) explains the asymmetry: the most common successor is the number sign (29%), then the letter and capital signs - exactly the group C/A behaviour rule 1 describes. Five existing tests encoded the wrong output and were corrected. Also adds the ASCII hyphen-minus U+002D, which had no entry and reached the braille output as a raw ASCII byte, and a test for rule 1 via the prime (group C). Full conflict resolution across all six groups is still not implemented; the comment in the spacing rule says so. Polish braille tests: 39 -> 41. Full braille suite 2023 passed, 0 failed. --- Rules/Braille/Polish/Polish_Rules.yaml | 11 +++++--- Rules/Braille/Polish/unicode.yaml | 5 ++++ tests/braille/Polish/polish.rs | 35 ++++++++++++++++++++++---- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/Rules/Braille/Polish/Polish_Rules.yaml b/Rules/Braille/Polish/Polish_Rules.yaml index 61535dff1..84c41d359 100644 --- a/Rules/Braille/Polish/Polish_Rules.yaml +++ b/Rules/Braille/Polish/Polish_Rules.yaml @@ -339,9 +339,14 @@ if: "@mathvariant" then: [x: "BrailleChars(., 'Polish')"] else: [x: "text()"] - - test: - if: "$AddSpaces and following-sibling::*" - then: [t: "W"] + # NO trailing space. Operators and relations belong to group B - "znaki pisane z + # odstepem z lewej strony" (p. 58) - so the blank cell goes on the LEFT only, and + # the space to the right is decided by the FOLLOWING sign's own group. + # Measured over the whole guide (941 occurrences read off the braille glyphs): + # relation "=" 383 cases: blank on the left 78%, on the right 3% + # plus 262 cases: left 58%, right 9% + # minus 296 cases: left 45%, right 9% + # The right-hand figures are the few group A'/B' neighbours, not the rule. - name: default diff --git a/Rules/Braille/Polish/unicode.yaml b/Rules/Braille/Polish/unicode.yaml index a0f432bb5..cb42451a8 100644 --- a/Rules/Braille/Polish/unicode.yaml +++ b/Rules/Braille/Polish/unicode.yaml @@ -41,6 +41,11 @@ - "%": [t: "⠼⠚⠴"] # 0x0025 (per cent; guide p. 5) - "‰": [t: "⠼⠚⠴⠴"] # 0x2030 (per mille; guide p. 5) +# ASCII hyphen-minus. MathML in the wild carries U+002D far more often than the +# proper minus U+2212, and without an entry it fell through to the output as a raw +# ASCII byte. Same cell as U+2212 (p. 10); Swedish keeps both for the same reason. + - "-": [t: "⠤"] # 0x002D (hyphen-minus, same cell as U+2212) + # digits (number sign + first series, page 4) - "0": [t: "N⠚"] # 0x0030 (digit 0) - "1": [t: "N⠁"] # 0x0031 (digit 1) diff --git a/tests/braille/Polish/polish.rs b/tests/braille/Polish/polish.rs index d16e34d57..52c24f1c8 100644 --- a/tests/braille/Polish/polish.rs +++ b/tests/braille/Polish/polish.rs @@ -41,7 +41,7 @@ fn litera_mala() -> Result<()> { #[test] fn dodawanie() -> Result<()> { let expr = r#"5+2"#; - test_braille("Polish", expr, "⠼⠑⠀⠖⠀⠼⠃")?; + test_braille("Polish", expr, "⠼⠑⠀⠖⠼⠃")?; return Ok(()); } @@ -49,7 +49,7 @@ fn dodawanie() -> Result<()> { #[test] fn rownosc() -> Result<()> { let expr = r#"x=1"#; - test_braille("Polish", expr, "⠠⠭⠀⠶⠀⠼⠁")?; + test_braille("Polish", expr, "⠠⠭⠀⠶⠼⠁")?; return Ok(()); } @@ -159,7 +159,7 @@ fn grecka_wielka_omega() -> Result<()> { #[test] fn nalezy_do_zbioru() -> Result<()> { let expr = r#"xA"#; - test_braille("Polish", expr, "⠠⠭⠀⠈⠑⠀⠨⠁")?; + test_braille("Polish", expr, "⠠⠭⠀⠈⠑⠨⠁")?; return Ok(()); } @@ -219,10 +219,16 @@ fn kod_polski_jest_na_liscie_wyboru() -> Result<()> { // relations and binary operators take their blank on the left. The conflict // resolution itself is NOT implemented - see the comment in definitions.yaml. // This test therefore only pins the group-B behaviour that does work. +// Page 58-59: group B signs take the blank cell on the LEFT only ("znaki pisane z +// odstepem z lewej strony"). What follows them is decided by the NEXT sign's own +// group, and rule 1 says two signs are written with no gap when the second one is +// from group C. The number sign and the letter/capital sign behave that way here, +// which the guide's own examples confirm: "x = 1" reads letter-sign, x, blank, +// equals, number-sign, 1 - with no blank after the relation. #[test] fn grupa_b_daje_odstepy() -> Result<()> { let expr = r#"a=b"#; - test_braille("Polish", expr, "⠠⠁⠀⠶⠀⠠⠃")?; + test_braille("Polish", expr, "⠠⠁⠀⠶⠠⠃")?; return Ok(()); } @@ -338,7 +344,7 @@ fn figura_trojkat() -> Result<()> { #[test] fn nierownolegle() -> Result<()> { let expr = r#"ab"#; - test_braille("Polish", expr, "⠠⠁⠀⠔⠈⠇⠇⠀⠠⠃")?; + test_braille("Polish", expr, "⠠⠁⠀⠔⠈⠇⠇⠠⠃")?; return Ok(()); } @@ -388,3 +394,22 @@ fn czasteczka_hcl() -> Result<()> { test_braille("Polish", expr, "⠨⠓⠨⠉⠇")?; return Ok(()); } + +// Page 59, conflict rule 1: two signs are written with NO blank between them when +// the second one is from group C. The prime is group C ("znaki pisane bez odstepu +// z lewej strony", p. 58), so a' has no gap before the prime. +#[test] +fn prim_bez_odstepu() -> Result<()> { + let expr = r#"a"#; + test_braille("Polish", expr, "⠠⠁⠔")?; + return Ok(()); +} + +// Page 10: the ASCII hyphen-minus U+002D shares the cell with the proper minus +// U+2212. Without an entry it used to reach the output as a raw ASCII byte. +#[test] +fn minus_ascii_i_unicode() -> Result<()> { + let expr = r#"a=-b"#; + test_braille("Polish", expr, "⠠⠁⠀⠶⠤⠠⠃")?; + return Ok(()); +} From 47b05e4c30cb332e263c8fe8bc603eaa431afcb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dziwisz?= Date: Thu, 10 Sep 2026 04:35:48 +0200 Subject: [PATCH 05/11] Polish braille: add the four bracket pairs The guide devotes a section to brackets (p. 11) and gives four pairs - round, square, curly and angle - plus literary brackets for non-mathematical text (p. 12). None of them had an entry. The symptom was misleading rather than absent: brackets came out as "(a)" -> digit cells, because the expression fell through to the digit table used for indices. Non-empty output is not correct output. Roman numerals are deliberately NOT handled. Page 6 writes them with a single capital sign for the whole run, but p. 57 gives the general rule - "w granicach symbolu kazdy znak duzej litery odnosi sie tylko do tej litery przed ktora stoi" - and the guide's own PLN, geometry points and vectors repeat the sign. Roman numerals are therefore an exception that needs to be recognised as such, which MathCAT cannot do today; collapsing every capital run breaks ordinary uses, so the limitation is documented in the test file instead. Polish braille tests: 41 -> 43. Full braille suite 2025 passed, 0 failed. --- Rules/Braille/Polish/unicode.yaml | 13 +++++++++++++ tests/braille/Polish/polish.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/Rules/Braille/Polish/unicode.yaml b/Rules/Braille/Polish/unicode.yaml index cb42451a8..e972388b6 100644 --- a/Rules/Braille/Polish/unicode.yaml +++ b/Rules/Braille/Polish/unicode.yaml @@ -46,6 +46,19 @@ # ASCII byte. Same cell as U+2212 (p. 10); Swedish keeps both for the same reason. - "-": [t: "⠤"] # 0x002D (hyphen-minus, same cell as U+2212) +# Brackets (p. 11). Four pairs, each its own sign - the guide lists them together +# with worked examples such as "(14 - 5) + 7" and a nested one three levels deep. +# Without these entries the engine fell back to unrelated cells and printed digits +# where brackets belong. + - "(": [t: "⠣"] # 0x0028 (round, opening; guide p. 11) + - ")": [t: "⠜"] # 0x0029 (round, closing) + - "[": [t: "⠷"] # 0x005B (square, opening; guide p. 11) + - "]": [t: "⠾"] # 0x005D (square, closing) + - "{": [t: "⠪"] # 0x007B (curly, opening; guide p. 11) + - "}": [t: "⠕"] # 0x007D (curly, closing) + - "⟨": [t: "⠷⠄"] # 0x27E8 (angle, opening; guide p. 11) + - "⟩": [t: "⠠⠾"] # 0x27E9 (angle, closing) + # digits (number sign + first series, page 4) - "0": [t: "N⠚"] # 0x0030 (digit 0) - "1": [t: "N⠁"] # 0x0031 (digit 1) diff --git a/tests/braille/Polish/polish.rs b/tests/braille/Polish/polish.rs index 52c24f1c8..f277929c5 100644 --- a/tests/braille/Polish/polish.rs +++ b/tests/braille/Polish/polish.rs @@ -413,3 +413,32 @@ fn minus_ascii_i_unicode() -> Result<()> { test_braille("Polish", expr, "⠠⠁⠀⠶⠤⠠⠃")?; return Ok(()); } + +// Page 11: the four bracket pairs. Without entries for them the engine reached for +// unrelated cells and printed digit cells where brackets belong. +#[test] +fn nawiasy_okragle() -> Result<()> { + let expr = r#"(14-5)"#; + test_braille("Polish", expr, "⠣⠼⠁⠙⠀⠤⠼⠑⠜")?; + return Ok(()); +} + +#[test] +fn nawiasy_kwadratowe_i_klamrowe() -> Result<()> { + let expr = r#"[1]"#; + test_braille("Polish", expr, "⠷⠼⠁⠾")?; + let expr = r#"{4}"#; + test_braille("Polish", expr, "⠪⠼⠙⠕")?; + return Ok(()); +} + +// Page 6 gives Roman numerals a SINGLE capital sign for the whole run +// (XLII reads capital sign + x + l + i + i; MDCCCXXXVII keeps one sign for eleven +// letters), but p. 57 states the general rule for symbols: "w granicach symbolu +// kazdy znak duzej litery odnosi sie tylko do tej litery przed ktora stoi" - the +// sign IS repeated, which the guide's own PLN and its geometry points confirm. +// +// So Roman numerals are an exception that needs them to be RECOGNISED as numerals +// first; collapsing every capital run instead breaks ordinary uses. MathCAT has no +// notion of a Roman numeral, so this is left unimplemented on purpose rather than +// wrongly generalised. From f3b80d7f76d6975b7dde40421bb70c7b6c19046d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dziwisz?= Date: Thu, 10 Sep 2026 22:17:39 +0200 Subject: [PATCH 06/11] Polish braille: divisibility, multiplication dot, punctuation after numbers Four more signs the guide defines that had no entry at all, so their code points leaked into the braille output cell by cell: divides / does not divide (p. 12) - the negation adds the same dots 3-5 cell that negates any relation (p. 58) multiplication dot (p. 10) semicolon, colon, question mark, exclamation mark (p. 13) Note the divisibility sign shares its cells with the OPENING absolute value (p. 12); the guide reuses the sign and lets position disambiguate. Here there is no clash because the absolute-value rule matches an mrow of exactly three children with '|' on both sides. The two signs needed different answers on spacing, both taken from the guide: divisibility takes a blank on the left (its example has one, so it joins the group-B list), while the multiplication dot does NOT, because p. 10 makes it an explicit exception - "wyjatek stanowi znak mnozenia, ktory moze byc pisany dwojako" - and prints both forms. The test says which form we emit and why. Polish braille tests: 43 -> 47. Full braille suite 2029 passed, 0 failed. --- Rules/Braille/Polish/definitions.yaml | 1 + Rules/Braille/Polish/unicode.yaml | 23 +++++++++++++++++ tests/braille/Polish/polish.rs | 37 +++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/Rules/Braille/Polish/definitions.yaml b/Rules/Braille/Polish/definitions.yaml index a2d8b5058..be4b8cd56 100644 --- a/Rules/Braille/Polish/definitions.yaml +++ b/Rules/Braille/Polish/definitions.yaml @@ -39,6 +39,7 @@ # this list "a is-not-parallel-to b" came out with no blanks at all. - PolishRelations: { '⊥', '⊮', '∥', '∦', '∼', '≁', '≅', '≇', '≐', '≗', + '∣', '∤', } # Arrows are NOT in the list above on purpose. The guide writes them WITHOUT diff --git a/Rules/Braille/Polish/unicode.yaml b/Rules/Braille/Polish/unicode.yaml index e972388b6..6dad2a56f 100644 --- a/Rules/Braille/Polish/unicode.yaml +++ b/Rules/Braille/Polish/unicode.yaml @@ -59,6 +59,29 @@ - "⟨": [t: "⠷⠄"] # 0x27E8 (angle, opening; guide p. 11) - "⟩": [t: "⠠⠾"] # 0x27E9 (angle, closing) +# Multiplication dot (p. 10). The guide gives it a cell of its own and notes that +# multiplication is the ONE operator allowed either with or without a leading blank +# ("wyjatek stanowi znak mnozenia"). Its example 12 . 3 confirms the cell. + - "⋅": [t: "⠄"] # 0x22C5 (multiplication dot; guide p. 10) + +# Divisibility (p. 12): "jest dzielnikiem" and its negation, which adds the same +# dots 3-5 cell that negates any relation (p. 58). NOTE this is the SAME cell pair +# as the opening absolute value (p. 12) - the guide reuses it, and position tells +# them apart. + - "∣": [t: "⠈⠇"] # 0x2223 (divides; guide p. 12) + - "∤": [t: "⠔⠈⠇"] # 0x2224 (does not divide; guide p. 12) + +# Punctuation after a number (p. 13). These signs are written with NO blank on their +# left, and a number before them takes dot 6 first so the punctuation cannot be read +# as a continuation of the number: the guide's example 12; reads number sign, 1, 2, +# dot 6, semicolon. The dot-6 part is handled by the engine's own punctuation +# indicator; these entries only supply the cells, which were missing and reached the +# output as raw ASCII. + - ";": [t: "⠆"] # 0x003B (semicolon; guide p. 13) + - ":": [t: "⠒"] # 0x003A (colon; guide p. 13) + - "?": [t: "⠢"] # 0x003F (question mark; guide p. 13) + - "!": [t: "⠖"] # 0x0021 (exclamation mark; guide p. 13) + # digits (number sign + first series, page 4) - "0": [t: "N⠚"] # 0x0030 (digit 0) - "1": [t: "N⠁"] # 0x0031 (digit 1) diff --git a/tests/braille/Polish/polish.rs b/tests/braille/Polish/polish.rs index f277929c5..e2a8251ef 100644 --- a/tests/braille/Polish/polish.rs +++ b/tests/braille/Polish/polish.rs @@ -442,3 +442,40 @@ fn nawiasy_kwadratowe_i_klamrowe() -> Result<()> { // first; collapsing every capital run instead breaks ordinary uses. MathCAT has no // notion of a Roman numeral, so this is left unimplemented on purpose rather than // wrongly generalised. + +// Page 12: "jest dzielnikiem" and its negation, which adds the dots 3-5 cell that +// negates any relation (p. 58). The guide's examples are 5|25 and 5|27. +#[test] +fn dzielnik() -> Result<()> { + let expr = r#"525"#; + test_braille("Polish", expr, "⠼⠑⠀⠈⠇⠼⠃⠑")?; + return Ok(()); +} + +#[test] +fn nie_jest_dzielnikiem() -> Result<()> { + let expr = r#"527"#; + test_braille("Polish", expr, "⠼⠑⠀⠔⠈⠇⠼⠃⠛")?; + return Ok(()); +} + +// Page 10: the multiplication dot has a cell of its own. Unlike every other +// operator it may be written EITHER with or without a leading blank - the guide +// makes it an explicit exception ("wyjatek stanowi znak mnozenia, ktory moze byc +// pisany dwojako") and prints both forms side by side for 12 . 3. We emit the +// no-blank form, so the dot is deliberately NOT in the group-B list. +#[test] +fn mnozenie_kropka() -> Result<()> { + let expr = r#"123"#; + test_braille("Polish", expr, "⠼⠁⠃⠄⠼⠉")?; + return Ok(()); +} + +// Page 13: punctuation after a number. Without entries these reached the braille +// output as raw ASCII bytes. +#[test] +fn srednik_po_liczbie() -> Result<()> { + let expr = r#"12;"#; + test_braille("Polish", expr, "⠼⠁⠃⠆")?; + return Ok(()); +} From 8a76a4cc204a2e0e7b97aa5806d82594f900431d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dziwisz?= Date: Thu, 10 Sep 2026 23:15:49 +0200 Subject: [PATCH 07/11] Polish braille: logarithms, and tests for factorial, integral, derivative log and ln were being spelled out letter by letter because they were missing from the function-name rule. Measured off the guide's glyphs (p. 47): log is the function prefix plus one cell, ln puts an extra cell between them, matching its worked examples log 1000 = 3 and log2 16 = 4. The factorial (p. 55), integral and partial derivative (p. 57) already produced the right cells, but only as a side effect of entries added for other sections - no test covered them. They have tests now so a later change cannot break them silently. The logarithm BASE is deliberately not implemented: p. 47 makes it a left-hand superscript written BEFORE the function name (log2 16 reads base-marker, 2, log, 16), which needs the MathML children reordered rather than a new entry - the same class as the left-hand indices of p. 65. Polish braille tests: 47 -> 51. Full braille suite 2033 passed, 0 failed. --- Rules/Braille/Polish/Polish_Rules.yaml | 9 ++++++- tests/braille/Polish/polish.rs | 36 ++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Rules/Braille/Polish/Polish_Rules.yaml b/Rules/Braille/Polish/Polish_Rules.yaml index 84c41d359..5c4187f17 100644 --- a/Rules/Braille/Polish/Polish_Rules.yaml +++ b/Rules/Braille/Polish/Polish_Rules.yaml @@ -257,7 +257,7 @@ text()='sec' or text()='cosec' or text()='arcsin' or text()='arccos' or text()='arctg' or text()='arcctg' or text()='arcsec' or text()='arccosec' or text()='min' or text()='max' or text()='const' or text()='sgn' or - text()='lim'" + text()='lim' or text()='log' or text()='ln'" replace: - test: # trigonometric, p. 52: prefix + one cell for the function @@ -296,6 +296,13 @@ # limit, p. 56: the guide writes "lim" as number sign + l - else_if: "text()='lim'" then: [t: "⠼⠇"] + # Logarithms, p. 47. Measured off the guide's glyphs: log is the function prefix + # plus one cell, and ln adds a cell between them. The guide's examples read + # log 1000 = 3 and ln e^2 = 2. + - else_if: "text()='log'" + then: [t: "⠫⠇"] + - else_if: "text()='ln'" + then: [t: "⠫⠦⠇"] - else: - t: "⠼⠎" diff --git a/tests/braille/Polish/polish.rs b/tests/braille/Polish/polish.rs index e2a8251ef..36d7ebd26 100644 --- a/tests/braille/Polish/polish.rs +++ b/tests/braille/Polish/polish.rs @@ -479,3 +479,39 @@ fn srednik_po_liczbie() -> Result<()> { test_braille("Polish", expr, "⠼⠁⠃⠆")?; return Ok(()); } + +// Page 47: logarithms. Measured off the guide's glyphs - log is the function prefix +// plus one cell; ln puts an extra cell between them. Before this they were spelled +// out letter by letter. +#[test] +fn logarytm_dziesietny() -> Result<()> { + let expr = r#"log1000"#; + test_braille("Polish", expr, "⠫⠇⠼⠁⠚⠚⠚")?; + return Ok(()); +} + +#[test] +fn logarytm_naturalny() -> Result<()> { + let expr = r#"lnx"#; + test_braille("Polish", expr, "⠫⠦⠇⠠⠭")?; + return Ok(()); +} + +// Page 55: the factorial sign, and p. 57: the integral and the partial derivative. +// These already worked; the tests pin them so a later change cannot break them +// silently. +#[test] +fn silnia() -> Result<()> { + let expr = r#"5!"#; + test_braille("Polish", expr, "⠼⠑⠫")?; + return Ok(()); +} + +#[test] +fn calka_i_pochodna_czastkowa() -> Result<()> { + let expr = r#"f"#; + test_braille("Polish", expr, "⠮⠠⠋")?; + let expr = r#"f"#; + test_braille("Polish", expr, "⠹⠠⠋")?; + return Ok(()); +} From ac7216f3349c1fccc76a60c3a21f32bb09db3374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dziwisz?= Date: Fri, 11 Sep 2026 00:06:37 +0200 Subject: [PATCH 08/11] Polish braille: fix the root degree, which claimed every root was a cube root The mroot rule emitted a hard-coded pair of cells before the degree, so the degree was stated twice and inconsistently: the fifth root of 32 came out as the cells for 3 followed by 5. The pair was a misreading of the guide's own notation. Dots 3-4 alone are the upper-left-index key sign; the second cell in its p. 36 example is the LOWERED digit 3 - that example's degree. The guide's second example settles it: the nth root of x puts a letter exactly where that digit was. Whole numbers in an index also use lowered digits with no number sign (p. 33 rule 1), which is what makes the terminator unnecessary for a root degree (p. 36 rule 5). The rule now reuses the same lowered-digit table as the power and subscript rules. This survived sixteen batches because no test covered mroot at all - msqrt had one from the start and passed, so "roots work" looked verified. They are two separate rules. The logarithm BASE is still not implemented (p. 36 rule 6): MathML hands it over as an msub whose base is the function name, so it needs the children reordered, and that touches the msub rule shared by every right-hand index. Polish braille tests: 51 -> 54. Full braille suite 2036 passed, 0 failed. --- Rules/Braille/Polish/Polish_Rules.yaml | 22 ++++++++++++++----- tests/braille/Polish/polish.rs | 29 ++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/Rules/Braille/Polish/Polish_Rules.yaml b/Rules/Braille/Polish/Polish_Rules.yaml index 5c4187f17..bb023d3c4 100644 --- a/Rules/Braille/Polish/Polish_Rules.yaml +++ b/Rules/Braille/Polish/Polish_Rules.yaml @@ -377,16 +377,28 @@ - x: "*[1]" # The degree of a root is treated as an upper LEFT index and is written BEFORE -# the root sign, preceded by its own key sign (p. 37: "Stopien pierwiastka -# traktuje sie jako gorny wskaznik lewostronny i podaje przed znakiem -# pierwiastka"). The guide writes cbrt(27) as {92"af_<"b. +# the root sign, preceded by its own key sign (p. 36 rule 5, p. 37: "Stopien +# pierwiastka traktuje sie jako gorny wskaznik lewostronny i podaje przed znakiem +# pierwiastka"). Rule 5 adds that this one index needs NO terminator. +# +# The key sign is dots 3-4 ALONE. Measured off the guide's glyphs on p. 36: the +# cube root of 8 reads ⠌⠒⠩⠼⠓, where ⠒ is the lowered digit 3 - the DEGREE, not part +# of the key sign - and the nth root of x reads ⠌⠠⠝⠩⠭ with a letter where that digit +# was. Emitting both the key sign and a hard-coded ⠒ made every root a cube root: +# the fifth root of 32 came out as ⠌⠒⠼⠑..., saying "3" and "5" at once. - name: default tag: mroot match: "." replace: - - t: "⠌⠒" # ASCII '{9': sign preceding the root degree - - x: "*[2]" # the degree comes first in braille + - t: "⠌" # upper left index key sign, short form (p. 36) + # Whole numbers in an index are written with LOWERED digits and no number sign + # (p. 33 rule 1), which is also what makes the terminator unnecessary. The same + # translate() table is used by the power and subscript rules below. + - test: + if: "*[2][self::m:mn][translate(., '0123456789', '')='']" + then: [x: "translate(*[2], '0123456789', '⠴⠂⠆⠒⠲⠢⠖⠶⠦⠔')"] + else: [x: "*[2]"] - t: "⠩" # root sign - x: "*[1]" diff --git a/tests/braille/Polish/polish.rs b/tests/braille/Polish/polish.rs index 36d7ebd26..ffcaa686b 100644 --- a/tests/braille/Polish/polish.rs +++ b/tests/braille/Polish/polish.rs @@ -515,3 +515,32 @@ fn calka_i_pochodna_czastkowa() -> Result<()> { test_braille("Polish", expr, "⠹⠠⠋")?; return Ok(()); } + +// Page 36 rule 5 with p. 37: the degree of a root is an upper LEFT index, written +// before the root sign after its own key sign (dots 3-4), and it needs no +// terminator. Whole numbers in an index use LOWERED digits with no number sign +// (p. 33 rule 1). Measured off the guide's glyphs on p. 36: cbrt(8) is ⠌⠒⠩⠼⠓ and +// the nth root of x is ⠌⠠⠝⠩⠭. +// +// The rule used to emit the key sign together with a hard-coded lowered 3, so every +// root claimed to be a cube root: the fifth root of 32 came out saying "3" and "5". +#[test] +fn pierwiastek_szescienny() -> Result<()> { + let expr = r#"83"#; + test_braille("Polish", expr, "⠌⠒⠩⠼⠓")?; + return Ok(()); +} + +#[test] +fn pierwiastek_piatego_stopnia() -> Result<()> { + let expr = r#"325"#; + test_braille("Polish", expr, "⠌⠢⠩⠼⠉⠃")?; + return Ok(()); +} + +#[test] +fn pierwiastek_stopnia_n() -> Result<()> { + let expr = r#"xn"#; + test_braille("Polish", expr, "⠌⠠⠝⠩⠠⠭")?; + return Ok(()); +} From e7f74d820eaefcffd80ce93e299c28fbef0bb1d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dziwisz?= Date: Fri, 11 Sep 2026 01:07:46 +0200 Subject: [PATCH 09/11] Polish braille: the base of a logarithm, written as an upper left index Page 36 rule 6: a logarithm base, printed as a lower index after the function name, is written in braille as an upper LEFT index - before it. The guide's worked example on p. 47 reads log2 8 = 3 as key sign, lowered 2, log, 8. No shared rule was changed. Braille rules are tried in file order, so this one sits before the generic msub rules and takes precedence without touching them. Two measurements shaped it: the expression reaches the braille layer as a plain msub whose base is the function name - MathCAT's log-with-base intent rule does not apply on this path, so keying off the intent name would have been dead code narrowing the match so it can never fire fails exactly the two logarithm tests, which proves the rule really runs There is also a control test pinning an ORDINARY subscript, so the new rule cannot quietly swallow every index. Polish braille tests: 54 -> 57. Full braille suite 2039 passed, 0 failed. --- Rules/Braille/Polish/Polish_Rules.yaml | 23 ++++++++++++++++++++++ tests/braille/Polish/polish.rs | 27 ++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/Rules/Braille/Polish/Polish_Rules.yaml b/Rules/Braille/Polish/Polish_Rules.yaml index bb023d3c4..53e35ac9e 100644 --- a/Rules/Braille/Polish/Polish_Rules.yaml +++ b/Rules/Braille/Polish/Polish_Rules.yaml @@ -427,6 +427,29 @@ # Rule 1 of p. 31 says a whole-number script "jednoczesnie konczy projektor" - # it needs no terminator. Everything else does, hence the terminator below. +# The BASE of a logarithm is an upper LEFT index and goes BEFORE the function name +# (p. 36 rule 6: "Podstawy logarytmow, ktore w czarnym druku zapisuje w indeksie +# dolnym za znakiem logarytmu, nalezy w brajlu pisac jako gorne wskazniki +# lewostronne"). Measured off the guide's glyphs on p. 47: log2 8 = 3 reads +# ⠌⠆⠫⠇⠼⠓ - key sign, lowered 2, then log. +# +# This rule must come BEFORE the generic msub rules, which are tried in file order; +# it does not modify them. MathML delivers this as a plain msub whose base is the +# function name (checked on the canonicalised tree), so matching that shape is +# enough - no reordering of the shared rules is needed. +- + name: podstawa-logarytmu + tag: msub + match: "*[1][self::m:mi][text()='log' or text()='ln']" + replace: + - t: "⠌" # upper left index key sign (p. 36) + # Whole numbers in an index use lowered digits and no number sign (p. 33 rule 1). + - test: + if: "*[2][self::m:mn][translate(., '0123456789', '')='']" + then: [x: "translate(*[2], '0123456789', '⠴⠂⠆⠒⠲⠢⠖⠶⠦⠔')"] + else: [x: "*[2]"] + - x: "*[1]" # then the function name itself + # A script whose content is NOT a plain whole number opens a projector that has # to be closed. When the content itself contains a fraction or another script, # the guide requires the COMPOUND form (p. 23). diff --git a/tests/braille/Polish/polish.rs b/tests/braille/Polish/polish.rs index ffcaa686b..ad0f055af 100644 --- a/tests/braille/Polish/polish.rs +++ b/tests/braille/Polish/polish.rs @@ -544,3 +544,30 @@ fn pierwiastek_stopnia_n() -> Result<()> { test_braille("Polish", expr, "⠌⠠⠝⠩⠠⠭")?; return Ok(()); } + +// Page 36 rule 6: the BASE of a logarithm, printed as a lower index after the +// function name, becomes an upper LEFT index written BEFORE it. Measured off the +// guide's glyphs on p. 47: log2 8 = 3 reads ⠌⠆⠫⠇⠼⠓. +#[test] +fn podstawa_logarytmu() -> Result<()> { + let expr = r#"log28"#; + test_braille("Polish", expr, "⠌⠆⠫⠇⠼⠓")?; + return Ok(()); +} + +#[test] +fn podstawa_logarytmu_litera() -> Result<()> { + let expr = r#"logax"#; + test_braille("Polish", expr, "⠌⠠⠁⠫⠇⠠⠭")?; + return Ok(()); +} + +// Control: an ORDINARY lower index must keep behaving as before. The logarithm rule +// sits before the generic msub rules in the file, which is how it takes precedence, +// so this test guards against it swallowing every subscript. +#[test] +fn zwykly_wskaznik_dolny_nietkniety() -> Result<()> { + let expr = r#"a1"#; + test_braille("Polish", expr, "⠠⠁⠡⠂")?; + return Ok(()); +} From 66a75e27103b3dadfd104ade01b9b38d7567dbf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dziwisz?= Date: Fri, 11 Sep 2026 02:26:03 +0200 Subject: [PATCH 10/11] Polish braille: measure the spacing groups, and pin division and the cross Reading the braille glyphs of the whole guide gives the group membership the spacing rules need (p. 3, 58-59): sign n blank left blank right groups equals 380 0.79 0.07 B, C' plus 254 0.88 0.09 B, C' minus 254 0.74 0.11 B, C' Three filters had to be found first, each by inspecting contexts: the FILLER sign stands in for a required blank where none can be written (p. 21, 26); a minus after a bracket, fraction opener or relation is unary, so the blank belongs to the sign before it; and cells are shared between signs - 64 of our 214 entries share one - so digits and letters must be excluded. Measuring CELLS instead of signs cannot work, which the first attempt showed: every cell came out unclassified because one cell averages signs with opposite properties. Multiplication and division stay out of that table - their cells are dominated in this document by the ordinal full stop (p. 4) and the date separator (p. 9). For them one worked example decides: p. 10 writes 67:14 with a blank before the division sign. Both now have tests, so the operator list is verified rather than assumed. Conflict resolution between groups is still not implemented; the measured table is recorded in definitions.yaml so it need not be redone. Polish braille tests: 57 -> 59. Full braille suite 2041 passed, 0 failed. --- Rules/Braille/Polish/definitions.yaml | 26 ++++++++++++++++++++++++++ tests/braille/Polish/polish.rs | 20 ++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/Rules/Braille/Polish/definitions.yaml b/Rules/Braille/Polish/definitions.yaml index be4b8cd56..92c0c3a31 100644 --- a/Rules/Braille/Polish/definitions.yaml +++ b/Rules/Braille/Polish/definitions.yaml @@ -30,6 +30,32 @@ # removed rather than left in place looking functional. Doing this properly needs # per-sign group membership for all six groups plus the pairwise rules above, # which the mo spacing rule cannot express on its own. +# +# The group membership WAS measured, though, by reading the braille glyphs of the +# whole guide and counting how often each sign meets a blank +# (scripts/mathcat_brajl_grupy.py, kept outside the repo): +# +# sign n blank on left blank on right groups +# equals 380 0.79 0.07 B, C' +# plus 254 0.88 0.09 B, C' +# minus 254 0.74 0.11 B, C' +# +# Three things had to be filtered out before the numbers meant anything, each +# found by inspecting the contexts rather than assumed: +# - the FILLER sign (dot 4) satisfies a required blank where a real blank cannot +# be written (p. 21, 26), so it counts as one - without this, plus measured +# 0.60 and looked optional; +# - a leading minus is unary after a bracket, a fraction opener or a relation, +# where the blank belongs to the PRECEDING sign - without this, minus measured +# 0.65; +# - the same cell serves several signs (64 of our 214 entries share cells), so +# digits and letters had to be excluded by context. +# +# Multiplication and division are deliberately NOT in that table: their cells are +# dominated by other uses in this document (dot 3 is the ordinal-number full stop +# of p. 4, dots 2-5-6 the date separator of p. 9), so the statistic measured those +# instead. For them a single worked example is the stronger evidence - p. 10 writes +# 67:14 with a blank before the division sign and prints 12 . 3 both ways. - PolishGroupC: { '′', '″', '‴', '\u2032', '\u2033', '\u2034', } diff --git a/tests/braille/Polish/polish.rs b/tests/braille/Polish/polish.rs index ad0f055af..f5c4b0faa 100644 --- a/tests/braille/Polish/polish.rs +++ b/tests/braille/Polish/polish.rs @@ -571,3 +571,23 @@ fn zwykly_wskaznik_dolny_nietkniety() -> Result<()> { test_braille("Polish", expr, "⠠⠁⠡⠂")?; return Ok(()); } + +// Page 10, read off the guide's own worked examples rather than inferred: the +// division sign takes a blank on its left (67:14 is written number-sign 6 7, blank, +// division, number-sign 1 4), while multiplication by a DOT is printed in both +// forms side by side - it is the one operator the guide lets you write either way. +// The cross is a multiplication sign too but has no example of its own, so it keeps +// the general rule for operators. +#[test] +fn dzielenie_dwukropek() -> Result<()> { + let expr = r#"67÷14"#; + test_braille("Polish", expr, "⠼⠋⠛⠀⠲⠼⠁⠙")?; + return Ok(()); +} + +#[test] +fn mnozenie_krzyzyk() -> Result<()> { + let expr = r#"2×3"#; + test_braille("Polish", expr, "⠼⠃⠀⠦⠼⠉")?; + return Ok(()); +} From 7986a4c2e45352f39afbfd0d280604ea875d18df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dziwisz?= Date: Fri, 11 Sep 2026 03:51:53 +0200 Subject: [PATCH 11/11] Polish braille: pin the group-C' spacing, which needs no rule of its own Conflict rule 1 (p. 59) says two signs are written with no blank when the first is from group C' - the guide's example of that group being '>'. Measuring the guide's glyphs against our output shows the rule already holds: x > 0 ours key-sign x, blank, >, number-sign 0 guide: the same, 4 times 7 > -1 ours blank before the relation, none after guide: the same The reason is structural: since the spacing rule emits the blank on the LEFT only (p. 58, group B), nothing can put one after a sign. A rule keyed on "the previous sign is group C'" would therefore be dead code, so these are tests instead. One of them records a wrong assumption worth keeping: a CLOSING BRACKET is not in group C'. It looks like it should be, but the guide writes (14 - 5) + 7 with a blank between the bracket and the plus. No rules changed in this commit. Polish braille tests: 59 -> 62. --- tests/braille/Polish/polish.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/braille/Polish/polish.rs b/tests/braille/Polish/polish.rs index f5c4b0faa..8e96d123c 100644 --- a/tests/braille/Polish/polish.rs +++ b/tests/braille/Polish/polish.rs @@ -591,3 +591,37 @@ fn mnozenie_krzyzyk() -> Result<()> { test_braille("Polish", expr, "⠼⠃⠀⠦⠼⠉")?; return Ok(()); } + +// Page 59 conflict rule 1, second half: two signs are written with NO blank when +// the FIRST is from group C' ("znaki pisane bez odstepu z prawej strony", p. 58), +// where the guide's example of that group is ">". +// +// This needs no rule of its own - it already holds, because the spacing rule emits +// the blank on the LEFT only (see the mo rule and p. 58 group B). These tests pin +// that, since a rule keyed on "the previous sign is group C'" would be DEAD CODE: +// it could not change any output. Measured against the guide's glyphs on p. 11, +// where "x > 0" appears four times as key-sign x, blank, >, number-sign 0. +#[test] +fn brak_odstepu_po_grupie_c_prim() -> Result<()> { + let expr = r#"x>0"#; + test_braille("Polish", expr, "⠠⠭⠀⠕⠂⠼⠚")?; + return Ok(()); +} + +// The same for a NEGATIVE right-hand side: the guide writes 7 > -1 with a blank +// before the relation and none after it, the minus following immediately. +#[test] +fn relacja_przed_liczba_ujemna() -> Result<()> { + let expr = r#"7>-1"#; + test_braille("Polish", expr, "⠼⠛⠀⠕⠂⠤⠼⠁")?; + return Ok(()); +} + +// A CLOSING BRACKET is NOT in group C', which is easy to assume wrongly: the guide's +// own example (14 - 5) + 7 on p. 11 keeps a blank between the bracket and the plus. +#[test] +fn nawias_zamykajacy_nie_jest_grupa_c_prim() -> Result<()> { + let expr = r#"(a)+b"#; + test_braille("Polish", expr, "⠣⠠⠁⠜⠀⠖⠠⠃")?; + return Ok(()); +}