Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/rtl 2 #340

Merged
merged 6 commits into from
Dec 11, 2024
Merged
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
2 changes: 1 addition & 1 deletion .github/actions/waffles/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected].1#egg=notifications-utils
git+https://github.com/cds-snc/[email protected].2#egg=notifications-utils
39 changes: 39 additions & 0 deletions notifications_utils/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"<br\s?/>"


Expand Down Expand Up @@ -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.
Expand All @@ -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, '<div dir="rtl">')
_content = _content.replace(RTL_CLOSE_LITERAL, "</div>")

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.

Expand Down
6 changes: 6 additions & 0 deletions notifications_utils/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
)

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
89 changes: 89 additions & 0 deletions tests/test_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<div lang="fr-ca">' in html


class TestRTLTags:
def test_rtl_tags_in_templates(self):
content = "[[rtl]]\nRTL content\n[[/rtl]]"
html = get_html_email_body(content, {})
assert '<div dir="rtl">' 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 '<div dir="rtl">' 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 '<div dir="rtl">' 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 '<div dir="rtl">' 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 '<div dir="rtl">' in html
assert "RTL CONTENT" in html
assert "<{}".format(extra_tag) in html
Loading