diff --git a/.gitignore b/.gitignore index e330edc72..22b92b87a 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ nosetests.xml .venv .idea +src/scielo-scholarly-data diff --git a/packtools/sps/validation/formula.py b/packtools/sps/validation/formula.py index 52364f263..b3f6ac2fa 100644 --- a/packtools/sps/validation/formula.py +++ b/packtools/sps/validation/formula.py @@ -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" } @@ -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())] @@ -290,6 +292,74 @@ 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: A validation result dictionary. + """ + has_mml_math = bool(self.data.get("mml_math")) + has_tex_math = bool(self.data.get("tex_math")) + item_id = self.data.get("id") + + # No codification found - return OK response reflecting this condition + if not has_mml_math and not has_tex_math: + return build_response( + title="MathML recommendation", + parent=self.data, + item="mml:math", + sub_item=None, + validation_type="exist", + is_valid=True, + expected="mml:math or tex-math", + obtained=_("no codification found"), + advice=None, + data=self.data, + error_level=self.rules["mathml_error_level"], + advice_text=None, + advice_params=None, + ) + + # Only warn if there's tex-math but no mml:math + if has_tex_math and not has_mml_math: + 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 in . 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 in . 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 build_response( + title="MathML recommendation", + parent=self.data, + item="mml:math", + sub_item=None, + validation_type="exist", + is_valid=True, + expected="mml:math", + obtained="mml:math", + advice=None, + data=self.data, + error_level=self.rules["mathml_error_level"], + advice_text=None, + advice_params=None, + ) + def validate_alternatives(self): """ Validates the presence of the 'alternatives' attribute in a element. @@ -432,6 +502,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" } @@ -449,6 +520,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())] @@ -611,6 +683,74 @@ 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: A validation result dictionary. + """ + has_mml_math = bool(self.data.get("mml_math")) + has_tex_math = bool(self.data.get("tex_math")) + item_id = self.data.get("id") + + # No codification found - return OK response reflecting this condition + if not has_mml_math and not has_tex_math: + return build_response( + title="MathML recommendation", + parent=self.data, + item="mml:math", + sub_item=None, + validation_type="exist", + is_valid=True, + expected="mml:math or tex-math", + obtained=_("no codification found"), + advice=None, + data=self.data, + error_level=self.rules["mathml_error_level"], + advice_text=None, + advice_params=None, + ) + + # Only warn if there's tex-math but no mml:math + if has_tex_math and not has_mml_math: + 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 in . 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 in . 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 build_response( + title="MathML recommendation", + parent=self.data, + item="mml:math", + sub_item=None, + validation_type="exist", + is_valid=True, + expected="mml:math", + obtained="mml:math", + advice=None, + data=self.data, + error_level=self.rules["mathml_error_level"], + advice_text=None, + advice_params=None, + ) + def validate_alternatives(self): """ Validates the presence of alternatives in a element. diff --git a/packtools/sps/validation_rules/formula_rules.json b/packtools/sps/validation_rules/formula_rules.json index 38f9b8f8c..3038996d9 100644 --- a/packtools/sps/validation_rules/formula_rules.json +++ b/packtools/sps/validation_rules/formula_rules.json @@ -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": { @@ -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" } } diff --git a/tests/sps/validation/test_formula.py b/tests/sps/validation/test_formula.py index 400ae1591..c8364fb0c 100644 --- a/tests/sps/validation/test_formula.py +++ b/tests/sps/validation/test_formula.py @@ -608,3 +608,177 @@ 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 ", 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( + '
' + "" + '' + "" + '\\[ E = mc^2 \\]' + "" + "" + "
" + ) + 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 OK response is returned when disp-formula has mml:math""" + self.maxDiff = None + xml_tree = etree.fromstring( + '
' + "" + '' + "" + '' + "" + "E=mc2" + "" + "" + "" + "" + "
" + ) + obtained = list( + ArticleDispFormulaValidation( + xml_tree=xml_tree, rules={"mathml_error_level": "WARNING"} + ).validate() + ) + + # Deve retornar OK para MathML recommendation + mathml_responses = [item for item in obtained if item["title"] == "MathML recommendation"] + self.assertEqual(len(mathml_responses), 1) + self.assertEqual(mathml_responses[0]["response"], "OK") + + def test_validate_mathml_recommendation_in_disp_formula_with_both(self): + """Test that OK response is returned when disp-formula has both tex-math and mml:math""" + self.maxDiff = None + xml_tree = etree.fromstring( + '
' + "" + '' + "" + "" + '' + "" + "E=mc2" + "" + "" + '\\[ E = mc^2 \\]' + "" + "" + "" + "
" + ) + obtained = list( + ArticleDispFormulaValidation( + xml_tree=xml_tree, rules={"mathml_error_level": "WARNING"} + ).validate() + ) + + # Deve retornar OK para MathML recommendation + mathml_responses = [item for item in obtained if item["title"] == "MathML recommendation"] + self.assertEqual(len(mathml_responses), 1) + self.assertEqual(mathml_responses[0]["response"], "OK") + + 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( + '
' + "" + '

Some text with ' + 'x^2' + " in text.

" + "" + "
" + ) + 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 OK response is returned when inline-formula has mml:math""" + self.maxDiff = None + xml_tree = etree.fromstring( + '
' + "" + '

Some text with ' + '' + "x2" + "" + " in text.

" + "" + "
" + ) + obtained = list( + ArticleInlineFormulaValidation( + xml_tree=xml_tree, rules={"mathml_error_level": "WARNING"} + ).validate() + ) + + # Deve retornar OK para MathML recommendation + mathml_responses = [item for item in obtained if item["title"] == "MathML recommendation"] + self.assertEqual(len(mathml_responses), 1) + self.assertEqual(mathml_responses[0]["response"], "OK") + + def test_validate_mathml_recommendation_returns_ok_without_codification(self): + """Test that mathml recommendation returns OK when there's no codification at all""" + self.maxDiff = None + xml_tree = etree.fromstring( + '
' + "" + '' + "" + '' + "" + "" + "
" + ) + obtained = list( + ArticleDispFormulaValidation( + xml_tree=xml_tree, rules={"mathml_error_level": "WARNING"} + ).validate() + ) + + # Deve retornar OK para MathML recommendation quando não há codificação + mathml_responses = [item for item in obtained if item["title"] == "MathML recommendation"] + self.assertEqual(len(mathml_responses), 1) + self.assertEqual(mathml_responses[0]["response"], "OK") + self.assertIn("no codification found", mathml_responses[0]["got_value"])