From 0604c86779cdea98ace30bdd78eb4db6888ffc40 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sat, 19 Sep 2020 15:30:00 +0100
Subject: [PATCH 01/32] added katex package and import

---
 package.json      | 1 +
 src/HtmlUtils.tsx | 1 +
 yarn.lock         | 7 +++++++
 3 files changed, 9 insertions(+)

diff --git a/package.json b/package.json
index 156cbb1bc8..7aa3df136b 100644
--- a/package.json
+++ b/package.json
@@ -76,6 +76,7 @@
     "highlight.js": "^10.1.2",
     "html-entities": "^1.3.1",
     "is-ip": "^2.0.0",
+    "katex": "^0.12.0",
     "linkifyjs": "^2.1.9",
     "lodash": "^4.17.19",
     "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index bd314c2e5f..99acbfcb0c 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -26,6 +26,7 @@ import _linkifyString from 'linkifyjs/string';
 import classNames from 'classnames';
 import EMOJIBASE_REGEX from 'emojibase-regex';
 import url from 'url';
+import katex from 'katex';
 
 import {MatrixClientPeg} from './MatrixClientPeg';
 import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
diff --git a/yarn.lock b/yarn.lock
index efc1f0eae1..34b99708fc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5607,6 +5607,13 @@ jsx-ast-utils@^2.4.1:
     array-includes "^3.1.1"
     object.assign "^4.1.0"
 
+katex@^0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/katex/-/katex-0.12.0.tgz#2fb1c665dbd2b043edcf8a1f5c555f46beaa0cb9"
+  integrity sha512-y+8btoc/CK70XqcHqjxiGWBOeIL8upbS0peTPXTvgrh21n1RiWWcIpSWM+4uXq+IAgNh9YYQWdc7LVDPDAEEAg==
+  dependencies:
+    commander "^2.19.0"
+
 kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"

From becc79d67a29a0886f4a6f800daabebae16d655c Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sun, 20 Sep 2020 12:59:22 +0100
Subject: [PATCH 02/32] send tex math as data-mx-maths attribute

---
 src/HtmlUtils.tsx       | 26 +++++++++++++++++++++++++-
 src/editor/serialize.ts | 23 ++++++++++++++++++++++-
 2 files changed, 47 insertions(+), 2 deletions(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 99acbfcb0c..344fb3514c 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -27,6 +27,7 @@ import classNames from 'classnames';
 import EMOJIBASE_REGEX from 'emojibase-regex';
 import url from 'url';
 import katex from 'katex';
+import { AllHtmlEntities } from 'html-entities';
 
 import {MatrixClientPeg} from './MatrixClientPeg';
 import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
@@ -236,7 +237,8 @@ const sanitizeHtmlParams: sanitizeHtml.IOptions = {
     allowedAttributes: {
         // custom ones first:
         font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
-        span: ['data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'style'], // custom to matrix
+        span: ['data-mx-maths', 'data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'style'], // custom to matrix
+        div: ['data-mx-maths'],
         a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
         img: ['src', 'width', 'height', 'alt', 'title'],
         ol: ['start'],
@@ -409,6 +411,27 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
         if (isHtmlMessage) {
             isDisplayedWithHtml = true;
             safeBody = sanitizeHtml(formattedBody, sanitizeParams);
+            if (true) { // TODO: add katex setting
+                const mathDelimiters = [
+                    { left: "<div data-mx-maths=\"", right: "\">.*?</div>", display: true },
+                    { left: "<span data-mx-maths=\"", right: "\">.*?</span>", display: false }
+                ];
+
+                mathDelimiters.forEach(function (d) {
+                    var reg = RegExp(d.left + "(.*?)" + d.right, "g");
+
+                    safeBody = safeBody.replace(reg, function(match, p1) {
+                        return katex.renderToString(
+                            AllHtmlEntities.decode(p1),
+                            {
+                                throwOnError: false,
+                                displayMode: d.display,
+                                output: "mathml"
+                            })
+                    });
+                });
+        }
+
         }
     } finally {
         delete sanitizeParams.textFilter;
@@ -450,6 +473,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
         'markdown-body': isHtmlMessage && !emojiBody,
     });
 
+
     return isDisplayedWithHtml ?
         <span
             key="body"
diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index c550f54291..8ec590cba5 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -18,6 +18,7 @@ limitations under the License.
 import Markdown from '../Markdown';
 import {makeGenericPermalink} from "../utils/permalinks/Permalinks";
 import EditorModel from "./model";
+import { AllHtmlEntities } from 'html-entities';
 
 export function mdSerialize(model: EditorModel) {
     return model.parts.reduce((html, part) => {
@@ -38,7 +39,27 @@ export function mdSerialize(model: EditorModel) {
 }
 
 export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) {
-    const md = mdSerialize(model);
+    var md = mdSerialize(model);
+
+    if (true) { // TODO: add katex setting
+        const mathDelimiters = [ // TODO: make customizable
+            { left: "\\$\\$\\$", right: "\\$\\$\\$", display: true },
+            { left: "\\$\\$", right: "\\$\\$", display: false }
+        ];
+
+        mathDelimiters.forEach(function (d) {
+            var reg = RegExp(d.left + "(.*?)" + d.right, "g");
+            md = md.replace(reg, function(match, p1) {
+                const p1e = AllHtmlEntities.encode(p1);
+                if (d.display == true) {
+                    return `<div data-mx-maths="${p1e}"><code>${p1e}</code></div>`;
+                } else {
+                    return `<span data-mx-maths="${p1e}"><code>${p1e}</code></span>`;
+                }
+            });
+        });
+    }
+
     const parser = new Markdown(md);
     if (!parser.isPlainText() || forceHTML) {
         return parser.toHTML();

From e78734bbf6b2fbf1ebee530921998ff97c56f203 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sun, 20 Sep 2020 14:20:35 +0100
Subject: [PATCH 03/32] Deserialize back to math delimiters for editing

---
 src/HtmlUtils.tsx         |  4 +++-
 src/editor/deserialize.ts | 12 ++++++++++++
 2 files changed, 15 insertions(+), 1 deletion(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 344fb3514c..46bc7b441c 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -534,7 +534,6 @@ export function checkBlockNode(node: Node) {
         case "H6":
         case "PRE":
         case "BLOCKQUOTE":
-        case "DIV":
         case "P":
         case "UL":
         case "OL":
@@ -547,6 +546,9 @@ export function checkBlockNode(node: Node) {
         case "TH":
         case "TD":
             return true;
+        case "DIV":
+            // don't treat math nodes as block nodes for deserializing
+            return !(node as HTMLElement).hasAttribute("data-mx-maths");
         default:
             return false;
     }
diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts
index ec697b193c..edaa330e50 100644
--- a/src/editor/deserialize.ts
+++ b/src/editor/deserialize.ts
@@ -130,6 +130,18 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl
             }
             break;
         }
+        case "DIV":
+        case "SPAN": {
+            // math nodes are translated back into delimited latex strings
+            if (n.hasAttribute("data-mx-maths")) {
+                const delim = (n.nodeName == "SPAN") ? "$$" : "$$$";
+                const tex = n.getAttribute("data-mx-maths");
+                return partCreator.plain(delim + tex + delim);
+            } else if (!checkDescendInto(n)) {
+                return partCreator.plain(n.textContent);
+            }
+            break;
+        }
         case "OL":
             state.listIndex.push((<HTMLOListElement>n).start || 1);
             /* falls through */

From 428a6b94ff5c34533b8684e5ae8b019a4dbec07c Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sun, 20 Sep 2020 15:07:12 +0100
Subject: [PATCH 04/32] math off by default, enable with latex_maths flag

---
 src/HtmlUtils.tsx       | 4 +++-
 src/editor/serialize.ts | 3 ++-
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 46bc7b441c..047a891847 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -28,6 +28,7 @@ import EMOJIBASE_REGEX from 'emojibase-regex';
 import url from 'url';
 import katex from 'katex';
 import { AllHtmlEntities } from 'html-entities';
+import SdkConfig from './SdkConfig';
 
 import {MatrixClientPeg} from './MatrixClientPeg';
 import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
@@ -50,6 +51,7 @@ const ZWJ_REGEX = new RegExp("\u200D|\u2003", "g");
 // Regex pattern for whitespace characters
 const WHITESPACE_REGEX = new RegExp("\\s", "g");
 
+
 const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
 
 const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
@@ -411,7 +413,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
         if (isHtmlMessage) {
             isDisplayedWithHtml = true;
             safeBody = sanitizeHtml(formattedBody, sanitizeParams);
-            if (true) { // TODO: add katex setting
+            if (SdkConfig.get()['latex_maths']) {
                 const mathDelimiters = [
                     { left: "<div data-mx-maths=\"", right: "\">.*?</div>", display: true },
                     { left: "<span data-mx-maths=\"", right: "\">.*?</span>", display: false }
diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index 8ec590cba5..72a551a4a3 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -19,6 +19,7 @@ import Markdown from '../Markdown';
 import {makeGenericPermalink} from "../utils/permalinks/Permalinks";
 import EditorModel from "./model";
 import { AllHtmlEntities } from 'html-entities';
+import SdkConfig from '../SdkConfig';
 
 export function mdSerialize(model: EditorModel) {
     return model.parts.reduce((html, part) => {
@@ -41,7 +42,7 @@ export function mdSerialize(model: EditorModel) {
 export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) {
     var md = mdSerialize(model);
 
-    if (true) { // TODO: add katex setting
+    if (SdkConfig.get()['latex_maths']) {
         const mathDelimiters = [ // TODO: make customizable
             { left: "\\$\\$\\$", right: "\\$\\$\\$", display: true },
             { left: "\\$\\$", right: "\\$\\$", display: false }

From e4448ae1ad87cbd3e47c73a589012494ec7d4189 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sun, 20 Sep 2020 16:52:29 +0100
Subject: [PATCH 05/32] send fallback in pre tags, not code

---
 src/editor/serialize.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index 72a551a4a3..c0d9509ffa 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -53,9 +53,9 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
             md = md.replace(reg, function(match, p1) {
                 const p1e = AllHtmlEntities.encode(p1);
                 if (d.display == true) {
-                    return `<div data-mx-maths="${p1e}"><code>${p1e}</code></div>`;
+                    return `<div data-mx-maths="${p1e}"><pre>${p1e}</pre></div>`;
                 } else {
-                    return `<span data-mx-maths="${p1e}"><code>${p1e}</code></span>`;
+                    return `<span data-mx-maths="${p1e}"><pre>${p1e}</pre></span>`;
                 }
             });
         });

From 7e6d7053e0a6c55f082153a521de079c7db2d77c Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sun, 20 Sep 2020 17:02:27 +0100
Subject: [PATCH 06/32] Revert "send fallback in pre tags, not code" (code
 looks better)

This reverts commit e4448ae1ad87cbd3e47c73a589012494ec7d4189.
---
 src/editor/serialize.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index c0d9509ffa..72a551a4a3 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -53,9 +53,9 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
             md = md.replace(reg, function(match, p1) {
                 const p1e = AllHtmlEntities.encode(p1);
                 if (d.display == true) {
-                    return `<div data-mx-maths="${p1e}"><pre>${p1e}</pre></div>`;
+                    return `<div data-mx-maths="${p1e}"><code>${p1e}</code></div>`;
                 } else {
-                    return `<span data-mx-maths="${p1e}"><pre>${p1e}</pre></span>`;
+                    return `<span data-mx-maths="${p1e}"><code>${p1e}</code></span>`;
                 }
             });
         });

From 1f24b5b90c9fe6a743db17d14b726e1aefd15f6f Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sun, 20 Sep 2020 17:48:42 +0100
Subject: [PATCH 07/32] made math display slightly larger

---
 res/css/structures/_RoomView.scss | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index 572c7166d2..571c34fcb0 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -205,6 +205,10 @@ limitations under the License.
     clear: both;
 }
 
+.mx_RoomView_MessageList .katex {
+    font-size: 1.3em;
+}
+
 li.mx_RoomView_myReadMarker_container {
     height: 0px;
     margin: 0px;

From 24a1834f9b37993b79ec92c1c3081d6aa7777d37 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Mon, 21 Sep 2020 09:00:24 +0100
Subject: [PATCH 08/32] support multi-line and escaped $

---
 src/HtmlUtils.tsx       | 6 +++---
 src/editor/serialize.ts | 6 +++---
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 047a891847..569b1662fe 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -415,12 +415,12 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
             safeBody = sanitizeHtml(formattedBody, sanitizeParams);
             if (SdkConfig.get()['latex_maths']) {
                 const mathDelimiters = [
-                    { left: "<div data-mx-maths=\"", right: "\">.*?</div>", display: true },
-                    { left: "<span data-mx-maths=\"", right: "\">.*?</span>", display: false }
+                    { pattern: "<div data-mx-maths=\"([^\"]*)\">(.|\\s)*?</div>", display: true },
+                    { pattern: "<span data-mx-maths=\"([^\"]*)\">(.|\\s)*?</span>", display: false }
                 ];
 
                 mathDelimiters.forEach(function (d) {
-                    var reg = RegExp(d.left + "(.*?)" + d.right, "g");
+                    var reg = RegExp(d.pattern, "gm");
 
                     safeBody = safeBody.replace(reg, function(match, p1) {
                         return katex.renderToString(
diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index 72a551a4a3..d0a28266eb 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -44,12 +44,12 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
 
     if (SdkConfig.get()['latex_maths']) {
         const mathDelimiters = [ // TODO: make customizable
-            { left: "\\$\\$\\$", right: "\\$\\$\\$", display: true },
-            { left: "\\$\\$", right: "\\$\\$", display: false }
+            { pattern: "\\$\\$\\$(([^$]|\\\\\\$)*)\\$\\$\\$", display: true },
+            { pattern: "\\$\\$(([^$]|\\\\\\$)*)\\$\\$", display: false }
         ];
 
         mathDelimiters.forEach(function (d) {
-            var reg = RegExp(d.left + "(.*?)" + d.right, "g");
+            var reg = RegExp(d.pattern, "gm");
             md = md.replace(reg, function(match, p1) {
                 const p1e = AllHtmlEntities.encode(p1);
                 if (d.display == true) {

From 4df8754aad0333c840eceb1892faa9f3c90f2405 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Mon, 21 Sep 2020 11:00:39 +0100
Subject: [PATCH 09/32] allow custom latex delimiters in config.json

---
 src/editor/deserialize.ts | 10 ++++++++--
 src/editor/serialize.ts   | 26 ++++++++++++--------------
 2 files changed, 20 insertions(+), 16 deletions(-)

diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts
index edaa330e50..e27eecd2db 100644
--- a/src/editor/deserialize.ts
+++ b/src/editor/deserialize.ts
@@ -21,6 +21,7 @@ import { walkDOMDepthFirst } from "./dom";
 import { checkBlockNode } from "../HtmlUtils";
 import { getPrimaryPermalinkEntity } from "../utils/permalinks/Permalinks";
 import { PartCreator } from "./parts";
+import SdkConfig from "../SdkConfig";
 
 function parseAtRoomMentions(text: string, partCreator: PartCreator) {
     const ATROOM = "@room";
@@ -134,9 +135,14 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl
         case "SPAN": {
             // math nodes are translated back into delimited latex strings
             if (n.hasAttribute("data-mx-maths")) {
-                const delim = (n.nodeName == "SPAN") ? "$$" : "$$$";
+                const delimLeft = (n.nodeName == "SPAN") ?
+                    (SdkConfig.get()['latex_maths_delims'] || {})['inline_left'] || "$$" :
+                    (SdkConfig.get()['latex_maths_delims'] || {})['display_left'] || "$$$";
+                const delimRight = (n.nodeName == "SPAN") ?
+                    (SdkConfig.get()['latex_maths_delims'] || {})['inline_right'] || "$$" :
+                    (SdkConfig.get()['latex_maths_delims'] || {})['display_right'] || "$$$";
                 const tex = n.getAttribute("data-mx-maths");
-                return partCreator.plain(delim + tex + delim);
+                return partCreator.plain(delimLeft + tex + delimRight);
             } else if (!checkDescendInto(n)) {
                 return partCreator.plain(n.textContent);
             }
diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index d0a28266eb..da8ae4e820 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -43,21 +43,19 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
     var md = mdSerialize(model);
 
     if (SdkConfig.get()['latex_maths']) {
-        const mathDelimiters = [ // TODO: make customizable
-            { pattern: "\\$\\$\\$(([^$]|\\\\\\$)*)\\$\\$\\$", display: true },
-            { pattern: "\\$\\$(([^$]|\\\\\\$)*)\\$\\$", display: false }
-        ];
+        const displayPattern = (SdkConfig.get()['latex_maths_delims'] || {})['display_pattern'] ||
+            "\\$\\$\\$(([^$]|\\\\\\$)*)\\$\\$\\$";
+        const inlinePattern = (SdkConfig.get()['latex_maths_delims'] || {})['inline_pattern'] ||
+            "\\$\\$(([^$]|\\\\\\$)*)\\$\\$";
 
-        mathDelimiters.forEach(function (d) {
-            var reg = RegExp(d.pattern, "gm");
-            md = md.replace(reg, function(match, p1) {
-                const p1e = AllHtmlEntities.encode(p1);
-                if (d.display == true) {
-                    return `<div data-mx-maths="${p1e}"><code>${p1e}</code></div>`;
-                } else {
-                    return `<span data-mx-maths="${p1e}"><code>${p1e}</code></span>`;
-                }
-            });
+        md = md.replace(RegExp(displayPattern, "gm"), function(m,p1) {
+            const p1e = AllHtmlEntities.encode(p1);
+            return `<div data-mx-maths="${p1e}"><code>${p1e}</code></div>`;
+        });
+
+        md = md.replace(RegExp(inlinePattern, "gm"), function(m,p1) {
+            const p1e = AllHtmlEntities.encode(p1);
+            return `<span data-mx-maths="${p1e}"><code>${p1e}</code></span>`;
         });
     }
 

From 1b689bb4e11c1329072a85002ea90abfaf9043df Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Mon, 21 Sep 2020 22:02:19 +0100
Subject: [PATCH 10/32] tell markdown to ignore math tags

---
 src/Markdown.js | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/Markdown.js b/src/Markdown.js
index 492450e87d..dc15e7d6b3 100644
--- a/src/Markdown.js
+++ b/src/Markdown.js
@@ -16,13 +16,19 @@ limitations under the License.
 
 import commonmark from 'commonmark';
 import {escape} from "lodash";
+import SdkConfig from './SdkConfig';
 
-const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
+const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u', 'code'];
 
 // These types of node are definitely text
 const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
 
 function is_allowed_html_tag(node) {
+    if (SdkConfig.get()['latex_maths'] &&
+        node.literal.match(/^<\/?(div|span)( data-mx-maths="[^"]*")?>$/) != null) {
+        return true;
+    }
+
     // Regex won't work for tags with attrs, but we only
     // allow <del> anyway.
     const matches = /^<\/?(.*)>$/.exec(node.literal);
@@ -30,6 +36,7 @@ function is_allowed_html_tag(node) {
         const tag = matches[1];
         return ALLOWED_HTML_TAGS.indexOf(tag) > -1;
     }
+
     return false;
 }
 

From aded3c9de2b14010612b7d9581b10366d9dc3be2 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Tue, 22 Sep 2020 11:54:23 +0100
Subject: [PATCH 11/32] cosmetic changes (lint)

---
 src/HtmlUtils.tsx       | 13 +++++--------
 src/editor/serialize.ts |  6 +++---
 2 files changed, 8 insertions(+), 11 deletions(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 569b1662fe..7bccd47622 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -416,24 +416,21 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
             if (SdkConfig.get()['latex_maths']) {
                 const mathDelimiters = [
                     { pattern: "<div data-mx-maths=\"([^\"]*)\">(.|\\s)*?</div>", display: true },
-                    { pattern: "<span data-mx-maths=\"([^\"]*)\">(.|\\s)*?</span>", display: false }
+                    { pattern: "<span data-mx-maths=\"([^\"]*)\">(.|\\s)*?</span>", display: false },
                 ];
 
-                mathDelimiters.forEach(function (d) {
-                    var reg = RegExp(d.pattern, "gm");
-
-                    safeBody = safeBody.replace(reg, function(match, p1) {
+                mathDelimiters.forEach(function(d) {
+                    safeBody = safeBody.replace(RegExp(d.pattern, "gm"), function(m, p1) {
                         return katex.renderToString(
                             AllHtmlEntities.decode(p1),
                             {
                                 throwOnError: false,
                                 displayMode: d.display,
-                                output: "mathml"
+                                output: "mathml",
                             })
                     });
                 });
-        }
-
+            }
         }
     } finally {
         delete sanitizeParams.textFilter;
diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index da8ae4e820..02194a1d59 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -40,7 +40,7 @@ export function mdSerialize(model: EditorModel) {
 }
 
 export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) {
-    var md = mdSerialize(model);
+    let md = mdSerialize(model);
 
     if (SdkConfig.get()['latex_maths']) {
         const displayPattern = (SdkConfig.get()['latex_maths_delims'] || {})['display_pattern'] ||
@@ -48,12 +48,12 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
         const inlinePattern = (SdkConfig.get()['latex_maths_delims'] || {})['inline_pattern'] ||
             "\\$\\$(([^$]|\\\\\\$)*)\\$\\$";
 
-        md = md.replace(RegExp(displayPattern, "gm"), function(m,p1) {
+        md = md.replace(RegExp(displayPattern, "gm"), function(m, p1) {
             const p1e = AllHtmlEntities.encode(p1);
             return `<div data-mx-maths="${p1e}"><code>${p1e}</code></div>`;
         });
 
-        md = md.replace(RegExp(inlinePattern, "gm"), function(m,p1) {
+        md = md.replace(RegExp(inlinePattern, "gm"), function(m, p1) {
             const p1e = AllHtmlEntities.encode(p1);
             return `<span data-mx-maths="${p1e}"><code>${p1e}</code></span>`;
         });

From d2054ea685bad49af11ec9a64b5aa4218bc204c0 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Fri, 25 Sep 2020 09:05:22 +0100
Subject: [PATCH 12/32] HTML output for cross-browser support

---
 res/css/structures/_RoomView.scss | 4 ----
 src/HtmlUtils.tsx                 | 2 +-
 2 files changed, 1 insertion(+), 5 deletions(-)

diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index 571c34fcb0..572c7166d2 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -205,10 +205,6 @@ limitations under the License.
     clear: both;
 }
 
-.mx_RoomView_MessageList .katex {
-    font-size: 1.3em;
-}
-
 li.mx_RoomView_myReadMarker_container {
     height: 0px;
     margin: 0px;
diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 7bccd47622..70a2a3f000 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -426,7 +426,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
                             {
                                 throwOnError: false,
                                 displayMode: d.display,
-                                output: "mathml",
+                                output: "htmlAndMathml",
                             })
                     });
                 });

From 65c4460abcdb64bac14bdd72e3b970a96dd52299 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Fri, 9 Oct 2020 15:47:11 +0100
Subject: [PATCH 13/32] whitespace fixes

---
 src/HtmlUtils.tsx | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 70a2a3f000..da3eb3b128 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -51,7 +51,6 @@ const ZWJ_REGEX = new RegExp("\u200D|\u2003", "g");
 // Regex pattern for whitespace characters
 const WHITESPACE_REGEX = new RegExp("\\s", "g");
 
-
 const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
 
 const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
@@ -472,7 +471,6 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
         'markdown-body': isHtmlMessage && !emojiBody,
     });
 
-
     return isDisplayedWithHtml ?
         <span
             key="body"

From 919a1a8125cbd0e44a5e6702c32f7ce3ac92bd81 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sat, 10 Oct 2020 09:12:53 +0100
Subject: [PATCH 14/32] only allow code tags inside math tag

---
 src/Markdown.js | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/src/Markdown.js b/src/Markdown.js
index dc15e7d6b3..9914cff85a 100644
--- a/src/Markdown.js
+++ b/src/Markdown.js
@@ -18,14 +18,21 @@ import commonmark from 'commonmark';
 import {escape} from "lodash";
 import SdkConfig from './SdkConfig';
 
-const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u', 'code'];
+const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
 
 // These types of node are definitely text
 const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
 
+function is_math_node(node) {
+    return node != null &&
+        node.literal != null &&
+        node.literal.match(/^<((div|span) data-mx-maths="[^"]*"|\/(div|span))>$/) != null;
+}
+
 function is_allowed_html_tag(node) {
     if (SdkConfig.get()['latex_maths'] &&
-        node.literal.match(/^<\/?(div|span)( data-mx-maths="[^"]*")?>$/) != null) {
+        (is_math_node(node) ||
+         (node.literal.match(/^<\/?code>$/) && is_math_node(node.parent)))) {
         return true;
     }
 

From 96742fc3093cc88cd609d731d932a05ab094262f Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sat, 10 Oct 2020 16:32:49 +0100
Subject: [PATCH 15/32] latex math as labs setting

---
 src/HtmlUtils.tsx        | 4 ++--
 src/Markdown.js          | 4 ++--
 src/editor/serialize.ts  | 3 ++-
 src/settings/Settings.ts | 6 ++++++
 4 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index da3eb3b128..ca718cd9aa 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -28,7 +28,7 @@ import EMOJIBASE_REGEX from 'emojibase-regex';
 import url from 'url';
 import katex from 'katex';
 import { AllHtmlEntities } from 'html-entities';
-import SdkConfig from './SdkConfig';
+import SettingsStore from './settings/SettingsStore';
 
 import {MatrixClientPeg} from './MatrixClientPeg';
 import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
@@ -412,7 +412,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
         if (isHtmlMessage) {
             isDisplayedWithHtml = true;
             safeBody = sanitizeHtml(formattedBody, sanitizeParams);
-            if (SdkConfig.get()['latex_maths']) {
+            if (SettingsStore.getValue("feature_latex_maths")) {
                 const mathDelimiters = [
                     { pattern: "<div data-mx-maths=\"([^\"]*)\">(.|\\s)*?</div>", display: true },
                     { pattern: "<span data-mx-maths=\"([^\"]*)\">(.|\\s)*?</span>", display: false },
diff --git a/src/Markdown.js b/src/Markdown.js
index 9914cff85a..329dcdd996 100644
--- a/src/Markdown.js
+++ b/src/Markdown.js
@@ -16,7 +16,7 @@ limitations under the License.
 
 import commonmark from 'commonmark';
 import {escape} from "lodash";
-import SdkConfig from './SdkConfig';
+import SettingsStore from './settings/SettingsStore';
 
 const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
 
@@ -30,7 +30,7 @@ function is_math_node(node) {
 }
 
 function is_allowed_html_tag(node) {
-    if (SdkConfig.get()['latex_maths'] &&
+    if (SettingsStore.getValue("feature_latex_maths") &&
         (is_math_node(node) ||
          (node.literal.match(/^<\/?code>$/) && is_math_node(node.parent)))) {
         return true;
diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index 02194a1d59..9f24cd5eb2 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -19,6 +19,7 @@ import Markdown from '../Markdown';
 import {makeGenericPermalink} from "../utils/permalinks/Permalinks";
 import EditorModel from "./model";
 import { AllHtmlEntities } from 'html-entities';
+import SettingsStore from '../settings/SettingsStore';
 import SdkConfig from '../SdkConfig';
 
 export function mdSerialize(model: EditorModel) {
@@ -42,7 +43,7 @@ export function mdSerialize(model: EditorModel) {
 export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) {
     let md = mdSerialize(model);
 
-    if (SdkConfig.get()['latex_maths']) {
+    if (SettingsStore.getValue("feature_latex_maths")) {
         const displayPattern = (SdkConfig.get()['latex_maths_delims'] || {})['display_pattern'] ||
             "\\$\\$\\$(([^$]|\\\\\\$)*)\\$\\$\\$";
         const inlinePattern = (SdkConfig.get()['latex_maths_delims'] || {})['inline_pattern'] ||
diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index 737c882919..2f817c264c 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -116,6 +116,12 @@ export interface ISetting {
 }
 
 export const SETTINGS: {[setting: string]: ISetting} = {
+    "feature_latex_maths": {
+        isFeature: true,
+        displayName: _td("LaTeX math in messages"),
+        supportedLevels: LEVELS_FEATURE,
+        default: false,
+    },
     "feature_communities_v2_prototypes": {
         isFeature: true,
         displayName: _td(

From a89adb86a5912d3ce71171583181175fe2564a23 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sat, 10 Oct 2020 16:33:25 +0100
Subject: [PATCH 16/32] i18n en+nl for latex math labs setting

---
 src/i18n/strings/en_EN.json | 1 +
 src/i18n/strings/en_US.json | 1 +
 src/i18n/strings/nl.json    | 1 +
 3 files changed, 3 insertions(+)

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index d7360430ae..d7b40fc198 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -856,6 +856,7 @@
     "click to reveal": "click to reveal",
     "Clear cache and reload": "Clear cache and reload",
     "Labs": "Labs",
+    "LaTeX math in messages": "LaTeX math in messages",
     "Customise your experience with experimental labs features. <a>Learn more</a>.": "Customise your experience with experimental labs features. <a>Learn more</a>.",
     "Ignored/Blocked": "Ignored/Blocked",
     "Error adding ignored user/server": "Error adding ignored user/server",
diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json
index a1275fb089..c00bf03b29 100644
--- a/src/i18n/strings/en_US.json
+++ b/src/i18n/strings/en_US.json
@@ -128,6 +128,7 @@
     "Kick": "Kick",
     "Kicks user with given id": "Kicks user with given id",
     "Labs": "Labs",
+    "LaTeX math in messages": "LaTeX math in messages",
     "Ignore": "Ignore",
     "Unignore": "Unignore",
     "You are now ignoring %(userId)s": "You are now ignoring %(userId)s",
diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json
index bb0fb5def6..d991962eec 100644
--- a/src/i18n/strings/nl.json
+++ b/src/i18n/strings/nl.json
@@ -199,6 +199,7 @@
     "%(targetName)s joined the room.": "%(targetName)s is tot het gesprek toegetreden.",
     "Jump to first unread message.": "Spring naar het eerste ongelezen bericht.",
     "Labs": "Experimenteel",
+    "LaTeX math in messages": "LaTeX wiskunde in berichten",
     "Last seen": "Laatst gezien",
     "Leave room": "Gesprek verlaten",
     "%(targetName)s left the room.": "%(targetName)s heeft het gesprek verlaten.",

From bdd332c8b5366398d4af166b49b3eaf1cddb6230 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sat, 10 Oct 2020 20:05:35 +0100
Subject: [PATCH 17/32] ran yarn i18n

---
 src/i18n/strings/en_EN.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index a33104ab12..b41a19aa21 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -438,6 +438,7 @@
     "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
     "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
     "Change notification settings": "Change notification settings",
+    "LaTeX math in messages": "LaTeX math in messages",
     "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.",
     "New spinner design": "New spinner design",
     "Message Pinning": "Message Pinning",
@@ -848,7 +849,6 @@
     "click to reveal": "click to reveal",
     "Clear cache and reload": "Clear cache and reload",
     "Labs": "Labs",
-    "LaTeX math in messages": "LaTeX math in messages",
     "Customise your experience with experimental labs features. <a>Learn more</a>.": "Customise your experience with experimental labs features. <a>Learn more</a>.",
     "Ignored/Blocked": "Ignored/Blocked",
     "Error adding ignored user/server": "Error adding ignored user/server",

From f0c4473107d0c3589479809d8accd79b9c4dba08 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Mon, 12 Oct 2020 21:01:11 +0100
Subject: [PATCH 18/32] tell markdown parser to ignore properly-formatted math
 tags

---
 src/Markdown.js | 51 +++++++++++++++++++++++++++++++++++++++----------
 1 file changed, 41 insertions(+), 10 deletions(-)

diff --git a/src/Markdown.js b/src/Markdown.js
index 329dcdd996..564a2ed0a8 100644
--- a/src/Markdown.js
+++ b/src/Markdown.js
@@ -23,19 +23,47 @@ const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
 // These types of node are definitely text
 const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
 
-function is_math_node(node) {
-    return node != null &&
-        node.literal != null &&
-        node.literal.match(/^<((div|span) data-mx-maths="[^"]*"|\/(div|span))>$/) != null;
+// prevent renderer from interpreting contents of AST node
+function freeze_node(walker, node) {
+    const newNode = new commonmark.Node('custom_inline', node.sourcepos);
+    newNode.onEnter = node.literal;
+    node.insertAfter(newNode);
+    node.unlink();
+    walker.resumeAt(newNode.next, true);
+}
+
+// prevent renderer from interpreting contents of latex math tags
+function freeze_math(parsed) {
+    const walker = parsed.walker();
+    let ev;
+    let inMath = false;
+    while ( (ev = walker.next()) ) {
+        const node = ev.node;
+        if (ev.entering) {
+            if (!inMath) {
+                // entering a math tag
+                if (node.literal != null && node.literal.match('^<(div|span) data-mx-maths="[^"]*">$') != null) {
+                    inMath = true;
+                    freeze_node(walker, node);
+                }
+            } else {
+                // math tags should only contain a single code block, with URL-escaped latex as fallback output
+                if (node.literal != null && node.literal.match('^(<code>|</code>|[^<>]*)$')) {
+                    freeze_node(walker, node);
+                // leave when span or div is closed
+                } else if (node.literal == '</span>' || node.literal == '</div>') {
+                    inMath = false;
+                    freeze_node(walker, node);
+                // this case only happens if we have improperly formatted math tags, so bail
+                } else {
+                    inMath = false;
+                }
+            }
+        }
+    }
 }
 
 function is_allowed_html_tag(node) {
-    if (SettingsStore.getValue("feature_latex_maths") &&
-        (is_math_node(node) ||
-         (node.literal.match(/^<\/?code>$/) && is_math_node(node.parent)))) {
-        return true;
-    }
-
     // Regex won't work for tags with attrs, but we only
     // allow <del> anyway.
     const matches = /^<\/?(.*)>$/.exec(node.literal);
@@ -173,6 +201,9 @@ export default class Markdown {
 */
         };
 
+        // prevent strange behaviour when mixing latex math and markdown
+        freeze_math(this.parsed);
+
         return renderer.render(this.parsed);
     }
 

From 38d1aac978d49160bed9c96b2a1205a4e7fb707f Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Mon, 12 Oct 2020 21:15:38 +0100
Subject: [PATCH 19/32] removed useless import and whitespace

---
 src/Markdown.js | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/Markdown.js b/src/Markdown.js
index 564a2ed0a8..2e6f391818 100644
--- a/src/Markdown.js
+++ b/src/Markdown.js
@@ -16,7 +16,6 @@ limitations under the License.
 
 import commonmark from 'commonmark';
 import {escape} from "lodash";
-import SettingsStore from './settings/SettingsStore';
 
 const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
 
@@ -71,7 +70,6 @@ function is_allowed_html_tag(node) {
         const tag = matches[1];
         return ALLOWED_HTML_TAGS.indexOf(tag) > -1;
     }
-
     return false;
 }
 

From cc713aff72c56478edb4f1eafbdc55b8c9fd4248 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Wed, 14 Oct 2020 09:35:57 +0100
Subject: [PATCH 20/32] add fallback output in code block AFTER markdown
 processing

---
 src/Markdown.js         | 49 +++++------------------------------------
 src/editor/serialize.ts | 18 ++++++++++++---
 2 files changed, 21 insertions(+), 46 deletions(-)

diff --git a/src/Markdown.js b/src/Markdown.js
index 2e6f391818..dc4d442aff 100644
--- a/src/Markdown.js
+++ b/src/Markdown.js
@@ -22,47 +22,12 @@ const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
 // These types of node are definitely text
 const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
 
-// prevent renderer from interpreting contents of AST node
-function freeze_node(walker, node) {
-    const newNode = new commonmark.Node('custom_inline', node.sourcepos);
-    newNode.onEnter = node.literal;
-    node.insertAfter(newNode);
-    node.unlink();
-    walker.resumeAt(newNode.next, true);
-}
-
-// prevent renderer from interpreting contents of latex math tags
-function freeze_math(parsed) {
-    const walker = parsed.walker();
-    let ev;
-    let inMath = false;
-    while ( (ev = walker.next()) ) {
-        const node = ev.node;
-        if (ev.entering) {
-            if (!inMath) {
-                // entering a math tag
-                if (node.literal != null && node.literal.match('^<(div|span) data-mx-maths="[^"]*">$') != null) {
-                    inMath = true;
-                    freeze_node(walker, node);
-                }
-            } else {
-                // math tags should only contain a single code block, with URL-escaped latex as fallback output
-                if (node.literal != null && node.literal.match('^(<code>|</code>|[^<>]*)$')) {
-                    freeze_node(walker, node);
-                // leave when span or div is closed
-                } else if (node.literal == '</span>' || node.literal == '</div>') {
-                    inMath = false;
-                    freeze_node(walker, node);
-                // this case only happens if we have improperly formatted math tags, so bail
-                } else {
-                    inMath = false;
-                }
-            }
-        }
-    }
-}
-
 function is_allowed_html_tag(node) {
+    if (node.literal != null &&
+        node.literal.match('^<((div|span) data-mx-maths="[^"]*"|\/(div|span))>$') != null) {
+        return true;
+    }
+
     // Regex won't work for tags with attrs, but we only
     // allow <del> anyway.
     const matches = /^<\/?(.*)>$/.exec(node.literal);
@@ -70,6 +35,7 @@ function is_allowed_html_tag(node) {
         const tag = matches[1];
         return ALLOWED_HTML_TAGS.indexOf(tag) > -1;
     }
+
     return false;
 }
 
@@ -199,9 +165,6 @@ export default class Markdown {
 */
         };
 
-        // prevent strange behaviour when mixing latex math and markdown
-        freeze_math(this.parsed);
-
         return renderer.render(this.parsed);
     }
 
diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index 9f24cd5eb2..88fd1c90fc 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -21,6 +21,7 @@ import EditorModel from "./model";
 import { AllHtmlEntities } from 'html-entities';
 import SettingsStore from '../settings/SettingsStore';
 import SdkConfig from '../SdkConfig';
+import cheerio from 'cheerio';
 
 export function mdSerialize(model: EditorModel) {
     return model.parts.reduce((html, part) => {
@@ -51,18 +52,29 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
 
         md = md.replace(RegExp(displayPattern, "gm"), function(m, p1) {
             const p1e = AllHtmlEntities.encode(p1);
-            return `<div data-mx-maths="${p1e}"><code>${p1e}</code></div>`;
+            return `<div data-mx-maths="${p1e}"></div>`;
         });
 
         md = md.replace(RegExp(inlinePattern, "gm"), function(m, p1) {
             const p1e = AllHtmlEntities.encode(p1);
-            return `<span data-mx-maths="${p1e}"><code>${p1e}</code></span>`;
+            return `<span data-mx-maths="${p1e}"></span>`;
         });
     }
 
     const parser = new Markdown(md);
     if (!parser.isPlainText() || forceHTML) {
-        return parser.toHTML();
+        // feed Markdown output to HTML parser
+        const phtml = cheerio.load(parser.toHTML(),
+            { _useHtmlParser2: true, decodeEntities: false })
+
+        // add fallback output for latex math, which should not be interpreted as markdown
+        phtml('div, span').each(function() {
+            const tex = phtml(this).attr('data-mx-maths')
+            if (tex) {
+                phtml(this).html(`<code>${tex}</code>`)
+            }
+        });
+        return phtml.html();
     }
     // ensure removal of escape backslashes in non-Markdown messages
     if (md.indexOf("\\") > -1) {

From 10b732131a7315aca652677857a285d7dabb243b Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Wed, 14 Oct 2020 22:16:28 +0100
Subject: [PATCH 21/32] use html parser rather than regexes

---
 src/HtmlUtils.tsx | 28 +++++++++++++---------------
 1 file changed, 13 insertions(+), 15 deletions(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 6bae0b25b6..dc2f45210b 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -30,6 +30,7 @@ import url from 'url';
 import katex from 'katex';
 import { AllHtmlEntities } from 'html-entities';
 import SettingsStore from './settings/SettingsStore';
+import cheerio from 'cheerio';
 
 import {MatrixClientPeg} from './MatrixClientPeg';
 import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
@@ -414,23 +415,20 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
         if (isHtmlMessage) {
             isDisplayedWithHtml = true;
             safeBody = sanitizeHtml(formattedBody, sanitizeParams);
-            if (SettingsStore.getValue("feature_latex_maths")) {
-                const mathDelimiters = [
-                    { pattern: "<div data-mx-maths=\"([^\"]*)\">(.|\\s)*?</div>", display: true },
-                    { pattern: "<span data-mx-maths=\"([^\"]*)\">(.|\\s)*?</span>", display: false },
-                ];
+            const phtml = cheerio.load(safeBody,
+                { _useHtmlParser2: true, decodeEntities: false })
 
-                mathDelimiters.forEach(function(d) {
-                    safeBody = safeBody.replace(RegExp(d.pattern, "gm"), function(m, p1) {
-                        return katex.renderToString(
-                            AllHtmlEntities.decode(p1),
-                            {
-                                throwOnError: false,
-                                displayMode: d.display,
-                                output: "htmlAndMathml",
-                            })
-                    });
+            if (SettingsStore.getValue("feature_latex_maths")) {
+                phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) {
+                    return katex.renderToString(
+                        AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')),
+                        {
+                            throwOnError: false,
+                            displayMode: e.name == 'div',
+                            output: "htmlAndMathml",
+                        });
                 });
+                safeBody = phtml.html();
             }
         }
     } finally {

From 173d79886544bc57c8de0b1ae4b16a346cd73bae Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Fri, 23 Oct 2020 18:41:24 +0100
Subject: [PATCH 22/32] added cheerio as explicit dep in package.json

---
 package.json | 1 +
 1 file changed, 1 insertion(+)

diff --git a/package.json b/package.json
index 0a3fd7a8b7..ca7d6ee0b7 100644
--- a/package.json
+++ b/package.json
@@ -77,6 +77,7 @@
     "html-entities": "^1.3.1",
     "is-ip": "^2.0.0",
     "katex": "^0.12.0",
+    "cheerio": "^1.0.0-rc.3",
     "linkifyjs": "^2.1.9",
     "lodash": "^4.17.19",
     "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",

From 06b20fad9543063409823540fcd4416a12c3ee21 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Fri, 23 Oct 2020 18:49:56 +0100
Subject: [PATCH 23/32] removed implicit "this"

---
 src/editor/serialize.ts | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index 88fd1c90fc..f31dd67ae7 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -68,10 +68,10 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
             { _useHtmlParser2: true, decodeEntities: false })
 
         // add fallback output for latex math, which should not be interpreted as markdown
-        phtml('div, span').each(function() {
-            const tex = phtml(this).attr('data-mx-maths')
+        phtml('div, span').each(function(i, e) {
+            const tex = phtml(e).attr('data-mx-maths')
             if (tex) {
-                phtml(this).html(`<code>${tex}</code>`)
+                phtml(e).html(`<code>${tex}</code>`)
             }
         });
         return phtml.html();

From 2204e6c64e0042e0b937cf7d42e07816608e0234 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sun, 25 Oct 2020 18:32:24 +0000
Subject: [PATCH 24/32] generate valid block html for commonmark spec

---
 src/editor/serialize.ts | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index f31dd67ae7..bd7845315e 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -52,13 +52,17 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
 
         md = md.replace(RegExp(displayPattern, "gm"), function(m, p1) {
             const p1e = AllHtmlEntities.encode(p1);
-            return `<div data-mx-maths="${p1e}"></div>`;
+            return `<div data-mx-maths="${p1e}">\n\n</div>\n\n`;
         });
 
         md = md.replace(RegExp(inlinePattern, "gm"), function(m, p1) {
             const p1e = AllHtmlEntities.encode(p1);
             return `<span data-mx-maths="${p1e}"></span>`;
         });
+
+        // make sure div tags always start on a new line, otherwise it will confuse
+        // the markdown parser
+        md = md.replace(/(.)<div/g, function(m, p1) { return `${p1}\n<div`; });
     }
 
     const parser = new Markdown(md);

From 3f9f1d03c8445002e053ff15054aa538cc83c514 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Thu, 29 Oct 2020 13:22:09 +0000
Subject: [PATCH 25/32] stubbed isGuest for unit tests

---
 test/components/views/messages/TextualBody-test.js | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/test/components/views/messages/TextualBody-test.js b/test/components/views/messages/TextualBody-test.js
index 07cd51edbd..bf55e9c430 100644
--- a/test/components/views/messages/TextualBody-test.js
+++ b/test/components/views/messages/TextualBody-test.js
@@ -36,6 +36,7 @@ describe("<TextualBody />", () => {
         MatrixClientPeg.matrixClient = {
             getRoom: () => mkStubRoom("room_id"),
             getAccountData: () => undefined,
+            isGuest: () => false,
         };
 
         const ev = mkEvent({
@@ -59,6 +60,7 @@ describe("<TextualBody />", () => {
         MatrixClientPeg.matrixClient = {
             getRoom: () => mkStubRoom("room_id"),
             getAccountData: () => undefined,
+            isGuest: () => false,
         };
 
         const ev = mkEvent({
@@ -83,6 +85,7 @@ describe("<TextualBody />", () => {
             MatrixClientPeg.matrixClient = {
                 getRoom: () => mkStubRoom("room_id"),
                 getAccountData: () => undefined,
+                isGuest: () => false,
             };
         });
 
@@ -135,6 +138,7 @@ describe("<TextualBody />", () => {
                 getHomeserverUrl: () => "https://my_server/",
                 on: () => undefined,
                 removeListener: () => undefined,
+                isGuest: () => false,
             };
         });
 

From 839bae21ae5078e25b7e6a03cc4a99725014b029 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Tue, 10 Nov 2020 18:18:53 +0000
Subject: [PATCH 26/32] made single and double $ default delimiters

---
 src/editor/deserialize.ts | 8 ++++----
 src/editor/serialize.ts   | 4 ++--
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts
index e27eecd2db..6336b4c46b 100644
--- a/src/editor/deserialize.ts
+++ b/src/editor/deserialize.ts
@@ -136,11 +136,11 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl
             // math nodes are translated back into delimited latex strings
             if (n.hasAttribute("data-mx-maths")) {
                 const delimLeft = (n.nodeName == "SPAN") ?
-                    (SdkConfig.get()['latex_maths_delims'] || {})['inline_left'] || "$$" :
-                    (SdkConfig.get()['latex_maths_delims'] || {})['display_left'] || "$$$";
+                    (SdkConfig.get()['latex_maths_delims'] || {})['inline_left'] || "$" :
+                    (SdkConfig.get()['latex_maths_delims'] || {})['display_left'] || "$$";
                 const delimRight = (n.nodeName == "SPAN") ?
-                    (SdkConfig.get()['latex_maths_delims'] || {})['inline_right'] || "$$" :
-                    (SdkConfig.get()['latex_maths_delims'] || {})['display_right'] || "$$$";
+                    (SdkConfig.get()['latex_maths_delims'] || {})['inline_right'] || "$" :
+                    (SdkConfig.get()['latex_maths_delims'] || {})['display_right'] || "$$";
                 const tex = n.getAttribute("data-mx-maths");
                 return partCreator.plain(delimLeft + tex + delimRight);
             } else if (!checkDescendInto(n)) {
diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index bd7845315e..c1f4da306b 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -46,9 +46,9 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
 
     if (SettingsStore.getValue("feature_latex_maths")) {
         const displayPattern = (SdkConfig.get()['latex_maths_delims'] || {})['display_pattern'] ||
-            "\\$\\$\\$(([^$]|\\\\\\$)*)\\$\\$\\$";
-        const inlinePattern = (SdkConfig.get()['latex_maths_delims'] || {})['inline_pattern'] ||
             "\\$\\$(([^$]|\\\\\\$)*)\\$\\$";
+        const inlinePattern = (SdkConfig.get()['latex_maths_delims'] || {})['inline_pattern'] ||
+            "\\$(([^$]|\\\\\\$)*)\\$";
 
         md = md.replace(RegExp(displayPattern, "gm"), function(m, p1) {
             const p1e = AllHtmlEntities.encode(p1);

From 8233ce77cbeda9706932a5ff5d7083a6775a52e0 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Tue, 10 Nov 2020 18:26:09 +0000
Subject: [PATCH 27/32] fixed duplicate import from merge

---
 src/HtmlUtils.tsx | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index d25c420bc9..44fbffb97f 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -33,7 +33,6 @@ import SettingsStore from './settings/SettingsStore';
 import cheerio from 'cheerio';
 
 import {MatrixClientPeg} from './MatrixClientPeg';
-import SettingsStore from './settings/SettingsStore';
 import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
 import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
 import ReplyThread from "./components/views/elements/ReplyThread";

From ca9e43f118bb7386e3bd65e0c92755b0fd26a8bb Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Thu, 19 Nov 2020 07:58:37 +0000
Subject: [PATCH 28/32] reverted translation

---
 src/i18n/strings/nl.json | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json
index 000beb915d..1ec887c364 100644
--- a/src/i18n/strings/nl.json
+++ b/src/i18n/strings/nl.json
@@ -199,7 +199,6 @@
     "%(targetName)s joined the room.": "%(targetName)s is tot het gesprek toegetreden.",
     "Jump to first unread message.": "Spring naar het eerste ongelezen bericht.",
     "Labs": "Experimenteel",
-    "LaTeX math in messages": "LaTeX wiskunde in berichten",
     "Last seen": "Laatst gezien",
     "Leave room": "Gesprek verlaten",
     "%(targetName)s left the room.": "%(targetName)s heeft het gesprek verlaten.",

From dacef10fa605107ab6b512a0e5e255c5696e051c Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Thu, 26 Nov 2020 16:22:10 +0000
Subject: [PATCH 29/32] reverted US translation

---
 src/i18n/strings/en_US.json | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json
index c00bf03b29..a1275fb089 100644
--- a/src/i18n/strings/en_US.json
+++ b/src/i18n/strings/en_US.json
@@ -128,7 +128,6 @@
     "Kick": "Kick",
     "Kicks user with given id": "Kicks user with given id",
     "Labs": "Labs",
-    "LaTeX math in messages": "LaTeX math in messages",
     "Ignore": "Ignore",
     "Unignore": "Unignore",
     "You are now ignoring %(userId)s": "You are now ignoring %(userId)s",

From 7013483dadfeea29d4aa9a942537d30aff240f24 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Thu, 26 Nov 2020 17:26:42 +0000
Subject: [PATCH 30/32] UK spelling maths

---
 src/i18n/strings/en_EN.json | 2 +-
 src/settings/Settings.ts    | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index e8a2fb53c2..faa376f333 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -699,7 +699,7 @@
     "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
     "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
     "Change notification settings": "Change notification settings",
-    "LaTeX math in messages": "LaTeX math in messages",
+    "Render LaTeX maths in messages": "Render LaTeX maths in messages",
     "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.",
     "New spinner design": "New spinner design",
     "Message Pinning": "Message Pinning",
diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index 5600a1346d..a7c1f849fc 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -118,7 +118,7 @@ export interface ISetting {
 export const SETTINGS: {[setting: string]: ISetting} = {
     "feature_latex_maths": {
         isFeature: true,
-        displayName: _td("LaTeX math in messages"),
+        displayName: _td("Render LaTeX maths in messages"),
         supportedLevels: LEVELS_FEATURE,
         default: false,
     },

From 494ae3e4215cc2fe0583c316b8ea1d895503d39e Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Thu, 26 Nov 2020 17:45:11 +0000
Subject: [PATCH 31/32] parse html for latex rendering inside settings block

---
 src/HtmlUtils.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 44fbffb97f..43aeae24e6 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -418,10 +418,10 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
         if (isHtmlMessage) {
             isDisplayedWithHtml = true;
             safeBody = sanitizeHtml(formattedBody, sanitizeParams);
-            const phtml = cheerio.load(safeBody,
-                { _useHtmlParser2: true, decodeEntities: false })
 
             if (SettingsStore.getValue("feature_latex_maths")) {
+                const phtml = cheerio.load(safeBody,
+                { _useHtmlParser2: true, decodeEntities: false })
                 phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) {
                     return katex.renderToString(
                         AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')),

From 79baea9c4a7c236e63622b5189806016ecd5f999 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Thu, 26 Nov 2020 17:54:11 +0000
Subject: [PATCH 32/32] fixed indent

---
 src/HtmlUtils.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 43aeae24e6..2301ad250b 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -421,7 +421,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
 
             if (SettingsStore.getValue("feature_latex_maths")) {
                 const phtml = cheerio.load(safeBody,
-                { _useHtmlParser2: true, decodeEntities: false })
+                    { _useHtmlParser2: true, decodeEntities: false })
                 phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) {
                     return katex.renderToString(
                         AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')),