Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions PythonScripts/audit_translations/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def invalid(message: str) -> AuditError:

if isinstance(value, dict):
if not value:
raise invalid("empty mappings have no unambiguous definition kind")
return DefinitionKind.SET
if not all(isinstance(key, str) for key in value):
raise invalid("mapping keys must all be strings")

Expand All @@ -136,7 +136,7 @@ def invalid(message: str) -> AuditError:
raise invalid("mixed set/map values are not supported")
raise invalid("mapping values must be all null (set) or all strings (map)")

raise invalid("value must be a non-empty sequence or mapping")
raise invalid("value must be a sequence or mapping")


def format_tag(tag_value: Any) -> str | None:
Expand Down
17 changes: 11 additions & 6 deletions PythonScripts/audit_translations/tests/test_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from ..auditor import compare_definition_files
from ..errors import AuditError
from ..models.definitions import DefinitionKind
from ..parsers import parse_definitions
from ..parsers import parse_definitions, parse_definitions_file


def parse(content: str):
Expand Down Expand Up @@ -66,19 +66,24 @@ def test_parse_definitions_rejects_empty_vector() -> None:
assert_invalid("- Foo: []\n", "empty sequences")


def test_parse_definitions_rejects_empty_mapping() -> None:
"""An empty mapping is rejected because it could be either a set or a map."""
assert_invalid("- Foo: {}\n", "empty mappings")
def test_parse_empty_greek_known_words_as_set() -> None:
"""Greek KnownWords uses an empty mapping, which MathCAT loads as an empty set."""
repo_root = Path(__file__).resolve().parents[3]
definitions, _ = parse_definitions_file(repo_root / "Rules/Languages/el/definitions.yaml")

known_words = definitions["KnownWords"]
assert known_words.kind is DefinitionKind.SET
assert known_words.data == {}


def test_parse_definitions_rejects_string_scalar() -> None:
"""A string scalar cannot be used as a definition value."""
assert_invalid("- Foo: bar\n", "non-empty sequence or mapping")
assert_invalid("- Foo: bar\n", "sequence or mapping")


def test_parse_definitions_rejects_numeric_scalar() -> None:
"""A numeric scalar cannot be used as a definition value."""
assert_invalid("- Foo: 123\n", "non-empty sequence or mapping")
assert_invalid("- Foo: 123\n", "sequence or mapping")


def test_parse_definitions_rejects_nested_mapping_value() -> None:
Expand Down
1 change: 1 addition & 0 deletions Rules/Languages/el/ClearSpeak_Rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,7 @@
replace:
- x: "*[2]"

- include: "SharedRules/reader-intents.yaml"
- include: "SharedRules/geometry.yaml"
- include: "SharedRules/linear-algebra.yaml"
- include: "SharedRules/general.yaml"
Expand Down
213 changes: 213 additions & 0 deletions Rules/Languages/el/GreekMathReader_Rules.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
---
# Greek Math Reader mathematical reading conventions, implemented as native MathCAT rules.
# NVDA Greek Math (Greek Math Reader) by Bouronikos Christos (cbouronikos@uth.gr)
# New wording remains lowercase until Greek expert and screen-reader listening review.
# Native SimpleSpeak supplies structures not overridden here, including tables and units.

- name: reader-silent-token
tag: [mi, mo, mn]
match: "contains(@data-intent-property, ':silent:')"
replace: []

- name: reader-equality
tag: mo
match: ".='='"
replace: [t: "ίσον"]

- name: reader-plus-minus
tag: mo
match: ".='±'"
replace: [t: "συν πλην"]

- name: reader-decimal
tag: mn
match: "contains(., $DecimalSeparators)"
replace:
- bookmark: "@id"
- x: "translate(substring-before(., $DecimalSeparators), $BlockSeparators, '')"
- t: "κόμμα"
- x: "substring-after(., $DecimalSeparators)"

# Recognize conventional notation only within this speech style. Author-supplied
# literal intent and unrelated notation retain MathCAT's structural reading.
- name: reader-scalar-multiplication
tag: mo
match: ".='·' and not(ancestor-or-self::*[contains(@data-intent-property, ':literal:')])"
replace: [t: "επί"]

- name: reader-first-derivative
tag: fraction
match:
- "count(*)=2 and *[1][self::m:mrow] and *[2][self::m:mrow] and"
- "*[1]/*[1][.='d' or .='∂'] and *[1]/*[1]=*[2]/*[1] and"
- "((count(*[1]/*)=3 and count(*[2]/*)=3 and *[1]/*[2]='⁢' and *[2]/*[2]='⁢') or"
- "(count(*[1]/*)=2 and count(*[2]/*)=2 and *[1]/*[1]='∂')) and"
- "*[2]/*[last()][self::m:mi] and not(ancestor-or-self::*[contains(@data-intent-property, ':literal:')])"
replace:
- test:
if: "*[1]/*[1][.='∂']"
then: [t: "μερική παράγωγος του"]
else: [t: "παράγωγος του"]
- x: "*[1]/*[last()]"
- t: "ως προς"
- x: "*[2]/*[last()]"

- name: reader-integral-differential
tag: mrow
match:
- "count(*)>=5 and preceding-sibling::*[1][self::m:large-op and *[1][contains('∫∬∭∮∯∰', .)]] and"
- "*[last()-3][self::m:mo and .='⁢'] and *[last()-2][self::m:mi and .='d'] and"
- "*[last()-1][self::m:mo and .='⁢'] and *[last()][self::m:mi] and"
- "not(ancestor-or-self::*[contains(@data-intent-property, ':literal:')])"
replace:
- x: "*[position()<last()-3]"
- t: "ως προς"
- x: "*[last()]"

- name: reader-statistical-function
tag: mrow
match:
- "count(*)=3 and *[1][self::m:mi and (.='E' or .='P' or .='Var' or .='Cov')] and"
- "*[2][self::m:mo and (.='⁡' or .='⁢')] and"
- "*[3][self::m:mrow and count(*)=3 and ((*[1]='(' and *[3]=')') or (*[1]='[' and *[3]=']'))] and"
- "not(ancestor-or-self::*[contains(@data-intent-property, ':literal:')])"
variables:
- Argument: "*[3]/*[2]"
replace:
- test:
- if: "*[1]='Cov' and $Argument[self::m:mrow and count(*)=3 and *[2]=',']"
then:
- t: "συνδιακύμανση των"
- x: "$Argument/*[1]"
- t: "και"
- x: "$Argument/*[3]"
- else_if: "(*[1]='E' or *[1]='P') and $Argument[self::m:mrow and count(*)=3 and *[2]='|']"
then:
- test:
if: "*[1]='E'"
then: [t: "αναμενόμενη τιμή του"]
else: [t: "πιθανότητα του"]
- x: "$Argument/*[1]"
- t: "δεδομένου του"
- x: "$Argument/*[3]"
else:
- test:
- if: "*[1]='E'"
then: [t: "αναμενόμενη τιμή του"]
- else_if: "*[1]='P'"
then: [t: "πιθανότητα του"]
- else_if: "*[1]='Var'"
then: [t: "διακύμανση του"]
else: [t: "συνδιακύμανση του"]
- x: "$Argument"

- name: reader-bra-ket-notation
tag: mrow
match:
- "count(*)=3 and *[1]='⟨' and *[3]='⟩' and *[2][self::m:mrow and count(*)=3 and *[2]='|'] and"
- "not(ancestor-or-self::*[contains(@data-intent-property, ':literal:')])"
replace:
- t: "εσωτερικό γινόμενο"
- x: "*[2]/*[1]"
- t: "με"
- x: "*[2]/*[3]"

- name: reader-matrix-element-notation
tag: mrow
match:
- "count(*)=3 and *[1]='⟨' and *[3]='⟩' and *[2][self::m:mrow and count(*)=5] and"
- "*[2]/*[2]='⁢' and *[2]/*[4]='⁢' and"
- "*[2]/*[3][self::m:absolute-value and count(*)=1 and @data-changed='added' and not(@intent)] and"
- "not(ancestor-or-self::*[contains(@data-intent-property, ':literal:')])"
replace:
- t: "στοιχείο πίνακα με μπρα"
- x: "*[2]/*[1]"
- x: "*[2]/*[3]/*[1]"
- t: "κετ"
- x: "*[2]/*[5]"

- name: reader-simple-fraction
tag: fraction
match: "count(*)=2 and IsNode(*[1], 'leaf') and IsNode(*[2], 'leaf') and not(ancestor::m:fraction)"
replace:
- x: "*[1]"
- t: "διά"
- x: "*[2]"

- name: reader-fraction
tag: fraction
match: "count(*)=2"
replace:
- t: "κλάσμα με αριθμητή"
- x: "*[1]"
- pause: short
- t: "και παρονομαστή"
- x: "*[2]"
- pause: short
- t: "τέλος κλάσματος"

- name: reader-square-root
tag: square-root
match: "count(*)=1"
replace:
- t: "τετραγωνική ρίζα του"
- x: "*[1]"
- test:
if: "not(IsNode(*[1], 'simple'))"
then: [pause: short, t: "τέλος ρίζας"]

- name: reader-root
tag: root
match: "count(*)=2"
replace:
- test:
if: "*[2][.='3']"
then: [t: "κυβική ρίζα του"]
else:
- t: "ρίζα τάξης"
- x: "*[2]"
- t: "του"
- x: "*[1]"
- test:
if: "not(IsNode(*[1], 'simple'))"
then: [pause: short, t: "τέλος ρίζας"]

- name: reader-power
tag: power
match: "count(*)=2"
replace:
- x: "*[1]"
- test:
- if: "*[2][self::m:mn and .='2']"
then: [t: "στο τετράγωνο"]
- else_if: "*[2][self::m:mn and .='3']"
then: [t: "στον κύβο"]
else:
- t: "στη δύναμη"
- x: "*[2]"
- test:
if: "not(IsNode(*[2], 'simple'))"
then: [pause: short, t: "τέλος εκθέτη"]

- name: reader-index
tag: indexed-by
match: "count(*)=2"
replace:
- x: "*[1]"
- t: "δείκτης"
- x: "*[2]"
- test:
if: "not(IsNode(*[2], 'simple'))"
then: [pause: short, t: "τέλος δείκτη"]

- name: reader-interval
tag: [interval, open-interval, closed-interval, open-closed-interval, closed-open-interval]
match: "count(*)=2"
replace:
- x: "SpeakIntentName(name(.), $Verbosity, 'function')"
- t: "από"
- x: "*[1]"
- t: "έως"
- x: "*[2]"

- include: "SimpleSpeak_Rules.yaml"
53 changes: 42 additions & 11 deletions Rules/Languages/el/SharedRules/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,15 @@

# otherwise let definitions/default infix handle it

- name: power-indexed-by
tag: power-indexed-by
match: "."
replace:
- x: "*[1]"
- t: "δείκτης" # phrase(x 'sub' 2)
- x: "*[2]"
- pause: short

- name: literal
tag: msub
match: "."
Expand Down Expand Up @@ -699,6 +708,13 @@
then: [pause: short]
else: [pause: auto]

- name: nofix-intent
# uncaught intent -- the args have been inserted in the order of speech
tag: "*"
match: "contains(@data-intent-property, ':nofix:') "
replace:
- x: "SpeakIntentName(name(.), $Verbosity, 'nofix')"

- name: prefix-intent
# uncaught intent -- the args have been inserted in the order of speech
tag: "*"
Expand Down Expand Up @@ -752,23 +768,38 @@
else: [pause: auto]

- name: function-intent
# uncaught intent -- speak as foo of arg1 comma arg2 .... The MathML spec requires arguments to functions
# uncaught intent -- speak as foo of arg1 comma arg2 ....
# IntentMappings may add "| glue" options after the name:
# * one option without ',': binary separator after "of" (e.g. "| divided by" → "... of x divided by y")
# * multiple options / options with ',': arity templates; k glue words => k+1 args,
# spoken as name + glue1 + arg1 + ... + "of" + lastArg (e.g. "| over | from,to")
# * unmatched arity (or binary glue with ≠2 args): commas between all args
tag: "*"
match: "count(*)>0"
replace:
- x: "SpeakIntentName(name(.), $Verbosity, 'function')"
- test:
if: "$Verbosity != 'Terse' and not(contains(@data-intent-property, ':literal:')) and
not(count(*)=2 and (IsInDefinition(*[1], 'TrigFunctionNames') or IsInDefinition(name(.), 'TerseFunctionNames')) and IsNode(*[2], 'simple'))"
then: [t: "του", pause: auto] # phrase(sine 'of' 5)

- insert:
nodes: "*"
replace:
- test:
if: "not(contains(@data-intent-property, ':literal:'))"
then: [x: "','"]
- if: "IntentFunctionUseArityPath(name(.), 'function', count(*))"
then:
- x: "IntentFunctionGlueBefore(name(.), 'function', 1, count(*))"
- pause: auto
- insert:
nodes: "*"
replace:
- x: "IntentFunctionGlueBefore(name(.), 'function', $InsertIndex, count(*))"
- pause: auto
else:
- test:
if: "$Verbosity != 'Terse' and not(contains(@data-intent-property, ':literal:')) and
not(count(*)=2 and (IsInDefinition(*[1], 'TrigFunctionNames') or IsInDefinition(name(.), 'TerseFunctionNames')) and IsNode(*[2], 'simple'))"
then: [t: "του", pause: auto] # phrase(sine 'of' 5)
- insert:
nodes: "*"
replace:
- test:
if: "not(contains(@data-intent-property, ':literal:'))"
then: [x: "IntentFunctionArgSeparator(name(.), 'function', count(*))"]
- pause: auto
- test:
# speak "end ..." if not bracketed or last child is not simple and not last node
if: "$Impairment = 'Blindness' and not(*[last()][IsBracketed(., '', '') or IsNode(., 'simple')] )"
Expand Down
19 changes: 18 additions & 1 deletion Rules/Languages/el/SharedRules/general.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,23 @@
then: [t: "ινφ"]
else: [t: "ινφίμουμ"]
#####################
- name: currency
tag: mrow
match: "count(*)=3 and *[2][self::m:mo and .='⁢'] and *[3][self::m:mn] and DefinitionValue(*[1], 'Speech', 'CurrencySymbols') != ''"
variables:
# If the amount is singular, we don't add the plural ending.
- IsSingular: "*[3][self::m:mn and .=1] and *[2]='\u2062'"
- CurrencyWord: "DefinitionValue(*[1], 'Speech', 'CurrencySymbols')"
replace:
- bookmark: "*[3]/@id"
- x: "*[3]"
- test:
- if: "$IsSingular"
then: [x: "$CurrencyWord"]
- else_if: "DefinitionValue($CurrencyWord, 'Speech', 'PluralForms') != ''"
then: [x: "DefinitionValue($CurrencyWord, 'Speech', 'PluralForms')"]
else: [x: "DefinitionValue(*[1], 'Speech', 'CurrencySymbols')"]

- name: sin
tag: mi
match: ".='sin'"
Expand Down Expand Up @@ -1439,7 +1456,7 @@
replace:
- insert:
nodes: "*"
replace: [t: "by", pause: auto] # phrase(3 'by' 5 matrix)
replace: [t: "επί", pause: auto] # phrase(3 'by' 5 matrix)

- name: chemistry-msup
tag: [chemical-formula]
Expand Down
Loading