Repository: zulip/zulip

Issue:
Notifications triple-include latex math formulas
When I receive a message with latex math, the notification (in all of email, android, and desktop) includes the math three times in a row (once as unicode, once as latex, again as unicode).  This seems to happen every time.

One recent example:  someone sent me

"...we have <span>$$</span>d^* = +\infty$$, but..."

and I got the email:

"...we have d∗=+∞d^*=+\inftyd∗=+∞, but..."



Discussion:
Thanks for the report @ecprice! The root cause is that this is what happens when you display KaTeX HTML without the CSS :).

We might be able to do something fancy with including the CSS in emails, but that's definitely not possible with push notifications. The discussion in https://chat.zulip.org/#narrow/stream/9-issues/topic/Notifications.20triple-include.20latex.20math.20formulas.20.2325289/near/1556210 concluded that our immediate plan should be to just transform the KaTeX HTML blocks back into the raw LaTeX source, and then we can potentially as a follow-up look at whether `css-inline` (and the annoying limitations in CSS in email clients) is capable of allowing us to display the actual rendered LaTeX in that context.

Resolving patch:
diff --git a/zerver/lib/email_notifications.py b/zerver/lib/email_notifications.py
--- a/zerver/lib/email_notifications.py
+++ b/zerver/lib/email_notifications.py
@@ -27,6 +27,7 @@
 from zerver.lib.queue import queue_json_publish
 from zerver.lib.send_email import FromAddress, send_future_email
 from zerver.lib.soft_deactivation import soft_reactivate_if_personal_notification
+from zerver.lib.tex import change_katex_to_raw_latex
 from zerver.lib.topic import get_topic_resolution_and_bare_name
 from zerver.lib.url_encoding import (
     huddle_narrow_url,
@@ -248,19 +249,7 @@ def build_message_payload(message: Message, sender: Optional[str] = None) -> Dic
         relative_to_full_url(fragment, user.realm.uri)
         fix_emojis(fragment, user.emojiset)
         fix_spoilers_in_html(fragment, user.default_language)
-
-        # Selecting the <span> elements with class 'katex'
-        katex_spans = fragment.xpath("//span[@class='katex']")
-
-        # Iterate through 'katex_spans' and replace with a new <span> having LaTeX text.
-        for katex_span in katex_spans:
-            latex_text = katex_span.xpath(".//annotation[@encoding='application/x-tex']")[0].text
-            # We store 'tail' to insert them back as the replace operation removes it.
-            tail = katex_span.tail
-            latex_span = lxml.html.Element("span")
-            latex_span.text = f"$${latex_text}$$"
-            katex_span.getparent().replace(katex_span, latex_span)
-            latex_span.tail = tail
+        change_katex_to_raw_latex(fragment)
 
         html = lxml.html.tostring(fragment, encoding="unicode")
         if sender:
diff --git a/zerver/lib/push_notifications.py b/zerver/lib/push_notifications.py
--- a/zerver/lib/push_notifications.py
+++ b/zerver/lib/push_notifications.py
@@ -40,6 +40,7 @@
 from zerver.lib.outgoing_http import OutgoingSession
 from zerver.lib.remote_server import send_json_to_push_bouncer, send_to_push_bouncer
 from zerver.lib.soft_deactivation import soft_reactivate_if_personal_notification
+from zerver.lib.tex import change_katex_to_raw_latex
 from zerver.lib.timestamp import datetime_to_timestamp
 from zerver.models import (
     AbstractPushDeviceToken,
@@ -812,6 +813,7 @@ def process(elem: lxml.html.HtmlElement) -> str:
         )
 
     elem = lxml.html.fragment_fromstring(rendered_content, create_parent=True)
+    change_katex_to_raw_latex(elem)
     plain_text = process(elem)
     return plain_text
 
diff --git a/zerver/lib/tex.py b/zerver/lib/tex.py
--- a/zerver/lib/tex.py
+++ b/zerver/lib/tex.py
@@ -3,6 +3,7 @@
 import subprocess
 from typing import Optional
 
+import lxml.html
 from django.conf import settings
 
 from zerver.lib.storage import static_path
@@ -39,3 +40,18 @@ def render_tex(tex: str, is_inline: bool = True) -> Optional[str]:
         return stdout.strip()
     except subprocess.CalledProcessError:
         return None
+
+
+def change_katex_to_raw_latex(fragment: lxml.html.HtmlElement) -> None:
+    # Selecting the <span> elements with class 'katex'
+    katex_spans = fragment.xpath("//span[@class='katex']")
+
+    # Iterate through 'katex_spans' and replace with a new <span> having LaTeX text.
+    for katex_span in katex_spans:
+        latex_text = katex_span.xpath(".//annotation[@encoding='application/x-tex']")[0].text
+        # We store 'tail' to insert them back as the replace operation removes it.
+        tail = katex_span.tail
+        latex_span = lxml.html.Element("span")
+        latex_span.text = f"$${latex_text}$$"
+        katex_span.getparent().replace(katex_span, latex_span)
+        latex_span.tail = tail


Test patch:
diff --git a/web/tests/notifications.test.js b/web/tests/notifications.test.js
--- a/web/tests/notifications.test.js
+++ b/web/tests/notifications.test.js
@@ -340,6 +340,7 @@ test("message_is_notifiable", () => {
 
 test("basic_notifications", () => {
     $("<div>").set_find_results(".emoji", {replaceWith() {}});
+    $("<div>").set_find_results("span.katex", {each() {}});
 
     let n; // Object for storing all notification data for assertions.
     let last_closed_message_id = null;
diff --git a/zerver/tests/fixtures/markdown_test_cases.json b/zerver/tests/fixtures/markdown_test_cases.json
--- a/zerver/tests/fixtures/markdown_test_cases.json
+++ b/zerver/tests/fixtures/markdown_test_cases.json
@@ -847,7 +847,8 @@
     {
       "name": "tex_inline",
       "input": "$$1 \\oplus 0 = 1$$",
-      "expected_output": "<p><span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\"><semantics><mrow><mn>1</mn><mo>⊕</mo><mn>0</mn><mo>=</mo><mn>1</mn></mrow><annotation encoding=\"application/x-tex\">1 \\oplus 0 = 1</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.7278em;vertical-align:-0.0833em;\"></span><span class=\"mord\">1</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span><span class=\"mbin\">⊕</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.6444em;\"></span><span class=\"mord\">0</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.6444em;\"></span><span class=\"mord\">1</span></span></span></span></p>"
+      "expected_output": "<p><span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\"><semantics><mrow><mn>1</mn><mo>⊕</mo><mn>0</mn><mo>=</mo><mn>1</mn></mrow><annotation encoding=\"application/x-tex\">1 \\oplus 0 = 1</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.7278em;vertical-align:-0.0833em;\"></span><span class=\"mord\">1</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span><span class=\"mbin\">⊕</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.6444em;\"></span><span class=\"mord\">0</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.6444em;\"></span><span class=\"mord\">1</span></span></span></span></p>",
+      "text_content": "$$1 \\oplus 0 = 1$$"
     },
     {
       "name": "tex_complex",
@@ -862,7 +863,8 @@
     {
       "name": "tex_fenced_math",
       "input": "```math\na^2 = b^2 + c^2\n```",
-      "expected_output": "<p><span class=\"katex-display\"><span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\"><semantics><mrow><msup><mi>a</mi><mn>2</mn></msup><mo>=</mo><msup><mi>b</mi><mn>2</mn></msup><mo>+</mo><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding=\"application/x-tex\">a^2 = b^2 + c^2</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.8641em;\"></span><span class=\"mord\"><span class=\"mord mathnormal\">a</span><span class=\"msupsub\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.8641em;\"><span style=\"top:-3.113em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\">2</span></span></span></span></span></span></span></span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.9474em;vertical-align:-0.0833em;\"></span><span class=\"mord\"><span class=\"mord mathnormal\">b</span><span class=\"msupsub\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.8641em;\"><span style=\"top:-3.113em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\">2</span></span></span></span></span></span></span></span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span><span class=\"mbin\">+</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.8641em;\"></span><span class=\"mord\"><span class=\"mord mathnormal\">c</span><span class=\"msupsub\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.8641em;\"><span style=\"top:-3.113em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\">2</span></span></span></span></span></span></span></span></span></span></span></span></p>"
+      "expected_output": "<p><span class=\"katex-display\"><span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\"><semantics><mrow><msup><mi>a</mi><mn>2</mn></msup><mo>=</mo><msup><mi>b</mi><mn>2</mn></msup><mo>+</mo><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding=\"application/x-tex\">a^2 = b^2 + c^2</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.8641em;\"></span><span class=\"mord\"><span class=\"mord mathnormal\">a</span><span class=\"msupsub\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.8641em;\"><span style=\"top:-3.113em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\">2</span></span></span></span></span></span></span></span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.9474em;vertical-align:-0.0833em;\"></span><span class=\"mord\"><span class=\"mord mathnormal\">b</span><span class=\"msupsub\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.8641em;\"><span style=\"top:-3.113em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\">2</span></span></span></span></span></span></span></span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span><span class=\"mbin\">+</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.8641em;\"></span><span class=\"mord\"><span class=\"mord mathnormal\">c</span><span class=\"msupsub\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.8641em;\"><span style=\"top:-3.113em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\">2</span></span></span></span></span></span></span></span></span></span></span></span></p>",
+      "text_content": "$$a^2 = b^2 + c^2$$"
     },
     {
       "name": "tex_fenced_tex",