Latex rendering with react-native-math-view (#5651)

* 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 <kobe.bergmans@student.kuleuven.be>
Co-authored-by: Administrator <admin@Macintosh-3.localdomain>
Co-authored-by: kobe bergmans <kobebergmans@kobes-MacBook-Pro.local>
This commit is contained in:
KobeBergmans 2022-04-19 15:16:38 +02:00 committed by GitHub
parent 70d025fe1d
commit 21adcf6085
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1677 additions and 4 deletions

View file

@ -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',
};
}

View file

@ -0,0 +1,278 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LatexCodeBlock should have 2 lines 1`] = `
<TouchableWithFeedbackIOS
onLongPress={[Function]}
onPress={[Function]}
type="opacity"
>
<View
style={
Object {
"borderColor": "rgba(63,67,80,0.15)",
"borderRadius": 3,
"borderWidth": 0.5,
"flex": 1,
"flexDirection": "row",
}
}
>
<View
style={
Object {
"flex": 1,
"flexDirection": "column",
"paddingLeft": 6,
"paddingVertical": 4,
}
}
>
<View
style={
Object {
"flexDirection": "row",
"justifyContent": "flex-start",
"marginLeft": 5,
"paddingVertical": 4,
}
}
>
<ForwardRef
color="black"
math="\\\\frac{2}{1} = 2"
onError={[Function]}
renderError={[Function]}
resizeMode="cover"
/>
</View>
<View
style={
Object {
"flexDirection": "row",
"justifyContent": "flex-start",
"marginLeft": 5,
"paddingVertical": 4,
}
}
>
<ForwardRef
color="black"
math="2_{nd} = line"
onError={[Function]}
renderError={[Function]}
resizeMode="cover"
/>
</View>
</View>
<View
style={
Object {
"alignItems": "center",
"backgroundColor": "#192a4d",
"justifyContent": "center",
"opacity": 0.8,
"padding": 6,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<Text
style={
Object {
"color": "#ffffff",
"fontSize": 12,
}
}
>
LaTeX
</Text>
</View>
</View>
</TouchableWithFeedbackIOS>
`;
exports[`LatexCodeBlock should have moreLinesText 1`] = `
<TouchableWithFeedbackIOS
onLongPress={[Function]}
onPress={[Function]}
type="opacity"
>
<View
style={
Object {
"borderColor": "rgba(63,67,80,0.15)",
"borderRadius": 3,
"borderWidth": 0.5,
"flex": 1,
"flexDirection": "row",
}
}
>
<View
style={
Object {
"flex": 1,
"flexDirection": "column",
"paddingLeft": 6,
"paddingVertical": 4,
}
}
>
<View
style={
Object {
"flexDirection": "row",
"justifyContent": "flex-start",
"marginLeft": 5,
"paddingVertical": 4,
}
}
>
<ForwardRef
color="black"
math="\\\\frac{5}{2} = 2"
onError={[Function]}
renderError={[Function]}
resizeMode="cover"
/>
</View>
<View
style={
Object {
"flexDirection": "row",
"justifyContent": "flex-start",
"marginLeft": 5,
"paddingVertical": 4,
}
}
>
<ForwardRef
color="black"
math="2_{nd} = line"
onError={[Function]}
renderError={[Function]}
resizeMode="cover"
/>
</View>
<InjectIntl(FormattedText)
defaultMessage="+{count, number} more {count, plural, one {line} other {lines}}"
id="mobile.markdown.code.plusMoreLines"
style={
Object {
"color": "rgba(63,67,80,0.4)",
"fontSize": 11,
"marginTop": 2,
}
}
values={
Object {
"count": 1,
}
}
/>
</View>
<View
style={
Object {
"alignItems": "center",
"backgroundColor": "#192a4d",
"justifyContent": "center",
"opacity": 0.8,
"padding": 6,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<Text
style={
Object {
"color": "#ffffff",
"fontSize": 12,
}
}
>
LaTeX
</Text>
</View>
</View>
</TouchableWithFeedbackIOS>
`;
exports[`LatexCodeBlock should match snapshot 1`] = `
<TouchableWithFeedbackIOS
onLongPress={[Function]}
onPress={[Function]}
type="opacity"
>
<View
style={
Object {
"borderColor": "rgba(63,67,80,0.15)",
"borderRadius": 3,
"borderWidth": 0.5,
"flex": 1,
"flexDirection": "row",
}
}
>
<View
style={
Object {
"flex": 1,
"flexDirection": "column",
"paddingLeft": 6,
"paddingVertical": 4,
}
}
>
<View
style={
Object {
"flexDirection": "row",
"justifyContent": "flex-start",
"marginLeft": 5,
"paddingVertical": 4,
}
}
>
<ForwardRef
color="black"
math="\\\\frac{1}{2} == 0.5^{0*\\\\pi} - 0.5"
onError={[Function]}
renderError={[Function]}
resizeMode="cover"
/>
</View>
</View>
<View
style={
Object {
"alignItems": "center",
"backgroundColor": "#192a4d",
"justifyContent": "center",
"opacity": 0.8,
"padding": 6,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<Text
style={
Object {
"color": "#ffffff",
"fontSize": 12,
}
}
>
LaTeX
</Text>
</View>
</View>
</TouchableWithFeedbackIOS>
`;

View file

@ -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);

View file

@ -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 <Text style={style.errorText}>{'Error: ' + errorMsg.message}</Text>;
};
onRenderErrorMessage = (errorMsg) => {
const style = getStyleSheet(this.props.theme);
return <Text style={style.errorText}>{'Render error: ' + errorMsg.error.message}</Text>;
};
render() {
const style = getStyleSheet(this.props.theme);
let language = null;
const languageDisplayName = getDisplayNameForLanguage('latex');
if (languageDisplayName) {
language = (
<View style={style.language}>
<Text style={style.languageText}>
{languageDisplayName}
</Text>
</View>
);
}
const {content, numberOfLines} = this.splitContent(this.props.content);
let plusMoreLines = null;
if (numberOfLines > MAX_LINES) {
plusMoreLines = (
<FormattedText
style={style.plusMoreLinesText}
id='mobile.markdown.code.plusMoreLines'
defaultMessage='+{count, number} more {count, plural, one {line} other {lines}}'
values={{
count: numberOfLines - MAX_LINES,
}}
/>
);
}
/**
* 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 (
<TouchableWithFeedback
onPress={this.handlePress}
onLongPress={this.handleLongPress}
type={'opacity'}
>
<View style={style.container}>
<View style={style.rightColumn}>
{content.map((latexCode) => (
<View
style={style.code}
key={latexCode}
>
<MathView
math={latexCode}
onError={this.onErrorMessage}
renderError={this.onRenderErrorMessage}
resizeMode={'cover'}
/>
</View>
))}
{plusMoreLines}
</View>
{language}
</View>
</TouchableWithFeedback>
);
}
}
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,
},
};
});

View file

@ -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(
<LatexCodeBlock
{...baseProps}
content={'\\frac{1}{2} == 0.5^{0*\\pi} - 0.5'}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should have 2 lines', () => {
const wrapper = shallow(
<LatexCodeBlock
{...baseProps}
content={'\\frac{2}{1} = 2 \\\\ 2_{nd} = line'}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should have moreLinesText', () => {
const wrapper = shallow(
<LatexCodeBlock
{...baseProps}
content={'\\frac{5}{2} = 2 \\\\ 2_{nd} = line \\\\ \\pi = 3.14'}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,71 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LatexCodeBlock should have maxWidth 10 1`] = `
<View
style={
Array [
Object {
"flexDirection": "row",
"flexWrap": "wrap",
"marginBottom": -7,
},
Object {
"height": undefined,
},
]
}
>
<ForwardRef
color="black"
math="\\\\frac{1}{2} == 0.5^{0*\\\\pi} - 0.5"
onError={[Function]}
renderError={[Function]}
resizeMode="contain"
style={
Array [
Object {
"marginBottom": -7,
},
Object {
"maxWidth": 10,
},
]
}
/>
</View>
`;
exports[`LatexCodeBlock should match snapshot 1`] = `
<View
style={
Array [
Object {
"flexDirection": "row",
"flexWrap": "wrap",
"marginBottom": -7,
},
Object {
"height": undefined,
},
]
}
>
<ForwardRef
color="black"
math="\\\\frac{1}{2} == 0.5^{0*\\\\pi} - 0.5"
onError={[Function]}
renderError={[Function]}
resizeMode="contain"
style={
Array [
Object {
"marginBottom": -7,
},
Object {
"maxWidth": "100%",
},
]
}
/>
</View>
`;

View file

@ -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);

View file

@ -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 <Text style={style.errorText}>{'Latex error: ' + errorMsg.message}</Text>;
};
onRenderErrorMessage = (errorMsg) => {
const style = getStyleSheet(this.props.theme);
return <Text style={style.errorText}>{'Latex render error: ' + errorMsg.error.message}</Text>;
};
render() {
const style = getStyleSheet(this.props.theme);
const viewStyleObj = Platform.select({
ios: () => [style.viewStyle, {height: this.props.mathHeight}],
android: () => [style.viewStyle],
})();
return (
<View
style={viewStyleObj}
key={this.props.content}
onLayout={this.props.onLayout}
>
<MathView
style={[style.mathStyle, {maxWidth: this.props.maxMathWidth ? this.props.maxMathWidth : '100%'}]}
math={this.props.content}
onError={this.onErrorMessage}
renderError={this.onRenderErrorMessage}
resizeMode={'contain'}
/>
</View>
);
}
}
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',
},
};
});

View file

@ -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(
<LatexInline
{...baseProps}
content={'\\frac{1}{2} == 0.5^{0*\\pi} - 0.5'}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should have maxWidth 10', () => {
const wrapper = shallow(
<LatexInline
{...baseProps}
content={'\\frac{1}{2} == 0.5^{0*\\pi} - 0.5'}
maxMathWidth={10}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
});

View file

@ -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 (
<Text
style={{lineHeight: this.state.inlineLatexHeight}}
>
<LatexInline
content={latexCode}
onLayout={this.onInlineLatexLayout}
maxMathWidth={Dimensions.get('window').width * 0.75}
mathHeight={this.state.inlineLatexHeight}
/>
</Text>
);
};
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 (
<LatexCodeBlock
content={content}
language={props.language}
textStyle={this.props.textStyles.codeBlock}
/>
);
}
return (
<MarkdownCodeBlock
content={content}

View file

@ -62,6 +62,7 @@ export type Config = {
EnableGuestAccounts: string;
EnableIncomingWebhooks: string;
EnableLatex: string;
EnableInlineLatex: string;
EnableLdap: string;
EnableLegacySidebar: string;
EnableLinkPreviews: string;

View file

@ -111,6 +111,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
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;

View file

@ -0,0 +1,205 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Latex should have 2 lines 1`] = `
<RNCSafeAreaView
edges={
Array [
"bottom",
"left",
"right",
]
}
style={
Object {
"flex": 1,
}
}
>
<ScrollView
contentContainerStyle={
Object {
"minHeight": "100%",
}
}
style={
Array [
Object {
"flex": 1,
},
]
}
>
<ScrollView
contentContainerStyle={
Object {
"flexDirection": "column",
"minHeight": "100%",
"paddingLeft": 10,
"paddingVertical": 10,
}
}
horizontal={true}
style={
Object {
"flex": 1,
}
}
>
<View
style={
Object {
"flexDirection": "row",
"justifyContent": "flex-start",
"marginHorizontal": 5,
"paddingVertical": 4,
}
}
>
<ForwardRef
color="black"
math="\\\\frac{1}{2} = 0.5^1 + (x^0 - e^{\\\\pi - \\\\frac{2*pi}}{2}} \\\\\\\\ Test = 1"
onError={[Function]}
renderError={[Function]}
resizeMode="cover"
/>
</View>
</ScrollView>
</ScrollView>
</RNCSafeAreaView>
`;
exports[`Latex should have 4 lines 1`] = `
<RNCSafeAreaView
edges={
Array [
"bottom",
"left",
"right",
]
}
style={
Object {
"flex": 1,
}
}
>
<ScrollView
contentContainerStyle={
Object {
"minHeight": "100%",
}
}
style={
Array [
Object {
"flex": 1,
},
]
}
>
<ScrollView
contentContainerStyle={
Object {
"flexDirection": "column",
"minHeight": "100%",
"paddingLeft": 10,
"paddingVertical": 10,
}
}
horizontal={true}
style={
Object {
"flex": 1,
}
}
>
<View
style={
Object {
"flexDirection": "row",
"justifyContent": "flex-start",
"marginHorizontal": 5,
"paddingVertical": 4,
}
}
>
<ForwardRef
color="black"
math="\\\\frac{1}{2} = 0.5^1 + (x^0 - e^{\\\\pi - \\\\frac{2*pi}}{2}} \\\\\\\\ Test = 1 \\\\\\\\ \\\\alpha = \\\\beta \\\\\\\\ 1 != 0"
onError={[Function]}
renderError={[Function]}
resizeMode="cover"
/>
</View>
</ScrollView>
</ScrollView>
</RNCSafeAreaView>
`;
exports[`Latex should match snapshot 1`] = `
<RNCSafeAreaView
edges={
Array [
"bottom",
"left",
"right",
]
}
style={
Object {
"flex": 1,
}
}
>
<ScrollView
contentContainerStyle={
Object {
"minHeight": "100%",
}
}
style={
Array [
Object {
"flex": 1,
},
]
}
>
<ScrollView
contentContainerStyle={
Object {
"flexDirection": "column",
"minHeight": "100%",
"paddingLeft": 10,
"paddingVertical": 10,
}
}
horizontal={true}
style={
Object {
"flex": 1,
}
}
>
<View
style={
Object {
"flexDirection": "row",
"justifyContent": "flex-start",
"marginHorizontal": 5,
"paddingVertical": 4,
}
}
>
<ForwardRef
color="black"
math="\\\\frac{1}{2} = 0.5^1 + (x^0 - e^{\\\\pi - \\\\frac{2*pi}}{2}}"
onError={[Function]}
renderError={[Function]}
resizeMode="cover"
/>
</View>
</ScrollView>
</ScrollView>
</RNCSafeAreaView>
`;

View file

@ -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);

102
app/screens/latex/latex.js Normal file
View file

@ -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 <Text style={style.errorText}>{'Error: ' + errorMsg.message}</Text>;
};
onRenderErrorMessage = (errorMsg) => {
const style = getStyleSheet(this.props.theme);
return <Text style={style.errorText}>{'Render error: ' + errorMsg.error.message}</Text>;
};
render() {
const style = getStyleSheet(this.props.theme);
const lines = splitLatexCodeInLines(this.props.content);
return (
<SafeAreaView
edges={['bottom', 'left', 'right']}
style={style.scrollContainer}
>
<ScrollView
style={[style.scrollContainer]}
contentContainerStyle={style.container}
>
<ScrollView
style={style.scrollContainer}
contentContainerStyle={style.scrollCode}
horizontal={true}
>
{lines.map((latexCode) => (
<View
style={style.code}
key={latexCode}
>
<MathView
math={latexCode}
onError={this.onErrorMessage}
renderError={this.onRenderErrorMessage}
resizeMode={'cover'}
/>
</View>
))}
</ScrollView>
</ScrollView>
</SafeAreaView>
);
}
}
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,
},
};
});

View file

@ -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(
<Latex
{...baseProps}
content={'\\frac{1}{2} = 0.5^1 + (x^0 - e^{\\pi - \\frac{2*pi}}{2}}'}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should have 2 lines', () => {
const wrapper = shallow(
<Latex
{...baseProps}
content={'\\frac{1}{2} = 0.5^1 + (x^0 - e^{\\pi - \\frac{2*pi}}{2}} \\\\ Test = 1'}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should have 4 lines', () => {
const wrapper = shallow(
<Latex
{...baseProps}
content={'\\frac{1}{2} = 0.5^1 + (x^0 - e^{\\pi - \\frac{2*pi}}{2}} \\\\ Test = 1 \\\\ \\alpha = \\beta \\\\ 1 != 0'}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
});

66
app/utils/latex.test.ts Normal file
View file

@ -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');
});
});

96
app/utils/latex.ts Normal file
View file

@ -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;
}

View file

@ -89,6 +89,7 @@ GEM
PLATFORMS
ruby
x64-mingw32
DEPENDENCIES
cocoapods (= 1.11.2)

View file

@ -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;

View file

@ -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

View file

@ -28,6 +28,6 @@ module.exports = {
'assets/images/video_player/(.*).png': '<rootDir>/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))',
],
};

322
package-lock.json generated
View file

@ -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=="
}
}
}

View file

@ -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",