From 21adcf60851558f4d4531d735420be3dff464a70 Mon Sep 17 00:00:00 2001 From: KobeBergmans <55985776+KobeBergmans@users.noreply.github.com> Date: Tue, 19 Apr 2022 15:16:38 +0200 Subject: [PATCH] Latex rendering with react-native-math-view (#5651) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added a Latex code window that displays the given code using katex * Latex is now displayed without having to go to the next screen * MathView test * MathView WIP: Displays math good, but in verry small font View does not load if there is a syntax error and we are hard stuck in the app * Math view now is rendered properly, few working points: Text is not fixed size, it renders in a weird size Multi Line subscript is not working * Latex is now rendered properly * Fixed Error message and moved helper function to utils * Added some padding and vertical scroll on latex screen * Updated config option to work on real phone too * Removed unused code and fixed tests * Added LatexInline and tried to modify parser, not yet finished though. InlineLatexOption is not yet coded for * Bugfixes and better line break checking * Few margin changes for ios, also updated pod files * padding is now dependend on system * Add platform import * Reverted package-lock change * Added tests * Bugfixes, UI tweaks and added unit test for latex util * Added enableLatex to markdown options instead getting the state and config * Changed the rendering a little bit, it is better but still far from perfect * Width and height is now correctely displayed. Still not clean enough though * Inline latex is now rendered properly for most equations * Inline latex is now working properly * tests for escaping and unescaping functions * Added inline latex setting support * Added inline latex tests * Delete unused comment * Added extra regex rule: there must be no word character in front of the inline latex code * Upgraded math-view package and deleted duplicate entry from gemfile.lock * Inline error functions are now class functions * Updated error color to theme error color * Latex screen now extends the code screen * Markdown changes: Now uses dimensions instead of windowdimensions Removed inline latex function extracted latex rendering into separate function * updated latex util test * Hardcoded latex language in latex code block * Added proptypes and cleaned up latex code block * Bugfix latex split lines * ÃDeleted language prop from latex code block * Changed commonmark packages to personal repo's * Latex rendering now goes by commonmark * Deleted unused function and tests * Commonmark packages back to normal versions with patch package * Updated commonmark patch file to right file and changed package lock to point at right repo * Updated patch files to latest version * Updated commonmark patch * Fixed linter errors * Integrety of commonmark fix * First fix of ios problems * Updated snapshot Co-authored-by: Kobe Bergmans Co-authored-by: Administrator Co-authored-by: kobe bergmans --- app/components/markdown/index.js | 2 + .../latex_code_block.test.js.snap | 278 +++++++++++++++ .../markdown/latex_code_block/index.js | 16 + .../latex_code_block/latex_code_block.js | 209 ++++++++++++ .../latex_code_block/latex_code_block.test.js | 50 +++ .../__snapshots__/latex_inline.test.js.snap | 71 ++++ app/components/markdown/latex_inline/index.js | 16 + .../markdown/latex_inline/latex_inline.js | 78 +++++ .../latex_inline/latex_inline.test.js | 38 +++ app/components/markdown/markdown.js | 45 ++- app/mm-redux/types/config.ts | 1 + app/screens/index.js | 3 + .../latex/__snapshots__/latex.test.js.snap | 205 +++++++++++ app/screens/latex/index.js | 16 + app/screens/latex/latex.js | 102 ++++++ app/screens/latex/latex.test.js | 48 +++ app/utils/latex.test.ts | 66 ++++ app/utils/latex.ts | 96 ++++++ ios/Gemfile.lock | 1 + ios/Mattermost.xcodeproj/project.pbxproj | 2 + ios/Podfile.lock | 13 +- jest.config.js | 2 +- package-lock.json | 322 ++++++++++++++++++ package.json | 1 + 24 files changed, 1677 insertions(+), 4 deletions(-) create mode 100644 app/components/markdown/latex_code_block/__snapshots__/latex_code_block.test.js.snap create mode 100644 app/components/markdown/latex_code_block/index.js create mode 100644 app/components/markdown/latex_code_block/latex_code_block.js create mode 100644 app/components/markdown/latex_code_block/latex_code_block.test.js create mode 100644 app/components/markdown/latex_inline/__snapshots__/latex_inline.test.js.snap create mode 100644 app/components/markdown/latex_inline/index.js create mode 100644 app/components/markdown/latex_inline/latex_inline.js create mode 100644 app/components/markdown/latex_inline/latex_inline.test.js create mode 100644 app/screens/latex/__snapshots__/latex.test.js.snap create mode 100644 app/screens/latex/index.js create mode 100644 app/screens/latex/latex.js create mode 100644 app/screens/latex/latex.test.js create mode 100644 app/utils/latex.test.ts create mode 100644 app/utils/latex.ts 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",