Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

LaTeX rendering in element-web using KaTeX #5244

Merged
merged 35 commits into from
Nov 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0604c86
added katex package and import
akissinger Sep 19, 2020
becc79d
send tex math as data-mx-maths attribute
akissinger Sep 20, 2020
e78734b
Deserialize back to math delimiters for editing
akissinger Sep 20, 2020
428a6b9
math off by default, enable with latex_maths flag
akissinger Sep 20, 2020
e4448ae
send fallback in pre tags, not code
akissinger Sep 20, 2020
7e6d705
Revert "send fallback in pre tags, not code" (code looks better)
akissinger Sep 20, 2020
1f24b5b
made math display slightly larger
akissinger Sep 20, 2020
24a1834
support multi-line and escaped $
akissinger Sep 21, 2020
4df8754
allow custom latex delimiters in config.json
akissinger Sep 21, 2020
1b689bb
tell markdown to ignore math tags
akissinger Sep 21, 2020
aded3c9
cosmetic changes (lint)
akissinger Sep 22, 2020
d2054ea
HTML output for cross-browser support
akissinger Sep 25, 2020
65c4460
whitespace fixes
akissinger Oct 9, 2020
919a1a8
only allow code tags inside math tag
akissinger Oct 10, 2020
96742fc
latex math as labs setting
akissinger Oct 10, 2020
a89adb8
i18n en+nl for latex math labs setting
akissinger Oct 10, 2020
aafaf34
Merge branch 'develop' into katex
akissinger Oct 10, 2020
bdd332c
ran yarn i18n
akissinger Oct 10, 2020
f0c4473
tell markdown parser to ignore properly-formatted math tags
akissinger Oct 12, 2020
38d1aac
removed useless import and whitespace
akissinger Oct 12, 2020
cc713af
add fallback output in code block AFTER markdown processing
akissinger Oct 14, 2020
10b7321
use html parser rather than regexes
akissinger Oct 14, 2020
173d798
added cheerio as explicit dep in package.json
akissinger Oct 23, 2020
06b20fa
removed implicit "this"
akissinger Oct 23, 2020
4536f51
Merge branch 'develop' into katex
akissinger Oct 25, 2020
2204e6c
generate valid block html for commonmark spec
akissinger Oct 25, 2020
3f9f1d0
stubbed isGuest for unit tests
akissinger Oct 29, 2020
839bae2
made single and double $ default delimiters
akissinger Nov 10, 2020
5f3af78
Merge branch 'develop' into katex
akissinger Nov 10, 2020
8233ce7
fixed duplicate import from merge
akissinger Nov 10, 2020
ca9e43f
reverted translation
akissinger Nov 19, 2020
dacef10
reverted US translation
akissinger Nov 26, 2020
7013483
UK spelling maths
akissinger Nov 26, 2020
494ae3e
parse html for latex rendering inside settings block
akissinger Nov 26, 2020
79baea9
fixed indent
akissinger Nov 26, 2020
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: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@
"highlight.js": "^10.1.2",
"html-entities": "^1.3.1",
"is-ip": "^2.0.0",
"katex": "^0.12.0",
"cheerio": "^1.0.0-rc.3",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to the next reviewer: cheerio is already pulled in by another dependency, so this isn't adding a new dependency, but is added here because it is now being used by HtmlUtils

"linkifyjs": "^2.1.9",
"lodash": "^4.17.19",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
Expand Down
27 changes: 24 additions & 3 deletions src/HtmlUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ import _linkifyString from 'linkifyjs/string';
import classNames from 'classnames';
import EMOJIBASE_REGEX from 'emojibase-regex';
import url from 'url';
import katex from 'katex';
import { AllHtmlEntities } from 'html-entities';
import SettingsStore from './settings/SettingsStore';
import cheerio from 'cheerio';
akissinger marked this conversation as resolved.
Show resolved Hide resolved

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";
Expand Down Expand Up @@ -240,7 +243,8 @@ const sanitizeHtmlParams: IExtendedSanitizeOptions = {
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'],
Expand Down Expand Up @@ -414,6 +418,21 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
if (isHtmlMessage) {
isDisplayedWithHtml = true;
safeBody = sanitizeHtml(formattedBody, sanitizeParams);

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')),
{
throwOnError: false,
displayMode: e.name == 'div',
output: "htmlAndMathml",
});
});
safeBody = phtml.html();
}
}
} finally {
delete sanitizeParams.textFilter;
Expand Down Expand Up @@ -515,7 +534,6 @@ export function checkBlockNode(node: Node) {
case "H6":
case "PRE":
case "BLOCKQUOTE":
case "DIV":
case "P":
case "UL":
case "OL":
Expand All @@ -528,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;
}
Expand Down
6 changes: 6 additions & 0 deletions src/Markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,19 @@ const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];

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);
if (matches && matches.length == 2) {
const tag = matches[1];
return ALLOWED_HTML_TAGS.indexOf(tag) > -1;
}

return false;
}

uhoreg marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
18 changes: 18 additions & 0 deletions src/editor/deserialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -130,6 +131,23 @@ 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 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(delimLeft + tex + delimRight);
} else if (!checkDescendInto(n)) {
return partCreator.plain(n.textContent);
}
break;
}
case "OL":
state.listIndex.push((<HTMLOListElement>n).start || 1);
/* falls through */
Expand Down
41 changes: 39 additions & 2 deletions src/editor/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ limitations under the License.
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';
import cheerio from 'cheerio';

export function mdSerialize(model: EditorModel) {
return model.parts.reduce((html, part) => {
Expand All @@ -38,10 +42,43 @@ export function mdSerialize(model: EditorModel) {
}

export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) {
const md = mdSerialize(model);
let md = mdSerialize(model);

if (SettingsStore.getValue("feature_latex_maths")) {
const displayPattern = (SdkConfig.get()['latex_maths_delims'] || {})['display_pattern'] ||
"\\$\\$(([^$]|\\\\\\$)*)\\$\\$";
jryans marked this conversation as resolved.
Show resolved Hide resolved
const inlinePattern = (SdkConfig.get()['latex_maths_delims'] || {})['inline_pattern'] ||
"\\$(([^$]|\\\\\\$)*)\\$";

md = md.replace(RegExp(displayPattern, "gm"), function(m, p1) {
const p1e = AllHtmlEntities.encode(p1);
return `<div data-mx-maths="${p1e}">\n\n</div>\n\n`;
});

md = md.replace(RegExp(inlinePattern, "gm"), function(m, p1) {
akissinger marked this conversation as resolved.
Show resolved Hide resolved
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);
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(i, e) {
const tex = phtml(e).attr('data-mx-maths')
if (tex) {
phtml(e).html(`<code>${tex}</code>`)
}
});
return phtml.html();
}
// ensure removal of escape backslashes in non-Markdown messages
if (md.indexOf("\\") > -1) {
Expand Down
1 change: 1 addition & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,7 @@
"%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
"Change notification settings": "Change notification settings",
"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",
Expand Down
6 changes: 6 additions & 0 deletions src/settings/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ export interface ISetting {
}

export const SETTINGS: {[setting: string]: ISetting} = {
"feature_latex_maths": {
isFeature: true,
displayName: _td("Render LaTeX maths in messages"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
"feature_communities_v2_prototypes": {
isFeature: true,
displayName: _td(
Expand Down
4 changes: 4 additions & 0 deletions test/components/views/messages/TextualBody-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ describe("<TextualBody />", () => {
MatrixClientPeg.matrixClient = {
getRoom: () => mkStubRoom("room_id"),
getAccountData: () => undefined,
isGuest: () => false,
};

const ev = mkEvent({
Expand All @@ -59,6 +60,7 @@ describe("<TextualBody />", () => {
MatrixClientPeg.matrixClient = {
getRoom: () => mkStubRoom("room_id"),
getAccountData: () => undefined,
isGuest: () => false,
};

const ev = mkEvent({
Expand All @@ -83,6 +85,7 @@ describe("<TextualBody />", () => {
MatrixClientPeg.matrixClient = {
getRoom: () => mkStubRoom("room_id"),
getAccountData: () => undefined,
isGuest: () => false,
};
});

Expand Down Expand Up @@ -135,6 +138,7 @@ describe("<TextualBody />", () => {
getHomeserverUrl: () => "https://my_server/",
on: () => undefined,
removeListener: () => undefined,
isGuest: () => false,
};
});

Expand Down
7 changes: 7 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6206,6 +6206,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"
Expand Down