From d1be3ceea2349060ceacd94a41174fcc0f31ae73 Mon Sep 17 00:00:00 2001 From: Andrew Leith Date: Mon, 9 Dec 2024 13:45:32 +0000 Subject: [PATCH 1/3] feat(rtl): add rtl feature within template language --- notifications_utils/formatters.py | 39 +++++++++++++++++++++++++++++++ notifications_utils/template.py | 6 +++++ 2 files changed, 45 insertions(+) diff --git a/notifications_utils/formatters.py b/notifications_utils/formatters.py index c6c48d67..aa5d8034 100644 --- a/notifications_utils/formatters.py +++ b/notifications_utils/formatters.py @@ -31,10 +31,14 @@ FR_CLOSE = r"\[\[/fr\]\]" # matches [[/fr]] EN_OPEN = r"\[\[en\]\]" # matches [[en]] EN_CLOSE = r"\[\[/en\]\]" # matches [[/en]] +RTL_OPEN = r"\[\[rtl\]\]" # matches [[rtl]] +RTL_CLOSE = r"\[\[/rtl\]\]" # matches [[/rtl]] FR_OPEN_LITERAL = "[[fr]]" FR_CLOSE_LITERAL = "[[/fr]]" EN_OPEN_LITERAL = "[[en]]" EN_CLOSE_LITERAL = "[[/en]]" +RTL_OPEN_LITERAL = "[[rtl]]" +RTL_CLOSE_LITERAL = "[[/rtl]]" BR_TAG = r"" @@ -621,6 +625,20 @@ def escape_lang_tags(_content: str) -> str: return _content +def escape_rtl_tags(_content: str) -> str: + """ + Escape RTL tags into code tags in the content so mistune doesn't put them inside p tags. This makes it simple + to replace them afterwards, and avoids creating invalid HTML in the process + """ + + # check to ensure we have the same number of opening and closing tags before escaping tags + if _content.count(RTL_OPEN_LITERAL) == _content.count(RTL_CLOSE_LITERAL): + _content = _content.replace(RTL_OPEN_LITERAL, f"\n```\n{RTL_OPEN_LITERAL}\n```\n") + _content = _content.replace(RTL_CLOSE_LITERAL, f"\n```\n{RTL_CLOSE_LITERAL}\n```\n") + + return _content + + def add_language_divs(_content: str) -> str: """ Custom parser to add the language divs. @@ -646,6 +664,27 @@ def remove_language_divs(_content: str) -> str: return remove_tags(_content, FR_OPEN, FR_CLOSE, EN_OPEN, EN_CLOSE) +def add_rtl_divs(_content: str) -> str: + """ + Custom parser to add the language divs. + + String replace language tags in-place + """ + + # check to ensure we have the same number of opening and closing tags before escaping tags + if _content.count(RTL_OPEN_LITERAL) == _content.count(RTL_CLOSE_LITERAL): + _content = _content.replace(RTL_OPEN_LITERAL, '
') + _content = _content.replace(RTL_CLOSE_LITERAL, "
") + + return _content + + +def remove_rtl_divs(_content: str) -> str: + """Remove the tags from content. This fn is for use in the email + preheader, since this is plain text not html""" + return remove_tags(_content, RTL_OPEN, RTL_CLOSE) + + def remove_tags(_content: str, *tags) -> str: """Remove the tags in parameters from content. diff --git a/notifications_utils/template.py b/notifications_utils/template.py index c6e00d77..54738c13 100644 --- a/notifications_utils/template.py +++ b/notifications_utils/template.py @@ -13,10 +13,12 @@ from notifications_utils.formatters import ( add_language_divs, add_prefix, + add_rtl_divs, add_trailing_newline, autolink_sms, escape_html, escape_lang_tags, + escape_rtl_tags, make_quotes_smart, nl2br, nl2li, @@ -28,6 +30,7 @@ notify_plain_text_email_markdown, remove_empty_lines, remove_language_divs, + remove_rtl_divs, remove_smart_quotes_from_email_addresses, remove_whitespace_before_punctuation, replace_hyphens_with_en_dashes, @@ -398,6 +401,7 @@ def preheader(self): .then(add_trailing_newline) .then(notify_email_preheader_markdown) .then(remove_language_divs) + .then(remove_rtl_divs) .then(do_nice_typography) .split() )[: self.PREHEADER_LENGTH_IN_CHARACTERS].strip() @@ -805,8 +809,10 @@ def get_html_email_body(template_content, template_values, redact_missing_person .then(strip_unsupported_characters) .then(add_trailing_newline) .then(escape_lang_tags) + .then(escape_rtl_tags) .then(notify_email_markdown) .then(add_language_divs) + .then(add_rtl_divs) .then(do_nice_typography) ) From ad1a72ef467f823a7dc7ba3359bd1d433f6856f8 Mon Sep 17 00:00:00 2001 From: Andrew Leith Date: Mon, 9 Dec 2024 14:17:12 +0000 Subject: [PATCH 2/3] test(rtl): add tests! --- tests/test_template.py | 89 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/tests/test_template.py b/tests/test_template.py index e9dd4006..2d1a8107 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -38,3 +38,92 @@ def test_lang_tags_in_templates_bad_content(bad_content: str): def test_lang_tags_in_templates_good_content(good_content: str): html = get_html_email_body(good_content, {}) assert '
' in html + + +class TestRTLTags: + def test_rtl_tags_in_templates(self): + content = "[[rtl]]\nRTL content\n[[/rtl]]" + html = get_html_email_body(content, {}) + assert '
' in html + assert "RTL content" in html + + @pytest.mark.parametrize( + "nested_content", + [ + "[[rtl]]\nRTL content\n[[/rtl]]\n[[rtl]]\nMore RTL content\n[[/rtl]]", + "[[rtl]]\nRTL content with [[en]]\nEN content\n[[/en]]\n[[/rtl]]", + ], + ) + def test_rtl_tags_in_templates_nested_content(self, nested_content: str): + html = get_html_email_body(nested_content, {}) + assert '
' in html + assert "RTL content" in html + + @pytest.mark.parametrize( + "bad_content", + [ + "[[rtl]\nRTL content\n[[/rtl]]", # missing bracket + "[[RTL]]\nRTL content\n[[/RTL]]", # tags not lowercase + "[[rtl]]\nRTL content\n", # tag missing + "RTL content\n[[/rtl]]", # tag missing + "((rtl))\nRTL content\n((/rtl))", # wrong brackets + ], + ) + def test_rtl_tags_in_templates_bad_content(self, bad_content: str): + html = get_html_email_body(bad_content, {}) + assert '
' not in html + + @pytest.mark.parametrize( + "mixed_content", + [ + "[[rtl]]\nRTL content\n[[/rtl]]\nLTR content", + "LTR content\n[[rtl]]\nRTL content\n[[/rtl]]", + ], + ) + def test_rtl_tags_in_templates_mixed_content(self, mixed_content: str): + html = get_html_email_body(mixed_content, {}) + assert '
' in html + assert "RTL content" in html + assert "LTR content" in html + + @pytest.mark.parametrize( + "content, extra_tag", + [ + ("[[rtl]] # RTL CONTENT [[/rtl]]", "h2"), + ("[[rtl]] ## RTL CONTENT [[/rtl]]", "h3"), + ("[[rtl]]\n- RTL CONTENT 1\n-item 2\n[[/rtl]]", "ul"), + ("[[rtl]]\n1. RTL CONTENT 1\n1. item 2\n[[/rtl]]", "ol"), + ("[[rtl]]**RTL CONTENT**[[/rtl]]", "strong"), + ("[[rtl]]_RTL CONTENT_[[/rtl]]", "em"), + ("[[rtl]]---\nRTL CONTENT[[/rtl]]", "hr"), + ( + "[[rtl]]1. RTL CONTENT\n1. First level\n 1. Second level\n 1. Second level\n 1. Second level\n 1. Third level\n 1. Third level\n 1. Fourth level\n 1. Fourth level\n 1. Fifth level\n 1. Fifth level[[/rtl]]", + "ol", + ), + ("[[rtl]]^RTL CONTENT[[/rtl]]", "blockquote"), + ("[[rtl]]RTL CONTENT now at https://www.canada.ca[[/rtl]]", "a"), + ("[[rtl]][RTL CONTENT](https://www.canada.ca/sign-in)[[/rtl]]", "a"), + ("[[rtl]][[en]]RTL CONTENT[[/en]][[/rtl]]", 'div lang="en-ca"'), + ("[[rtl]][[fr]]RTL CONTENT[[/fr]][[/rtl]]", 'div lang="fr-ca"'), + ], + ids=[ + "heading_1", + "heading_2", + "list_unordered", + "list_ordered", + "bold", + "italic", + "hr", + "nested_list", + "blockquote", + "link", + "link_with_text", + "nested_lang_tags_en", + "nested_lang_tags_fr", + ], + ) + def test_rtl_tags_work_with_other_features(self, content: str, extra_tag: str): + html = get_html_email_body(content, {}) + assert '
' in html + assert "RTL CONTENT" in html + assert "<{}".format(extra_tag) in html From 88860aab6c1e9b3c46bea3718c23425f23b9f077 Mon Sep 17 00:00:00 2001 From: Andrew Leith Date: Mon, 9 Dec 2024 14:17:58 +0000 Subject: [PATCH 3/3] chore: bump version --- .github/actions/waffles/requirements.txt | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/waffles/requirements.txt b/.github/actions/waffles/requirements.txt index 44c043b0..c6ee73ab 100644 --- a/.github/actions/waffles/requirements.txt +++ b/.github/actions/waffles/requirements.txt @@ -2,4 +2,4 @@ docopt==0.6.2 Flask==2.3.3 markupsafe==2.1.5 setuptools==75.6.0 # required for distutils in Python 3.12 -git+https://github.com/cds-snc/notifier-utils.git@53.0.1#egg=notifications-utils +git+https://github.com/cds-snc/notifier-utils.git@53.0.2#egg=notifications-utils diff --git a/pyproject.toml b/pyproject.toml index c83e8250..dba1474c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "notifications-utils" -version = "53.0.1" +version = "53.0.2" description = "Shared python code for Notification - Provides logging utils etc." authors = ["Canadian Digital Service"] license = "MIT license"