Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ nosetests.xml
.venv

.idea
src/scielo-scholarly-data
84 changes: 84 additions & 0 deletions packtools/sps/validation/formula.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def get_default_params(self):
"codification_error_level": "CRITICAL",
"mml_math_id_error_level": "CRITICAL",
"mml_math_id_prefix_error_level": "ERROR",
"mathml_error_level": "WARNING",
"alternatives_error_level": "CRITICAL"
}

Expand All @@ -101,6 +102,7 @@ def validate(self):
self.validate_codification,
self.validate_mml_math_id,
self.validate_mml_math_id_prefix,
self.validate_mathml_recommendation,
self.validate_alternatives
]
return [response for validate in validations if (response := validate())]
Expand Down Expand Up @@ -290,6 +292,46 @@ def validate_mml_math_id_prefix(self):
advice_params={"mml_id": mml_math_id, "formula_id": item_id},
)

def validate_mathml_recommendation(self):
"""
Validates and recommends MathML when only TeX math is present for accessibility.

Returns:
dict or None: A validation result dictionary if the validation fails; otherwise, None.
"""
has_mml_math = bool(self.data.get("mml_math"))
has_tex_math = bool(self.data.get("tex_math"))

# Return None if there's no codification at all
if not has_mml_math and not has_tex_math:
return None

# Only warn if there's tex-math but no mml:math
if has_tex_math and not has_mml_math:
item_id = self.data.get("id")
is_valid = False
expected = "mml:math"
obtained = "tex-math"

return build_response(
title="MathML recommendation",
parent=self.data,
item="mml:math",
sub_item=None,
validation_type="exist",
is_valid=is_valid,
expected=expected,
obtained=obtained,
advice=_('For accessibility, consider adding <mml:math> in <disp-formula id="{formula_id}">. MathML improves accessibility for screen readers. Consult SPS documentation for more detail.').format(formula_id=item_id),
data=self.data,
error_level=self.rules["mathml_error_level"],
advice_text=_('For accessibility, consider adding <mml:math> in <disp-formula id="{formula_id}">. MathML improves accessibility for screen readers. Consult SPS documentation for more detail.'),
advice_params={"formula_id": item_id},
)

# Otherwise, it's valid (has mml:math or both)
return None

def validate_alternatives(self):
"""
Validates the presence of the 'alternatives' attribute in a <disp-formula> element.
Expand Down Expand Up @@ -432,6 +474,7 @@ def get_default_params(self):
"codification_error_level": "CRITICAL",
"mml_math_id_error_level": "CRITICAL",
"mml_math_id_prefix_error_level": "ERROR",
"mathml_error_level": "WARNING",
"alternatives_error_level": "CRITICAL"
}

Expand All @@ -449,6 +492,7 @@ def validate(self):
self.validate_codification,
self.validate_mml_math_id,
self.validate_mml_math_id_prefix,
self.validate_mathml_recommendation,
self.validate_alternatives
]
return [response for validate in validations if (response := validate())]
Expand Down Expand Up @@ -611,6 +655,46 @@ def validate_mml_math_id_prefix(self):
advice_params={"mml_id": mml_math_id, "formula_id": item_id},
)

def validate_mathml_recommendation(self):
"""
Validates and recommends MathML when only TeX math is present for accessibility.

Returns:
dict or None: A validation result dictionary if the validation fails; otherwise, None.
"""
has_mml_math = bool(self.data.get("mml_math"))
has_tex_math = bool(self.data.get("tex_math"))

# Return None if there's no codification at all
if not has_mml_math and not has_tex_math:
return None

# Only warn if there's tex-math but no mml:math
if has_tex_math and not has_mml_math:
item_id = self.data.get("id")
is_valid = False
expected = "mml:math"
obtained = "tex-math"

return build_response(
title="MathML recommendation",
parent=self.data,
item="mml:math",
sub_item=None,
validation_type="exist",
is_valid=is_valid,
expected=expected,
obtained=obtained,
advice=_('For accessibility, consider adding <mml:math> in <inline-formula id="{formula_id}">. MathML improves accessibility for screen readers. Consult SPS documentation for more detail.').format(formula_id=item_id),
data=self.data,
error_level=self.rules["mathml_error_level"],
advice_text=_('For accessibility, consider adding <mml:math> in <inline-formula id="{formula_id}">. MathML improves accessibility for screen readers. Consult SPS documentation for more detail.'),
advice_params={"formula_id": item_id},
)

# Otherwise, it's valid (has mml:math or both)
return None

def validate_alternatives(self):
"""
Validates the presence of alternatives in a <inline-formula> element.
Expand Down
2 changes: 2 additions & 0 deletions packtools/sps/validation_rules/formula_rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"codification_error_level": "CRITICAL",
"mml_math_id_error_level": "CRITICAL",
"mml_math_id_prefix_error_level": "ERROR",
"mathml_error_level": "WARNING",
"alternatives_error_level": "CRITICAL"
},
"inline_formula_rules": {
Expand All @@ -16,6 +17,7 @@
"codification_error_level": "CRITICAL",
"mml_math_id_error_level": "CRITICAL",
"mml_math_id_prefix_error_level": "ERROR",
"mathml_error_level": "WARNING",
"alternatives_error_level": "CRITICAL"
}
}
169 changes: 169 additions & 0 deletions tests/sps/validation/test_formula.py
Original file line number Diff line number Diff line change
Expand Up @@ -608,3 +608,172 @@ def test_validate_alternatives_not_required_in_disp_formula(self):
self.assertEqual(error["response"], "CRITICAL")
self.assertEqual(error["got_value"], "alternatives")
self.assertIn("Remove the <alternatives>", error["advice"])

def test_validate_mathml_recommendation_in_disp_formula_with_only_tex(self):
"""Test MathML recommendation when disp-formula has only tex-math"""
self.maxDiff = None
xml_tree = etree.fromstring(
'<article xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:mml="http://www.w3.org/1998/Math/MathML" '
'dtd-version="1.0" article-type="research-article" xml:lang="pt">'
"<body>"
'<disp-formula id="e10">'
"<label>(1)</label>"
'<tex-math id="tx1">\\[ E = mc^2 \\]</tex-math>'
"</disp-formula>"
"</body>"
"</article>"
)
obtained = list(
ArticleDispFormulaValidation(
xml_tree=xml_tree, rules={"mathml_error_level": "WARNING"}
).validate()
)

# Deve retornar aviso recomendando MathML
warnings = [item for item in obtained if item["title"] == "MathML recommendation"]
self.assertEqual(len(warnings), 1)

warning = warnings[0]
self.assertEqual(warning["response"], "WARNING")
self.assertEqual(warning["expected_value"], "mml:math")
self.assertEqual(warning["got_value"], "tex-math")
self.assertIn("accessibility", warning["advice"])
self.assertIn("mml:math", warning["advice"])

def test_validate_mathml_recommendation_in_disp_formula_with_mml(self):
"""Test that no warning is issued when disp-formula has mml:math"""
self.maxDiff = None
xml_tree = etree.fromstring(
'<article xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:mml="http://www.w3.org/1998/Math/MathML" '
'dtd-version="1.0" article-type="research-article" xml:lang="pt">'
"<body>"
'<disp-formula id="e10">'
"<label>(1)</label>"
'<mml:math id="m1">'
"<mml:mrow>"
"<mml:mi>E</mml:mi><mml:mo>=</mml:mo><mml:mi>m</mml:mi><mml:msup><mml:mi>c</mml:mi><mml:mn>2</mml:mn></mml:msup>"
"</mml:mrow>"
"</mml:math>"
"</disp-formula>"
"</body>"
"</article>"
)
obtained = list(
ArticleDispFormulaValidation(
xml_tree=xml_tree, rules={"mathml_error_level": "WARNING"}
).validate()
)

# Não deve retornar aviso de MathML
warnings = [item for item in obtained if item["title"] == "MathML recommendation"]
self.assertEqual(len(warnings), 0)

def test_validate_mathml_recommendation_in_disp_formula_with_both(self):
"""Test that no warning is issued when disp-formula has both tex-math and mml:math"""
self.maxDiff = None
xml_tree = etree.fromstring(
'<article xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:mml="http://www.w3.org/1998/Math/MathML" '
'dtd-version="1.0" article-type="research-article" xml:lang="pt">'
"<body>"
'<disp-formula id="e10">'
"<label>(1)</label>"
"<alternatives>"
'<mml:math id="m1">'
"<mml:mrow>"
"<mml:mi>E</mml:mi><mml:mo>=</mml:mo><mml:mi>m</mml:mi><mml:msup><mml:mi>c</mml:mi><mml:mn>2</mml:mn></mml:msup>"
"</mml:mrow>"
"</mml:math>"
'<tex-math id="tx1">\\[ E = mc^2 \\]</tex-math>'
"</alternatives>"
"</disp-formula>"
"</body>"
"</article>"
)
obtained = list(
ArticleDispFormulaValidation(
xml_tree=xml_tree, rules={"mathml_error_level": "WARNING"}
).validate()
)

# Não deve retornar aviso de MathML
warnings = [item for item in obtained if item["title"] == "MathML recommendation"]
self.assertEqual(len(warnings), 0)

def test_validate_mathml_recommendation_in_inline_formula_with_only_tex(self):
"""Test MathML recommendation when inline-formula has only tex-math"""
self.maxDiff = None
xml_tree = etree.fromstring(
'<article xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:mml="http://www.w3.org/1998/Math/MathML" '
'dtd-version="1.0" article-type="research-article" xml:lang="pt">'
"<body>"
'<p>Some text with <inline-formula id="e10">'
'<tex-math id="tx1">x^2</tex-math>'
"</inline-formula> in text.</p>"
"</body>"
"</article>"
)
obtained = list(
ArticleInlineFormulaValidation(
xml_tree=xml_tree, rules={"mathml_error_level": "WARNING"}
).validate()
)

# Deve retornar aviso recomendando MathML
warnings = [item for item in obtained if item["title"] == "MathML recommendation"]
self.assertEqual(len(warnings), 1)

warning = warnings[0]
self.assertEqual(warning["response"], "WARNING")
self.assertEqual(warning["expected_value"], "mml:math")
self.assertEqual(warning["got_value"], "tex-math")
self.assertIn("accessibility", warning["advice"])
self.assertIn("mml:math", warning["advice"])

def test_validate_mathml_recommendation_in_inline_formula_with_mml(self):
"""Test that no warning is issued when inline-formula has mml:math"""
self.maxDiff = None
xml_tree = etree.fromstring(
'<article xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:mml="http://www.w3.org/1998/Math/MathML" '
'dtd-version="1.0" article-type="research-article" xml:lang="pt">'
"<body>"
'<p>Some text with <inline-formula id="e10">'
'<mml:math id="m1">'
"<mml:msup><mml:mi>x</mml:mi><mml:mn>2</mml:mn></mml:msup>"
"</mml:math>"
"</inline-formula> in text.</p>"
"</body>"
"</article>"
)
obtained = list(
ArticleInlineFormulaValidation(
xml_tree=xml_tree, rules={"mathml_error_level": "WARNING"}
).validate()
)

# Não deve retornar aviso de MathML
warnings = [item for item in obtained if item["title"] == "MathML recommendation"]
self.assertEqual(len(warnings), 0)

def test_validate_mathml_recommendation_returns_none_without_codification(self):
"""Test that mathml recommendation returns None when there's no codification at all"""
self.maxDiff = None
xml_tree = etree.fromstring(
'<article xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:mml="http://www.w3.org/1998/Math/MathML" '
'dtd-version="1.0" article-type="research-article" xml:lang="pt">'
"<body>"
'<disp-formula id="e10">'
"<label>(1)</label>"
'<graphic xlink:href="formula.png"/>'
"</disp-formula>"
"</body>"
"</article>"
)
obtained = list(
ArticleDispFormulaValidation(
xml_tree=xml_tree, rules={"mathml_error_level": "WARNING"}
).validate()
)

# Não deve retornar aviso de MathML (já retornará erro de codificação)
warnings = [item for item in obtained if item["title"] == "MathML recommendation"]
self.assertEqual(len(warnings), 0)