diff --git a/app/components/markdown/index.js b/app/components/markdown/index.js
index c4f61fb06..d314b462b 100644
--- a/app/components/markdown/index.js
+++ b/app/components/markdown/index.js
@@ -17,6 +17,8 @@ function mapStateToProps(state, ownProps) {
mentionKeys: ownProps.mentionKeys || getAllUserMentionKeys(state),
minimumHashtagLength: MinimumHashtagLength ? parseInt(MinimumHashtagLength, 10) : 3,
theme: getTheme(state),
+ enableLatex: getConfig(state).EnableLatex === 'true',
+ enableInlineLatex: getConfig(state).EnableInlineLatex === 'true',
};
}
diff --git a/app/components/markdown/latex_code_block/__snapshots__/latex_code_block.test.js.snap b/app/components/markdown/latex_code_block/__snapshots__/latex_code_block.test.js.snap
new file mode 100644
index 000000000..8549c043d
--- /dev/null
+++ b/app/components/markdown/latex_code_block/__snapshots__/latex_code_block.test.js.snap
@@ -0,0 +1,278 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`LatexCodeBlock should have 2 lines 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+ LaTeX
+
+
+
+
+`;
+
+exports[`LatexCodeBlock should have moreLinesText 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ LaTeX
+
+
+
+
+`;
+
+exports[`LatexCodeBlock should match snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+ LaTeX
+
+
+
+
+`;
diff --git a/app/components/markdown/latex_code_block/index.js b/app/components/markdown/latex_code_block/index.js
new file mode 100644
index 000000000..d0c21cf0d
--- /dev/null
+++ b/app/components/markdown/latex_code_block/index.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {connect} from 'react-redux';
+
+import {getTheme} from '@mm-redux/selectors/entities/preferences';
+
+import LatexCodeBlock from './latex_code_block';
+
+function mapStateToProps(state) {
+ return {
+ theme: getTheme(state),
+ };
+}
+
+export default connect(mapStateToProps)(LatexCodeBlock);
diff --git a/app/components/markdown/latex_code_block/latex_code_block.js b/app/components/markdown/latex_code_block/latex_code_block.js
new file mode 100644
index 000000000..cf0c32e32
--- /dev/null
+++ b/app/components/markdown/latex_code_block/latex_code_block.js
@@ -0,0 +1,209 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {PropTypes} from 'prop-types';
+import React from 'react';
+import {Keyboard, View, Text, StyleSheet, Platform} from 'react-native';
+import MathView from 'react-native-math-view';
+
+import {goToScreen} from '@actions/navigation';
+import FormattedText from '@components/formatted_text';
+import TouchableWithFeedback from '@components/touchable_with_feedback';
+import {splitLatexCodeInLines} from '@utils/latex';
+import {getDisplayNameForLanguage} from '@utils/markdown';
+import {preventDoubleTap} from '@utils/tap';
+import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
+
+import MarkdownCodeBlock from '../markdown_code_block/markdown_code_block';
+
+const MAX_LINES = 2;
+
+export default class LatexCodeBlock extends MarkdownCodeBlock {
+ static propTypes = {
+ theme: PropTypes.object.isRequired,
+ content: PropTypes.string.isRequired,
+ textStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
+ };
+
+ handlePress = preventDoubleTap(() => {
+ const {content} = this.props;
+ const {intl} = this.context;
+ const screen = 'Latex';
+ const passProps = {
+ content,
+ };
+
+ const languageDisplayName = getDisplayNameForLanguage('latex');
+ let title;
+ if (languageDisplayName) {
+ title = intl.formatMessage(
+ {
+ id: 'mobile.routes.code',
+ defaultMessage: '{language} Code',
+ },
+ {
+ language: languageDisplayName,
+ },
+ );
+ } else {
+ title = intl.formatMessage({
+ id: 'mobile.routes.code.noLanguage',
+ defaultMessage: 'LaTeX Code',
+ });
+ }
+
+ Keyboard.dismiss();
+ requestAnimationFrame(() => {
+ goToScreen(screen, title, passProps);
+ });
+ });
+
+ splitContent = (content) => {
+ const lines = splitLatexCodeInLines(content);
+
+ const numberOfLines = lines.length;
+
+ if (numberOfLines > MAX_LINES) {
+ return {
+ content: lines.slice(0, MAX_LINES),
+ numberOfLines,
+ };
+ }
+
+ return {
+ content: lines,
+ numberOfLines,
+ };
+ };
+
+ onErrorMessage = (errorMsg) => {
+ const style = getStyleSheet(this.props.theme);
+
+ return {'Error: ' + errorMsg.message};
+ };
+
+ onRenderErrorMessage = (errorMsg) => {
+ const style = getStyleSheet(this.props.theme);
+
+ return {'Render error: ' + errorMsg.error.message};
+ };
+
+ render() {
+ const style = getStyleSheet(this.props.theme);
+
+ let language = null;
+ const languageDisplayName = getDisplayNameForLanguage('latex');
+
+ if (languageDisplayName) {
+ language = (
+
+
+ {languageDisplayName}
+
+
+ );
+ }
+
+ const {content, numberOfLines} = this.splitContent(this.props.content);
+
+ let plusMoreLines = null;
+ if (numberOfLines > MAX_LINES) {
+ plusMoreLines = (
+
+ );
+ }
+
+ /**
+ * Note on the error behavior of math view:
+ * - onError returns an Error object
+ * - renderError returns an options object with an error attribute that contains the real Error.
+ */
+ return (
+
+
+
+ {content.map((latexCode) => (
+
+
+
+ ))}
+ {plusMoreLines}
+
+ {language}
+
+
+ );
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ const codeVerticalPadding = Platform.select({
+ ios: 4,
+ android: 0,
+ });
+
+ return {
+ container: {
+ borderColor: changeOpacity(theme.centerChannelColor, 0.15),
+ borderRadius: 3,
+ borderWidth: StyleSheet.hairlineWidth,
+ flexDirection: 'row',
+ flex: 1,
+ },
+ rightColumn: {
+ flexDirection: 'column',
+ flex: 1,
+ paddingLeft: 6,
+ paddingVertical: 4,
+ },
+ code: {
+ flexDirection: 'row',
+ justifyContent: 'flex-start',
+ marginLeft: 5,
+ paddingVertical: codeVerticalPadding,
+ },
+ plusMoreLinesText: {
+ color: changeOpacity(theme.centerChannelColor, 0.4),
+ fontSize: 11,
+ marginTop: 2,
+ },
+ language: {
+ alignItems: 'center',
+ backgroundColor: theme.sidebarHeaderBg,
+ justifyContent: 'center',
+ opacity: 0.8,
+ padding: 6,
+ position: 'absolute',
+ right: 0,
+ top: 0,
+ },
+ languageText: {
+ color: theme.sidebarHeaderTextColor,
+ fontSize: 12,
+ },
+ errorText: {
+ fontSize: 14,
+ marginHorizontal: 5,
+ color: theme.errorTextColor,
+ },
+ };
+});
diff --git a/app/components/markdown/latex_code_block/latex_code_block.test.js b/app/components/markdown/latex_code_block/latex_code_block.test.js
new file mode 100644
index 000000000..fe40a1a64
--- /dev/null
+++ b/app/components/markdown/latex_code_block/latex_code_block.test.js
@@ -0,0 +1,50 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {shallow} from 'enzyme';
+import React from 'react';
+
+import Preferences from '@mm-redux/constants/preferences';
+
+import LatexCodeBlock from './latex_code_block';
+
+describe('LatexCodeBlock', () => {
+ const baseProps = {
+ theme: Preferences.THEMES.denim,
+ language: 'latex',
+ textStyle: {},
+ };
+
+ test('should match snapshot', () => {
+ const wrapper = shallow(
+ ,
+ );
+
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+
+ test('should have 2 lines', () => {
+ const wrapper = shallow(
+ ,
+ );
+
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+
+ test('should have moreLinesText', () => {
+ const wrapper = shallow(
+ ,
+ );
+
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+});
diff --git a/app/components/markdown/latex_inline/__snapshots__/latex_inline.test.js.snap b/app/components/markdown/latex_inline/__snapshots__/latex_inline.test.js.snap
new file mode 100644
index 000000000..6cbdb31e8
--- /dev/null
+++ b/app/components/markdown/latex_inline/__snapshots__/latex_inline.test.js.snap
@@ -0,0 +1,71 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`LatexCodeBlock should have maxWidth 10 1`] = `
+
+
+
+`;
+
+exports[`LatexCodeBlock should match snapshot 1`] = `
+
+
+
+`;
diff --git a/app/components/markdown/latex_inline/index.js b/app/components/markdown/latex_inline/index.js
new file mode 100644
index 000000000..80f275def
--- /dev/null
+++ b/app/components/markdown/latex_inline/index.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {connect} from 'react-redux';
+
+import {getTheme} from '@mm-redux/selectors/entities/preferences';
+
+import LatexInline from './latex_inline';
+
+function mapStateToProps(state) {
+ return {
+ theme: getTheme(state),
+ };
+}
+
+export default connect(mapStateToProps)(LatexInline);
diff --git a/app/components/markdown/latex_inline/latex_inline.js b/app/components/markdown/latex_inline/latex_inline.js
new file mode 100644
index 000000000..fe12e2aa4
--- /dev/null
+++ b/app/components/markdown/latex_inline/latex_inline.js
@@ -0,0 +1,78 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import PropTypes from 'prop-types';
+import React, {PureComponent} from 'react';
+import {Platform, Text, View} from 'react-native';
+import MathView from 'react-native-math-view';
+
+import {makeStyleSheetFromTheme} from '@utils/theme';
+
+export default class LatexInline extends PureComponent {
+ static propTypes = {
+ content: PropTypes.string.isRequired,
+ theme: PropTypes.object.isRequired,
+ onLayout: PropTypes.func,
+ maxMathWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ mathHeight: PropTypes.number,
+ };
+
+ onErrorMessage = (errorMsg) => {
+ const style = getStyleSheet(this.props.theme);
+
+ return {'Latex error: ' + errorMsg.message};
+ };
+
+ onRenderErrorMessage = (errorMsg) => {
+ const style = getStyleSheet(this.props.theme);
+
+ return {'Latex render error: ' + errorMsg.error.message};
+ };
+
+ render() {
+ const style = getStyleSheet(this.props.theme);
+
+ const viewStyleObj = Platform.select({
+ ios: () => [style.viewStyle, {height: this.props.mathHeight}],
+ android: () => [style.viewStyle],
+ })();
+
+ return (
+
+
+
+ );
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ mathStyle: {
+ marginBottom: -7,
+ },
+ viewStyle: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ ...Platform.select({
+ ios: {
+ marginBottom: -7,
+ },
+ }),
+ },
+ errorText: {
+ flexDirection: 'row',
+ color: theme.errorTextColor,
+ flexWrap: 'wrap',
+ },
+ };
+});
diff --git a/app/components/markdown/latex_inline/latex_inline.test.js b/app/components/markdown/latex_inline/latex_inline.test.js
new file mode 100644
index 000000000..43daebe5f
--- /dev/null
+++ b/app/components/markdown/latex_inline/latex_inline.test.js
@@ -0,0 +1,38 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {shallow} from 'enzyme';
+import React from 'react';
+
+import Preferences from '@mm-redux/constants/preferences';
+
+import LatexInline from './latex_inline';
+
+describe('LatexCodeBlock', () => {
+ const baseProps = {
+ theme: Preferences.THEMES.denim,
+ };
+
+ test('should match snapshot', () => {
+ const wrapper = shallow(
+ ,
+ );
+
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+
+ test('should have maxWidth 10', () => {
+ const wrapper = shallow(
+ ,
+ );
+
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+});
diff --git a/app/components/markdown/markdown.js b/app/components/markdown/markdown.js
index e62322904..1d41415d5 100644
--- a/app/components/markdown/markdown.js
+++ b/app/components/markdown/markdown.js
@@ -9,6 +9,7 @@ import {
Platform,
Text,
View,
+ Dimensions,
} from 'react-native';
import AtMention from '@components/at_mention';
@@ -19,6 +20,8 @@ import Hashtag from '@components/markdown/hashtag';
import {blendColors, concatStyles, makeStyleSheetFromTheme} from '@utils/theme';
import {getScheme} from '@utils/url';
+import LatexCodeBlock from './latex_code_block';
+import LatexInline from './latex_inline';
import MarkdownBlockQuote from './markdown_block_quote';
import MarkdownCodeBlock from './markdown_code_block';
import MarkdownImage from './markdown_image';
@@ -59,6 +62,8 @@ export default class Markdown extends PureComponent {
disableChannelLink: PropTypes.bool,
disableAtChannelMentionHighlight: PropTypes.bool,
disableGallery: PropTypes.bool,
+ enableLatex: PropTypes.bool,
+ enableInlineLatex: PropTypes.bool,
};
static defaultProps = {
@@ -78,6 +83,10 @@ export default class Markdown extends PureComponent {
this.parser = this.createParser();
this.renderer = this.createRenderer();
+
+ this.state = {
+ inlineLatexHeight: 20,
+ };
}
createParser = () => {
@@ -108,7 +117,7 @@ export default class Markdown extends PureComponent {
channelLink: this.renderChannelLink,
emoji: this.renderEmoji,
hashtag: this.renderHashtag,
- latexinline: this.renderParagraph,
+ latexInline: this.renderLatexInline,
paragraph: this.renderParagraph,
heading: this.renderHeading,
@@ -158,6 +167,11 @@ export default class Markdown extends PureComponent {
return contextStyles.length ? concatStyles(baseStyle, contextStyles) : baseStyle;
};
+ onInlineLatexLayout = (event) => {
+ const mathLineHeight = Math.max(event.nativeEvent.layout.height, this.state.inlineLatexHeight);
+ this.setState({inlineLatexHeight: mathLineHeight});
+ };
+
renderText = ({context, literal}) => {
if (context.indexOf('image') !== -1) {
// If this text is displayed, it will be styled by the image component
@@ -280,6 +294,25 @@ export default class Markdown extends PureComponent {
);
};
+ renderLatexInline = ({context, latexCode}) => {
+ if (!this.props.enableInlineLatex) {
+ return this.renderText({context, literal: `$${latexCode}$`});
+ }
+
+ return (
+
+
+
+ );
+ };
+
renderParagraph = ({children, first}) => {
if (!children || children.length === 0) {
return null;
@@ -319,6 +352,16 @@ export default class Markdown extends PureComponent {
// These sometimes include a trailing newline
const content = props.literal.replace(/\n$/, '');
+ if (this.props.enableLatex && props.language === 'latex') {
+ return (
+
+ );
+ }
+
return (
{
case 'InteractiveDialog':
screen = require('@screens/interactive_dialog').default;
break;
+ case 'Latex':
+ screen = require('@screens/latex').default;
+ break;
case 'Login':
screen = require('@screens/login').default;
break;
diff --git a/app/screens/latex/__snapshots__/latex.test.js.snap b/app/screens/latex/__snapshots__/latex.test.js.snap
new file mode 100644
index 000000000..777eee835
--- /dev/null
+++ b/app/screens/latex/__snapshots__/latex.test.js.snap
@@ -0,0 +1,205 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Latex should have 2 lines 1`] = `
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Latex should have 4 lines 1`] = `
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Latex should match snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+`;
diff --git a/app/screens/latex/index.js b/app/screens/latex/index.js
new file mode 100644
index 000000000..1e8fc557d
--- /dev/null
+++ b/app/screens/latex/index.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {connect} from 'react-redux';
+
+import {getTheme} from '@mm-redux/selectors/entities/preferences';
+
+import Latex from './latex';
+
+function mapStateToProps(state) {
+ return {
+ theme: getTheme(state),
+ };
+}
+
+export default connect(mapStateToProps)(Latex);
diff --git a/app/screens/latex/latex.js b/app/screens/latex/latex.js
new file mode 100644
index 000000000..74949e453
--- /dev/null
+++ b/app/screens/latex/latex.js
@@ -0,0 +1,102 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import {
+ ScrollView,
+ Text,
+ View,
+ Platform,
+} from 'react-native';
+import MathView from 'react-native-math-view';
+import {SafeAreaView} from 'react-native-safe-area-context';
+
+import {splitLatexCodeInLines} from '@utils/latex';
+import {makeStyleSheetFromTheme} from '@utils/theme';
+
+import Code from '../code/code';
+
+export default class Latex extends Code {
+ onErrorMessage = (errorMsg) => {
+ const style = getStyleSheet(this.props.theme);
+
+ return {'Error: ' + errorMsg.message};
+ };
+
+ onRenderErrorMessage = (errorMsg) => {
+ const style = getStyleSheet(this.props.theme);
+
+ return {'Render error: ' + errorMsg.error.message};
+ };
+
+ render() {
+ const style = getStyleSheet(this.props.theme);
+
+ const lines = splitLatexCodeInLines(this.props.content);
+
+ return (
+
+
+
+ {lines.map((latexCode) => (
+
+
+
+ ))}
+
+
+
+ );
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ const codeVerticalPadding = Platform.select({
+ ios: 4,
+ android: 0,
+ });
+
+ return {
+ scrollContainer: {
+ flex: 1,
+ },
+ container: {
+ minHeight: '100%',
+ },
+ scrollCode: {
+ minHeight: '100%',
+ flexDirection: 'column',
+ paddingLeft: 10,
+ paddingVertical: 10,
+ },
+ code: {
+ flexDirection: 'row',
+ justifyContent: 'flex-start',
+ marginHorizontal: 5,
+ paddingVertical: codeVerticalPadding,
+ },
+ errorText: {
+ fontSize: 14,
+ marginHorizontal: 5,
+ color: theme.errorTextColor,
+ },
+ };
+});
diff --git a/app/screens/latex/latex.test.js b/app/screens/latex/latex.test.js
new file mode 100644
index 000000000..6a16c91ca
--- /dev/null
+++ b/app/screens/latex/latex.test.js
@@ -0,0 +1,48 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {shallow} from 'enzyme';
+import React from 'react';
+
+import Preferences from '@mm-redux/constants/preferences';
+
+import Latex from './latex';
+
+describe('Latex', () => {
+ const baseProps = {
+ theme: Preferences.THEMES.denim,
+ };
+
+ test('should match snapshot', () => {
+ const wrapper = shallow(
+ ,
+ );
+
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+
+ test('should have 2 lines', () => {
+ const wrapper = shallow(
+ ,
+ );
+
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+
+ test('should have 4 lines', () => {
+ const wrapper = shallow(
+ ,
+ );
+
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+});
diff --git a/app/utils/latex.test.ts b/app/utils/latex.test.ts
new file mode 100644
index 000000000..10413cd66
--- /dev/null
+++ b/app/utils/latex.test.ts
@@ -0,0 +1,66 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+/* eslint-disable no-useless-escape */
+
+import {splitLatexCodeInLines} from './latex';
+
+describe('LatexUtilTest', () => {
+ test('Simple lines test', () => {
+ const content = '\\frac{1}{2} = 0.5 \\\\ \\pi == 3';
+
+ const result = splitLatexCodeInLines(content);
+
+ expect(result.length).toEqual(2);
+ expect(result[0]).toEqual('\\frac{1}{2} = 0.5');
+ expect(result[1]).toEqual('\\pi == 3');
+ });
+
+ test('Multi line with cases test', () => {
+ const content = `b_n=\\frac{1}{\\pi}\\int\\limits_{-\\pi}^{\\pi}f(x)\\sin nx\\,\\mathrm{d}x=\\frac{1}{\\pi}\\int\\limits_{-\\pi}^{\\pi}x^2\\sin nx\\,\\mathrm{d}x\\\\
+X(m, n) = \\left.
+\\begin{cases}
+x(n), & \\text{for } 0 \\leq n \\leq 1 \\\\
+x(n - 1), & \\text{for } 0 \\leq n \\leq 1 \\\\
+x(n - 1), & \\text{for } 0 \\leq n \\leq 1
+\\end{cases} \\right\\} = xy\\\\
+\\lim_{a\\to \\infty} \\tfrac{1}{a}\\\\
+\\lim_{a \\underset{>}{\\to} 0} \\frac{1}{a}\\\\
+x = a_0 + \\frac{1}{a_1 + \\frac{1}{a_2 + \\frac{1}{a_3 + a_4}}}`;
+
+ const result = splitLatexCodeInLines(content);
+
+ expect(result.length).toEqual(5);
+ expect(result[0]).toEqual('b_n=\\frac{1}{\\pi}\\int\\limits_{-\\pi}^{\\pi}f(x)\\sin nx\\,\\mathrm{d}x=\\frac{1}{\\pi}\\int\\limits_{-\\pi}^{\\pi}x^2\\sin nx\\,\\mathrm{d}x');
+ expect(result[1]).toEqual(`X(m, n) = \\left.
+\\begin{cases}
+x(n), & \\text{for } 0 \\leq n \\leq 1 \\\\
+x(n - 1), & \\text{for } 0 \\leq n \\leq 1 \\\\
+x(n - 1), & \\text{for } 0 \\leq n \\leq 1
+\\end{cases} \\right\\} = xy`);
+ expect(result[2]).toEqual('\\lim_{a\\to \\infty} \\tfrac{1}{a}');
+ expect(result[3]).toEqual('\\lim_{a \\underset{>}{\\to} 0} \\frac{1}{a}');
+ expect(result[4]).toEqual('x = a_0 + \\frac{1}{a_1 + \\frac{1}{a_2 + \\frac{1}{a_3 + a_4}}}');
+ });
+
+ test('Escaped bracket test', () => {
+ const content = 'test = \\frac{1\\{}{2} = \\alpha \\\\ line = 2';
+
+ const result = splitLatexCodeInLines(content);
+
+ expect(result.length).toEqual(2);
+ expect(result[0]).toEqual('test = \\frac{1\\{}{2} = \\alpha');
+ expect(result[1]).toEqual('line = 2');
+ });
+
+ test('Escaped begin and end statement', () => {
+ const content = 'test = \\\\begin \\\\ line = 2';
+
+ const result = splitLatexCodeInLines(content);
+
+ expect(result.length).toEqual(3);
+ expect(result[0]).toEqual('test =');
+ expect(result[1]).toEqual('begin');
+ expect(result[2]).toEqual('line = 2');
+ });
+});
diff --git a/app/utils/latex.ts b/app/utils/latex.ts
new file mode 100644
index 000000000..4d6516e88
--- /dev/null
+++ b/app/utils/latex.ts
@@ -0,0 +1,96 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+/**
+ * Splits up latex code in an array consisting of each line of latex code.
+ * A latex linebreak is denoted by `\\`.
+ * A line is not broken in 2 cases:
+ * - The linebreak is in between brackets.
+ * - The linebreak occurs inbetween a `\begin` and `\end` statement.
+ */
+export function splitLatexCodeInLines(content: string): string[] {
+ let outLines = content.split('\\\\');
+
+ let i = 0;
+ while (i < outLines.length) {
+ if (testLatexLineBreak(outLines[i])) { //Line has no linebreak in between brackets
+ i += 1;
+ } else if (i < outLines.length - 2) {
+ outLines = outLines.slice(0, i).concat([outLines[i] + '\\\\' + outLines[i + 1]], outLines.slice(i + 2));
+ } else if (i === outLines.length - 2) {
+ outLines = outLines.slice(0, i).concat([outLines[i] + '\\\\' + outLines[i + 1]]);
+ } else {
+ break;
+ }
+ }
+
+ return outLines.map((line) => line.trim());
+}
+
+function testLatexLineBreak(latexCode: string): boolean {
+ /**
+ * These checks are special because they need to check if the braces and statements are not escaped
+ */
+
+ //Check for cases
+ let beginCases = 0;
+ let endCases = 0;
+
+ let i = 0;
+ while (i < latexCode.length) {
+ const firstBegin = latexCode.indexOf('\\begin{', i);
+ const firstEnd = latexCode.indexOf('\\end{', i);
+
+ if (firstBegin === -1 && firstEnd === -1) {
+ break;
+ }
+
+ if (firstBegin !== -1 && (firstBegin < firstEnd || firstEnd === -1)) {
+ if (latexCode[firstBegin - 1] !== '\\') {
+ beginCases += 1;
+ }
+ i = firstBegin + '\\begin{'.length;
+ } else {
+ if (latexCode[firstEnd - 1] !== '\\') {
+ endCases += 1;
+ }
+ i = firstEnd + '\\end{'.length;
+ }
+ }
+
+ if (beginCases !== endCases) {
+ return false;
+ }
+
+ //Check for braces
+ let curlyOpenCases = 0;
+ let curlyCloseCases = 0;
+
+ i = 0;
+ while (i < latexCode.length) {
+ const firstBegin = latexCode.indexOf('{', i);
+ const firstEnd = latexCode.indexOf('}', i);
+
+ if (firstBegin === -1 && firstEnd === -1) {
+ break;
+ }
+
+ if (firstBegin !== -1 && (firstBegin < firstEnd || firstEnd === -1)) {
+ if (latexCode[firstBegin - 1] !== '\\') {
+ curlyOpenCases += 1;
+ }
+ i = firstBegin + 1;
+ } else {
+ if (latexCode[firstEnd - 1] !== '\\') {
+ curlyCloseCases += 1;
+ }
+ i = firstEnd + 1;
+ }
+ }
+
+ if (curlyOpenCases !== curlyCloseCases) {
+ return false;
+ }
+
+ return true;
+}
diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock
index ae41a46c2..ce6b18877 100644
--- a/ios/Gemfile.lock
+++ b/ios/Gemfile.lock
@@ -89,6 +89,7 @@ GEM
PLATFORMS
ruby
+ x64-mingw32
DEPENDENCIES
cocoapods (= 1.11.2)
diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj
index 6683e00f0..c136c1a00 100644
--- a/ios/Mattermost.xcodeproj/project.pbxproj
+++ b/ios/Mattermost.xcodeproj/project.pbxproj
@@ -787,6 +787,7 @@
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Zocial.ttf",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle",
"${PODS_ROOT}/YoutubePlayer-in-WKWebView/WKYTPlayerView/WKYTPlayerView.bundle",
+ "${PODS_CONFIGURATION_BUILD_DIR}/iosMath/mathFonts.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
@@ -808,6 +809,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Zocial.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/WKYTPlayerView.bundle",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/mathFonts.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 62d40f085..5e0472aab 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -14,6 +14,7 @@ PODS:
- fmt (6.2.1)
- glog (0.3.5)
- HMSegmentedControl (1.5.6)
+ - iosMath (0.9.4)
- jail-monkey (2.6.0):
- React-Core
- libwebp (1.2.1):
@@ -386,6 +387,8 @@ PODS:
- React-Core
- RNLocalize (2.1.9):
- React-Core
+ - RNMathView (1.0.0):
+ - iosMath
- RNPermissions (3.2.0):
- React-Core
- RNReactNativeHapticFeedback (1.13.0):
@@ -521,6 +524,7 @@ DEPENDENCIES:
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNKeychain (from `../node_modules/react-native-keychain`)
- RNLocalize (from `../node_modules/react-native-localize`)
+ - RNMathView (from `../node_modules/react-native-math-view/ios`)
- RNPermissions (from `../node_modules/react-native-permissions`)
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
- RNReanimated (from `../node_modules/react-native-reanimated`)
@@ -538,6 +542,7 @@ SPEC REPOS:
trunk:
- fmt
- HMSegmentedControl
+ - iosMath
- libwebp
- MMKV
- MMKVCore
@@ -689,6 +694,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-keychain"
RNLocalize:
:path: "../node_modules/react-native-localize"
+ RNMathView:
+ :path: "../node_modules/react-native-math-view/ios"
RNPermissions:
:path: "../node_modules/react-native-permissions"
RNReactNativeHapticFeedback:
@@ -726,8 +733,9 @@ SPEC CHECKSUMS:
FBLazyVector: 244195e30d63d7f564c55da4410b9a24e8fbceaa
FBReactNativeSpec: c94002c1d93da3658f4d5119c6994d19961e3d52
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
- glog: 85ecdd10ee8d8ec362ef519a6a45ff9aa27b2e85
+ glog: 5337263514dd6f09803962437687240c5dc39aa4
HMSegmentedControl: 34c1f54d822d8308e7b24f5d901ec674dfa31352
+ iosMath: f7a6cbadf9d836d2149c2a84c435b1effc244cba
jail-monkey: 07b83767601a373db876e939b8dbf3f5eb15f073
libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc
MMKV: 76033b9ace2006623308910a3afcc0e25eba3140
@@ -738,7 +746,7 @@ SPEC CHECKSUMS:
Permission-Notifications: bb420c3d28328df24de1b476b41ed8249ccf2537
Permission-PhotoLibrary: 7bec836dcdd04a0bfb200c314f1aae06d4476357
Permission-PhotoLibraryAddOnly: 06fb0cdb1d35683b235ad8c464ef0ecc88859ea3
- RCT-Folly: 803a9cfd78114b2ec0f140cfa6fa2a6bafb2d685
+ RCT-Folly: a21c126816d8025b547704b777a2ba552f3d9fa9
RCTRequired: cd47794163052d2b8318c891a7a14fcfaccc75ab
RCTTypeSafety: 393bb40b3e357b224cde53d3fec26813c52428b1
RCTYouTube: a8bb45705622a6fc9decf64be04128d3658ed411
@@ -795,6 +803,7 @@ SPEC CHECKSUMS:
RNGestureHandler: bf572f552ea324acd5b5464b8d30755b2d8c1de6
RNKeychain: 4f63aada75ebafd26f4bc2c670199461eab85d94
RNLocalize: 291e5e21b53cd30b5205c10ed95d31b0dc9b8d63
+ RNMathView: 4c8a3c081fa671ab3136c51fa0bdca7ffb708bd5
RNPermissions: f7ebe52db07c00901127966ca080b4ec6a6ceb0a
RNReactNativeHapticFeedback: b83bfb4b537bdd78eb4f6ffe63c6884f7b049ead
RNReanimated: 1326679461fa5d2399d54c18ca1432ba3e816b9e
diff --git a/jest.config.js b/jest.config.js
index d0afcd231..3f175e287 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -28,6 +28,6 @@ module.exports = {
'assets/images/video_player/(.*).png': '/dist/assets/images/video_player/$1@2x.png',
},
transformIgnorePatterns: [
- 'node_modules/(?!(react-native|@react-native|jail-monkey|serialize-error|@sentry/react-native|react-navigation|@react-native-community/cameraroll))',
+ 'node_modules/(?!(react-native|@react-native|jail-monkey|serialize-error|@sentry/react-native|react-navigation|@react-native-community/cameraroll|hast-util-from-selector|hastscript|property-information|hast-util-parse-selector|space-separated-tokens|comma-separated-tokens|zwitch))',
],
};
diff --git a/package-lock.json b/package-lock.json
index 5b79d1e1c..3a2151493 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -64,6 +64,7 @@
"react-native-linear-gradient": "2.5.6",
"react-native-local-auth": "1.6.0",
"react-native-localize": "2.1.9",
+ "react-native-math-view": "3.9.5",
"react-native-mmkv-storage": "0.6.11",
"react-native-navigation": "7.25.1",
"react-native-notifications": "4.1.3",
@@ -4629,6 +4630,14 @@
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz",
"integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA=="
},
+ "node_modules/@types/hast": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz",
+ "integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
@@ -4850,6 +4859,11 @@
"integrity": "sha512-uO4CD2ELOjw8tasUrAhvnn2W4A0ZECOvMjCivJr4gA9pGgjv+qxKWY9GLTMVEK8ej85BxQOocUyE7hImmSQYcg==",
"dev": true
},
+ "node_modules/@types/unist": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
+ "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
+ },
"node_modules/@types/url-parse": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/@types/url-parse/-/url-parse-1.4.7.tgz",
@@ -7190,6 +7204,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/command-exists": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
@@ -7465,6 +7488,11 @@
"url": "https://github.com/sponsors/fb55"
}
},
+ "node_modules/css-selector-parser": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-1.4.1.tgz",
+ "integrity": "sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g=="
+ },
"node_modules/css-tree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
@@ -9167,6 +9195,14 @@
"node": ">= 8"
}
},
+ "node_modules/esm": {
+ "version": "3.2.25",
+ "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
+ "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/espree": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.3.0.tgz",
@@ -10583,6 +10619,49 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/hast-util-from-selector": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-from-selector/-/hast-util-from-selector-2.0.0.tgz",
+ "integrity": "sha512-ynzm+z7xEecWF8DvnJ5onpGIsfmXphKRsZUnWCfinKwP+fL1/2mYW1nWOVife61syQpF74j4h/57BT6e5niDwA==",
+ "dependencies": {
+ "@types/hast": "^2.0.0",
+ "css-selector-parser": "^1.0.0",
+ "hastscript": "^7.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-parse-selector": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.0.tgz",
+ "integrity": "sha512-AyjlI2pTAZEOeu7GeBPZhROx0RHBnydkQIXlhnFzDi0qfXTmGUWoCYZtomHbrdrheV4VFUlPcfJ6LMF5T6sQzg==",
+ "dependencies": {
+ "@types/hast": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hastscript": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.0.2.tgz",
+ "integrity": "sha512-uA8ooUY4ipaBvKcMuPehTAB/YfFLSSzCwFSwT6ltJbocFUKH/GDHLN+tflq7lSRf9H86uOuxOFkh1KgIy3Gg2g==",
+ "dependencies": {
+ "@types/hast": "^2.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "hast-util-parse-selector": "^3.0.0",
+ "property-information": "^6.0.0",
+ "space-separated-tokens": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
@@ -15652,6 +15731,17 @@
"node": ">=0.10.0"
}
},
+ "node_modules/mathjax-full": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.0.tgz",
+ "integrity": "sha512-D2EBNvUG+mJyhn+M1C858k0f2Fc4KxXvbEX2WCMXroV10212JwfYqaBJ336ECBSz5X9L5LRoamxb7AJtg3KaJA==",
+ "dependencies": {
+ "esm": "^3.2.25",
+ "mhchemparser": "^4.1.0",
+ "mj-context-menu": "^0.6.1",
+ "speech-rule-engine": "^3.3.3"
+ }
+ },
"node_modules/mdn-data": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
@@ -16871,6 +16961,11 @@
"node": ">=6"
}
},
+ "node_modules/mhchemparser": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.1.1.tgz",
+ "integrity": "sha512-R75CUN6O6e1t8bgailrF1qPq+HhVeFTM3XQ0uzI+mXTybmphy3b6h4NbLOYhemViQ3lUs+6CKRkC3Ws1TlYREA=="
+ },
"node_modules/micromatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
@@ -16950,6 +17045,11 @@
"node": ">=0.10.0"
}
},
+ "node_modules/mj-context-menu": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz",
+ "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA=="
+ },
"node_modules/mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
@@ -18963,6 +19063,15 @@
"signal-exit": "^3.0.2"
}
},
+ "node_modules/property-information": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz",
+ "integrity": "sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -19460,6 +19569,21 @@
}
}
},
+ "node_modules/react-native-math-view": {
+ "version": "3.9.5",
+ "resolved": "https://registry.npmjs.org/react-native-math-view/-/react-native-math-view-3.9.5.tgz",
+ "integrity": "sha512-UJxrisNafszfqIW+utoSDylb72SkZ92cKz1IfE5Dm0s+uIaHxOxepF2DdRbktAV8c0FEFllnXfErcGdh8sfIBw==",
+ "dependencies": {
+ "hast-util-from-selector": "^2.0.0",
+ "lodash": "^4.17.21",
+ "mathjax-full": "^3.1.4",
+ "transformation-matrix": "^2.8.0"
+ },
+ "peerDependencies": {
+ "react-native": "*",
+ "react-native-svg": "*"
+ }
+ },
"node_modules/react-native-mmkv-storage": {
"version": "0.6.11",
"resolved": "https://registry.npmjs.org/react-native-mmkv-storage/-/react-native-mmkv-storage-0.6.11.tgz",
@@ -21701,6 +21825,15 @@
"integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
"deprecated": "See https://github.com/lydell/source-map-url#deprecated"
},
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.1.tgz",
+ "integrity": "sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/spawn-wrap": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz",
@@ -21789,6 +21922,27 @@
"integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==",
"dev": true
},
+ "node_modules/speech-rule-engine": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-3.3.3.tgz",
+ "integrity": "sha512-0exWw+0XauLjat+f/aFeo5T8SiDsO1JtwpY3qgJE4cWt+yL/Stl0WP4VNDWdh7lzGkubUD9lWP4J1ASnORXfyQ==",
+ "dependencies": {
+ "commander": ">=7.0.0",
+ "wicked-good-xpath": "^1.3.0",
+ "xmldom-sre": "^0.1.31"
+ },
+ "bin": {
+ "sre": "bin/sre"
+ }
+ },
+ "node_modules/speech-rule-engine/node_modules/commander": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-9.1.0.tgz",
+ "integrity": "sha512-i0/MaqBtdbnJ4XQs4Pmyb+oFQl+q0lsAmokVUH92SlSw4fkeAcG3bVon+Qt7hmtF+u3Het6o4VgrcY3qAoEB6w==",
+ "engines": {
+ "node": "^12.20.0 || >=14"
+ }
+ },
"node_modules/split-on-first": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
@@ -22687,6 +22841,14 @@
"node": ">=12"
}
},
+ "node_modules/transformation-matrix": {
+ "version": "2.11.1",
+ "resolved": "https://registry.npmjs.org/transformation-matrix/-/transformation-matrix-2.11.1.tgz",
+ "integrity": "sha512-srlkTrmetYwTBJ1RHdukkwJ8S8D+2JjgSb1DbvmTwj+DsIpCpRYHbWgOXe/Ql2rX37WqlKLIgidpYlHPGsrwgA==",
+ "funding": {
+ "url": "https://www.paypal.me/chrvadala/25"
+ }
+ },
"node_modules/truncate-utf8-bytes": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
@@ -23617,6 +23779,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/wicked-good-xpath": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz",
+ "integrity": "sha1-gbDpXoZQ5JyUsiKY//hoa1VTz2w="
+ },
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
@@ -23813,6 +23980,14 @@
"sax": "^1.2.1"
}
},
+ "node_modules/xmldom-sre": {
+ "version": "0.1.31",
+ "resolved": "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz",
+ "integrity": "sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==",
+ "engines": {
+ "node": ">=0.1"
+ }
+ },
"node_modules/xregexp": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-5.1.0.tgz",
@@ -23964,6 +24139,15 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.2.tgz",
+ "integrity": "sha512-JZxotl7SxAJH0j7dN4pxsTV6ZLXoLdGME+PsjkL/DaBrVryK9kTGq06GfKrwcSOqypP+fdXGoCHE36b99fWVoA==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
}
},
"dependencies": {
@@ -27188,6 +27372,14 @@
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz",
"integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA=="
},
+ "@types/hast": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz",
+ "integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==",
+ "requires": {
+ "@types/unist": "*"
+ }
+ },
"@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
@@ -27408,6 +27600,11 @@
"integrity": "sha512-uO4CD2ELOjw8tasUrAhvnn2W4A0ZECOvMjCivJr4gA9pGgjv+qxKWY9GLTMVEK8ej85BxQOocUyE7hImmSQYcg==",
"dev": true
},
+ "@types/unist": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
+ "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
+ },
"@types/url-parse": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/@types/url-parse/-/url-parse-1.4.7.tgz",
@@ -29197,6 +29394,11 @@
"delayed-stream": "~1.0.0"
}
},
+ "comma-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg=="
+ },
"command-exists": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
@@ -29426,6 +29628,11 @@
"nth-check": "^2.0.1"
}
},
+ "css-selector-parser": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-1.4.1.tgz",
+ "integrity": "sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g=="
+ },
"css-tree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
@@ -30728,6 +30935,11 @@
"integrity": "sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==",
"dev": true
},
+ "esm": {
+ "version": "3.2.25",
+ "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
+ "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA=="
+ },
"espree": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.3.0.tgz",
@@ -31827,6 +32039,37 @@
"type-fest": "^0.8.0"
}
},
+ "hast-util-from-selector": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-from-selector/-/hast-util-from-selector-2.0.0.tgz",
+ "integrity": "sha512-ynzm+z7xEecWF8DvnJ5onpGIsfmXphKRsZUnWCfinKwP+fL1/2mYW1nWOVife61syQpF74j4h/57BT6e5niDwA==",
+ "requires": {
+ "@types/hast": "^2.0.0",
+ "css-selector-parser": "^1.0.0",
+ "hastscript": "^7.0.0",
+ "zwitch": "^2.0.0"
+ }
+ },
+ "hast-util-parse-selector": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.0.tgz",
+ "integrity": "sha512-AyjlI2pTAZEOeu7GeBPZhROx0RHBnydkQIXlhnFzDi0qfXTmGUWoCYZtomHbrdrheV4VFUlPcfJ6LMF5T6sQzg==",
+ "requires": {
+ "@types/hast": "^2.0.0"
+ }
+ },
+ "hastscript": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.0.2.tgz",
+ "integrity": "sha512-uA8ooUY4ipaBvKcMuPehTAB/YfFLSSzCwFSwT6ltJbocFUKH/GDHLN+tflq7lSRf9H86uOuxOFkh1KgIy3Gg2g==",
+ "requires": {
+ "@types/hast": "^2.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "hast-util-parse-selector": "^3.0.0",
+ "property-information": "^6.0.0",
+ "space-separated-tokens": "^2.0.0"
+ }
+ },
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
@@ -35749,6 +35992,17 @@
"object-visit": "^1.0.0"
}
},
+ "mathjax-full": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.0.tgz",
+ "integrity": "sha512-D2EBNvUG+mJyhn+M1C858k0f2Fc4KxXvbEX2WCMXroV10212JwfYqaBJ336ECBSz5X9L5LRoamxb7AJtg3KaJA==",
+ "requires": {
+ "esm": "^3.2.25",
+ "mhchemparser": "^4.1.0",
+ "mj-context-menu": "^0.6.1",
+ "speech-rule-engine": "^3.3.3"
+ }
+ },
"mdn-data": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
@@ -36780,6 +37034,11 @@
"nullthrows": "^1.1.1"
}
},
+ "mhchemparser": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.1.1.tgz",
+ "integrity": "sha512-R75CUN6O6e1t8bgailrF1qPq+HhVeFTM3XQ0uzI+mXTybmphy3b6h4NbLOYhemViQ3lUs+6CKRkC3Ws1TlYREA=="
+ },
"micromatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
@@ -36835,6 +37094,11 @@
"is-extendable": "^1.0.1"
}
},
+ "mj-context-menu": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz",
+ "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA=="
+ },
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
@@ -38364,6 +38628,11 @@
"signal-exit": "^3.0.2"
}
},
+ "property-information": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz",
+ "integrity": "sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w=="
+ },
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -38837,6 +39106,17 @@
"integrity": "sha512-n2Bi5P0cII8BrnG23Fs9PmpYcufD3JRbr1IBn3ArIA3Z3cGWg4eebv6am+CgdFC4kTPWUlVzX9fEgEMA+X8d6w==",
"requires": {}
},
+ "react-native-math-view": {
+ "version": "3.9.5",
+ "resolved": "https://registry.npmjs.org/react-native-math-view/-/react-native-math-view-3.9.5.tgz",
+ "integrity": "sha512-UJxrisNafszfqIW+utoSDylb72SkZ92cKz1IfE5Dm0s+uIaHxOxepF2DdRbktAV8c0FEFllnXfErcGdh8sfIBw==",
+ "requires": {
+ "hast-util-from-selector": "^2.0.0",
+ "lodash": "^4.17.21",
+ "mathjax-full": "^3.1.4",
+ "transformation-matrix": "^2.8.0"
+ }
+ },
"react-native-mmkv-storage": {
"version": "0.6.11",
"resolved": "https://registry.npmjs.org/react-native-mmkv-storage/-/react-native-mmkv-storage-0.6.11.tgz",
@@ -40527,6 +40807,11 @@
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
"integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw=="
},
+ "space-separated-tokens": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.1.tgz",
+ "integrity": "sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw=="
+ },
"spawn-wrap": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz",
@@ -40599,6 +40884,23 @@
"integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==",
"dev": true
},
+ "speech-rule-engine": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-3.3.3.tgz",
+ "integrity": "sha512-0exWw+0XauLjat+f/aFeo5T8SiDsO1JtwpY3qgJE4cWt+yL/Stl0WP4VNDWdh7lzGkubUD9lWP4J1ASnORXfyQ==",
+ "requires": {
+ "commander": ">=7.0.0",
+ "wicked-good-xpath": "^1.3.0",
+ "xmldom-sre": "^0.1.31"
+ },
+ "dependencies": {
+ "commander": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-9.1.0.tgz",
+ "integrity": "sha512-i0/MaqBtdbnJ4XQs4Pmyb+oFQl+q0lsAmokVUH92SlSw4fkeAcG3bVon+Qt7hmtF+u3Het6o4VgrcY3qAoEB6w=="
+ }
+ }
+ },
"split-on-first": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
@@ -41274,6 +41576,11 @@
"punycode": "^2.1.1"
}
},
+ "transformation-matrix": {
+ "version": "2.11.1",
+ "resolved": "https://registry.npmjs.org/transformation-matrix/-/transformation-matrix-2.11.1.tgz",
+ "integrity": "sha512-srlkTrmetYwTBJ1RHdukkwJ8S8D+2JjgSb1DbvmTwj+DsIpCpRYHbWgOXe/Ql2rX37WqlKLIgidpYlHPGsrwgA=="
+ },
"truncate-utf8-bytes": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
@@ -41978,6 +42285,11 @@
"is-typed-array": "^1.1.7"
}
},
+ "wicked-good-xpath": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz",
+ "integrity": "sha1-gbDpXoZQ5JyUsiKY//hoa1VTz2w="
+ },
"wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
@@ -42128,6 +42440,11 @@
"sax": "^1.2.1"
}
},
+ "xmldom-sre": {
+ "version": "0.1.31",
+ "resolved": "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz",
+ "integrity": "sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw=="
+ },
"xregexp": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-5.1.0.tgz",
@@ -42240,6 +42557,11 @@
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true,
"peer": true
+ },
+ "zwitch": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.2.tgz",
+ "integrity": "sha512-JZxotl7SxAJH0j7dN4pxsTV6ZLXoLdGME+PsjkL/DaBrVryK9kTGq06GfKrwcSOqypP+fdXGoCHE36b99fWVoA=="
}
}
}
diff --git a/package.json b/package.json
index 74a250b5d..4021da899 100644
--- a/package.json
+++ b/package.json
@@ -62,6 +62,7 @@
"react-native-linear-gradient": "2.5.6",
"react-native-local-auth": "1.6.0",
"react-native-localize": "2.1.9",
+ "react-native-math-view": "3.9.5",
"react-native-mmkv-storage": "0.6.11",
"react-native-navigation": "7.25.1",
"react-native-notifications": "4.1.3",