"flux": "^4.0.3",
"formik": "^2.2.9",
"html-react-parser": "^1.4.11",
+ "katex": "^0.15.2",
"linkifyjs": "^2.1.9",
"matrix-js-sdk": "^17.0.0",
"micromark": "^3.0.10",
"micromark-extension-gfm": "^2.0.1",
+ "micromark-extension-math": "^2.0.2",
"micromark-util-chunked": "^1.0.0",
"micromark-util-resolve-all": "^1.0.0",
"micromark-util-symbol": "^1.0.1",
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
+ "node_modules/@types/katex": {
+ "version": "0.11.1",
+ "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.11.1.tgz",
+ "integrity": "sha512-DUlIj2nk0YnJdlWgsFuVKcX27MLW0KbKmGVoUHmFr+74FYYNUDAaj9ZqTADvsbE8rfxuVmSFc7KczYn5Y09ozg=="
+ },
"node_modules/@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.9.0.tgz",
"integrity": "sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ==",
"dev": true,
+ "optional": true,
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "dev": true
+ "dev": true,
+ "optional": true,
+ "peer": true
},
"node_modules/ajv-keywords": {
"version": "3.5.2",
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
- "dev": true,
"engines": {
"node": ">= 12"
}
"node": ">=4.0"
}
},
+ "node_modules/katex": {
+ "version": "0.15.2",
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.15.2.tgz",
+ "integrity": "sha512-FfZ/f6f8bQdLmJ3McXDNTkKenQkoXkItpW0I9bsG2wgb+8JAY5bwpXFtI8ZVrg5hc1wo1X/UIhdkVMpok46tEQ==",
+ "funding": [
+ "https://opencollective.com/katex",
+ "https://github.com/sponsors/katex"
+ ],
+ "dependencies": {
+ "commander": "^8.0.0"
+ },
+ "bin": {
+ "katex": "cli.js"
+ }
+ },
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/micromark-extension-math": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-2.0.2.tgz",
+ "integrity": "sha512-cFv2B/E4pFPBBFuGgLHkkNiFAIQv08iDgPH2HCuR2z3AUgMLecES5Cq7AVtwOtZeRrbA80QgMUk8VVW0Z+D2FA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/katex": "^0.11.0",
+ "katex": "^0.13.0",
+ "micromark-factory-space": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "uvu": "^0.5.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-math/node_modules/katex": {
+ "version": "0.13.24",
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.13.24.tgz",
+ "integrity": "sha512-jZxYuKCma3VS5UuxOx/rFV1QyGSl3Uy/i0kTJF3HgQ5xMinCQVF8Zd4bMY/9aI9b9A2pjIBOsjSSm68ykTAr8w==",
+ "funding": [
+ "https://opencollective.com/katex",
+ "https://github.com/sponsors/katex"
+ ],
+ "dependencies": {
+ "commander": "^8.0.0"
+ },
+ "bin": {
+ "katex": "cli.js"
+ }
+ },
"node_modules/micromark-factory-destination": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz",
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
+ "@types/katex": {
+ "version": "0.11.1",
+ "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.11.1.tgz",
+ "integrity": "sha512-DUlIj2nk0YnJdlWgsFuVKcX27MLW0KbKmGVoUHmFr+74FYYNUDAaj9ZqTADvsbE8rfxuVmSFc7KczYn5Y09ozg=="
+ },
"@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"dev": true,
- "requires": {
- "ajv": "^8.0.0"
- },
+ "requires": {},
"dependencies": {
"ajv": {
- "version": "8.9.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.9.0.tgz",
+ "version": "https://registry.npmjs.org/ajv/-/ajv-8.9.0.tgz",
"integrity": "sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ==",
"dev": true,
+ "optional": true,
+ "peer": true,
"requires": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "dev": true
+ "dev": true,
+ "optional": true,
+ "peer": true
}
}
},
"commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
- "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
- "dev": true
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="
},
"commondir": {
"version": "1.0.1",
"object.assign": "^4.1.2"
}
},
+ "katex": {
+ "version": "0.15.2",
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.15.2.tgz",
+ "integrity": "sha512-FfZ/f6f8bQdLmJ3McXDNTkKenQkoXkItpW0I9bsG2wgb+8JAY5bwpXFtI8ZVrg5hc1wo1X/UIhdkVMpok46tEQ==",
+ "requires": {
+ "commander": "^8.0.0"
+ }
+ },
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"uvu": "^0.5.0"
}
},
+ "micromark-extension-math": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-2.0.2.tgz",
+ "integrity": "sha512-cFv2B/E4pFPBBFuGgLHkkNiFAIQv08iDgPH2HCuR2z3AUgMLecES5Cq7AVtwOtZeRrbA80QgMUk8VVW0Z+D2FA==",
+ "requires": {
+ "@types/katex": "^0.11.0",
+ "katex": "^0.13.0",
+ "micromark-factory-space": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "uvu": "^0.5.0"
+ },
+ "dependencies": {
+ "katex": {
+ "version": "0.13.24",
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.13.24.tgz",
+ "integrity": "sha512-jZxYuKCma3VS5UuxOx/rFV1QyGSl3Uy/i0kTJF3HgQ5xMinCQVF8Zd4bMY/9aI9b9A2pjIBOsjSSm68ykTAr8w==",
+ "requires": {
+ "commander": "^8.0.0"
+ }
+ }
+ }
+ },
"micromark-factory-destination": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz",
"flux": "^4.0.3",
"formik": "^2.2.9",
"html-react-parser": "^1.4.11",
+ "katex": "^0.15.2",
"linkifyjs": "^2.1.9",
"matrix-js-sdk": "^17.0.0",
"micromark": "^3.0.10",
"micromark-extension-gfm": "^2.0.1",
+ "micromark-extension-math": "^2.0.2",
"micromark-util-chunked": "^1.0.0",
"micromark-util-resolve-all": "^1.0.0",
"micromark-util-symbol": "^1.0.1",
--- /dev/null
+import React, { useEffect, useRef } from 'react';
+import PropTypes from 'prop-types';
+
+import katex from 'katex';
+import 'katex/dist/katex.min.css';
+
+import 'katex/dist/contrib/copy-tex';
+import 'katex/dist/contrib/copy-tex.css';
+
+const Math = React.memo(({
+ content, throwOnError, errorColor, displayMode,
+}) => {
+ const ref = useRef(null);
+
+ useEffect(() => {
+ katex.render(content, ref.current, { throwOnError, errorColor, displayMode });
+ }, [content, throwOnError, errorColor, displayMode]);
+
+ return <span ref={ref} />;
+});
+Math.defaultProps = {
+ throwOnError: null,
+ errorColor: null,
+ displayMode: null,
+};
+Math.propTypes = {
+ content: PropTypes.string.isRequired,
+ throwOnError: PropTypes.bool,
+ errorColor: PropTypes.string,
+ displayMode: PropTypes.bool,
+};
+
+export default Math;
let content = null;
if (isCustomHTML) {
try {
- content = twemojify(sanitizeCustomHtml(body), undefined, true, false);
+ content = twemojify(sanitizeCustomHtml(body), undefined, true, false, true);
} catch {
console.error('Malformed custom html: ', body);
content = twemojify(body, undefined);
import { micromark } from 'micromark';
import { gfm, gfmHtml } from 'micromark-extension-gfm';
import encrypt from 'browser-encrypt-attachment';
+import { math } from 'micromark-extension-math';
import { getShortcodeToEmoji } from '../../app/organisms/emoji-board/custom-emoji';
-import { spoilerExtension, spoilerExtensionHtml } from '../../util/markdown';
+import { mathExtensionHtml, spoilerExtension, spoilerExtensionHtml } from '../../util/markdown';
import cons from './cons';
import settings from './settings';
function getFormattedBody(markdown) {
const result = micromark(markdown, {
- extensions: [gfm(), spoilerExtension()],
- htmlExtensions: [gfmHtml(), spoilerExtensionHtml],
+ extensions: [gfm(), spoilerExtension(), math()],
+ htmlExtensions: [gfmHtml(), spoilerExtensionHtml, mathExtensionHtml],
});
const bodyParts = result.match(/^(<p>)(.*)(<\/p>)$/);
if (bodyParts === null) return result;
},
};
-export { inlineExtension, spoilerExtension, spoilerExtensionHtml };
+const mathExtensionHtml = {
+ enter: {
+ mathFlow() {
+ this.lineEndingIfNeeded();
+ },
+ mathFlowFenceMeta() {
+ this.buffer();
+ },
+ mathText() {
+ this.buffer();
+ },
+ },
+ exit: {
+ mathFlow() {
+ const value = this.encode(this.resume().replace(/(?:\r?\n|\r)$/, ''));
+ this.tag('<div data-mx-maths="');
+ this.tag(value);
+ this.tag('"><code>');
+ this.raw(value);
+ this.tag('</code></div>');
+ this.setData('mathFlowOpen');
+ this.setData('slurpOneLineEnding');
+ },
+ mathFlowFence() {
+ // After the first fence.
+ if (!this.getData('mathFlowOpen')) {
+ this.setData('mathFlowOpen', true);
+ this.setData('slurpOneLineEnding', true);
+ this.buffer();
+ }
+ },
+ mathFlowFenceMeta() {
+ this.resume();
+ },
+ mathFlowValue(token) {
+ this.raw(this.sliceSerialize(token));
+ },
+ mathText() {
+ const value = this.encode(this.resume());
+ this.tag('<span data-mx-maths="');
+ this.tag(value);
+ this.tag('"><code>');
+ this.raw(value);
+ this.tag('</code></span>');
+ },
+ mathTextData(token) {
+ this.raw(this.sliceSerialize(token));
+ },
+ },
+};
+
+export {
+ inlineExtension,
+ spoilerExtension, spoilerExtensionHtml,
+ mathExtensionHtml,
+};
const permittedTagToAttributes = {
font: ['style', 'data-mx-bg-color', 'data-mx-color', 'color'],
- span: ['style', 'data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'data-mx-pill', 'data-mx-ping'],
+ span: ['style', 'data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'data-mx-maths', 'data-mx-pill', 'data-mx-ping'],
+ div: ['data-mx-maths'],
a: ['name', 'target', 'href', 'rel'],
img: ['width', 'height', 'alt', 'title', 'src', 'data-mx-emoticon'],
o: ['start'],
+++ /dev/null
-/* eslint-disable import/prefer-default-export */
-import linkifyHtml from 'linkifyjs/html';
-import parse from 'html-react-parser';
-import twemoji from 'twemoji';
-import { sanitizeText } from './sanitize';
-
-/**
- * @param {string} text - text to twemojify
- * @param {object|undefined} opts - options for tweomoji.parse
- * @param {boolean} [linkify=false] - convert links to html tags (default: false)
- * @param {boolean} [sanitize=true] - sanitize html text (default: true)
- * @returns React component
- */
-export function twemojify(text, opts, linkify = false, sanitize = true) {
- if (typeof text !== 'string') return text;
- let content = text;
-
- if (sanitize) {
- content = sanitizeText(content);
- }
- content = twemoji.parse(content, opts);
- if (linkify) {
- content = linkifyHtml(content, {
- target: '_blank',
- rel: 'noreferrer noopener',
- });
- }
- return parse(content);
-}
--- /dev/null
+/* eslint-disable import/prefer-default-export */
+import React, { lazy, Suspense } from 'react';
+
+import linkifyHtml from 'linkifyjs/html';
+import parse from 'html-react-parser';
+import twemoji from 'twemoji';
+import { sanitizeText } from './sanitize';
+
+const Math = lazy(() => import('../app/atoms/math/Math'));
+
+const mathOptions = {
+ replace: (node) => {
+ const maths = node.attribs?.['data-mx-maths'];
+ if (maths) {
+ return (
+ <Suspense fallback={<code>{maths}</code>}>
+ <Math
+ content={maths}
+ throwOnError={false}
+ errorColor="var(--tc-danger-normal)"
+ displayMode={node.name === 'div'}
+ />
+ </Suspense>
+ );
+ }
+ return null;
+ },
+};
+
+/**
+ * @param {string} text - text to twemojify
+ * @param {object|undefined} opts - options for tweomoji.parse
+ * @param {boolean} [linkify=false] - convert links to html tags (default: false)
+ * @param {boolean} [sanitize=true] - sanitize html text (default: true)
+ * @param {boolean} [maths=false] - render maths (default: false)
+ * @returns React component
+ */
+export function twemojify(text, opts, linkify = false, sanitize = true, maths = false) {
+ if (typeof text !== 'string') return text;
+ let content = text;
+
+ if (sanitize) {
+ content = sanitizeText(content);
+ }
+ content = twemoji.parse(content, opts);
+ if (linkify) {
+ content = linkifyHtml(content, {
+ target: '_blank',
+ rel: 'noreferrer noopener',
+ });
+ }
+ return parse(content, maths ? mathOptions : null);
+}